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 @@
@@ -149,16 +146,16 @@ const compareRows: {
-
Free Use Bible API
-
- An easy-to-use and fully featured JSON API for
- Scripture.
-
+
{{ frontmatter.title }}
+
{{ labels.home.tagline }}
@@ -170,15 +167,16 @@ const compareRows: {
href="https://seedbible.org/?translation=AAB&book=MAT&chapter=10&verse=8"
target="_blank"
rel="noopener noreferrer"
- aria-label="Open Matthew 10:8 in Seed Bible"
+ :aria-label="labels.home.freelyGiveOpen"
>
- “Freely you have received; freely give.”
+ “{{ labels.home.freelyGiveVerse }}”
- Matthew 10:8 · AAB ↗
+ {{ labels.home.freelyGiveReference }} · AAB
+ ↗
@@ -187,16 +185,24 @@ const compareRows: {
- {{ translations }} translations
- across {{ languages }} languages
+ {{
+ fillLabel(labels.home.translationCount, {
+ count: translations,
+ })
+ }}
+ {{
+ fillLabel(labels.home.languageCount, {
+ count: languages,
+ })
+ }}
- No API key
- Nothing to sign up for
+ {{ labels.home.noApiKey }}
+ {{ labels.home.noApiKeyDetail }}
- No usage limits
- Free forever
+ {{ labels.home.noUsageLimits }}
+ {{ labels.home.noUsageLimitsDetail }}
@@ -204,54 +210,51 @@ const compareRows: {
- Bible Translations
+ {{ labels.home.translationsTitle }}
- {{ translations }} translations as static JSON,
- with headings, poetry, line breaks and
- footnotes kept intact.
+ {{
+ fillLabel(labels.home.translationsBody, {
+ count: translations,
+ })
+ }}
- API reference →
+ {{ labels.home.translationsLink }} →
- Bible Audio
-
- Scripture plainly read aloud and dramatized, for
- easy listening and accessibility.
-
- Audio endpoints →
+ {{ labels.home.audioTitle }}
+ {{ labels.home.audioBody }}
+ {{ labels.home.audioLink }} →
- Bible Data Sets
-
- Cross references and other Bible data sets, in
- formats you can build with rather than parse
- around.
-
- Browse datasets →
+ {{ labels.home.datasetsTitle }}
+ {{ labels.home.datasetsBody }}
+ {{ labels.home.datasetsLink }} →
-
Seed Bible Developer Docs
-
- Utilize and extend our open platform instead of
- starting from scratch.
-
-
Coming December
+
{{ labels.home.seedBibleTitle }}
+
{{ labels.home.seedBibleBody }}
+
{{
+ labels.home.seedBibleComing
+ }}
@@ -260,25 +263,26 @@ const compareRows: {
-
A Few Bible APIs Compared
+ {{ labels.home.compareTitle }}
- What the Free Use Bible API and three widely
- used Bible APIs let you do
+ {{ labels.home.compareCaption }}
|
- What you may do
+ {{ labels.home.compareCorner }}
|
- {{ col }}
+ {{
+ labels.home.compareColumns[col]
+ }}
|
@@ -299,7 +303,7 @@ const compareRows: {
:title="note"
>
- {{ MARK_LABEL[mark] }}. {{ note }}
+ {{ markLabel[mark] }}. {{ note }}
@@ -308,13 +312,21 @@ const compareRows: {
@@ -323,19 +335,17 @@ const compareRows: {
- Help us expand the frontier.
+ {{ labels.home.contributeTitle }}
-
- Let’s create an open ecosystem of public
- domain content and open source tools.
-
+
{{ labels.home.contributeBody }}
@@ -346,17 +356,16 @@ const compareRows: {
href="https://seedbible.org/?translation=AAB&book=1PE&chapter=4&verse=10"
target="_blank"
rel="noopener noreferrer"
- aria-label="Open 1 Peter 4:10 in Seed Bible"
+ :aria-label="labels.home.stewardsOpen"
>
- “As good stewards of the manifold grace of
- God, each of you should use whatever gift he has
- received to serve one another.”
+ “{{ labels.home.stewardsVerse }}”
- 1 Peter 4:10 · AAB ↗
+ {{ labels.home.stewardsReference }} · AAB
+ ↗
@@ -366,28 +375,26 @@ const compareRows: {
-
Let’s start building.
+ {{ labels.home.buildTitle }}
- Quick Start
+ {{ labels.home.quickStart }}
→
diff --git a/docs/.vuepress/components/SiteFooter.vue b/docs/.vuepress/components/SiteFooter.vue
index 311a338..c1e61df 100644
--- a/docs/.vuepress/components/SiteFooter.vue
+++ b/docs/.vuepress/components/SiteFooter.vue
@@ -1,6 +1,8 @@