@tuwaio/docs-ui 0.6.0 → 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.
package/README.md CHANGED
@@ -197,7 +197,7 @@ declare module '*.mdx' {
197
197
  }
198
198
  ```
199
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; add it to the build and to the pre-commit hook:
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
201
 
202
202
  ```json
203
203
  {
@@ -210,6 +210,8 @@ A translation keeps the structure of its English page: the same code blocks in t
210
210
 
211
211
  Client components inside `DocsLayout` read the language and the interface texts with `useDocsI18n()`.
212
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
+
213
215
  ### Search index
214
216
 
215
217
  Run the command after `next build`. It uses the `pagefind` package of the site, so add it to the site's dependencies:
@@ -19,7 +19,8 @@ var __copyProps = (to, from, except, desc) => {
19
19
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
20
  var DocsLayout_exports = {};
21
21
  __export(DocsLayout_exports, {
22
- DocsLayout: () => DocsLayout
22
+ DocsLayout: () => DocsLayout,
23
+ NextraDocsLayout: () => NextraDocsLayout
23
24
  });
24
25
  module.exports = __toCommonJS(DocsLayout_exports);
25
26
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -32,51 +33,73 @@ var import_pageMap = require("../pageMap");
32
33
  var import_Footer = require("./Footer");
33
34
  var import_LocaleSwitcher = require("./LocaleSwitcher");
34
35
  var import_NavBar = require("./NavBar");
35
- function LocalizedLayout({
36
+ function NextraDocsLayout({
36
37
  logo,
37
38
  links,
38
39
  footerLinks,
39
40
  pageMap,
40
41
  docsRepositoryBase,
41
- sidebarTitles,
42
+ locale,
43
+ onLocaleChange,
44
+ labels,
45
+ sidebar,
42
46
  children
43
47
  }) {
44
- const { locale, setLocale, t } = (0, import_docsI18n.useDocsI18n)();
45
- const siteTitles = locale === "en" ? void 0 : sidebarTitles?.[locale];
46
- const localizedPageMap = (0, import_react.useMemo)(
47
- () => (0, import_pageMap.localizePageMap)(pageMap, { ...t.sidebar, ...siteTitles }),
48
- [pageMap, t.sidebar, siteTitles]
49
- );
50
48
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
51
49
  import_nextra_theme_docs.Layout,
52
50
  {
53
- navbar: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_NavBar.Navbar, { logo, links, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_LocaleSwitcher.LocaleSwitcher, { locale, onSelect: setLocale, ariaLabel: t.selectLanguage }) }),
54
- footer: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Footer.Footer, { logo, links: footerLinks, labels: t.footer, policyLanguage: (0, import_locale.policyLanguageOf)(locale) }),
55
- pageMap: localizedPageMap,
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,
56
64
  docsRepositoryBase,
57
65
  navigation: { prev: true, next: true },
58
- editLink: t.nextra.editThisPage,
59
- feedback: { content: t.nextra.feedback },
60
- lastUpdated: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_nextra_theme_docs.LastUpdated, { locale, children: t.nextra.lastUpdated }),
61
- toc: { title: t.nextra.onThisPage, backToTop: t.nextra.scrollToTop },
62
- themeSwitch: { light: t.nextra.themeLight, dark: t.nextra.themeDark, system: t.nextra.themeSystem },
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
+ },
63
76
  search: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
64
77
  import_components.Search,
65
78
  {
66
- placeholder: t.nextra.searchPlaceholder,
67
- emptyResult: t.nextra.searchEmpty,
68
- errorText: t.nextra.searchError,
69
- loading: t.nextra.searchLoading
79
+ placeholder: labels.nextra.searchPlaceholder,
80
+ emptyResult: labels.nextra.searchEmpty,
81
+ errorText: labels.nextra.searchError,
82
+ loading: labels.nextra.searchLoading
70
83
  }
71
84
  ),
72
85
  children
73
86
  }
74
87
  );
75
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
+ }
76
98
  function DocsLayout({ initialLocale, ...props }) {
77
99
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_docsI18n.DocsI18nProvider, { initialLocale, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LocalizedLayout, { ...props }) });
78
100
  }
79
101
  // Annotate the CommonJS export names for ESM import in node:
80
102
  0 && (module.exports = {
81
- DocsLayout
103
+ DocsLayout,
104
+ NextraDocsLayout
82
105
  });
@@ -1,8 +1,11 @@
1
1
  import * as react from 'react';
2
- import { ReactNode } from 'react';
2
+ import { ReactNode, ComponentProps } from 'react';
3
3
  import { PageMapItem } from 'nextra';
4
+ import { Layout } from 'nextra-theme-docs';
5
+ import { DocsLabels } from '../docsLabels.cjs';
4
6
  import { TuwaLocale } from '../locale.cjs';
5
7
  import { SocialLink } from './NavBar.cjs';
8
+ import '../policies.cjs';
6
9
 
7
10
  /**
8
11
  * Props of {@link DocsLayout}.
@@ -29,12 +32,52 @@ interface DocsLayoutProps {
29
32
  children: ReactNode;
30
33
  }
31
34
  /**
32
- * The layout of a TUWA project documentation site in three languages (English, Ukrainian, Simplified Chinese): the
33
- * Nextra `Layout` with the docs-ui {@link Navbar} (the language switch after the links; the theme switch stays in the
34
- * Nextra sidebar and the mobile menu), the {@link Footer}, the search, the table of contents, the edit, feedback and
35
- * last-updated texts and the sidebar titles in the language of the reader. Render it in the `<body>` of the root
36
- * layout, around the page; pages rendered with `createDocsPage` (`@tuwaio/docs-ui/nextra/page`) switch to their
37
- * translation.
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.
38
81
  *
39
82
  * The URL never changes with the language. The server renders English; after mount the language is read from
40
83
  * `tuwa-locale` in Local Storage, then the browser languages and the time zone (no cookie, no request), and a pick in
@@ -46,5 +89,5 @@ interface DocsLayoutProps {
46
89
  */
47
90
  declare function DocsLayout({ initialLocale, ...props }: DocsLayoutProps): react.JSX.Element;
48
91
 
49
- export { DocsLayout };
50
- export type { DocsLayoutProps };
92
+ export { DocsLayout, NextraDocsLayout };
93
+ export type { DocsLayoutProps, NextraDocsLayoutProps };
@@ -1,8 +1,11 @@
1
1
  import * as react from 'react';
2
- import { ReactNode } from 'react';
2
+ import { ReactNode, ComponentProps } from 'react';
3
3
  import { PageMapItem } from 'nextra';
4
+ import { Layout } from 'nextra-theme-docs';
5
+ import { DocsLabels } from '../docsLabels.js';
4
6
  import { TuwaLocale } from '../locale.js';
5
7
  import { SocialLink } from './NavBar.js';
8
+ import '../policies.js';
6
9
 
7
10
  /**
8
11
  * Props of {@link DocsLayout}.
@@ -29,12 +32,52 @@ interface DocsLayoutProps {
29
32
  children: ReactNode;
30
33
  }
31
34
  /**
32
- * The layout of a TUWA project documentation site in three languages (English, Ukrainian, Simplified Chinese): the
33
- * Nextra `Layout` with the docs-ui {@link Navbar} (the language switch after the links; the theme switch stays in the
34
- * Nextra sidebar and the mobile menu), the {@link Footer}, the search, the table of contents, the edit, feedback and
35
- * last-updated texts and the sidebar titles in the language of the reader. Render it in the `<body>` of the root
36
- * layout, around the page; pages rendered with `createDocsPage` (`@tuwaio/docs-ui/nextra/page`) switch to their
37
- * translation.
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.
38
81
  *
39
82
  * The URL never changes with the language. The server renders English; after mount the language is read from
40
83
  * `tuwa-locale` in Local Storage, then the browser languages and the time zone (no cookie, no request), and a pick in
@@ -46,5 +89,5 @@ interface DocsLayoutProps {
46
89
  */
47
90
  declare function DocsLayout({ initialLocale, ...props }: DocsLayoutProps): react.JSX.Element;
48
91
 
49
- export { DocsLayout };
50
- export type { DocsLayoutProps };
92
+ export { DocsLayout, NextraDocsLayout };
93
+ export type { DocsLayoutProps, NextraDocsLayoutProps };
@@ -4,55 +4,77 @@ import { Search } from "nextra/components";
4
4
  import { LastUpdated, Layout } from "nextra-theme-docs";
5
5
  import { useMemo } from "react";
6
6
  import { DocsI18nProvider, useDocsI18n } from "../docsI18n";
7
- import { policyLanguageOf } from "../locale";
7
+ import { normalizeLocale, policyLanguageOf } from "../locale";
8
8
  import { localizePageMap } from "../pageMap";
9
9
  import { Footer } from "./Footer";
10
10
  import { LocaleSwitcher } from "./LocaleSwitcher";
11
11
  import { Navbar } from "./NavBar";
12
- function LocalizedLayout({
12
+ function NextraDocsLayout({
13
13
  logo,
14
14
  links,
15
15
  footerLinks,
16
16
  pageMap,
17
17
  docsRepositoryBase,
18
- sidebarTitles,
18
+ locale,
19
+ onLocaleChange,
20
+ labels,
21
+ sidebar,
19
22
  children
20
23
  }) {
21
- const { locale, setLocale, t } = useDocsI18n();
22
- const siteTitles = locale === "en" ? void 0 : sidebarTitles?.[locale];
23
- const localizedPageMap = useMemo(
24
- () => localizePageMap(pageMap, { ...t.sidebar, ...siteTitles }),
25
- [pageMap, t.sidebar, siteTitles]
26
- );
27
24
  return /* @__PURE__ */ jsx(
28
25
  Layout,
29
26
  {
30
- navbar: /* @__PURE__ */ jsx(Navbar, { logo, links, children: /* @__PURE__ */ jsx(LocaleSwitcher, { locale, onSelect: setLocale, ariaLabel: t.selectLanguage }) }),
31
- footer: /* @__PURE__ */ jsx(Footer, { logo, links: footerLinks, labels: t.footer, policyLanguage: policyLanguageOf(locale) }),
32
- pageMap: localizedPageMap,
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,
33
40
  docsRepositoryBase,
34
41
  navigation: { prev: true, next: true },
35
- editLink: t.nextra.editThisPage,
36
- feedback: { content: t.nextra.feedback },
37
- lastUpdated: /* @__PURE__ */ jsx(LastUpdated, { locale, children: t.nextra.lastUpdated }),
38
- toc: { title: t.nextra.onThisPage, backToTop: t.nextra.scrollToTop },
39
- themeSwitch: { light: t.nextra.themeLight, dark: t.nextra.themeDark, system: t.nextra.themeSystem },
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
+ },
40
52
  search: /* @__PURE__ */ jsx(
41
53
  Search,
42
54
  {
43
- placeholder: t.nextra.searchPlaceholder,
44
- emptyResult: t.nextra.searchEmpty,
45
- errorText: t.nextra.searchError,
46
- loading: t.nextra.searchLoading
55
+ placeholder: labels.nextra.searchPlaceholder,
56
+ emptyResult: labels.nextra.searchEmpty,
57
+ errorText: labels.nextra.searchError,
58
+ loading: labels.nextra.searchLoading
47
59
  }
48
60
  ),
49
61
  children
50
62
  }
51
63
  );
52
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
+ }
53
74
  function DocsLayout({ initialLocale, ...props }) {
54
75
  return /* @__PURE__ */ jsx(DocsI18nProvider, { initialLocale, children: /* @__PURE__ */ jsx(LocalizedLayout, { ...props }) });
55
76
  }
56
77
  export {
57
- DocsLayout
78
+ DocsLayout,
79
+ NextraDocsLayout
58
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
  }
package/dist/index.d.cts CHANGED
@@ -10,7 +10,7 @@ export { ThemeSwitcher, ThemeSwitcherProps } from './components/ThemeSwitcher.cj
10
10
  export { DocsLabels, docsLabels } from './docsLabels.cjs';
11
11
  export { I18nContextValue, I18nKit, I18nProviderProps, LocaleState, LocaleStateOptions, LocaleVariant, createI18n, useLocaleState } from './i18n.cjs';
12
12
  export { DEFAULT_LOCALE, LOCALE_STORAGE_KEY, LocaleSignals, TUWA_LOCALES, TuwaLocale, detectLocale, formatLabel, normalizeLocale, policyLanguageOf, resolveClientLocale } from './locale.cjs';
13
- export { localizePageMap } from './pageMap.cjs';
13
+ export { LocalizePageMapOptions, localizePageMap } from './pageMap.cjs';
14
14
  export { POLICIES_INDEX_URL, POLICY_DOCUMENT_LABELS, POLICY_LANGUAGES, POLICY_LANGUAGE_NAMES, POLICY_SITE_URL, POLICY_SOURCE, Policy, PolicyContent, PolicyDocumentLabels, PolicyLanguage, PolicySection, PolicySlug, footerPolicies, formatPolicyDate, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, policyLanguageFrom, publishedPolicies } from './policies.cjs';
15
15
  export { PREFERENCE_DAYS, PreferenceExpiry, expireStoredPreferences } from './preferences.cjs';
16
16
  export { baseFooterLinks, baseNavLinks } from './utils.cjs';
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export { ThemeSwitcher, ThemeSwitcherProps } from './components/ThemeSwitcher.js
10
10
  export { DocsLabels, docsLabels } from './docsLabels.js';
11
11
  export { I18nContextValue, I18nKit, I18nProviderProps, LocaleState, LocaleStateOptions, LocaleVariant, createI18n, useLocaleState } from './i18n.js';
12
12
  export { DEFAULT_LOCALE, LOCALE_STORAGE_KEY, LocaleSignals, TUWA_LOCALES, TuwaLocale, detectLocale, formatLabel, normalizeLocale, policyLanguageOf, resolveClientLocale } from './locale.js';
13
- export { localizePageMap } from './pageMap.js';
13
+ export { LocalizePageMapOptions, localizePageMap } from './pageMap.js';
14
14
  export { POLICIES_INDEX_URL, POLICY_DOCUMENT_LABELS, POLICY_LANGUAGES, POLICY_LANGUAGE_NAMES, POLICY_SITE_URL, POLICY_SOURCE, Policy, PolicyContent, PolicyDocumentLabels, PolicyLanguage, PolicySection, PolicySlug, footerPolicies, formatPolicyDate, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, policyLanguageFrom, publishedPolicies } from './policies.js';
15
15
  export { PREFERENCE_DAYS, PreferenceExpiry, expireStoredPreferences } from './preferences.js';
16
16
  export { baseFooterLinks, baseNavLinks } from './utils.js';
package/dist/nextra.d.cts CHANGED
@@ -1,10 +1,11 @@
1
- export { DocsLayout, DocsLayoutProps } from './components/DocsLayout.cjs';
1
+ export { DocsLayout, DocsLayoutProps, NextraDocsLayout, NextraDocsLayoutProps } from './components/DocsLayout.cjs';
2
2
  export { LocalizedPage, LocalizedPageProps, LocalizedPageVariant } from './components/LocalizedPage.cjs';
3
3
  export { DocsI18nProvider, useDocsI18n } from './docsI18n.cjs';
4
4
  import 'react';
5
5
  import 'nextra';
6
- import './locale.cjs';
7
- import './components/NavBar.cjs';
6
+ import 'nextra-theme-docs';
8
7
  import './docsLabels.cjs';
8
+ import './locale.cjs';
9
9
  import './policies.cjs';
10
+ import './components/NavBar.cjs';
10
11
  import './i18n.cjs';
package/dist/nextra.d.ts CHANGED
@@ -1,10 +1,11 @@
1
- export { DocsLayout, DocsLayoutProps } from './components/DocsLayout.js';
1
+ export { DocsLayout, DocsLayoutProps, NextraDocsLayout, NextraDocsLayoutProps } from './components/DocsLayout.js';
2
2
  export { LocalizedPage, LocalizedPageProps, LocalizedPageVariant } from './components/LocalizedPage.js';
3
3
  export { DocsI18nProvider, useDocsI18n } from './docsI18n.js';
4
4
  import 'react';
5
5
  import 'nextra';
6
- import './locale.js';
7
- import './components/NavBar.js';
6
+ import 'nextra-theme-docs';
8
7
  import './docsLabels.js';
8
+ import './locale.js';
9
9
  import './policies.js';
10
+ import './components/NavBar.js';
10
11
  import './i18n.js';
package/dist/pageMap.cjs CHANGED
@@ -21,34 +21,45 @@ __export(pageMap_exports, {
21
21
  localizePageMap: () => localizePageMap
22
22
  });
23
23
  module.exports = __toCommonJS(pageMap_exports);
24
- function localizeMetaValue(value, titles) {
25
- if (typeof value === "string") return titles[value] ?? value;
26
- if (value && typeof value === "object" && typeof value.title === "string") {
27
- const translated = titles[value.title];
28
- return translated ? { ...value, title: translated } : value;
29
- }
30
- return value;
24
+ function localizeMetaValue(value, routeTitle, titles) {
25
+ if (typeof value === "string") return routeTitle ?? titles[value] ?? value;
26
+ if (!value || typeof value !== "object" || !("title" in value)) return value;
27
+ const title = value.title;
28
+ const translated = routeTitle ?? (typeof title === "string" ? titles[title] : void 0);
29
+ return translated ? { ...value, title: translated } : value;
31
30
  }
32
- function localizePageMap(pageMap, titles) {
33
- if (Object.keys(titles).length === 0) return pageMap;
34
- return pageMap.map((item) => {
31
+ function localizeItems(items, titles, routes, parent) {
32
+ const routeTitle = (name) => routes[parent ? `${parent}/${name}` : name] ?? routes[name];
33
+ return items.map((item) => {
35
34
  if ("data" in item) {
36
35
  const data = Object.fromEntries(
37
- Object.entries(item.data).map(([key, value]) => [key, localizeMetaValue(value, titles)])
36
+ Object.entries(item.data).map(([key, value]) => [key, localizeMetaValue(value, routeTitle(key), titles)])
38
37
  );
39
38
  return { ...item, data };
40
39
  }
41
40
  const copy = { ...item };
41
+ const name = item.name;
42
+ const route = typeof name === "string" ? parent ? `${parent}/${name}` : name : parent;
43
+ const byRoute = typeof name === "string" ? routeTitle(name) : void 0;
42
44
  const title = item.title;
43
- if (typeof title === "string" && titles[title]) copy.title = titles[title];
44
- if ("frontMatter" in item && item.frontMatter && typeof item.frontMatter.title === "string") {
45
- const title2 = titles[item.frontMatter.title];
46
- if (title2) copy.frontMatter = { ...item.frontMatter, title: title2 };
45
+ if (byRoute) copy.title = byRoute;
46
+ else if (typeof title === "string" && titles[title]) copy.title = titles[title];
47
+ if ("frontMatter" in item && item.frontMatter) {
48
+ const frontTitle = item.frontMatter.title;
49
+ const translated = byRoute ?? (typeof frontTitle === "string" ? titles[frontTitle] : void 0);
50
+ if (translated) copy.frontMatter = { ...item.frontMatter, title: translated };
51
+ }
52
+ if ("children" in item && Array.isArray(item.children)) {
53
+ copy.children = localizeItems(item.children, titles, routes, route);
47
54
  }
48
- if ("children" in item && Array.isArray(item.children)) copy.children = localizePageMap(item.children, titles);
49
55
  return copy;
50
56
  });
51
57
  }
58
+ function localizePageMap(pageMap, titles, options = {}) {
59
+ const routes = options.routes ?? {};
60
+ if (Object.keys(titles).length === 0 && Object.keys(routes).length === 0) return pageMap;
61
+ return localizeItems(pageMap, titles, routes);
62
+ }
52
63
  // Annotate the CommonJS export names for ESM import in node:
53
64
  0 && (module.exports = {
54
65
  localizePageMap
@@ -1,20 +1,33 @@
1
1
  import { PageMapItem } from 'nextra';
2
2
 
3
+ /** Options of {@link localizePageMap}. */
4
+ interface LocalizePageMapOptions {
5
+ /**
6
+ * Translated titles by route instead of English title: `<folder>/<name>` (for example `guides/full-stack-react`),
7
+ * then the bare `<name>` (`guides`, `index`). A route title wins over an English title and also replaces the front
8
+ * matter `title`. The docs hub keys its sidebar this way.
9
+ */
10
+ routes?: Readonly<Record<string, string>>;
11
+ }
3
12
  /**
4
13
  * Translates the titles of a Nextra page map: the sidebar, the top-level menu, the breadcrumbs and the previous / next
5
- * links all read them. Titles are matched by their English text (the `_meta` titles, the `title` of each item and the
6
- * front matter `title`), so a site lists each English title once, whatever the page; titles without a translation,
7
- * such as package names, stay as they are. The page map is not modified: changed items are copied.
14
+ * links all read them. The project sites match titles by their English text (the `_meta` titles, the `title` of each
15
+ * item and the front matter `title`), so a site lists each English title once, whatever the page; titles without a
16
+ * translation, such as package names, stay as they are. The docs hub matches them by route instead
17
+ * (`options.routes`). The page map is not modified: changed items are copied.
8
18
  *
9
19
  * @example
10
20
  * ```ts
11
21
  * localizePageMap(pageMap, { Introduction: 'Вступ', Packages: 'Пакети' });
22
+ * localizePageMap(pageMap, {}, { routes: { guides: 'Посібники', 'guides/starter-templates': 'Стартові шаблони' } });
12
23
  * ```
13
24
  *
14
25
  * @param pageMap - The page map of the site, the result of `getPageMap()` of `nextra/page-map`.
15
26
  * @param titles - Translated titles by English title.
16
- * @returns The page map with translated titles.
27
+ * @param options - See {@link LocalizePageMapOptions}.
28
+ * @returns The page map with translated titles, or `pageMap` itself when there is nothing to translate.
17
29
  */
18
- declare function localizePageMap(pageMap: PageMapItem[], titles: Readonly<Record<string, string>>): PageMapItem[];
30
+ declare function localizePageMap(pageMap: PageMapItem[], titles: Readonly<Record<string, string>>, options?: LocalizePageMapOptions): PageMapItem[];
19
31
 
20
32
  export { localizePageMap };
33
+ export type { LocalizePageMapOptions };
package/dist/pageMap.d.ts CHANGED
@@ -1,20 +1,33 @@
1
1
  import { PageMapItem } from 'nextra';
2
2
 
3
+ /** Options of {@link localizePageMap}. */
4
+ interface LocalizePageMapOptions {
5
+ /**
6
+ * Translated titles by route instead of English title: `<folder>/<name>` (for example `guides/full-stack-react`),
7
+ * then the bare `<name>` (`guides`, `index`). A route title wins over an English title and also replaces the front
8
+ * matter `title`. The docs hub keys its sidebar this way.
9
+ */
10
+ routes?: Readonly<Record<string, string>>;
11
+ }
3
12
  /**
4
13
  * Translates the titles of a Nextra page map: the sidebar, the top-level menu, the breadcrumbs and the previous / next
5
- * links all read them. Titles are matched by their English text (the `_meta` titles, the `title` of each item and the
6
- * front matter `title`), so a site lists each English title once, whatever the page; titles without a translation,
7
- * such as package names, stay as they are. The page map is not modified: changed items are copied.
14
+ * links all read them. The project sites match titles by their English text (the `_meta` titles, the `title` of each
15
+ * item and the front matter `title`), so a site lists each English title once, whatever the page; titles without a
16
+ * translation, such as package names, stay as they are. The docs hub matches them by route instead
17
+ * (`options.routes`). The page map is not modified: changed items are copied.
8
18
  *
9
19
  * @example
10
20
  * ```ts
11
21
  * localizePageMap(pageMap, { Introduction: 'Вступ', Packages: 'Пакети' });
22
+ * localizePageMap(pageMap, {}, { routes: { guides: 'Посібники', 'guides/starter-templates': 'Стартові шаблони' } });
12
23
  * ```
13
24
  *
14
25
  * @param pageMap - The page map of the site, the result of `getPageMap()` of `nextra/page-map`.
15
26
  * @param titles - Translated titles by English title.
16
- * @returns The page map with translated titles.
27
+ * @param options - See {@link LocalizePageMapOptions}.
28
+ * @returns The page map with translated titles, or `pageMap` itself when there is nothing to translate.
17
29
  */
18
- declare function localizePageMap(pageMap: PageMapItem[], titles: Readonly<Record<string, string>>): PageMapItem[];
30
+ declare function localizePageMap(pageMap: PageMapItem[], titles: Readonly<Record<string, string>>, options?: LocalizePageMapOptions): PageMapItem[];
19
31
 
20
32
  export { localizePageMap };
33
+ export type { LocalizePageMapOptions };
package/dist/pageMap.js CHANGED
@@ -1,31 +1,42 @@
1
- function localizeMetaValue(value, titles) {
2
- if (typeof value === "string") return titles[value] ?? value;
3
- if (value && typeof value === "object" && typeof value.title === "string") {
4
- const translated = titles[value.title];
5
- return translated ? { ...value, title: translated } : value;
6
- }
7
- return value;
1
+ function localizeMetaValue(value, routeTitle, titles) {
2
+ if (typeof value === "string") return routeTitle ?? titles[value] ?? value;
3
+ if (!value || typeof value !== "object" || !("title" in value)) return value;
4
+ const title = value.title;
5
+ const translated = routeTitle ?? (typeof title === "string" ? titles[title] : void 0);
6
+ return translated ? { ...value, title: translated } : value;
8
7
  }
9
- function localizePageMap(pageMap, titles) {
10
- if (Object.keys(titles).length === 0) return pageMap;
11
- return pageMap.map((item) => {
8
+ function localizeItems(items, titles, routes, parent) {
9
+ const routeTitle = (name) => routes[parent ? `${parent}/${name}` : name] ?? routes[name];
10
+ return items.map((item) => {
12
11
  if ("data" in item) {
13
12
  const data = Object.fromEntries(
14
- Object.entries(item.data).map(([key, value]) => [key, localizeMetaValue(value, titles)])
13
+ Object.entries(item.data).map(([key, value]) => [key, localizeMetaValue(value, routeTitle(key), titles)])
15
14
  );
16
15
  return { ...item, data };
17
16
  }
18
17
  const copy = { ...item };
18
+ const name = item.name;
19
+ const route = typeof name === "string" ? parent ? `${parent}/${name}` : name : parent;
20
+ const byRoute = typeof name === "string" ? routeTitle(name) : void 0;
19
21
  const title = item.title;
20
- if (typeof title === "string" && titles[title]) copy.title = titles[title];
21
- if ("frontMatter" in item && item.frontMatter && typeof item.frontMatter.title === "string") {
22
- const title2 = titles[item.frontMatter.title];
23
- if (title2) copy.frontMatter = { ...item.frontMatter, title: title2 };
22
+ if (byRoute) copy.title = byRoute;
23
+ else if (typeof title === "string" && titles[title]) copy.title = titles[title];
24
+ if ("frontMatter" in item && item.frontMatter) {
25
+ const frontTitle = item.frontMatter.title;
26
+ const translated = byRoute ?? (typeof frontTitle === "string" ? titles[frontTitle] : void 0);
27
+ if (translated) copy.frontMatter = { ...item.frontMatter, title: translated };
28
+ }
29
+ if ("children" in item && Array.isArray(item.children)) {
30
+ copy.children = localizeItems(item.children, titles, routes, route);
24
31
  }
25
- if ("children" in item && Array.isArray(item.children)) copy.children = localizePageMap(item.children, titles);
26
32
  return copy;
27
33
  });
28
34
  }
35
+ function localizePageMap(pageMap, titles, options = {}) {
36
+ const routes = options.routes ?? {};
37
+ if (Object.keys(titles).length === 0 && Object.keys(routes).length === 0) return pageMap;
38
+ return localizeItems(pageMap, titles, routes);
39
+ }
29
40
  export {
30
41
  localizePageMap
31
42
  };
@@ -149,6 +149,11 @@ async function checkSiteTranslations(options = {}) {
149
149
  throw new Error(`No ${contentDir} in ${cwd}: run the check from the docs app of the site`);
150
150
  }
151
151
  const createSlugger = options.createSlugger ?? await loadNextraSlugger(cwd);
152
+ if (!createSlugger) {
153
+ throw new Error(
154
+ `Cannot load github-slugger through nextra from ${cwd}: the heading ids of the translations cannot be checked`
155
+ );
156
+ }
152
157
  const locales = import_locale.TUWA_LOCALES.filter((locale) => locale !== import_locale.DEFAULT_LOCALE);
153
158
  const filesByLocale = new Map(locales.map((locale) => [locale, pageFiles((0, import_node_path.join)(cwd, i18nDir, locale))]));
154
159
  const pages = [...new Set([...filesByLocale.values()].flat().map(withoutExtension))].sort();
@@ -68,7 +68,7 @@ interface CheckSiteTranslationsOptions {
68
68
  contentDir?: string;
69
69
  /** The translations, one folder per language, relative to `cwd` (default: `src/i18n`). */
70
70
  i18nDir?: string;
71
- /** The heading slugger (default: {@link loadNextraSlugger}). */
71
+ /** The heading slugger (default: {@link loadNextraSlugger}; the check fails when it cannot be loaded). */
72
72
  createSlugger?: HeadingSlugger;
73
73
  }
74
74
  /**
@@ -78,7 +78,8 @@ interface CheckSiteTranslationsOptions {
78
78
  *
79
79
  * @param options - See {@link CheckSiteTranslationsOptions}.
80
80
  * @returns The number of translated files and the issues found.
81
- * @throws When `contentDir` does not exist (the check runs from the wrong directory).
81
+ * @throws When `contentDir` does not exist (the check runs from the wrong directory), or when no `createSlugger` is
82
+ * given and the slugger of Nextra cannot be loaded from `cwd` (the heading ids would go unchecked).
82
83
  */
83
84
  declare function checkSiteTranslations(options?: CheckSiteTranslationsOptions): Promise<{
84
85
  files: number;
@@ -68,7 +68,7 @@ interface CheckSiteTranslationsOptions {
68
68
  contentDir?: string;
69
69
  /** The translations, one folder per language, relative to `cwd` (default: `src/i18n`). */
70
70
  i18nDir?: string;
71
- /** The heading slugger (default: {@link loadNextraSlugger}). */
71
+ /** The heading slugger (default: {@link loadNextraSlugger}; the check fails when it cannot be loaded). */
72
72
  createSlugger?: HeadingSlugger;
73
73
  }
74
74
  /**
@@ -78,7 +78,8 @@ interface CheckSiteTranslationsOptions {
78
78
  *
79
79
  * @param options - See {@link CheckSiteTranslationsOptions}.
80
80
  * @returns The number of translated files and the issues found.
81
- * @throws When `contentDir` does not exist (the check runs from the wrong directory).
81
+ * @throws When `contentDir` does not exist (the check runs from the wrong directory), or when no `createSlugger` is
82
+ * given and the slugger of Nextra cannot be loaded from `cwd` (the heading ids would go unchecked).
82
83
  */
83
84
  declare function checkSiteTranslations(options?: CheckSiteTranslationsOptions): Promise<{
84
85
  files: number;
@@ -121,6 +121,11 @@ async function checkSiteTranslations(options = {}) {
121
121
  throw new Error(`No ${contentDir} in ${cwd}: run the check from the docs app of the site`);
122
122
  }
123
123
  const createSlugger = options.createSlugger ?? await loadNextraSlugger(cwd);
124
+ if (!createSlugger) {
125
+ throw new Error(
126
+ `Cannot load github-slugger through nextra from ${cwd}: the heading ids of the translations cannot be checked`
127
+ );
128
+ }
124
129
  const locales = TUWA_LOCALES.filter((locale) => locale !== DEFAULT_LOCALE);
125
130
  const filesByLocale = new Map(locales.map((locale) => [locale, pageFiles(join(cwd, i18nDir, locale))]));
126
131
  const pages = [...new Set([...filesByLocale.values()].flat().map(withoutExtension))].sort();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuwaio/docs-ui",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "author": "Oleksandr Tkach",
6
6
  "license": "Apache-2.0",