@tuwaio/docs-ui 0.4.1 → 0.5.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
@@ -14,7 +14,7 @@
14
14
  - **Theme switch:** `ThemeSwitcher` is a light/dark switch for any theme state (for example `next-themes`).
15
15
  - **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).
16
16
  - **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).
17
- - **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, and `PolicyDocument` renders it with the date of the version, a Download PDF button, anchored sections 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.
17
+ - **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.
18
18
  - **Search index:** the `tuwa-docs-search-index` command builds the [Pagefind](https://pagefind.app) index that the Nextra search loads from `/_pagefind`, from the pages `next build` prerendered. It reads both places Next.js keeps them: `.next/server/app` and, in builds with a deployment adapter (Next.js 16.3.8 and later on Vercel), `.next/server/route-cache`, where `pagefind --site .next/server/app` finds no pages.
19
19
  - **Stylesheet:** `dist/index.css` contains the `--tuwa-*` theme variables of [`@tuwaio/nova-core`](https://stories.tuwa.io/?path=/docs/packages-nova-core-overview--docs) (light and `.dark`), the styles of the Nextra parts and the Tailwind CSS v4 utilities used by the components, prefixed with `tuwadocs:`.
20
20
 
@@ -85,13 +85,13 @@ export default async function RootLayout({ children }: { children: ReactNode })
85
85
 
86
86
  ### Policy pages
87
87
 
88
- A policy page in the Next.js App Router (a Server Component; the content is cached for an hour):
88
+ A policy page in the Next.js App Router (a Server Component; the content is cached for an hour). The language comes from `?lang=` (`en` by default, `uk` for Ukrainian); a policy without a translation loads in English, and the switch links each language with `?lang=<language>`:
89
89
 
90
90
  ```tsx
91
- import { loadPolicy, PolicyDocument } from '@tuwaio/docs-ui';
91
+ import { loadPolicy, PolicyDocument, policyLanguageFrom } from '@tuwaio/docs-ui';
92
92
 
93
- export default async function RefundPage() {
94
- const content = await loadPolicy('refund');
93
+ export default async function RefundPage({ searchParams }: { searchParams: Promise<{ lang?: string | string[] }> }) {
94
+ const content = await loadPolicy('refund', { language: policyLanguageFrom((await searchParams).lang) });
95
95
 
96
96
  return (
97
97
  <main className="mx-auto max-w-6xl px-4 py-12">
@@ -147,7 +147,7 @@ It writes the index to `public/_pagefind` (keep that directory in `.gitignore`)
147
147
 
148
148
  ## 🌐 External Services
149
149
 
150
- `RemoteLogo` requests its `url` on the server (by default `cdn.jsdelivr.net/gh/TuwaIO/workflows@main/preview/logo_v2.svg`), cached by Next.js for 24 hours. `loadPolicy` requests `raw.githubusercontent.com/TuwaIO/workflows/main/docs/<slug>.json` 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.
150
+ `RemoteLogo` requests its `url` on the server (by default `cdn.jsdelivr.net/gh/TuwaIO/workflows@main/preview/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.
151
151
 
152
152
  ---
153
153
 
@@ -73,13 +73,18 @@ function PolicyDocument({
73
73
  content,
74
74
  source = import_policies.POLICY_SOURCE,
75
75
  labels,
76
+ languageHref = (language) => `?lang=${language}`,
76
77
  className,
77
78
  classNames
78
79
  }) {
79
- const tocLabel = labels?.toc ?? "On this page";
80
+ const language = content.language ?? "en";
81
+ const languages = content.languages ?? ["en"];
82
+ const texts = { ...import_policies.POLICY_DOCUMENT_LABELS[language], ...labels };
83
+ const tocLabel = texts.toc;
80
84
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
81
85
  "div",
82
86
  {
87
+ lang: language,
83
88
  className: (0, import_utils.cn)(
84
89
  "tuwadocs:grid tuwadocs:grid-cols-1 tuwadocs:gap-10 tuwadocs:lg:grid-cols-[minmax(0,1fr)_14rem]",
85
90
  className
@@ -106,26 +111,53 @@ function PolicyDocument({
106
111
  ),
107
112
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:justify-between tuwadocs:gap-3", children: [
108
113
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: (0, import_utils.cn)("tuwadocs:text-sm tuwadocs:text-[var(--tuwa-text-secondary)]", classNames?.updated), children: [
109
- labels?.updated ?? "Last updated",
114
+ texts.updated,
110
115
  " ",
111
116
  content.updated
112
117
  ] }),
113
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
114
- "a",
115
- {
116
- href: (0, import_policies.getPolicyFileUrl)(slug, "pdf", source),
117
- target: "_blank",
118
- rel: "noopener noreferrer",
119
- className: (0, import_utils.cn)(
120
- "tuwadocs-policy-title tuwadocs:inline-flex tuwadocs:h-10 tuwadocs:no-underline tuwadocs:items-center tuwadocs:gap-2 tuwadocs:rounded-[var(--tuwa-rounded-corners)] tuwadocs:border tuwadocs:border-[var(--tuwa-border-primary)] tuwadocs:bg-[var(--tuwa-bg-primary)] tuwadocs:px-4 tuwadocs:text-sm tuwadocs:font-bold tuwadocs:text-[var(--tuwa-text-primary)] tuwadocs:transition-colors tuwadocs:hover:bg-[var(--tuwa-bg-muted)]",
121
- classNames?.download
122
- ),
123
- children: [
124
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_outline.ArrowDownTrayIcon, { className: "tuwadocs:h-4 tuwadocs:w-4", "aria-hidden": "true" }),
125
- labels?.download ?? "Download PDF"
126
- ]
127
- }
128
- )
118
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:gap-2", children: [
119
+ languages.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
120
+ "nav",
121
+ {
122
+ "aria-label": texts.language,
123
+ className: (0, import_utils.cn)(
124
+ "tuwadocs:inline-flex tuwadocs:rounded-[var(--tuwa-rounded-corners)] tuwadocs:border tuwadocs:border-[var(--tuwa-border-primary)] tuwadocs:p-0.5",
125
+ classNames?.languages
126
+ ),
127
+ children: languages.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
128
+ "a",
129
+ {
130
+ href: languageHref(item),
131
+ hrefLang: item,
132
+ lang: item,
133
+ "aria-current": item === language ? "page" : void 0,
134
+ className: (0, import_utils.cn)(
135
+ "tuwadocs:rounded-[var(--tuwa-rounded-corners)] tuwadocs:px-3 tuwadocs:py-1.5 tuwadocs:text-xs tuwadocs:font-bold tuwadocs:no-underline tuwadocs:transition-colors",
136
+ item === language ? "tuwadocs:bg-[var(--tuwa-bg-muted)] tuwadocs:text-[var(--tuwa-text-primary)]" : "tuwadocs:text-[var(--tuwa-text-secondary)] tuwadocs:hover:text-[var(--tuwa-text-primary)]"
137
+ ),
138
+ children: import_policies.POLICY_LANGUAGE_NAMES[item]
139
+ },
140
+ item
141
+ ))
142
+ }
143
+ ),
144
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
145
+ "a",
146
+ {
147
+ href: (0, import_policies.getPolicyFileUrl)(slug, "pdf", source, language),
148
+ target: "_blank",
149
+ rel: "noopener noreferrer",
150
+ className: (0, import_utils.cn)(
151
+ "tuwadocs-policy-title tuwadocs:inline-flex tuwadocs:h-10 tuwadocs:no-underline tuwadocs:items-center tuwadocs:gap-2 tuwadocs:rounded-[var(--tuwa-rounded-corners)] tuwadocs:border tuwadocs:border-[var(--tuwa-border-primary)] tuwadocs:bg-[var(--tuwa-bg-primary)] tuwadocs:px-4 tuwadocs:text-sm tuwadocs:font-bold tuwadocs:text-[var(--tuwa-text-primary)] tuwadocs:transition-colors tuwadocs:hover:bg-[var(--tuwa-bg-muted)]",
152
+ classNames?.download
153
+ ),
154
+ children: [
155
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_outline.ArrowDownTrayIcon, { className: "tuwadocs:h-4 tuwadocs:w-4", "aria-hidden": "true" }),
156
+ texts.download
157
+ ]
158
+ }
159
+ )
160
+ ] })
129
161
  ] })
130
162
  ]
131
163
  }
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { PolicySlug, PolicyContent } from '../policies.cjs';
2
+ import { PolicySlug, PolicyContent, PolicyDocumentLabels, PolicyLanguage } from '../policies.cjs';
3
3
 
4
4
  /**
5
5
  * Props of {@link PolicyDocument}.
@@ -11,15 +11,13 @@ interface PolicyDocumentProps {
11
11
  content: PolicyContent;
12
12
  /** Base URL of the policy files for the PDF link (default: `POLICY_SOURCE`). */
13
13
  source?: string;
14
- /** Texts of the page. */
15
- labels?: {
16
- /** Text before the date of the version (default: `Last updated`). */
17
- updated?: string;
18
- /** Text of the button that opens the PDF (default: `Download PDF`). */
19
- download?: string;
20
- /** Heading and accessible name of the list of sections (default: `On this page`). */
21
- toc?: string;
22
- };
14
+ /** Texts of the page; the defaults are those of the language of `content` ({@link POLICY_DOCUMENT_LABELS}). */
15
+ labels?: PolicyDocumentLabels;
16
+ /**
17
+ * Link of each choice of the language switch (default: `?lang=<language>`, the page itself in that language). The
18
+ * switch shows when `content.languages` has more than one language.
19
+ */
20
+ languageHref?: (language: PolicyLanguage) => string;
23
21
  /** Classes of the root element. */
24
22
  className?: string;
25
23
  /** Classes of the parts of the page. */
@@ -36,12 +34,16 @@ interface PolicyDocumentProps {
36
34
  body?: string;
37
35
  /** The list of sections (shown from the `lg` breakpoint). */
38
36
  toc?: string;
37
+ /** The language switch. */
38
+ languages?: string;
39
39
  };
40
40
  }
41
41
  /**
42
42
  * A TUWA policy page (Terms, Privacy, Cookie, Refund, Security): the title, the date of the version, a Download PDF
43
- * button with the official document, the text with anchored sections and, from the `lg` breakpoint, a sticky list of
44
- * the sections. The text is selectable and readable on phones, by search engines and by screen readers; the PDF is
43
+ * button with the official document in the language of the text, a language switch when the policy exists in more
44
+ * than one language, the text with anchored sections and, from the `lg` breakpoint, a sticky list of the sections.
45
+ * The sections keep their anchors in every language, and the page marks its language (`lang`) for screen readers and
46
+ * search engines. The text is selectable and readable on phones, by search engines and by screen readers; the PDF is
45
47
  * generated from the same source.
46
48
  *
47
49
  * Load the content on the server with `loadPolicy` and render the page inside the layout of the site. The body is
@@ -52,7 +54,7 @@ interface PolicyDocumentProps {
52
54
  * @param props - See {@link PolicyDocumentProps}.
53
55
  * @returns The policy page.
54
56
  */
55
- declare function PolicyDocument({ slug, content, source, labels, className, classNames, }: PolicyDocumentProps): React.JSX.Element;
57
+ declare function PolicyDocument({ slug, content, source, labels, languageHref, className, classNames, }: PolicyDocumentProps): React.JSX.Element;
56
58
 
57
59
  export { PolicyDocument };
58
60
  export type { PolicyDocumentProps };
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { PolicySlug, PolicyContent } from '../policies.js';
2
+ import { PolicySlug, PolicyContent, PolicyDocumentLabels, PolicyLanguage } from '../policies.js';
3
3
 
4
4
  /**
5
5
  * Props of {@link PolicyDocument}.
@@ -11,15 +11,13 @@ interface PolicyDocumentProps {
11
11
  content: PolicyContent;
12
12
  /** Base URL of the policy files for the PDF link (default: `POLICY_SOURCE`). */
13
13
  source?: string;
14
- /** Texts of the page. */
15
- labels?: {
16
- /** Text before the date of the version (default: `Last updated`). */
17
- updated?: string;
18
- /** Text of the button that opens the PDF (default: `Download PDF`). */
19
- download?: string;
20
- /** Heading and accessible name of the list of sections (default: `On this page`). */
21
- toc?: string;
22
- };
14
+ /** Texts of the page; the defaults are those of the language of `content` ({@link POLICY_DOCUMENT_LABELS}). */
15
+ labels?: PolicyDocumentLabels;
16
+ /**
17
+ * Link of each choice of the language switch (default: `?lang=<language>`, the page itself in that language). The
18
+ * switch shows when `content.languages` has more than one language.
19
+ */
20
+ languageHref?: (language: PolicyLanguage) => string;
23
21
  /** Classes of the root element. */
24
22
  className?: string;
25
23
  /** Classes of the parts of the page. */
@@ -36,12 +34,16 @@ interface PolicyDocumentProps {
36
34
  body?: string;
37
35
  /** The list of sections (shown from the `lg` breakpoint). */
38
36
  toc?: string;
37
+ /** The language switch. */
38
+ languages?: string;
39
39
  };
40
40
  }
41
41
  /**
42
42
  * A TUWA policy page (Terms, Privacy, Cookie, Refund, Security): the title, the date of the version, a Download PDF
43
- * button with the official document, the text with anchored sections and, from the `lg` breakpoint, a sticky list of
44
- * the sections. The text is selectable and readable on phones, by search engines and by screen readers; the PDF is
43
+ * button with the official document in the language of the text, a language switch when the policy exists in more
44
+ * than one language, the text with anchored sections and, from the `lg` breakpoint, a sticky list of the sections.
45
+ * The sections keep their anchors in every language, and the page marks its language (`lang`) for screen readers and
46
+ * search engines. The text is selectable and readable on phones, by search engines and by screen readers; the PDF is
45
47
  * generated from the same source.
46
48
  *
47
49
  * Load the content on the server with `loadPolicy` and render the page inside the layout of the site. The body is
@@ -52,7 +54,7 @@ interface PolicyDocumentProps {
52
54
  * @param props - See {@link PolicyDocumentProps}.
53
55
  * @returns The policy page.
54
56
  */
55
- declare function PolicyDocument({ slug, content, source, labels, className, classNames, }: PolicyDocumentProps): React.JSX.Element;
57
+ declare function PolicyDocument({ slug, content, source, labels, languageHref, className, classNames, }: PolicyDocumentProps): React.JSX.Element;
56
58
 
57
59
  export { PolicyDocument };
58
60
  export type { PolicyDocumentProps };
@@ -2,7 +2,12 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { ArrowDownTrayIcon } from "@heroicons/react/24/outline";
3
3
  import { cn } from "@tuwaio/nova-core/utils";
4
4
  import parse, { Element } from "html-react-parser";
5
- import { getPolicyFileUrl, POLICY_SOURCE } from "../policies";
5
+ import {
6
+ getPolicyFileUrl,
7
+ POLICY_DOCUMENT_LABELS,
8
+ POLICY_LANGUAGE_NAMES,
9
+ POLICY_SOURCE
10
+ } from "../policies";
6
11
  const BLOCKED_ELEMENTS = /* @__PURE__ */ new Set([
7
12
  "script",
8
13
  "style",
@@ -40,13 +45,18 @@ function PolicyDocument({
40
45
  content,
41
46
  source = POLICY_SOURCE,
42
47
  labels,
48
+ languageHref = (language) => `?lang=${language}`,
43
49
  className,
44
50
  classNames
45
51
  }) {
46
- const tocLabel = labels?.toc ?? "On this page";
52
+ const language = content.language ?? "en";
53
+ const languages = content.languages ?? ["en"];
54
+ const texts = { ...POLICY_DOCUMENT_LABELS[language], ...labels };
55
+ const tocLabel = texts.toc;
47
56
  return /* @__PURE__ */ jsxs(
48
57
  "div",
49
58
  {
59
+ lang: language,
50
60
  className: cn(
51
61
  "tuwadocs:grid tuwadocs:grid-cols-1 tuwadocs:gap-10 tuwadocs:lg:grid-cols-[minmax(0,1fr)_14rem]",
52
62
  className
@@ -73,26 +83,53 @@ function PolicyDocument({
73
83
  ),
74
84
  /* @__PURE__ */ jsxs("div", { className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:justify-between tuwadocs:gap-3", children: [
75
85
  /* @__PURE__ */ jsxs("p", { className: cn("tuwadocs:text-sm tuwadocs:text-[var(--tuwa-text-secondary)]", classNames?.updated), children: [
76
- labels?.updated ?? "Last updated",
86
+ texts.updated,
77
87
  " ",
78
88
  content.updated
79
89
  ] }),
80
- /* @__PURE__ */ jsxs(
81
- "a",
82
- {
83
- href: getPolicyFileUrl(slug, "pdf", source),
84
- target: "_blank",
85
- rel: "noopener noreferrer",
86
- className: cn(
87
- "tuwadocs-policy-title tuwadocs:inline-flex tuwadocs:h-10 tuwadocs:no-underline tuwadocs:items-center tuwadocs:gap-2 tuwadocs:rounded-[var(--tuwa-rounded-corners)] tuwadocs:border tuwadocs:border-[var(--tuwa-border-primary)] tuwadocs:bg-[var(--tuwa-bg-primary)] tuwadocs:px-4 tuwadocs:text-sm tuwadocs:font-bold tuwadocs:text-[var(--tuwa-text-primary)] tuwadocs:transition-colors tuwadocs:hover:bg-[var(--tuwa-bg-muted)]",
88
- classNames?.download
89
- ),
90
- children: [
91
- /* @__PURE__ */ jsx(ArrowDownTrayIcon, { className: "tuwadocs:h-4 tuwadocs:w-4", "aria-hidden": "true" }),
92
- labels?.download ?? "Download PDF"
93
- ]
94
- }
95
- )
90
+ /* @__PURE__ */ jsxs("div", { className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:gap-2", children: [
91
+ languages.length > 1 && /* @__PURE__ */ jsx(
92
+ "nav",
93
+ {
94
+ "aria-label": texts.language,
95
+ className: cn(
96
+ "tuwadocs:inline-flex tuwadocs:rounded-[var(--tuwa-rounded-corners)] tuwadocs:border tuwadocs:border-[var(--tuwa-border-primary)] tuwadocs:p-0.5",
97
+ classNames?.languages
98
+ ),
99
+ children: languages.map((item) => /* @__PURE__ */ jsx(
100
+ "a",
101
+ {
102
+ href: languageHref(item),
103
+ hrefLang: item,
104
+ lang: item,
105
+ "aria-current": item === language ? "page" : void 0,
106
+ className: cn(
107
+ "tuwadocs:rounded-[var(--tuwa-rounded-corners)] tuwadocs:px-3 tuwadocs:py-1.5 tuwadocs:text-xs tuwadocs:font-bold tuwadocs:no-underline tuwadocs:transition-colors",
108
+ item === language ? "tuwadocs:bg-[var(--tuwa-bg-muted)] tuwadocs:text-[var(--tuwa-text-primary)]" : "tuwadocs:text-[var(--tuwa-text-secondary)] tuwadocs:hover:text-[var(--tuwa-text-primary)]"
109
+ ),
110
+ children: POLICY_LANGUAGE_NAMES[item]
111
+ },
112
+ item
113
+ ))
114
+ }
115
+ ),
116
+ /* @__PURE__ */ jsxs(
117
+ "a",
118
+ {
119
+ href: getPolicyFileUrl(slug, "pdf", source, language),
120
+ target: "_blank",
121
+ rel: "noopener noreferrer",
122
+ className: cn(
123
+ "tuwadocs-policy-title tuwadocs:inline-flex tuwadocs:h-10 tuwadocs:no-underline tuwadocs:items-center tuwadocs:gap-2 tuwadocs:rounded-[var(--tuwa-rounded-corners)] tuwadocs:border tuwadocs:border-[var(--tuwa-border-primary)] tuwadocs:bg-[var(--tuwa-bg-primary)] tuwadocs:px-4 tuwadocs:text-sm tuwadocs:font-bold tuwadocs:text-[var(--tuwa-text-primary)] tuwadocs:transition-colors tuwadocs:hover:bg-[var(--tuwa-bg-muted)]",
124
+ classNames?.download
125
+ ),
126
+ children: [
127
+ /* @__PURE__ */ jsx(ArrowDownTrayIcon, { className: "tuwadocs:h-4 tuwadocs:w-4", "aria-hidden": "true" }),
128
+ texts.download
129
+ ]
130
+ }
131
+ )
132
+ ] })
96
133
  ] })
97
134
  ]
98
135
  }
package/dist/index.css CHANGED
@@ -749,6 +749,9 @@ html {
749
749
  border-color: color-mix(in oklab, var(--tuwadocs-color-black) 5%, transparent);
750
750
  }
751
751
  }
752
+ .tuwadocs\:bg-\[var\(--tuwa-bg-muted\)\] {
753
+ background-color: var(--tuwa-bg-muted);
754
+ }
752
755
  .tuwadocs\:bg-\[var\(--tuwa-bg-primary\)\] {
753
756
  background-color: var(--tuwa-bg-primary);
754
757
  }
@@ -766,6 +769,9 @@ html {
766
769
  --tw-gradient-from: var(--tuwadocs-color-slate-200);
767
770
  --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
768
771
  }
772
+ .tuwadocs\:p-0\.5 {
773
+ padding: calc(var(--tuwadocs-spacing) * 0.5);
774
+ }
769
775
  .tuwadocs\:p-1 {
770
776
  padding: var(--tuwadocs-spacing);
771
777
  }
package/dist/index.d.cts CHANGED
@@ -6,7 +6,7 @@ export { Orb, OrbProps, PackageType } from './components/Orb.cjs';
6
6
  export { PolicyDocument, PolicyDocumentProps } from './components/PolicyDocument.cjs';
7
7
  export { default as StarryBackground } from './components/StarryBackground.cjs';
8
8
  export { ThemeSwitcher, ThemeSwitcherProps } from './components/ThemeSwitcher.cjs';
9
- export { POLICIES_INDEX_URL, POLICY_SITE_URL, POLICY_SOURCE, Policy, PolicyContent, PolicySection, PolicySlug, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, publishedPolicies } from './policies.cjs';
9
+ export { POLICIES_INDEX_URL, POLICY_DOCUMENT_LABELS, POLICY_LANGUAGES, POLICY_LANGUAGE_NAMES, POLICY_SITE_URL, POLICY_SOURCE, Policy, PolicyContent, PolicyDocumentLabels, PolicyLanguage, PolicySection, PolicySlug, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, policyLanguageFrom, publishedPolicies } from './policies.cjs';
10
10
  export { PREFERENCE_DAYS, PreferenceExpiry, expireStoredPreferences } from './preferences.cjs';
11
11
  export { baseFooterLinks, baseNavLinks } from './utils.cjs';
12
12
  import 'react';
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { Orb, OrbProps, PackageType } from './components/Orb.js';
6
6
  export { PolicyDocument, PolicyDocumentProps } from './components/PolicyDocument.js';
7
7
  export { default as StarryBackground } from './components/StarryBackground.js';
8
8
  export { ThemeSwitcher, ThemeSwitcherProps } from './components/ThemeSwitcher.js';
9
- export { POLICIES_INDEX_URL, POLICY_SITE_URL, POLICY_SOURCE, Policy, PolicyContent, PolicySection, PolicySlug, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, publishedPolicies } from './policies.js';
9
+ export { POLICIES_INDEX_URL, POLICY_DOCUMENT_LABELS, POLICY_LANGUAGES, POLICY_LANGUAGE_NAMES, POLICY_SITE_URL, POLICY_SOURCE, Policy, PolicyContent, PolicyDocumentLabels, PolicyLanguage, PolicySection, PolicySlug, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, policyLanguageFrom, publishedPolicies } from './policies.js';
10
10
  export { PREFERENCE_DAYS, PreferenceExpiry, expireStoredPreferences } from './preferences.js';
11
11
  export { baseFooterLinks, baseNavLinks } from './utils.js';
12
12
  import 'react';
package/dist/policies.cjs CHANGED
@@ -19,6 +19,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var policies_exports = {};
20
20
  __export(policies_exports, {
21
21
  POLICIES_INDEX_URL: () => POLICIES_INDEX_URL,
22
+ POLICY_DOCUMENT_LABELS: () => POLICY_DOCUMENT_LABELS,
23
+ POLICY_LANGUAGES: () => POLICY_LANGUAGES,
24
+ POLICY_LANGUAGE_NAMES: () => POLICY_LANGUAGE_NAMES,
22
25
  POLICY_SITE_URL: () => POLICY_SITE_URL,
23
26
  POLICY_SOURCE: () => POLICY_SOURCE,
24
27
  footerPolicies: () => footerPolicies,
@@ -26,9 +29,23 @@ __export(policies_exports, {
26
29
  getPolicyPageUrl: () => getPolicyPageUrl,
27
30
  loadPolicy: () => loadPolicy,
28
31
  policies: () => policies,
32
+ policyLanguageFrom: () => policyLanguageFrom,
29
33
  publishedPolicies: () => publishedPolicies
30
34
  });
31
35
  module.exports = __toCommonJS(policies_exports);
36
+ const POLICY_LANGUAGES = ["en", "uk"];
37
+ const POLICY_LANGUAGE_NAMES = {
38
+ en: "English",
39
+ uk: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430"
40
+ };
41
+ function policyLanguageFrom(value) {
42
+ const first = Array.isArray(value) ? value[0] : value;
43
+ return POLICY_LANGUAGES.find((language) => language === first) ?? "en";
44
+ }
45
+ const POLICY_DOCUMENT_LABELS = {
46
+ en: { updated: "Last updated", download: "Download PDF", toc: "On this page", language: "Language" },
47
+ uk: { updated: "\u041E\u0441\u0442\u0430\u043D\u043D\u0454 \u043E\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044F", download: "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 PDF", toc: "\u041D\u0430 \u0446\u0456\u0439 \u0441\u0442\u043E\u0440\u0456\u043D\u0446\u0456", language: "\u041C\u043E\u0432\u0430" }
48
+ };
32
49
  const POLICY_SOURCE = "https://raw.githubusercontent.com/TuwaIO/workflows/main/docs";
33
50
  const POLICY_SITE_URL = "https://tuwa.io";
34
51
  const POLICIES_INDEX_URL = "https://docs.tuwa.io/policies";
@@ -79,11 +96,11 @@ const policies = [
79
96
  ];
80
97
  const publishedPolicies = policies.filter((policy) => policy.published);
81
98
  const footerPolicies = publishedPolicies.filter((policy) => policy.footer !== false);
82
- function getPolicyPageUrl(slug, site = POLICY_SITE_URL) {
83
- return `${site.replace(/\/+$/, "")}/${slug}`;
99
+ function getPolicyPageUrl(slug, site = POLICY_SITE_URL, language = "en") {
100
+ return `${site.replace(/\/+$/, "")}/${slug}${language === "en" ? "" : `?lang=${language}`}`;
84
101
  }
85
- function getPolicyFileUrl(slug, format, source = POLICY_SOURCE) {
86
- return `${source.replace(/\/+$/, "")}/${slug}.${format}`;
102
+ function getPolicyFileUrl(slug, format, source = POLICY_SOURCE, language = "en") {
103
+ return `${source.replace(/\/+$/, "")}${language === "en" ? "" : `/${language}`}/${slug}.${format}`;
87
104
  }
88
105
  function isPolicyContent(value) {
89
106
  if (!value || typeof value !== "object") return false;
@@ -93,8 +110,12 @@ function isPolicyContent(value) {
93
110
  );
94
111
  }
95
112
  async function loadPolicy(slug, options = {}) {
96
- const url = getPolicyFileUrl(slug, "json", options.source);
113
+ const language = options.language ?? "en";
114
+ const url = getPolicyFileUrl(slug, "json", options.source, language);
97
115
  const response = await fetch(url, { next: { revalidate: options.revalidate ?? 3600 } });
116
+ if (response.status === 404 && language !== "en") {
117
+ return loadPolicy(slug, { ...options, language: "en" });
118
+ }
98
119
  if (!response.ok) {
99
120
  throw new Error(`loadPolicy: ${url} answered ${response.status}`);
100
121
  }
@@ -102,16 +123,23 @@ async function loadPolicy(slug, options = {}) {
102
123
  if (!isPolicyContent(content)) {
103
124
  throw new Error(`loadPolicy: ${url} is not a policy page`);
104
125
  }
126
+ const raw = content;
127
+ const languages = Array.isArray(raw.languages) ? POLICY_LANGUAGES.filter((item) => raw.languages.includes(item)) : [];
105
128
  return {
106
129
  title: content.title,
107
130
  updated: content.updated,
108
131
  sections: content.sections.map(({ id, title }) => ({ id, title })),
109
- html: content.html
132
+ html: content.html,
133
+ language: policyLanguageFrom(raw.language ?? language),
134
+ languages: languages.length > 0 ? languages : ["en"]
110
135
  };
111
136
  }
112
137
  // Annotate the CommonJS export names for ESM import in node:
113
138
  0 && (module.exports = {
114
139
  POLICIES_INDEX_URL,
140
+ POLICY_DOCUMENT_LABELS,
141
+ POLICY_LANGUAGES,
142
+ POLICY_LANGUAGE_NAMES,
115
143
  POLICY_SITE_URL,
116
144
  POLICY_SOURCE,
117
145
  footerPolicies,
@@ -119,5 +147,6 @@ async function loadPolicy(slug, options = {}) {
119
147
  getPolicyPageUrl,
120
148
  loadPolicy,
121
149
  policies,
150
+ policyLanguageFrom,
122
151
  publishedPolicies
123
152
  });
@@ -15,6 +15,35 @@ interface Policy {
15
15
  /** Whether footers link the policy (default: `true`); documents linked from other policies set `false`. */
16
16
  footer?: boolean;
17
17
  }
18
+ /**
19
+ * A language of the TUWA policies: English, the original, and the translations generated next to it in
20
+ * `TuwaIO/workflows/docs/<language>/`.
21
+ */
22
+ type PolicyLanguage = 'en' | 'uk';
23
+ /** The languages of the policies, the original first. */
24
+ declare const POLICY_LANGUAGES: readonly PolicyLanguage[];
25
+ /** The name of each language in that language, for language switches. */
26
+ declare const POLICY_LANGUAGE_NAMES: Readonly<Record<PolicyLanguage, string>>;
27
+ /**
28
+ * Reads the language of a policy page from a value such as the `lang` search parameter.
29
+ *
30
+ * @param value - The value: a string, an array of strings (the first counts) or nothing.
31
+ * @returns The language it names, or `en` when it names none of {@link POLICY_LANGUAGES}.
32
+ */
33
+ declare function policyLanguageFrom(value: unknown): PolicyLanguage;
34
+ /** Texts of a policy page. */
35
+ interface PolicyDocumentLabels {
36
+ /** Text before the date of the version (default in English: `Last updated`). */
37
+ updated?: string;
38
+ /** Text of the button that opens the PDF (default in English: `Download PDF`). */
39
+ download?: string;
40
+ /** Heading and accessible name of the list of sections (default in English: `On this page`). */
41
+ toc?: string;
42
+ /** Accessible name of the language switch (default in English: `Language`). */
43
+ language?: string;
44
+ }
45
+ /** The texts of `PolicyDocument` in each language of the policies; its `labels` override them. */
46
+ declare const POLICY_DOCUMENT_LABELS: Readonly<Record<PolicyLanguage, Required<PolicyDocumentLabels>>>;
18
47
  /** A section of a policy: a level-2 heading with its anchor. */
19
48
  interface PolicySection {
20
49
  /** Anchor of the heading, without `#` (stable when sections are renumbered). */
@@ -28,10 +57,14 @@ interface PolicyContent {
28
57
  title: string;
29
58
  /** Date of the current version as written in the document (`October 8, 2026`). */
30
59
  updated: string;
31
- /** Level-2 sections in document order. */
60
+ /** Level-2 sections in document order; a translation keeps the anchors of the English sections. */
32
61
  sections: PolicySection[];
33
62
  /** Body of the document as HTML, without the title and the date line; headings carry their anchors as `id`. */
34
63
  html: string;
64
+ /** Language of this text. */
65
+ language: PolicyLanguage;
66
+ /** Languages the policy exists in, the original first: the choices of the language switch. */
67
+ languages: PolicyLanguage[];
35
68
  }
36
69
  /** Where the policy files live: the `docs` folder of `TuwaIO/workflows` on GitHub, branch `main`. */
37
70
  declare const POLICY_SOURCE = "https://raw.githubusercontent.com/TuwaIO/workflows/main/docs";
@@ -53,37 +86,44 @@ declare const footerPolicies: readonly Policy[];
53
86
  *
54
87
  * @param slug - Slug of the policy.
55
88
  * @param site - Origin of the site with the policy pages (default: {@link POLICY_SITE_URL}).
56
- * @returns The URL of the page, for example `https://tuwa.io/terms`.
89
+ * @param language - Language of the page (default: `en`); a translation adds `?lang=<language>`.
90
+ * @returns The URL of the page, for example `https://tuwa.io/terms` or `https://tuwa.io/terms?lang=uk`.
57
91
  */
58
- declare function getPolicyPageUrl(slug: PolicySlug, site?: string): string;
92
+ declare function getPolicyPageUrl(slug: PolicySlug, site?: string, language?: PolicyLanguage): string;
59
93
  /**
60
94
  * Returns the URL of a policy file.
61
95
  *
62
96
  * @param slug - Slug of the policy.
63
97
  * @param format - `json` for the page content, `pdf` for the official document, `md` for the source text.
64
98
  * @param source - Base URL of the policy files (default: {@link POLICY_SOURCE}).
65
- * @returns The URL of the file.
99
+ * @param language - Language of the file (default: `en`); a translation lives in the folder of its language.
100
+ * @returns The URL of the file, for example `…/docs/terms.pdf` or `…/docs/uk/terms.pdf`.
66
101
  */
67
- declare function getPolicyFileUrl(slug: PolicySlug, format: 'json' | 'pdf' | 'md', source?: string): string;
102
+ declare function getPolicyFileUrl(slug: PolicySlug, format: 'json' | 'pdf' | 'md', source?: string, language?: PolicyLanguage): string;
68
103
  /**
69
- * Loads the content of a policy page (its `json` file). Call it on the server, for example in a Server Component of
70
- * the Next.js App Router, and pass the result to `PolicyDocument`.
104
+ * Loads the content of a policy page (its `json` file) in a language. Call it on the server, for example in a Server
105
+ * Component of the Next.js App Router, and pass the result to `PolicyDocument`. A policy not translated into the
106
+ * language loads in English, so a page asked for in that language still shows the policy.
71
107
  *
72
- * Network: one request to `getPolicyFileUrl(slug, 'json', source)` per revalidation, cached by the Next.js `fetch`
73
- * cache for `revalidate` seconds (outside Next.js, a plain request).
108
+ * Network: one request to `getPolicyFileUrl(slug, 'json', source, language)` per revalidation, and one more for the
109
+ * English file when the translation does not exist; cached by the Next.js `fetch` cache for `revalidate` seconds
110
+ * (outside Next.js, plain requests).
74
111
  *
75
112
  * @param slug - Slug of the policy.
76
113
  * @param options - Optional settings.
77
114
  * @param options.source - Base URL of the policy files (default: {@link POLICY_SOURCE}).
78
115
  * @param options.revalidate - Seconds the Next.js `fetch` cache keeps the content, or `false` to cache it until the
79
116
  * next deployment (default: 3600).
80
- * @returns The content of the policy.
81
- * @throws Error when the request fails, the response is not `2xx`, or the file is not a policy page.
117
+ * @param options.language - Language of the text (default: `en`).
118
+ * @returns The content of the policy, with its language and the languages it exists in.
119
+ * @throws Error when the request fails, the response is not `2xx` (other than a missing translation), or the file is
120
+ * not a policy page.
82
121
  */
83
122
  declare function loadPolicy(slug: PolicySlug, options?: {
84
123
  source?: string;
85
124
  revalidate?: number | false;
125
+ language?: PolicyLanguage;
86
126
  }): Promise<PolicyContent>;
87
127
 
88
- export { POLICIES_INDEX_URL, POLICY_SITE_URL, POLICY_SOURCE, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, publishedPolicies };
89
- export type { Policy, PolicyContent, PolicySection, PolicySlug };
128
+ export { POLICIES_INDEX_URL, POLICY_DOCUMENT_LABELS, POLICY_LANGUAGES, POLICY_LANGUAGE_NAMES, POLICY_SITE_URL, POLICY_SOURCE, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, policyLanguageFrom, publishedPolicies };
129
+ export type { Policy, PolicyContent, PolicyDocumentLabels, PolicyLanguage, PolicySection, PolicySlug };
@@ -15,6 +15,35 @@ interface Policy {
15
15
  /** Whether footers link the policy (default: `true`); documents linked from other policies set `false`. */
16
16
  footer?: boolean;
17
17
  }
18
+ /**
19
+ * A language of the TUWA policies: English, the original, and the translations generated next to it in
20
+ * `TuwaIO/workflows/docs/<language>/`.
21
+ */
22
+ type PolicyLanguage = 'en' | 'uk';
23
+ /** The languages of the policies, the original first. */
24
+ declare const POLICY_LANGUAGES: readonly PolicyLanguage[];
25
+ /** The name of each language in that language, for language switches. */
26
+ declare const POLICY_LANGUAGE_NAMES: Readonly<Record<PolicyLanguage, string>>;
27
+ /**
28
+ * Reads the language of a policy page from a value such as the `lang` search parameter.
29
+ *
30
+ * @param value - The value: a string, an array of strings (the first counts) or nothing.
31
+ * @returns The language it names, or `en` when it names none of {@link POLICY_LANGUAGES}.
32
+ */
33
+ declare function policyLanguageFrom(value: unknown): PolicyLanguage;
34
+ /** Texts of a policy page. */
35
+ interface PolicyDocumentLabels {
36
+ /** Text before the date of the version (default in English: `Last updated`). */
37
+ updated?: string;
38
+ /** Text of the button that opens the PDF (default in English: `Download PDF`). */
39
+ download?: string;
40
+ /** Heading and accessible name of the list of sections (default in English: `On this page`). */
41
+ toc?: string;
42
+ /** Accessible name of the language switch (default in English: `Language`). */
43
+ language?: string;
44
+ }
45
+ /** The texts of `PolicyDocument` in each language of the policies; its `labels` override them. */
46
+ declare const POLICY_DOCUMENT_LABELS: Readonly<Record<PolicyLanguage, Required<PolicyDocumentLabels>>>;
18
47
  /** A section of a policy: a level-2 heading with its anchor. */
19
48
  interface PolicySection {
20
49
  /** Anchor of the heading, without `#` (stable when sections are renumbered). */
@@ -28,10 +57,14 @@ interface PolicyContent {
28
57
  title: string;
29
58
  /** Date of the current version as written in the document (`October 8, 2026`). */
30
59
  updated: string;
31
- /** Level-2 sections in document order. */
60
+ /** Level-2 sections in document order; a translation keeps the anchors of the English sections. */
32
61
  sections: PolicySection[];
33
62
  /** Body of the document as HTML, without the title and the date line; headings carry their anchors as `id`. */
34
63
  html: string;
64
+ /** Language of this text. */
65
+ language: PolicyLanguage;
66
+ /** Languages the policy exists in, the original first: the choices of the language switch. */
67
+ languages: PolicyLanguage[];
35
68
  }
36
69
  /** Where the policy files live: the `docs` folder of `TuwaIO/workflows` on GitHub, branch `main`. */
37
70
  declare const POLICY_SOURCE = "https://raw.githubusercontent.com/TuwaIO/workflows/main/docs";
@@ -53,37 +86,44 @@ declare const footerPolicies: readonly Policy[];
53
86
  *
54
87
  * @param slug - Slug of the policy.
55
88
  * @param site - Origin of the site with the policy pages (default: {@link POLICY_SITE_URL}).
56
- * @returns The URL of the page, for example `https://tuwa.io/terms`.
89
+ * @param language - Language of the page (default: `en`); a translation adds `?lang=<language>`.
90
+ * @returns The URL of the page, for example `https://tuwa.io/terms` or `https://tuwa.io/terms?lang=uk`.
57
91
  */
58
- declare function getPolicyPageUrl(slug: PolicySlug, site?: string): string;
92
+ declare function getPolicyPageUrl(slug: PolicySlug, site?: string, language?: PolicyLanguage): string;
59
93
  /**
60
94
  * Returns the URL of a policy file.
61
95
  *
62
96
  * @param slug - Slug of the policy.
63
97
  * @param format - `json` for the page content, `pdf` for the official document, `md` for the source text.
64
98
  * @param source - Base URL of the policy files (default: {@link POLICY_SOURCE}).
65
- * @returns The URL of the file.
99
+ * @param language - Language of the file (default: `en`); a translation lives in the folder of its language.
100
+ * @returns The URL of the file, for example `…/docs/terms.pdf` or `…/docs/uk/terms.pdf`.
66
101
  */
67
- declare function getPolicyFileUrl(slug: PolicySlug, format: 'json' | 'pdf' | 'md', source?: string): string;
102
+ declare function getPolicyFileUrl(slug: PolicySlug, format: 'json' | 'pdf' | 'md', source?: string, language?: PolicyLanguage): string;
68
103
  /**
69
- * Loads the content of a policy page (its `json` file). Call it on the server, for example in a Server Component of
70
- * the Next.js App Router, and pass the result to `PolicyDocument`.
104
+ * Loads the content of a policy page (its `json` file) in a language. Call it on the server, for example in a Server
105
+ * Component of the Next.js App Router, and pass the result to `PolicyDocument`. A policy not translated into the
106
+ * language loads in English, so a page asked for in that language still shows the policy.
71
107
  *
72
- * Network: one request to `getPolicyFileUrl(slug, 'json', source)` per revalidation, cached by the Next.js `fetch`
73
- * cache for `revalidate` seconds (outside Next.js, a plain request).
108
+ * Network: one request to `getPolicyFileUrl(slug, 'json', source, language)` per revalidation, and one more for the
109
+ * English file when the translation does not exist; cached by the Next.js `fetch` cache for `revalidate` seconds
110
+ * (outside Next.js, plain requests).
74
111
  *
75
112
  * @param slug - Slug of the policy.
76
113
  * @param options - Optional settings.
77
114
  * @param options.source - Base URL of the policy files (default: {@link POLICY_SOURCE}).
78
115
  * @param options.revalidate - Seconds the Next.js `fetch` cache keeps the content, or `false` to cache it until the
79
116
  * next deployment (default: 3600).
80
- * @returns The content of the policy.
81
- * @throws Error when the request fails, the response is not `2xx`, or the file is not a policy page.
117
+ * @param options.language - Language of the text (default: `en`).
118
+ * @returns The content of the policy, with its language and the languages it exists in.
119
+ * @throws Error when the request fails, the response is not `2xx` (other than a missing translation), or the file is
120
+ * not a policy page.
82
121
  */
83
122
  declare function loadPolicy(slug: PolicySlug, options?: {
84
123
  source?: string;
85
124
  revalidate?: number | false;
125
+ language?: PolicyLanguage;
86
126
  }): Promise<PolicyContent>;
87
127
 
88
- export { POLICIES_INDEX_URL, POLICY_SITE_URL, POLICY_SOURCE, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, publishedPolicies };
89
- export type { Policy, PolicyContent, PolicySection, PolicySlug };
128
+ export { POLICIES_INDEX_URL, POLICY_DOCUMENT_LABELS, POLICY_LANGUAGES, POLICY_LANGUAGE_NAMES, POLICY_SITE_URL, POLICY_SOURCE, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, policyLanguageFrom, publishedPolicies };
129
+ export type { Policy, PolicyContent, PolicyDocumentLabels, PolicyLanguage, PolicySection, PolicySlug };
package/dist/policies.js CHANGED
@@ -1,3 +1,16 @@
1
+ const POLICY_LANGUAGES = ["en", "uk"];
2
+ const POLICY_LANGUAGE_NAMES = {
3
+ en: "English",
4
+ uk: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430"
5
+ };
6
+ function policyLanguageFrom(value) {
7
+ const first = Array.isArray(value) ? value[0] : value;
8
+ return POLICY_LANGUAGES.find((language) => language === first) ?? "en";
9
+ }
10
+ const POLICY_DOCUMENT_LABELS = {
11
+ en: { updated: "Last updated", download: "Download PDF", toc: "On this page", language: "Language" },
12
+ uk: { updated: "\u041E\u0441\u0442\u0430\u043D\u043D\u0454 \u043E\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044F", download: "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 PDF", toc: "\u041D\u0430 \u0446\u0456\u0439 \u0441\u0442\u043E\u0440\u0456\u043D\u0446\u0456", language: "\u041C\u043E\u0432\u0430" }
13
+ };
1
14
  const POLICY_SOURCE = "https://raw.githubusercontent.com/TuwaIO/workflows/main/docs";
2
15
  const POLICY_SITE_URL = "https://tuwa.io";
3
16
  const POLICIES_INDEX_URL = "https://docs.tuwa.io/policies";
@@ -48,11 +61,11 @@ const policies = [
48
61
  ];
49
62
  const publishedPolicies = policies.filter((policy) => policy.published);
50
63
  const footerPolicies = publishedPolicies.filter((policy) => policy.footer !== false);
51
- function getPolicyPageUrl(slug, site = POLICY_SITE_URL) {
52
- return `${site.replace(/\/+$/, "")}/${slug}`;
64
+ function getPolicyPageUrl(slug, site = POLICY_SITE_URL, language = "en") {
65
+ return `${site.replace(/\/+$/, "")}/${slug}${language === "en" ? "" : `?lang=${language}`}`;
53
66
  }
54
- function getPolicyFileUrl(slug, format, source = POLICY_SOURCE) {
55
- return `${source.replace(/\/+$/, "")}/${slug}.${format}`;
67
+ function getPolicyFileUrl(slug, format, source = POLICY_SOURCE, language = "en") {
68
+ return `${source.replace(/\/+$/, "")}${language === "en" ? "" : `/${language}`}/${slug}.${format}`;
56
69
  }
57
70
  function isPolicyContent(value) {
58
71
  if (!value || typeof value !== "object") return false;
@@ -62,8 +75,12 @@ function isPolicyContent(value) {
62
75
  );
63
76
  }
64
77
  async function loadPolicy(slug, options = {}) {
65
- const url = getPolicyFileUrl(slug, "json", options.source);
78
+ const language = options.language ?? "en";
79
+ const url = getPolicyFileUrl(slug, "json", options.source, language);
66
80
  const response = await fetch(url, { next: { revalidate: options.revalidate ?? 3600 } });
81
+ if (response.status === 404 && language !== "en") {
82
+ return loadPolicy(slug, { ...options, language: "en" });
83
+ }
67
84
  if (!response.ok) {
68
85
  throw new Error(`loadPolicy: ${url} answered ${response.status}`);
69
86
  }
@@ -71,15 +88,22 @@ async function loadPolicy(slug, options = {}) {
71
88
  if (!isPolicyContent(content)) {
72
89
  throw new Error(`loadPolicy: ${url} is not a policy page`);
73
90
  }
91
+ const raw = content;
92
+ const languages = Array.isArray(raw.languages) ? POLICY_LANGUAGES.filter((item) => raw.languages.includes(item)) : [];
74
93
  return {
75
94
  title: content.title,
76
95
  updated: content.updated,
77
96
  sections: content.sections.map(({ id, title }) => ({ id, title })),
78
- html: content.html
97
+ html: content.html,
98
+ language: policyLanguageFrom(raw.language ?? language),
99
+ languages: languages.length > 0 ? languages : ["en"]
79
100
  };
80
101
  }
81
102
  export {
82
103
  POLICIES_INDEX_URL,
104
+ POLICY_DOCUMENT_LABELS,
105
+ POLICY_LANGUAGES,
106
+ POLICY_LANGUAGE_NAMES,
83
107
  POLICY_SITE_URL,
84
108
  POLICY_SOURCE,
85
109
  footerPolicies,
@@ -87,5 +111,6 @@ export {
87
111
  getPolicyPageUrl,
88
112
  loadPolicy,
89
113
  policies,
114
+ policyLanguageFrom,
90
115
  publishedPolicies
91
116
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuwaio/docs-ui",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "author": "Oleksandr Tkach",
6
6
  "license": "Apache-2.0",
@@ -61,7 +61,7 @@
61
61
  }
62
62
  ],
63
63
  "dependencies": {
64
- "@tuwaio/nova-core": "^0.10.0",
64
+ "@tuwaio/nova-core": "^0.10.1",
65
65
  "clsx": "^2.1.1",
66
66
  "html-react-parser": "^6.1.8",
67
67
  "tailwind-merge": "^3.7.0"
@@ -82,7 +82,7 @@
82
82
  "next": "16.4.1",
83
83
  "nextra": "^4.6.1",
84
84
  "nextra-theme-docs": "^4.6.1",
85
- "postcss": "^8.5.30",
85
+ "postcss": "^8.5.31",
86
86
  "postcss-cli": "^12.0.0",
87
87
  "react": "^19.3.1",
88
88
  "tailwindcss": "^4.3.3",