From 3bf897364566cbb4565675105c3f1a55e2731e4e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 19:43:39 +0000 Subject: [PATCH 1/5] Add a language selector to the docs site Register each machine-translated docs directory (found by its .translated-docs marker) as a VuePress locale, which turns on the default theme's language dropdown in the navbar. Languages are listed by their native names and switching keeps the reader on the same page. The navbar and sidebar are generated per locale, the custom home, footer and 404 links now stay in the current language, and Arabic and Urdu pages are rendered right to left. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0111WiCk7wYZYbBipgb3u7HG --- docs/.vuepress/client.ts | 42 +++- docs/.vuepress/components/HomePage.vue | 15 +- docs/.vuepress/components/SiteFooter.vue | 21 +- docs/.vuepress/composables/useLocalePath.ts | 11 + docs/.vuepress/config.ts | 237 ++++++++++++++------ docs/.vuepress/layouts/NotFound.vue | 7 +- 6 files changed, 242 insertions(+), 91 deletions(-) create mode 100644 docs/.vuepress/composables/useLocalePath.ts diff --git a/docs/.vuepress/client.ts b/docs/.vuepress/client.ts index 55e3518..b9ebbf4 100644 --- a/docs/.vuepress/client.ts +++ b/docs/.vuepress/client.ts @@ -1,4 +1,10 @@ -import { defineClientConfig, resolvers } from 'vuepress/client'; +import { watchEffect } from 'vue'; +import { + defineClientConfig, + resolvers, + usePageFrontmatter, + usePageLang, +} from 'vuepress/client'; import HomeLayout from './layouts/HomeLayout.vue'; import NotFound from './layouts/NotFound.vue'; @@ -12,9 +18,43 @@ resolvers.resolvePageHeadTitle = (page, siteLocale) => { : resolvePageHeadTitle(page, siteLocale); }; +function textDirection(lang: string): 'ltr' | 'rtl' { + try { + const locale = new Intl.Locale(lang) as Intl.Locale & { + getTextInfo?: () => { direction: 'ltr' | 'rtl' }; + textInfo?: { direction: 'ltr' | 'rtl' }; + }; + return ( + locale.getTextInfo?.().direction ?? + locale.textInfo?.direction ?? + 'ltr' + ); + } catch { + return 'ltr'; + } +} + export default defineClientConfig({ layouts: { HomeLayout, NotFound, }, + + // Arabic and Urdu read right to left. The page's `lang` is set from the + // locale, but nothing sets `dir`, so do it here as the reader switches + // languages. The home page's content lives in HomePage.vue and is only + // written in English, so it stays left to right in every locale. + setup() { + if (__VUEPRESS_SSR__) { + return; + } + const lang = usePageLang(); + const frontmatter = usePageFrontmatter(); + watchEffect(() => { + document.documentElement.dir = + frontmatter.value.layout === 'HomeLayout' + ? 'ltr' + : textDirection(lang.value); + }); + }, }); diff --git a/docs/.vuepress/components/HomePage.vue b/docs/.vuepress/components/HomePage.vue index a903a79..e45437b 100644 --- a/docs/.vuepress/components/HomePage.vue +++ b/docs/.vuepress/components/HomePage.vue @@ -2,11 +2,14 @@ import { onMounted, ref } from 'vue'; import { RouteLink, withBase } from 'vuepress/client'; import SiteFooter from './SiteFooter.vue'; +import { useLocalePath } from '../composables/useLocalePath'; const DONATE_URL = 'https://better.giving/marketplace/1118469'; const DISCORD_URL = 'https://discord.com/invite/NbEZMCJmqC'; const NEWSLETTER_URL = 'https://helloao.org'; +const localePath = useLocalePath(); + const logo = withBase('/seed_bible_logo.png'); const logoDark = withBase('/seed_bible_logo_dark.png'); @@ -152,7 +155,7 @@ const compareRows: { Scripture.

- + Quick Start Donate @@ -213,7 +216,7 @@ const compareRows: {

- +

Bible Translations

{{ translations }} translations as static JSON, @@ -224,7 +227,7 @@ const compareRows: {

Bible Audio

@@ -233,7 +236,7 @@ const compareRows: {

Audio endpoints →
- +

Bible Data Sets

Cross references and other Bible data sets, in @@ -364,7 +367,7 @@ const compareRows: {

Let’s start building.

- + Quick Start @@ -378,7 +381,7 @@ const compareRows: { inbox.

- + Want something to copy? See examples →

diff --git a/docs/.vuepress/components/SiteFooter.vue b/docs/.vuepress/components/SiteFooter.vue index 8eb28bc..311a338 100644 --- a/docs/.vuepress/components/SiteFooter.vue +++ b/docs/.vuepress/components/SiteFooter.vue @@ -1,5 +1,6 @@ @@ -49,21 +52,21 @@ const aoLabLogoDark = withBase('/ao_lab_logo_dark.png');

Docs

    -
  • Quick Start
  • -
  • Guide
  • -
  • Reference
  • -
  • SDKs
  • -
  • Downloads
  • +
  • Quick Start
  • +
  • Guide
  • +
  • Reference
  • +
  • SDKs
  • +
  • Downloads
  • Report an issue

Resources

    -
  • Bible translations
  • -
  • Bible audio
  • -
  • Bible commentaries
  • -
  • Bible data sets
  • +
  • Bible translations
  • +
  • Bible audio
  • +
  • Bible commentaries
  • +
  • Bible data sets
  • Seed Bible (Coming soon)
diff --git a/docs/.vuepress/composables/useLocalePath.ts b/docs/.vuepress/composables/useLocalePath.ts new file mode 100644 index 0000000..b1aa7c2 --- /dev/null +++ b/docs/.vuepress/composables/useLocalePath.ts @@ -0,0 +1,11 @@ +import { useRouteLocale } from 'vuepress/client'; + +/** + * Returns a function that turns an English page path ("/guide/") into the + * same page in the language being viewed ("/es/guide/"), so links in the + * custom layouts do not drop readers back into the English docs. + */ +export function useLocalePath() { + const routeLocale = useRouteLocale(); + return (path: string) => routeLocale.value + path.replace(/^\//, ''); +} diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index 41828ab..1bf74b9 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -8,6 +8,14 @@ import { seoPlugin } from '@vuepress/plugin-seo'; import { shikiPlugin } from '@vuepress/plugin-shiki'; import { sitemapPlugin } from '@vuepress/plugin-sitemap'; import { markdownIncludePlugin } from '@vuepress/plugin-markdown-include'; +import type { + DefaultThemeLocaleData, + NavbarOptions, + SidebarOptions, +} from '@vuepress/theme-default'; +import type { SiteLocaleConfig } from 'vuepress'; +import { existsSync, readdirSync } from 'node:fs'; +import path from 'node:path'; const hostname = 'https://bible.helloao.org'; const base = '/docs/'; @@ -68,9 +76,164 @@ const sectionNames: Record = { '/reference/': 'Reference', }; +/** + * Written by tools/translate-docs.ts to the root of every machine-translated + * copy of the docs. + */ +const TRANSLATED_DOCS_MARKER = '.translated-docs'; + +/** + * Names that read better in the language selector than the ones + * `Intl.DisplayNames` gives ("Indonesia", "中文(中国)"), or than capitalizing + * them below would (isiZulu). + */ +const languageNameOverrides: Record = { + id: 'Bahasa Indonesia', + 'zh-CN': '简体中文', + zu: 'isiZulu', +}; + +/** + * The language codes of the translated docs, found by looking for the + * directories that tools/translate-docs.ts generated. A newly translated + * language shows up in the language selector without touching this file. + */ +const docsDir = path.resolve(__dirname, '..'); +const translatedLanguages = readdirSync(docsDir, { withFileTypes: true }) + .filter( + (entry) => + entry.isDirectory() && + existsSync(path.join(docsDir, entry.name, TRANSLATED_DOCS_MARKER)) + ) + .map((entry) => entry.name) + .sort(); + +/** The name of a language, written in that language. */ +const nativeLanguageName = (lang: string) => { + if (languageNameOverrides[lang]) { + return languageNameOverrides[lang]; + } + const name = new Intl.DisplayNames([lang], { type: 'language' }).of(lang); + if (!name || name === lang) { + return lang; + } + // Some languages write their own names in lowercase ("español"), which + // looks out of place in a menu. + return name.charAt(0).toLocaleUpperCase(lang) + name.slice(1); +}; + +const navbar = (prefix: string): NavbarOptions => [ + { + text: 'Guide', + link: `${prefix}guide/`, + }, + { + text: 'Reference', + link: `${prefix}reference/`, + }, + { + text: 'SDKs', + link: `${prefix}sdks/`, + }, + { + text: 'GitHub', + link: 'https://github.com/HelloAOLab/bible-api', + }, + { + text: 'Donate', + link: 'https://better.giving/marketplace/1118469', + }, +]; + +const sidebar = (prefix: string): SidebarOptions => ({ + [`${prefix}guide/`]: [ + { + text: 'Guide', + collapsible: false, + children: [ + '', + 'getting-started', + 'making-requests', + 'downloads', + 'a-biblical-model-for-licensing-the-bible', + ], + }, + ], + [`${prefix}reference/`]: [ + { + text: 'Reference', + collapsible: false, + children: [ + '', + { + text: 'Translations, Books, & Chapters', + collapsible: true, + children: [ + 'translations/', + 'translations/standard', + 'translations/simplified', + ], + }, + { + text: 'Commentaries', + collapsible: true, + children: ['commentaries/'], + }, + { + text: 'Datasets', + collapsible: true, + children: ['datasets/'], + }, + 'openapi', + ], + }, + ], + [`${prefix}sdks/`]: [ + { + text: 'SDKs', + collapsible: false, + children: ['', 'javascript'], + }, + ], +}); + +/** Per-language site settings, keyed by the path each language lives under. */ +const siteLocales: Record = { + '/': { lang: 'en-US' }, + ...Object.fromEntries( + translatedLanguages.map((lang) => [`/${lang}/`, { lang }]) + ), +}; + +/** + * Per-language theme settings. `selectLanguageName` is what the navbar's + * language selector lists, which the theme only shows when there is more + * than one locale. + */ +const themeLocales: Record = { + '/': { + selectLanguageName: 'English', + navbar: navbar('/'), + sidebar: sidebar('/'), + }, + ...Object.fromEntries( + translatedLanguages.map((lang) => [ + `/${lang}/`, + { + selectLanguageName: nativeLanguageName(lang), + selectLanguageText: 'Languages', + selectLanguageAriaLabel: 'Select language', + navbar: navbar(`/${lang}/`), + sidebar: sidebar(`/${lang}/`), + }, + ]) + ), +}; + export default defineUserConfig({ base, lang: 'en-US', + locales: siteLocales, title, description: description, @@ -124,79 +287,7 @@ export default defineUserConfig({ editLinkText: '', lastUpdated: false, contributors: false, - navbar: [ - { - text: 'Guide', - link: '/guide/', - }, - { - text: 'Reference', - link: '/reference/', - }, - { - text: 'SDKs', - link: '/sdks/', - }, - { - text: 'GitHub', - link: 'https://github.com/HelloAOLab/bible-api', - }, - { - text: 'Donate', - link: 'https://better.giving/marketplace/1118469', - }, - ], - sidebar: { - '/guide/': [ - { - text: 'Guide', - collapsible: false, - children: [ - '', - 'getting-started', - 'making-requests', - 'downloads', - 'a-biblical-model-for-licensing-the-bible', - ], - }, - ], - '/reference/': [ - { - text: 'Reference', - collapsible: false, - children: [ - '', - { - text: 'Translations, Books, & Chapters', - collapsible: true, - children: [ - 'translations/', - 'translations/standard', - 'translations/simplified', - ], - }, - { - text: 'Commentaries', - collapsible: true, - children: ['commentaries/'], - }, - { - text: 'Datasets', - collapsible: true, - children: ['datasets/'], - }, - 'openapi', - ], - }, - ], - '/sdks/': [ - { - text: 'SDKs', - collapsible: false, - children: ['', 'javascript'], - }, - ], - }, + locales: themeLocales, }), plugins: [ diff --git a/docs/.vuepress/layouts/NotFound.vue b/docs/.vuepress/layouts/NotFound.vue index d3ddc57..9d1bdc3 100644 --- a/docs/.vuepress/layouts/NotFound.vue +++ b/docs/.vuepress/layouts/NotFound.vue @@ -3,11 +3,14 @@ import { onMounted, ref, resolveComponent } from 'vue'; import { RouteLink } from 'vuepress/client'; import ParentLayout from '@vuepress/theme-default/layouts/Layout.vue'; import SiteFooter from '../components/SiteFooter.vue'; +import { useLocalePath } from '../composables/useLocalePath'; // Registered globally by the search plugin. Resolved here, as the theme's // navbar does, so it renders in the build-time HTML as well as the browser. const SearchBox = resolveComponent('SearchBox'); +const localePath = useLocalePath(); + // The navbar already has a search box bound to the "s" and "/" hotkeys, so // the one on this page gets none of its own. const searchLocales = { '/': { placeholder: 'Search the docs' } }; @@ -83,11 +86,11 @@ onMounted(() => {

Or try one of these

- + Home page Start from the beginning of the docs. - + Guide Learn how to use the API, step by step. From 7941cc0620b613152dc7f28b1bdef5baae2db72c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 19:47:22 +0000 Subject: [PATCH 2/5] Translate the docs navbar and sidebar labels Move the navbar and sidebar labels (plus the language menu's text) into docs/.vuepress/labels.json. The site config builds each locale's menus from docs//labels.json, falling back to English for any label a translation lacks, and also uses them for breadcrumb names. tools/translate-docs.ts now translates labels.json alongside the pages. The label files for the 18 existing languages were written by hand, since the translation API was not available here. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0111WiCk7wYZYbBipgb3u7HG --- docs/.vuepress/config.ts | 77 +++++++++++++++++++++++++------------- docs/.vuepress/labels.json | 11 ++++++ docs/README.md | 1 + docs/af/labels.json | 11 ++++++ docs/am/labels.json | 11 ++++++ docs/ar/labels.json | 11 ++++++ docs/bn/labels.json | 11 ++++++ docs/de/labels.json | 11 ++++++ docs/es/labels.json | 11 ++++++ docs/fr/labels.json | 11 ++++++ docs/ha/labels.json | 11 ++++++ docs/hi/labels.json | 11 ++++++ docs/id/labels.json | 11 ++++++ docs/it/labels.json | 11 ++++++ docs/ja/labels.json | 11 ++++++ docs/pt/labels.json | 11 ++++++ docs/ru/labels.json | 11 ++++++ docs/sw/labels.json | 11 ++++++ docs/ur/labels.json | 11 ++++++ docs/zh-CN/labels.json | 11 ++++++ docs/zu/labels.json | 11 ++++++ tools/translate-docs.ts | 55 +++++++++++++++++++++++++++ 22 files changed, 317 insertions(+), 25 deletions(-) create mode 100644 docs/.vuepress/labels.json create mode 100644 docs/af/labels.json create mode 100644 docs/am/labels.json create mode 100644 docs/ar/labels.json create mode 100644 docs/bn/labels.json create mode 100644 docs/de/labels.json create mode 100644 docs/es/labels.json create mode 100644 docs/fr/labels.json create mode 100644 docs/ha/labels.json create mode 100644 docs/hi/labels.json create mode 100644 docs/id/labels.json create mode 100644 docs/it/labels.json create mode 100644 docs/ja/labels.json create mode 100644 docs/pt/labels.json create mode 100644 docs/ru/labels.json create mode 100644 docs/sw/labels.json create mode 100644 docs/ur/labels.json create mode 100644 docs/zh-CN/labels.json create mode 100644 docs/zu/labels.json diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index 1bf74b9..52cfef5 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -14,7 +14,7 @@ import type { SidebarOptions, } from '@vuepress/theme-default'; import type { SiteLocaleConfig } from 'vuepress'; -import { existsSync, readdirSync } from 'node:fs'; +import { existsSync, readdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; const hostname = 'https://bible.helloao.org'; @@ -122,17 +122,34 @@ const nativeLanguageName = (lang: string) => { return name.charAt(0).toLocaleUpperCase(lang) + name.slice(1); }; -const navbar = (prefix: string): NavbarOptions => [ +/** + * The navbar and sidebar labels. tools/translate-docs.ts translates this file + * into `docs//labels.json`; a label missing from a translation + * falls back to English. + */ +const englishLabels: Record = JSON.parse( + readFileSync(path.join(__dirname, 'labels.json'), 'utf8') +); +type Labels = typeof englishLabels; + +const labelsFor = (lang: string): Labels => { + const file = path.join(docsDir, lang, 'labels.json'); + return existsSync(file) + ? { ...englishLabels, ...JSON.parse(readFileSync(file, 'utf8')) } + : englishLabels; +}; + +const navbar = (prefix: string, labels: Labels): NavbarOptions => [ { - text: 'Guide', + text: labels.guide, link: `${prefix}guide/`, }, { - text: 'Reference', + text: labels.reference, link: `${prefix}reference/`, }, { - text: 'SDKs', + text: labels.sdks, link: `${prefix}sdks/`, }, { @@ -140,15 +157,15 @@ const navbar = (prefix: string): NavbarOptions => [ link: 'https://github.com/HelloAOLab/bible-api', }, { - text: 'Donate', + text: labels.donate, link: 'https://better.giving/marketplace/1118469', }, ]; -const sidebar = (prefix: string): SidebarOptions => ({ +const sidebar = (prefix: string, labels: Labels): SidebarOptions => ({ [`${prefix}guide/`]: [ { - text: 'Guide', + text: labels.guide, collapsible: false, children: [ '', @@ -161,12 +178,12 @@ const sidebar = (prefix: string): SidebarOptions => ({ ], [`${prefix}reference/`]: [ { - text: 'Reference', + text: labels.reference, collapsible: false, children: [ '', { - text: 'Translations, Books, & Chapters', + text: labels.translationsBooksChapters, collapsible: true, children: [ 'translations/', @@ -175,12 +192,12 @@ const sidebar = (prefix: string): SidebarOptions => ({ ], }, { - text: 'Commentaries', + text: labels.commentaries, collapsible: true, children: ['commentaries/'], }, { - text: 'Datasets', + text: labels.datasets, collapsible: true, children: ['datasets/'], }, @@ -190,7 +207,7 @@ const sidebar = (prefix: string): SidebarOptions => ({ ], [`${prefix}sdks/`]: [ { - text: 'SDKs', + text: labels.sdks, collapsible: false, children: ['', 'javascript'], }, @@ -213,23 +230,33 @@ const siteLocales: Record = { const themeLocales: Record = { '/': { selectLanguageName: 'English', - navbar: navbar('/'), - sidebar: sidebar('/'), + navbar: navbar('/', englishLabels), + sidebar: sidebar('/', englishLabels), }, ...Object.fromEntries( - translatedLanguages.map((lang) => [ - `/${lang}/`, - { - selectLanguageName: nativeLanguageName(lang), - selectLanguageText: 'Languages', - selectLanguageAriaLabel: 'Select language', - navbar: navbar(`/${lang}/`), - sidebar: sidebar(`/${lang}/`), - }, - ]) + translatedLanguages.map((lang) => { + const labels = labelsFor(lang); + return [ + `/${lang}/`, + { + selectLanguageName: nativeLanguageName(lang), + selectLanguageText: labels.languages, + selectLanguageAriaLabel: labels.selectLanguage, + navbar: navbar(`/${lang}/`, labels), + sidebar: sidebar(`/${lang}/`, labels), + }, + ]; + }) ), }; +// Translated section landing pages get translated crumb labels too. +for (const lang of translatedLanguages) { + const labels = labelsFor(lang); + sectionNames[`/${lang}/guide/`] = labels.guide; + sectionNames[`/${lang}/reference/`] = labels.reference; +} + export default defineUserConfig({ base, lang: 'en-US', diff --git a/docs/.vuepress/labels.json b/docs/.vuepress/labels.json new file mode 100644 index 0000000..2f26289 --- /dev/null +++ b/docs/.vuepress/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "Guide", + "reference": "Reference", + "sdks": "SDKs", + "donate": "Donate", + "translationsBooksChapters": "Translations, Books, & Chapters", + "commentaries": "Commentaries", + "datasets": "Datasets", + "languages": "Languages", + "selectLanguage": "Select language" +} diff --git a/docs/README.md b/docs/README.md index 3d76922..0673af5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,4 +34,5 @@ Comments in fenced code blocks are translated too (for languages such as TypeScr inline code, URLs and HTML are left untouched, and links are rewritten to point at the translated pages. Pass `--skip-code-comments` to keep code comments in English. Files whose translation is newer than the English source are skipped; pass `--force` to re-translate them. +The navbar and sidebar labels in `.vuepress/labels.json` are translated into `/labels.json`. Any label missing from a translation falls back to English. Run `pnpm translate:docs --help` for all options. diff --git a/docs/af/labels.json b/docs/af/labels.json new file mode 100644 index 0000000..e7daa76 --- /dev/null +++ b/docs/af/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "Gids", + "reference": "Naslaan", + "sdks": "SDK's", + "donate": "Skenk", + "translationsBooksChapters": "Vertalings, boeke en hoofstukke", + "commentaries": "Kommentare", + "datasets": "Datastelle", + "languages": "Tale", + "selectLanguage": "Kies taal" +} diff --git a/docs/am/labels.json b/docs/am/labels.json new file mode 100644 index 0000000..6ae9e96 --- /dev/null +++ b/docs/am/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "መመሪያ", + "reference": "ማጣቀሻ", + "sdks": "SDKዎች", + "donate": "ይለግሱ", + "translationsBooksChapters": "ትርጉሞች፣ መጻሕፍት እና ምዕራፎች", + "commentaries": "ማብራሪያዎች", + "datasets": "የውሂብ ስብስቦች", + "languages": "ቋንቋዎች", + "selectLanguage": "ቋንቋ ይምረጡ" +} diff --git a/docs/ar/labels.json b/docs/ar/labels.json new file mode 100644 index 0000000..b514444 --- /dev/null +++ b/docs/ar/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "الدليل", + "reference": "المرجع", + "sdks": "حزم SDK", + "donate": "تبرّع", + "translationsBooksChapters": "الترجمات والأسفار والإصحاحات", + "commentaries": "التفاسير", + "datasets": "مجموعات البيانات", + "languages": "اللغات", + "selectLanguage": "اختر اللغة" +} diff --git a/docs/bn/labels.json b/docs/bn/labels.json new file mode 100644 index 0000000..0235217 --- /dev/null +++ b/docs/bn/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "নির্দেশিকা", + "reference": "রেফারেন্স", + "sdks": "SDK", + "donate": "দান করুন", + "translationsBooksChapters": "অনুবাদ, পুস্তক ও অধ্যায়", + "commentaries": "ভাষ্য", + "datasets": "ডেটাসেট", + "languages": "ভাষা", + "selectLanguage": "ভাষা নির্বাচন করুন" +} diff --git a/docs/de/labels.json b/docs/de/labels.json new file mode 100644 index 0000000..f9ffe88 --- /dev/null +++ b/docs/de/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "Leitfaden", + "reference": "Referenz", + "sdks": "SDKs", + "donate": "Spenden", + "translationsBooksChapters": "Übersetzungen, Bücher und Kapitel", + "commentaries": "Kommentare", + "datasets": "Datensätze", + "languages": "Sprachen", + "selectLanguage": "Sprache auswählen" +} diff --git a/docs/es/labels.json b/docs/es/labels.json new file mode 100644 index 0000000..06154ef --- /dev/null +++ b/docs/es/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "Guía", + "reference": "Referencia", + "sdks": "SDK", + "donate": "Donar", + "translationsBooksChapters": "Traducciones, libros y capítulos", + "commentaries": "Comentarios", + "datasets": "Conjuntos de datos", + "languages": "Idiomas", + "selectLanguage": "Seleccionar idioma" +} diff --git a/docs/fr/labels.json b/docs/fr/labels.json new file mode 100644 index 0000000..f2c79bb --- /dev/null +++ b/docs/fr/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "Guide", + "reference": "Référence", + "sdks": "SDK", + "donate": "Faire un don", + "translationsBooksChapters": "Traductions, livres et chapitres", + "commentaries": "Commentaires", + "datasets": "Jeux de données", + "languages": "Langues", + "selectLanguage": "Choisir la langue" +} diff --git a/docs/ha/labels.json b/docs/ha/labels.json new file mode 100644 index 0000000..b767071 --- /dev/null +++ b/docs/ha/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "Jagora", + "reference": "Bayanan API", + "sdks": "SDKs", + "donate": "Ba da gudummawa", + "translationsBooksChapters": "Fassarori, Littattafai da Surori", + "commentaries": "Sharhohi", + "datasets": "Saitunan Bayanai", + "languages": "Harsuna", + "selectLanguage": "Zaɓi harshe" +} diff --git a/docs/hi/labels.json b/docs/hi/labels.json new file mode 100644 index 0000000..1b19b92 --- /dev/null +++ b/docs/hi/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "मार्गदर्शिका", + "reference": "संदर्भ", + "sdks": "SDK", + "donate": "दान करें", + "translationsBooksChapters": "अनुवाद, पुस्तकें और अध्याय", + "commentaries": "टीकाएँ", + "datasets": "डेटासेट", + "languages": "भाषाएँ", + "selectLanguage": "भाषा चुनें" +} diff --git a/docs/id/labels.json b/docs/id/labels.json new file mode 100644 index 0000000..1df3e1b --- /dev/null +++ b/docs/id/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "Panduan", + "reference": "Referensi", + "sdks": "SDK", + "donate": "Donasi", + "translationsBooksChapters": "Terjemahan, Kitab, dan Pasal", + "commentaries": "Tafsiran", + "datasets": "Kumpulan Data", + "languages": "Bahasa", + "selectLanguage": "Pilih bahasa" +} diff --git a/docs/it/labels.json b/docs/it/labels.json new file mode 100644 index 0000000..6a02b30 --- /dev/null +++ b/docs/it/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "Guida", + "reference": "Riferimento", + "sdks": "SDK", + "donate": "Dona", + "translationsBooksChapters": "Traduzioni, libri e capitoli", + "commentaries": "Commentari", + "datasets": "Set di dati", + "languages": "Lingue", + "selectLanguage": "Seleziona la lingua" +} diff --git a/docs/ja/labels.json b/docs/ja/labels.json new file mode 100644 index 0000000..466a94f --- /dev/null +++ b/docs/ja/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "ガイド", + "reference": "リファレンス", + "sdks": "SDK", + "donate": "寄付", + "translationsBooksChapters": "翻訳・書・章", + "commentaries": "注解書", + "datasets": "データセット", + "languages": "言語", + "selectLanguage": "言語を選択" +} diff --git a/docs/pt/labels.json b/docs/pt/labels.json new file mode 100644 index 0000000..ba179c2 --- /dev/null +++ b/docs/pt/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "Guia", + "reference": "Referência", + "sdks": "SDKs", + "donate": "Doar", + "translationsBooksChapters": "Traduções, livros e capítulos", + "commentaries": "Comentários", + "datasets": "Conjuntos de dados", + "languages": "Idiomas", + "selectLanguage": "Selecionar idioma" +} diff --git a/docs/ru/labels.json b/docs/ru/labels.json new file mode 100644 index 0000000..cb5be41 --- /dev/null +++ b/docs/ru/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "Руководство", + "reference": "Справочник", + "sdks": "SDK", + "donate": "Пожертвовать", + "translationsBooksChapters": "Переводы, книги и главы", + "commentaries": "Комментарии", + "datasets": "Наборы данных", + "languages": "Языки", + "selectLanguage": "Выбрать язык" +} diff --git a/docs/sw/labels.json b/docs/sw/labels.json new file mode 100644 index 0000000..3fad43d --- /dev/null +++ b/docs/sw/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "Mwongozo", + "reference": "Marejeo", + "sdks": "SDK", + "donate": "Changia", + "translationsBooksChapters": "Tafsiri, Vitabu na Sura", + "commentaries": "Maoni", + "datasets": "Seti za data", + "languages": "Lugha", + "selectLanguage": "Chagua lugha" +} diff --git a/docs/ur/labels.json b/docs/ur/labels.json new file mode 100644 index 0000000..5081e1b --- /dev/null +++ b/docs/ur/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "رہنما", + "reference": "حوالہ", + "sdks": "SDKs", + "donate": "عطیہ کریں", + "translationsBooksChapters": "تراجم، کتب اور ابواب", + "commentaries": "تفاسیر", + "datasets": "ڈیٹا سیٹس", + "languages": "زبانیں", + "selectLanguage": "زبان منتخب کریں" +} diff --git a/docs/zh-CN/labels.json b/docs/zh-CN/labels.json new file mode 100644 index 0000000..0e1fbec --- /dev/null +++ b/docs/zh-CN/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "指南", + "reference": "参考", + "sdks": "SDK", + "donate": "捐赠", + "translationsBooksChapters": "译本、书卷与章节", + "commentaries": "注释书", + "datasets": "数据集", + "languages": "语言", + "selectLanguage": "选择语言" +} diff --git a/docs/zu/labels.json b/docs/zu/labels.json new file mode 100644 index 0000000..d3c2eff --- /dev/null +++ b/docs/zu/labels.json @@ -0,0 +1,11 @@ +{ + "guide": "Umhlahlandlela", + "reference": "Inkomba", + "sdks": "Ama-SDK", + "donate": "Nikela", + "translationsBooksChapters": "Ukuhumusha, Izincwadi Nezahluko", + "commentaries": "Izincazelo", + "datasets": "Amasethi edatha", + "languages": "Izilimi", + "selectLanguage": "Khetha ulimi" +} diff --git a/tools/translate-docs.ts b/tools/translate-docs.ts index 2d7c316..b64f5d8 100644 --- a/tools/translate-docs.ts +++ b/tools/translate-docs.ts @@ -24,6 +24,9 @@ * inline code, HTML tags, HTML comments, URLs and link destinations are passed * through unchanged, and relative links are rewritten so they still resolve * from the translated file's location. + * + * The navbar and sidebar labels in `docs/.vuepress/labels.json` are translated + * into `docs//labels.json`, which the site config reads. */ import { GoogleAuth } from 'google-auth-library'; @@ -45,6 +48,14 @@ const SOURCE_LANGUAGE = 'en'; */ const MARKER_FILE = '.translated-docs'; +/** + * The navbar and sidebar labels, which live in the site config rather than in + * a page. Relative to the source root, and written to the root of each + * translation under the same file name. + */ +const LABELS_FILE = '.vuepress/labels.json'; +const TRANSLATED_LABELS_FILE = 'labels.json'; + /** Directories under the source root that never contain translatable pages. */ const IGNORED_DIRS = new Set(['node_modules', 'site-root']); @@ -789,6 +800,26 @@ function yamlQuote(value: string): string { return `'${value.replace(/'/g, "''")}'`; } +// --------------------------------------------------------------------------- +// Navbar and sidebar labels +// --------------------------------------------------------------------------- + +/** + * Translates the values of a flat `{ key: label }` object, keeping its keys. + */ +export async function translateLabels( + labels: Record, + translate: Translator +): Promise> { + const keys = Object.keys(labels); + const translated = await translate( + keys.map((key) => escapeHtml(labels[key])) + ); + return Object.fromEntries( + keys.map((key, i) => [key, unescapeHtml(translated[i]).trim()]) + ); +} + // --------------------------------------------------------------------------- // Files // --------------------------------------------------------------------------- @@ -1032,6 +1063,30 @@ async function main() { console.log(` done ${rel}`); } + const labelsSource = path.join(sourceRoot, LABELS_FILE); + const labelsOutput = path.join(outputRoot, TRANSLATED_LABELS_FILE); + if (await exists(labelsSource)) { + const upToDate = + !values.force && + (await exists(labelsOutput)) && + (await stat(labelsOutput)).mtimeMs >= + (await stat(labelsSource)).mtimeMs; + if (upToDate) { + console.log(` skip ${LABELS_FILE} (up to date)`); + } else if (values['dry-run']) { + console.log(` would translate ${LABELS_FILE}`); + } else { + const labels = JSON.parse(await readFile(labelsSource, 'utf8')); + const result = await translateLabels(labels, translate); + await mkdir(outputRoot, { recursive: true }); + await writeFile( + labelsOutput, + JSON.stringify(result, null, 4) + '\n' + ); + console.log(` done ${LABELS_FILE}`); + } + } + if (!values['dry-run']) { await mkdir(outputRoot, { recursive: true }); await writeFile( From 4f01664e0a88f8a9b671bde50f0290d1f39e3c6e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 20:00:19 +0000 Subject: [PATCH 3/5] Translate the docs home and 404 pages Move the text of the custom home page, footer and 404 page into docs/.vuepress/labels.json, grouped as nav, home, footer and notFound, and have the components read the current locale's labels. The config passes every locale's labels to the client, filling anything a translation lacks from English. tools/translate-docs.ts now translates nested label files, and runs each label through the same markdown conversion as page prose, so **bold** and {{ placeholders }} survive translation. The existing nav labels are moved under "nav"; the new text stays English until `pnpm translate:docs` is run. The 404 page switches to the reader's language once mounted, since one build-time page serves every locale, and text direction now follows the address's locale so the home and 404 pages are right to left in Arabic and Urdu too. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0111WiCk7wYZYbBipgb3u7HG --- docs/.vuepress/client.ts | 15 +- docs/.vuepress/components/HomePage.vue | 307 ++++++++++---------- docs/.vuepress/components/SiteFooter.vue | 75 +++-- docs/.vuepress/composables/formatLabel.ts | 39 +++ docs/.vuepress/composables/useLocale.ts | 58 ++++ docs/.vuepress/composables/useLocalePath.ts | 11 - docs/.vuepress/config.ts | 57 ++-- docs/.vuepress/labels.json | 149 +++++++++- docs/.vuepress/labels.ts | 31 ++ docs/.vuepress/layouts/NotFound.vue | 41 +-- docs/README.md | 2 +- docs/af/labels.json | 20 +- docs/am/labels.json | 20 +- docs/ar/labels.json | 20 +- docs/bn/labels.json | 20 +- docs/de/labels.json | 20 +- docs/es/labels.json | 20 +- docs/fr/labels.json | 20 +- docs/ha/labels.json | 20 +- docs/hi/labels.json | 20 +- docs/id/labels.json | 20 +- docs/it/labels.json | 20 +- docs/ja/labels.json | 20 +- docs/pt/labels.json | 20 +- docs/ru/labels.json | 20 +- docs/sw/labels.json | 20 +- docs/ur/labels.json | 20 +- docs/zh-CN/labels.json | 20 +- docs/zu/labels.json | 20 +- tools/translate-docs.ts | 50 +++- 30 files changed, 765 insertions(+), 430 deletions(-) create mode 100644 docs/.vuepress/composables/formatLabel.ts create mode 100644 docs/.vuepress/composables/useLocale.ts delete mode 100644 docs/.vuepress/composables/useLocalePath.ts create mode 100644 docs/.vuepress/labels.ts diff --git a/docs/.vuepress/client.ts b/docs/.vuepress/client.ts index b9ebbf4..b8057a0 100644 --- a/docs/.vuepress/client.ts +++ b/docs/.vuepress/client.ts @@ -2,8 +2,7 @@ import { watchEffect } from 'vue'; import { defineClientConfig, resolvers, - usePageFrontmatter, - usePageLang, + useSiteLocaleData, } from 'vuepress/client'; import HomeLayout from './layouts/HomeLayout.vue'; import NotFound from './layouts/NotFound.vue'; @@ -42,19 +41,15 @@ export default defineClientConfig({ // Arabic and Urdu read right to left. The page's `lang` is set from the // locale, but nothing sets `dir`, so do it here as the reader switches - // languages. The home page's content lives in HomePage.vue and is only - // written in English, so it stays left to right in every locale. + // languages. It follows the locale of the address rather than the page, + // since the one 404 page is shown in every locale. setup() { if (__VUEPRESS_SSR__) { return; } - const lang = usePageLang(); - const frontmatter = usePageFrontmatter(); + const siteLocale = useSiteLocaleData(); watchEffect(() => { - document.documentElement.dir = - frontmatter.value.layout === 'HomeLayout' - ? 'ltr' - : textDirection(lang.value); + document.documentElement.dir = textDirection(siteLocale.value.lang); }); }, }); diff --git a/docs/.vuepress/components/HomePage.vue b/docs/.vuepress/components/HomePage.vue index e45437b..ef3aa01 100644 --- a/docs/.vuepress/components/HomePage.vue +++ b/docs/.vuepress/components/HomePage.vue @@ -1,28 +1,48 @@