@tuwaio/docs-ui 0.5.7 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +93 -3
  2. package/bin/i18n-check.js +19 -0
  3. package/dist/components/DocsLayout.cjs +105 -0
  4. package/dist/components/DocsLayout.d.cts +93 -0
  5. package/dist/components/DocsLayout.d.ts +93 -0
  6. package/dist/components/DocsLayout.js +80 -0
  7. package/dist/components/Footer.cjs +1 -6
  8. package/dist/components/Footer.d.cts +3 -2
  9. package/dist/components/Footer.d.ts +3 -2
  10. package/dist/components/Footer.js +1 -6
  11. package/dist/components/LocalizedPage.cjs +42 -0
  12. package/dist/components/LocalizedPage.d.cts +37 -0
  13. package/dist/components/LocalizedPage.d.ts +37 -0
  14. package/dist/components/LocalizedPage.js +18 -0
  15. package/dist/docsI18n.cjs +33 -0
  16. package/dist/docsI18n.d.cts +16 -0
  17. package/dist/docsI18n.d.ts +16 -0
  18. package/dist/docsI18n.js +8 -0
  19. package/dist/docsLabels.cjs +140 -0
  20. package/dist/docsLabels.d.cts +72 -0
  21. package/dist/docsLabels.d.ts +72 -0
  22. package/dist/docsLabels.js +116 -0
  23. package/dist/docsPage.cjs +70 -0
  24. package/dist/docsPage.d.cts +56 -0
  25. package/dist/docsPage.d.ts +56 -0
  26. package/dist/docsPage.js +46 -0
  27. package/dist/i18n.d.cts +1 -1
  28. package/dist/i18n.d.ts +1 -1
  29. package/dist/index.cjs +4 -0
  30. package/dist/index.css +3 -0
  31. package/dist/index.d.cts +3 -0
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.js +2 -0
  34. package/dist/nextra.cjs +26 -0
  35. package/dist/nextra.d.cts +11 -0
  36. package/dist/nextra.d.ts +11 -0
  37. package/dist/nextra.js +3 -0
  38. package/dist/pageMap.cjs +66 -0
  39. package/dist/pageMap.d.cts +33 -0
  40. package/dist/pageMap.d.ts +33 -0
  41. package/dist/pageMap.js +42 -0
  42. package/dist/translationCheck.cjs +191 -0
  43. package/dist/translationCheck.d.cts +90 -0
  44. package/dist/translationCheck.d.ts +90 -0
  45. package/dist/translationCheck.js +162 -0
  46. package/package.json +15 -3
package/README.md CHANGED
@@ -13,6 +13,7 @@
13
13
  - **Logo:** `RemoteLogo` is an async React Server Component that fetches an SVG (by default the TUWA logo) and renders it as React elements, so it takes `className` and `currentColor`.
14
14
  - **Theme switch:** `ThemeSwitcher` is a light/dark switch for any theme state (for example `next-themes`).
15
15
  - **Language select:** `LocaleSwitcher` is an accessible glassmorphic select dropdown for switching languages (English, Українська, 简体中文, etc.), styled to match `ThemeSwitcher`. Features zero layout shift, Nova UI Kit animated directional chevron, collision detection with auto-flip (`placement="auto"`), multi-layout Latin typeahead navigation (independent of user OS keyboard language), and an invisible native `<select>` overlay on touch devices (`pointer: coarse`) for native mobile system pickers (iOS wheel, Android sheet). Full WCAG 2.1/2.2 AA/AAA compliance. It renders an invisible placeholder during SSR to avoid hydration shifts.
16
+ - **Three languages (project sites):** `@tuwaio/docs-ui/nextra` and `@tuwaio/docs-ui/nextra/page` turn a Nextra site into an English, Ukrainian and Simplified Chinese site with one URL per page, as on the [docs hub](https://docs.tuwa.io). `DocsLayout` is the Nextra layout with the navbar (the language switch after the links; the theme switch stays in the Nextra sidebar and the mobile menu), the footer, the search, the table of contents, the edit, feedback and last-updated texts and the sidebar titles in the language of the reader (`docsLabels`, `localizePageMap`). `createDocsPage` creates the `[[...mdxPath]]` route: each page is prerendered in English and in the languages it is translated into, and `LocalizedPage` shows the reader's language without a request; pages without a translation (package READMEs, the generated API reference) show English with a note in the reader's language. The language is detected after mount (`tuwa-locale`, then the browser languages and the time zone; no cookie, no request). The `tuwa-docs-i18n-check` command keeps the translations in line with their English pages.
16
17
  - **Interface settings for 30 days:** `PreferenceExpiry` is an inline script for the `<head>` of a site's root layout. It forgets the Local Storage values of the given keys (by default `theme`) 30 days after they were written, as the [Cookie Policy](https://tuwa.io/cookie) states, and dates each later write, whatever library writes it (`next-themes`, the `Tabs` of Nextra).
17
18
  - **Decoration:** `Orb` (an animated sphere in the brand color of a TUWA project), `StarryBackground` (an animated star field on a canvas) and `NoSSR` (renders its children only on the client).
18
19
  - **Policy pages:** `policies` lists the TUWA policies (Terms, Privacy, Cookie, Refund, Security, Data Processing Addendum) with their paths and whether they are in force (`publishedPolicies`; footers use `footerPolicies`). `loadPolicy` loads the text of a policy in a language, and `PolicyDocument` renders it with the date of the version, a Download PDF button, a language switch (English, Українська) when the policy is translated, anchored sections (the same anchors in every language) and a list of sections. The texts and PDFs come from [`TuwaIO/workflows/docs`](https://github.com/TuwaIO/workflows/tree/main/docs). Code that needs only the list or `loadPolicy` (footers, sitemaps, route handlers) imports them from `@tuwaio/docs-ui/policies`, which has no React, Next.js or Nextra imports.
@@ -27,7 +28,7 @@
27
28
  pnpm add @tuwaio/docs-ui next nextra nextra-theme-docs react react-dom @heroicons/react
28
29
  ```
29
30
 
30
- `nextra-theme-docs` (and its peer `nextra`) is needed by `Navbar` and `Footer`, `next` by `RemoteLogo` (a Server Component that uses the `fetch` cache of Next.js) and `@heroicons/react` by `ThemeSwitcher`.
31
+ `nextra-theme-docs` is needed by `Navbar`, `Footer` and `DocsLayout`, `nextra` by `DocsLayout`, `LocalizedPage` and `createDocsPage`, `next` by `RemoteLogo` (a Server Component that uses the `fetch` cache of Next.js) and `createDocsPage`, and `@heroicons/react` by `ThemeSwitcher`.
31
32
 
32
33
  Import the stylesheet after the Nextra theme:
33
34
 
@@ -122,6 +123,95 @@ export function PolicyLinks() {
122
123
 
123
124
  Sections have stable anchors (`/refund#how-to-request-a-refund`), so hints and FAQ answers can link to a section.
124
125
 
126
+ ### Languages of a project site
127
+
128
+ A TUWA project site (Orbit, SIWX, Satellite Connect, Pulsar, SDK) in English, Ukrainian and Simplified Chinese. The root layout renders `DocsLayout` around the page and keeps the language for 30 days:
129
+
130
+ ```tsx
131
+ import { PreferenceExpiry, SiteLogo } from '@tuwaio/docs-ui';
132
+ import { DocsLayout } from '@tuwaio/docs-ui/nextra';
133
+ import { Head } from 'nextra/components';
134
+ import { getPageMap } from 'nextra/page-map';
135
+ import type { ReactNode } from 'react';
136
+
137
+ const logo = <SiteLogo width={126} height={40} />;
138
+
139
+ export default async function RootLayout({ children }: { children: ReactNode }) {
140
+ return (
141
+ <html lang="en" dir="ltr" suppressHydrationWarning>
142
+ <Head>
143
+ <PreferenceExpiry keys={['theme', 'tuwa-locale']} />
144
+ </Head>
145
+ <body>
146
+ <DocsLayout
147
+ logo={logo}
148
+ pageMap={await getPageMap()}
149
+ docsRepositoryBase="https://github.com/TuwaIO/pulsar-core/tree/main/apps/docs"
150
+ sidebarTitles={{ uk: { 'EVM Trackers Standalone': 'EVM-трекери окремо' } }}
151
+ >
152
+ {children}
153
+ </DocsLayout>
154
+ </body>
155
+ </html>
156
+ );
157
+ }
158
+ ```
159
+
160
+ `src/app/[[...mdxPath]]/page.tsx` renders every page of `src/content` with its translations:
161
+
162
+ ```tsx
163
+ import { createDocsPage } from '@tuwaio/docs-ui/nextra/page';
164
+
165
+ import { localizedPages } from '@/i18n/pages';
166
+
167
+ const { generateStaticParams, generateMetadata, Page } = createDocsPage({ localizedPages });
168
+
169
+ export { generateMetadata, generateStaticParams };
170
+ export default Page;
171
+ ```
172
+
173
+ The translations are MDX files with the path of their English page under `src/i18n/<language>` (`src/i18n/uk/index.mdx` translates `src/content/index.mdx`), listed by route in `src/i18n/pages.ts`:
174
+
175
+ ```ts
176
+ import type { LocalizedPageLoaders } from '@tuwaio/docs-ui/nextra/page';
177
+
178
+ export const localizedPages: Record<string, LocalizedPageLoaders> = {
179
+ '/': {
180
+ uk: () => import('./uk/index.mdx'),
181
+ 'zh-CN': () => import('./zh-CN/index.mdx'),
182
+ },
183
+ };
184
+ ```
185
+
186
+ TypeScript types the imported MDX files with a declaration next to it (`src/i18n/mdx.d.ts`):
187
+
188
+ ```ts
189
+ declare module '*.mdx' {
190
+ import type { EvaluateResult } from 'nextra';
191
+
192
+ const MDXContent: EvaluateResult['default'];
193
+ export default MDXContent;
194
+ export const toc: EvaluateResult['toc'];
195
+ export const metadata: EvaluateResult['metadata'];
196
+ export const sourceCode: EvaluateResult['sourceCode'];
197
+ }
198
+ ```
199
+
200
+ A translation keeps the structure of its English page: the same code blocks in the same order, the same heading levels with the English heading ids written as `## Встановлення [#-installation]` (so anchors work in every language) and the same links. `tuwa-docs-i18n-check`, run from the docs app, checks it and that every translated page exists in every language (it gives headings their ids with the `github-slugger` of the site's `nextra` and fails when that cannot be loaded); add it to the build and to the pre-commit hook:
201
+
202
+ ```json
203
+ {
204
+ "scripts": {
205
+ "build": "tuwa-docs-i18n-check && next build && pnpm postbuild",
206
+ "i18n:check": "tuwa-docs-i18n-check"
207
+ }
208
+ }
209
+ ```
210
+
211
+ Client components inside `DocsLayout` read the language and the interface texts with `useDocsI18n()`.
212
+
213
+ `NextraDocsLayout` is the same layout without state: the language, its texts (`docsLabels[locale]`) and the page map come from props. The docs hub renders it with its own dictionaries and translates its sidebar by route with `localizePageMap(pageMap, {}, { routes })`.
214
+
125
215
  ### Search index
126
216
 
127
217
  Run the command after `next build`. It uses the `pagefind` package of the site, so add it to the site's dependencies:
@@ -144,11 +234,11 @@ It writes the index to `public/_pagefind` (keep that directory in `.gitignore`)
144
234
 
145
235
  ## 🗄️ Browser Storage
146
236
 
147
- `LocaleSwitcher` and `ThemeSwitcher` store nothing; they only call `onSelect` / `onToggle`, and the site keeps the choice. `PreferenceExpiry` stores `<key>:expires` (the time in epoch milliseconds) next to each key it is given, removes both once that time has passed, and wraps `Storage.prototype.setItem` on the page to date later writes of those keys.
237
+ `LocaleSwitcher` and `ThemeSwitcher` store nothing; they only call `onSelect` / `onToggle`, and the site keeps the choice. `DocsLayout`, `useLocaleState` and the providers of `createI18n` save the language the reader picks to `tuwa-locale` (`en`, `uk` or `zh-CN`) and read it on the next visit; list the key in `PreferenceExpiry` to forget it after 30 days. `PreferenceExpiry` stores `<key>:expires` (the time in epoch milliseconds) next to each key it is given, removes both once that time has passed, and wraps `Storage.prototype.setItem` on the page to date later writes of those keys.
148
238
 
149
239
  ## 🌐 External Services
150
240
 
151
- `RemoteLogo` requests its `url` on the server (by default `tuwa.io/brand/logo_v2.svg`), cached by Next.js for 24 hours. `loadPolicy` requests `raw.githubusercontent.com/TuwaIO/workflows/main/docs/<slug>.json` (a translation: `docs/<language>/<slug>.json`, and the English file when it does not exist) on the server, cached by Next.js for an hour by default. The Download PDF button of `PolicyDocument` links to the PDF on the same host; the browser requests it only when it is clicked. The other components send no requests.
241
+ `RemoteLogo` requests its `url` on the server (by default `tuwa.io/brand/logo_v2.svg`), cached by Next.js for 24 hours. `loadPolicy` requests `raw.githubusercontent.com/TuwaIO/workflows/main/docs/<slug>.json` (a translation: `docs/<language>/<slug>.json`, and the English file when it does not exist) on the server, cached by Next.js for an hour by default. The Download PDF button of `PolicyDocument` links to the PDF on the same host; the browser requests it only when it is clicked. `createDocsPage` and `LocalizedPage` send no requests: the translations are part of the page. The other components send no requests.
152
242
 
153
243
  ---
154
244
 
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ // `tuwa-docs-i18n-check`: checks the translated pages of a TUWA documentation site (src/i18n/<locale>) against their
3
+ // English pages (src/content), run from the docs app (see README)
4
+ import { checkSiteTranslations } from '../dist/translationCheck.js';
5
+
6
+ checkSiteTranslations().then(
7
+ ({ files, issues }) => {
8
+ for (const { file, message } of issues) console.error(`${file}: ${message}`);
9
+ if (issues.length > 0) {
10
+ console.error(`Translations: ${issues.length} problem(s) in ${files} translated page(s)`);
11
+ process.exit(1);
12
+ }
13
+ console.log(`Translations: ${files} translated page(s) match their English pages`);
14
+ },
15
+ (error) => {
16
+ console.error(error instanceof Error ? error.message : error);
17
+ process.exit(1);
18
+ },
19
+ );
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ "use client";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var DocsLayout_exports = {};
21
+ __export(DocsLayout_exports, {
22
+ DocsLayout: () => DocsLayout,
23
+ NextraDocsLayout: () => NextraDocsLayout
24
+ });
25
+ module.exports = __toCommonJS(DocsLayout_exports);
26
+ var import_jsx_runtime = require("react/jsx-runtime");
27
+ var import_components = require("nextra/components");
28
+ var import_nextra_theme_docs = require("nextra-theme-docs");
29
+ var import_react = require("react");
30
+ var import_docsI18n = require("../docsI18n");
31
+ var import_locale = require("../locale");
32
+ var import_pageMap = require("../pageMap");
33
+ var import_Footer = require("./Footer");
34
+ var import_LocaleSwitcher = require("./LocaleSwitcher");
35
+ var import_NavBar = require("./NavBar");
36
+ function NextraDocsLayout({
37
+ logo,
38
+ links,
39
+ footerLinks,
40
+ pageMap,
41
+ docsRepositoryBase,
42
+ locale,
43
+ onLocaleChange,
44
+ labels,
45
+ sidebar,
46
+ children
47
+ }) {
48
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
49
+ import_nextra_theme_docs.Layout,
50
+ {
51
+ navbar: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_NavBar.Navbar, { logo, links, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
52
+ import_LocaleSwitcher.LocaleSwitcher,
53
+ {
54
+ locale,
55
+ onSelect: (code) => {
56
+ const picked = (0, import_locale.normalizeLocale)(code);
57
+ if (picked) onLocaleChange(picked);
58
+ },
59
+ ariaLabel: labels.selectLanguage
60
+ }
61
+ ) }),
62
+ footer: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Footer.Footer, { logo, links: footerLinks, labels: labels.footer, policyLanguage: (0, import_locale.policyLanguageOf)(locale) }),
63
+ pageMap,
64
+ docsRepositoryBase,
65
+ navigation: { prev: true, next: true },
66
+ ...sidebar && { sidebar },
67
+ editLink: labels.nextra.editThisPage,
68
+ feedback: { content: labels.nextra.feedback },
69
+ lastUpdated: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_nextra_theme_docs.LastUpdated, { locale, children: labels.nextra.lastUpdated }),
70
+ toc: { title: labels.nextra.onThisPage, backToTop: labels.nextra.scrollToTop },
71
+ themeSwitch: {
72
+ light: labels.nextra.themeLight,
73
+ dark: labels.nextra.themeDark,
74
+ system: labels.nextra.themeSystem
75
+ },
76
+ search: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
77
+ import_components.Search,
78
+ {
79
+ placeholder: labels.nextra.searchPlaceholder,
80
+ emptyResult: labels.nextra.searchEmpty,
81
+ errorText: labels.nextra.searchError,
82
+ loading: labels.nextra.searchLoading
83
+ }
84
+ ),
85
+ children
86
+ }
87
+ );
88
+ }
89
+ function LocalizedLayout({ pageMap, sidebarTitles, ...props }) {
90
+ const { locale, setLocale, t } = (0, import_docsI18n.useDocsI18n)();
91
+ const siteTitles = locale === "en" ? void 0 : sidebarTitles?.[locale];
92
+ const localizedPageMap = (0, import_react.useMemo)(
93
+ () => (0, import_pageMap.localizePageMap)(pageMap, { ...t.sidebar, ...siteTitles }),
94
+ [pageMap, t.sidebar, siteTitles]
95
+ );
96
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NextraDocsLayout, { ...props, pageMap: localizedPageMap, locale, onLocaleChange: setLocale, labels: t });
97
+ }
98
+ function DocsLayout({ initialLocale, ...props }) {
99
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_docsI18n.DocsI18nProvider, { initialLocale, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LocalizedLayout, { ...props }) });
100
+ }
101
+ // Annotate the CommonJS export names for ESM import in node:
102
+ 0 && (module.exports = {
103
+ DocsLayout,
104
+ NextraDocsLayout
105
+ });
@@ -0,0 +1,93 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, ComponentProps } from 'react';
3
+ import { PageMapItem } from 'nextra';
4
+ import { Layout } from 'nextra-theme-docs';
5
+ import { DocsLabels } from '../docsLabels.cjs';
6
+ import { TuwaLocale } from '../locale.cjs';
7
+ import { SocialLink } from './NavBar.cjs';
8
+ import '../policies.cjs';
9
+
10
+ /**
11
+ * Props of {@link DocsLayout}.
12
+ */
13
+ interface DocsLayoutProps {
14
+ /** Logo of the site, for example `SiteLogo`; rendered in the navbar and the footer. */
15
+ logo: ReactNode;
16
+ /** External links of the navbar (default: `baseNavLinks`). */
17
+ links?: SocialLink[];
18
+ /** External links of the footer (default: `baseFooterLinks`). */
19
+ footerLinks?: SocialLink[];
20
+ /** The page map of the site, the result of `getPageMap()` of `nextra/page-map`. */
21
+ pageMap: PageMapItem[];
22
+ /** URL of the docs app in the repository, for the edit links (`https://github.com/TuwaIO/<repo>/tree/main/apps/docs`). */
23
+ docsRepositoryBase: string;
24
+ /**
25
+ * Sidebar titles of the site's own pages, by language and English title (for example `{ uk: { 'EVM Trackers
26
+ * Standalone': '…' } }`). `Introduction`, `Packages`, `Guides` and `Overview` are translated already.
27
+ */
28
+ sidebarTitles?: Partial<Record<Exclude<TuwaLocale, 'en'>, Record<string, string>>>;
29
+ /** Fixed language (tests, previews): detection and the stored choice are skipped. */
30
+ initialLocale?: TuwaLocale;
31
+ /** The page and anything rendered next to it, such as a top loading bar. */
32
+ children: ReactNode;
33
+ }
34
+ /**
35
+ * Props of {@link NextraDocsLayout}.
36
+ */
37
+ interface NextraDocsLayoutProps {
38
+ /** Logo of the site, for example `SiteLogo`; rendered in the navbar and the footer. */
39
+ logo: ReactNode;
40
+ /** External links of the navbar (default: `baseNavLinks`). */
41
+ links?: SocialLink[];
42
+ /** External links of the footer (default: `baseFooterLinks`). */
43
+ footerLinks?: SocialLink[];
44
+ /** The page map, already in the language of the reader (`localizePageMap`); passed to Nextra as it is. */
45
+ pageMap: PageMapItem[];
46
+ /** URL of the docs app in the repository, for the edit links. */
47
+ docsRepositoryBase: string;
48
+ /** The language of the reader, shown in the language switch and used for the dates and the policy links. */
49
+ locale: TuwaLocale;
50
+ /**
51
+ * Called with the language the reader picks in the switch.
52
+ *
53
+ * @param locale - The picked language.
54
+ */
55
+ onLocaleChange: (locale: TuwaLocale) => void;
56
+ /** The interface texts of that language, `docsLabels[locale]`. */
57
+ labels: DocsLabels;
58
+ /** Options of the Nextra sidebar (the `sidebar` prop of its `Layout`), for example `{ defaultMenuCollapseLevel: 1 }`. */
59
+ sidebar?: ComponentProps<typeof Layout>['sidebar'];
60
+ /** The page and anything rendered next to it. */
61
+ children: ReactNode;
62
+ }
63
+ /**
64
+ * The Nextra `Layout` of a TUWA documentation site in a given language: the docs-ui {@link Navbar} with the language
65
+ * switch after the links (the theme switch stays in the Nextra sidebar and the mobile menu), the {@link Footer}, the
66
+ * search, the table of contents, the edit, feedback and last-updated texts and the theme switch options from
67
+ * `labels`. It keeps no state: {@link DocsLayout} renders it with the language of its provider, and the docs hub with
68
+ * its own dictionaries, so every site words and wires its chrome the same way.
69
+ *
70
+ * @param props - See {@link NextraDocsLayoutProps}.
71
+ * @returns The layout.
72
+ */
73
+ declare function NextraDocsLayout({ logo, links, footerLinks, pageMap, docsRepositoryBase, locale, onLocaleChange, labels, sidebar, children, }: NextraDocsLayoutProps): react.JSX.Element;
74
+ /**
75
+ * The layout of a TUWA project documentation site in three languages (English, Ukrainian, Simplified Chinese):
76
+ * {@link NextraDocsLayout} (the Nextra `Layout` with the docs-ui {@link Navbar}, the language switch after the links,
77
+ * the theme switch in the Nextra sidebar and the mobile menu, the {@link Footer}, the search, the table of contents,
78
+ * the edit, feedback and last-updated texts) and the sidebar titles in the language of the reader. Render it in the
79
+ * `<body>` of the root layout, around the page; pages rendered with `createDocsPage` (`@tuwaio/docs-ui/nextra/page`)
80
+ * switch to their translation.
81
+ *
82
+ * The URL never changes with the language. The server renders English; after mount the language is read from
83
+ * `tuwa-locale` in Local Storage, then the browser languages and the time zone (no cookie, no request), and a pick in
84
+ * the switch is saved to `tuwa-locale` (add the key to `PreferenceExpiry`, so it is forgotten after 30 days) and sets
85
+ * `<html lang>`.
86
+ *
87
+ * @param props - See {@link DocsLayoutProps}.
88
+ * @returns The layout.
89
+ */
90
+ declare function DocsLayout({ initialLocale, ...props }: DocsLayoutProps): react.JSX.Element;
91
+
92
+ export { DocsLayout, NextraDocsLayout };
93
+ export type { DocsLayoutProps, NextraDocsLayoutProps };
@@ -0,0 +1,93 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, ComponentProps } from 'react';
3
+ import { PageMapItem } from 'nextra';
4
+ import { Layout } from 'nextra-theme-docs';
5
+ import { DocsLabels } from '../docsLabels.js';
6
+ import { TuwaLocale } from '../locale.js';
7
+ import { SocialLink } from './NavBar.js';
8
+ import '../policies.js';
9
+
10
+ /**
11
+ * Props of {@link DocsLayout}.
12
+ */
13
+ interface DocsLayoutProps {
14
+ /** Logo of the site, for example `SiteLogo`; rendered in the navbar and the footer. */
15
+ logo: ReactNode;
16
+ /** External links of the navbar (default: `baseNavLinks`). */
17
+ links?: SocialLink[];
18
+ /** External links of the footer (default: `baseFooterLinks`). */
19
+ footerLinks?: SocialLink[];
20
+ /** The page map of the site, the result of `getPageMap()` of `nextra/page-map`. */
21
+ pageMap: PageMapItem[];
22
+ /** URL of the docs app in the repository, for the edit links (`https://github.com/TuwaIO/<repo>/tree/main/apps/docs`). */
23
+ docsRepositoryBase: string;
24
+ /**
25
+ * Sidebar titles of the site's own pages, by language and English title (for example `{ uk: { 'EVM Trackers
26
+ * Standalone': '…' } }`). `Introduction`, `Packages`, `Guides` and `Overview` are translated already.
27
+ */
28
+ sidebarTitles?: Partial<Record<Exclude<TuwaLocale, 'en'>, Record<string, string>>>;
29
+ /** Fixed language (tests, previews): detection and the stored choice are skipped. */
30
+ initialLocale?: TuwaLocale;
31
+ /** The page and anything rendered next to it, such as a top loading bar. */
32
+ children: ReactNode;
33
+ }
34
+ /**
35
+ * Props of {@link NextraDocsLayout}.
36
+ */
37
+ interface NextraDocsLayoutProps {
38
+ /** Logo of the site, for example `SiteLogo`; rendered in the navbar and the footer. */
39
+ logo: ReactNode;
40
+ /** External links of the navbar (default: `baseNavLinks`). */
41
+ links?: SocialLink[];
42
+ /** External links of the footer (default: `baseFooterLinks`). */
43
+ footerLinks?: SocialLink[];
44
+ /** The page map, already in the language of the reader (`localizePageMap`); passed to Nextra as it is. */
45
+ pageMap: PageMapItem[];
46
+ /** URL of the docs app in the repository, for the edit links. */
47
+ docsRepositoryBase: string;
48
+ /** The language of the reader, shown in the language switch and used for the dates and the policy links. */
49
+ locale: TuwaLocale;
50
+ /**
51
+ * Called with the language the reader picks in the switch.
52
+ *
53
+ * @param locale - The picked language.
54
+ */
55
+ onLocaleChange: (locale: TuwaLocale) => void;
56
+ /** The interface texts of that language, `docsLabels[locale]`. */
57
+ labels: DocsLabels;
58
+ /** Options of the Nextra sidebar (the `sidebar` prop of its `Layout`), for example `{ defaultMenuCollapseLevel: 1 }`. */
59
+ sidebar?: ComponentProps<typeof Layout>['sidebar'];
60
+ /** The page and anything rendered next to it. */
61
+ children: ReactNode;
62
+ }
63
+ /**
64
+ * The Nextra `Layout` of a TUWA documentation site in a given language: the docs-ui {@link Navbar} with the language
65
+ * switch after the links (the theme switch stays in the Nextra sidebar and the mobile menu), the {@link Footer}, the
66
+ * search, the table of contents, the edit, feedback and last-updated texts and the theme switch options from
67
+ * `labels`. It keeps no state: {@link DocsLayout} renders it with the language of its provider, and the docs hub with
68
+ * its own dictionaries, so every site words and wires its chrome the same way.
69
+ *
70
+ * @param props - See {@link NextraDocsLayoutProps}.
71
+ * @returns The layout.
72
+ */
73
+ declare function NextraDocsLayout({ logo, links, footerLinks, pageMap, docsRepositoryBase, locale, onLocaleChange, labels, sidebar, children, }: NextraDocsLayoutProps): react.JSX.Element;
74
+ /**
75
+ * The layout of a TUWA project documentation site in three languages (English, Ukrainian, Simplified Chinese):
76
+ * {@link NextraDocsLayout} (the Nextra `Layout` with the docs-ui {@link Navbar}, the language switch after the links,
77
+ * the theme switch in the Nextra sidebar and the mobile menu, the {@link Footer}, the search, the table of contents,
78
+ * the edit, feedback and last-updated texts) and the sidebar titles in the language of the reader. Render it in the
79
+ * `<body>` of the root layout, around the page; pages rendered with `createDocsPage` (`@tuwaio/docs-ui/nextra/page`)
80
+ * switch to their translation.
81
+ *
82
+ * The URL never changes with the language. The server renders English; after mount the language is read from
83
+ * `tuwa-locale` in Local Storage, then the browser languages and the time zone (no cookie, no request), and a pick in
84
+ * the switch is saved to `tuwa-locale` (add the key to `PreferenceExpiry`, so it is forgotten after 30 days) and sets
85
+ * `<html lang>`.
86
+ *
87
+ * @param props - See {@link DocsLayoutProps}.
88
+ * @returns The layout.
89
+ */
90
+ declare function DocsLayout({ initialLocale, ...props }: DocsLayoutProps): react.JSX.Element;
91
+
92
+ export { DocsLayout, NextraDocsLayout };
93
+ export type { DocsLayoutProps, NextraDocsLayoutProps };
@@ -0,0 +1,80 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { Search } from "nextra/components";
4
+ import { LastUpdated, Layout } from "nextra-theme-docs";
5
+ import { useMemo } from "react";
6
+ import { DocsI18nProvider, useDocsI18n } from "../docsI18n";
7
+ import { normalizeLocale, policyLanguageOf } from "../locale";
8
+ import { localizePageMap } from "../pageMap";
9
+ import { Footer } from "./Footer";
10
+ import { LocaleSwitcher } from "./LocaleSwitcher";
11
+ import { Navbar } from "./NavBar";
12
+ function NextraDocsLayout({
13
+ logo,
14
+ links,
15
+ footerLinks,
16
+ pageMap,
17
+ docsRepositoryBase,
18
+ locale,
19
+ onLocaleChange,
20
+ labels,
21
+ sidebar,
22
+ children
23
+ }) {
24
+ return /* @__PURE__ */ jsx(
25
+ Layout,
26
+ {
27
+ navbar: /* @__PURE__ */ jsx(Navbar, { logo, links, children: /* @__PURE__ */ jsx(
28
+ LocaleSwitcher,
29
+ {
30
+ locale,
31
+ onSelect: (code) => {
32
+ const picked = normalizeLocale(code);
33
+ if (picked) onLocaleChange(picked);
34
+ },
35
+ ariaLabel: labels.selectLanguage
36
+ }
37
+ ) }),
38
+ footer: /* @__PURE__ */ jsx(Footer, { logo, links: footerLinks, labels: labels.footer, policyLanguage: policyLanguageOf(locale) }),
39
+ pageMap,
40
+ docsRepositoryBase,
41
+ navigation: { prev: true, next: true },
42
+ ...sidebar && { sidebar },
43
+ editLink: labels.nextra.editThisPage,
44
+ feedback: { content: labels.nextra.feedback },
45
+ lastUpdated: /* @__PURE__ */ jsx(LastUpdated, { locale, children: labels.nextra.lastUpdated }),
46
+ toc: { title: labels.nextra.onThisPage, backToTop: labels.nextra.scrollToTop },
47
+ themeSwitch: {
48
+ light: labels.nextra.themeLight,
49
+ dark: labels.nextra.themeDark,
50
+ system: labels.nextra.themeSystem
51
+ },
52
+ search: /* @__PURE__ */ jsx(
53
+ Search,
54
+ {
55
+ placeholder: labels.nextra.searchPlaceholder,
56
+ emptyResult: labels.nextra.searchEmpty,
57
+ errorText: labels.nextra.searchError,
58
+ loading: labels.nextra.searchLoading
59
+ }
60
+ ),
61
+ children
62
+ }
63
+ );
64
+ }
65
+ function LocalizedLayout({ pageMap, sidebarTitles, ...props }) {
66
+ const { locale, setLocale, t } = useDocsI18n();
67
+ const siteTitles = locale === "en" ? void 0 : sidebarTitles?.[locale];
68
+ const localizedPageMap = useMemo(
69
+ () => localizePageMap(pageMap, { ...t.sidebar, ...siteTitles }),
70
+ [pageMap, t.sidebar, siteTitles]
71
+ );
72
+ return /* @__PURE__ */ jsx(NextraDocsLayout, { ...props, pageMap: localizedPageMap, locale, onLocaleChange: setLocale, labels: t });
73
+ }
74
+ function DocsLayout({ initialLocale, ...props }) {
75
+ return /* @__PURE__ */ jsx(DocsI18nProvider, { initialLocale, children: /* @__PURE__ */ jsx(LocalizedLayout, { ...props }) });
76
+ }
77
+ export {
78
+ DocsLayout,
79
+ NextraDocsLayout
80
+ };
@@ -82,12 +82,7 @@ function Footer({ links, logo, labels, policyLanguage = "en" }) {
82
82
  }
83
83
  )
84
84
  ] }),
85
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:justify-center tuwadocs:gap-3 tuwadocs:mt-4 tuwadocs:pt-4 tuwa-footer-border", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tuwa-footer-copyright", children: [
86
- "\xA9 2025 - ",
87
- (/* @__PURE__ */ new Date()).getFullYear(),
88
- " TUWA. ",
89
- labels?.copyright ?? "All rights reserved."
90
- ] }) })
85
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:justify-center tuwadocs:gap-3 tuwadocs:mt-4 tuwadocs:pt-4 tuwa-footer-border", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tuwa-footer-copyright", suppressHydrationWarning: true, children: `\xA9 2025 - ${(/* @__PURE__ */ new Date()).getFullYear()} TUWA. ${labels?.copyright ?? "All rights reserved."}` }) })
91
86
  ] })
92
87
  ] }) });
93
88
  }
@@ -31,8 +31,9 @@ interface FooterProps extends NavProps {
31
31
  /**
32
32
  * The footer of a TUWA documentation site: the `Footer` of `nextra-theme-docs` with the logo, external links, the
33
33
  * TUWA description and the license on the left, links to the policies in force on tuwa.io (`footerPolicies`) and to
34
- * the list of all policies on the docs hub on the right, and the copyright line (the current year is read on render)
35
- * centered below; on narrow screens everything is centered. Render it in the `footer` prop of the Nextra `Layout`.
34
+ * the list of all policies on the docs hub on the right, and the copyright line (the current year is read on render;
35
+ * while a prerendered page hydrates, the year of its HTML stays without a hydration error) centered below; on narrow
36
+ * screens everything is centered. Render it in the `footer` prop of the Nextra `Layout`.
36
37
  *
37
38
  * @param props - See {@link FooterProps}.
38
39
  * @returns The footer.
@@ -31,8 +31,9 @@ interface FooterProps extends NavProps {
31
31
  /**
32
32
  * The footer of a TUWA documentation site: the `Footer` of `nextra-theme-docs` with the logo, external links, the
33
33
  * TUWA description and the license on the left, links to the policies in force on tuwa.io (`footerPolicies`) and to
34
- * the list of all policies on the docs hub on the right, and the copyright line (the current year is read on render)
35
- * centered below; on narrow screens everything is centered. Render it in the `footer` prop of the Nextra `Layout`.
34
+ * the list of all policies on the docs hub on the right, and the copyright line (the current year is read on render;
35
+ * while a prerendered page hydrates, the year of its HTML stays without a hydration error) centered below; on narrow
36
+ * screens everything is centered. Render it in the `footer` prop of the Nextra `Layout`.
36
37
  *
37
38
  * @param props - See {@link FooterProps}.
38
39
  * @returns The footer.
@@ -63,12 +63,7 @@ function Footer({ links, logo, labels, policyLanguage = "en" }) {
63
63
  }
64
64
  )
65
65
  ] }),
66
- /* @__PURE__ */ jsx("div", { className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:justify-center tuwadocs:gap-3 tuwadocs:mt-4 tuwadocs:pt-4 tuwa-footer-border", children: /* @__PURE__ */ jsxs("div", { className: "tuwa-footer-copyright", children: [
67
- "\xA9 2025 - ",
68
- (/* @__PURE__ */ new Date()).getFullYear(),
69
- " TUWA. ",
70
- labels?.copyright ?? "All rights reserved."
71
- ] }) })
66
+ /* @__PURE__ */ jsx("div", { className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:justify-center tuwadocs:gap-3 tuwadocs:mt-4 tuwadocs:pt-4 tuwa-footer-border", children: /* @__PURE__ */ jsx("div", { className: "tuwa-footer-copyright", suppressHydrationWarning: true, children: `\xA9 2025 - ${(/* @__PURE__ */ new Date()).getFullYear()} TUWA. ${labels?.copyright ?? "All rights reserved."}` }) })
72
67
  ] })
73
68
  ] }) });
74
69
  }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ "use client";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var LocalizedPage_exports = {};
21
+ __export(LocalizedPage_exports, {
22
+ LocalizedPage: () => LocalizedPage
23
+ });
24
+ module.exports = __toCommonJS(LocalizedPage_exports);
25
+ var import_jsx_runtime = require("react/jsx-runtime");
26
+ var import_components = require("nextra/components");
27
+ var import_nextra_theme_docs = require("nextra-theme-docs");
28
+ var import_docsI18n = require("../docsI18n");
29
+ function LocalizedPage({ variants }) {
30
+ const { locale, t } = (0, import_docsI18n.useDocsI18n)();
31
+ const Wrapper = (0, import_nextra_theme_docs.useMDXComponents)().wrapper;
32
+ const translated = variants[locale];
33
+ const page = translated ?? variants.en;
34
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Wrapper, { toc: page.toc, metadata: page.metadata, sourceCode: page.sourceCode, children: [
35
+ !translated && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tuwadocs:clear-both", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Callout, { type: "info", children: t.englishOnly }) }),
36
+ page.content
37
+ ] });
38
+ }
39
+ // Annotate the CommonJS export names for ESM import in node:
40
+ 0 && (module.exports = {
41
+ LocalizedPage
42
+ });