From 6255a5a9b3bd99856327d8f49bf3ddeb9ddd7edc Mon Sep 17 00:00:00 2001 From: Brian Deutsch Date: Tue, 1 Sep 2026 14:08:19 -0400 Subject: [PATCH 1/6] Point Astro i18n at shared/i18n via new @shared alias Co-Authored-By: Claude Opus 5 --- astro/astro.config.mjs | 4 +++- astro/src/lib/i18n/i18n.test.ts | 23 +++++++++++++++++++++++ astro/src/lib/i18n/i18n.ts | 15 ++++++++------- astro/tsconfig.json | 1 + 4 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 astro/src/lib/i18n/i18n.test.ts diff --git a/astro/astro.config.mjs b/astro/astro.config.mjs index 30b5022dce9..3448a55c890 100644 --- a/astro/astro.config.mjs +++ b/astro/astro.config.mjs @@ -22,6 +22,7 @@ import { staticApiGuard } from "./src/integrations/staticApiGuard.ts"; const websitesModules = resolveWebsitesModulesPath(import.meta.url); const hugoSite = fileURLToPath(new URL("../hugo", import.meta.url)); +const shared = fileURLToPath(new URL("../shared", import.meta.url)); const astroSite = fileURLToPath(new URL(".", import.meta.url)); const hugoDevPort = 1313; @@ -112,7 +113,7 @@ export default defineConfig({ ], server: { fs: { - allow: [astroSite, hugoSite, websitesModules], + allow: [astroSite, hugoSite, shared, websitesModules], }, ...(IS_PROXIED && { origin: `http://localhost:${PROXY_PORT}`, @@ -127,6 +128,7 @@ export default defineConfig({ resolve: { alias: { "@hugo-site": hugoSite, + "@shared": shared, "@websites-modules": websitesModules, "@layouts": fileURLToPath(new URL("./src/layouts", import.meta.url)), "@components": fileURLToPath( diff --git a/astro/src/lib/i18n/i18n.test.ts b/astro/src/lib/i18n/i18n.test.ts new file mode 100644 index 00000000000..0e1d6762383 --- /dev/null +++ b/astro/src/lib/i18n/i18n.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { i18n } from "./i18n"; + +describe("i18n", () => { + // Guards the shared/i18n glob: if the path breaks, the bundle silently + // empties and this is the only thing that notices. + it("resolves a key from the shared glossary", () => { + expect(i18n("overview", "ja")).toBe("概要"); + expect(i18n("code_example", "ja")).toBe("コード例"); + }); + + it("falls back to English for a locale that lacks the key", () => { + expect(i18n("overview", "en")).toBe("Overview"); + }); + + it("falls back to the key itself when it is unknown", () => { + expect(i18n("not_a_real_key", "ja")).toBe("not_a_real_key"); + }); + + it("returns an empty string for an undefined key", () => { + expect(i18n(undefined, "ja")).toBe(""); + }); +}); diff --git a/astro/src/lib/i18n/i18n.ts b/astro/src/lib/i18n/i18n.ts index 3b7bdd44836..0e340e3ee42 100644 --- a/astro/src/lib/i18n/i18n.ts +++ b/astro/src/lib/i18n/i18n.ts @@ -2,10 +2,11 @@ * i18n helper. Mirrors Hugo's `i18n(key)` call: resolves a lang_key into its * "other" string from the per-locale bundle. * - * Two sources are merged: Hugo's `i18n/*.json` at the repo root (the legacy, - * authoritative bundle) and `websites-modules/i18n/*.yaml` (the newer shared - * bundle). Hugo wins on key conflicts so we don't silently drift from the - * Hugo site's translations. + * Two sources are merged: the glossary at `shared/i18n/*.json` (the legacy, + * authoritative bundle, also mounted by the Hugo site) and + * `websites-modules/i18n/*.yaml` (the newer shared bundle). The glossary wins + * on key conflicts so we don't silently drift from the Hugo site's + * translations. * * Locale files are loaded eagerly via `import.meta.glob` so missing files are * a no-op at runtime — callers fall back to English entry-by-entry, then to @@ -23,8 +24,8 @@ const yamlModules: Record = import.meta.glob( { query: "?raw", import: "default", eager: true }, ); -const hugoModules: Record = import.meta.glob( - "@hugo-site/i18n/*.json", +const sharedModules: Record = import.meta.glob( + "@shared/i18n/*.json", { query: "?raw", import: "default", eager: true }, ); @@ -53,7 +54,7 @@ function mergeBundle( mergeBundle(yamlModules, (raw) => parseYaml(raw, { uniqueKeys: false }) as I18nTable, { override: false, }); -mergeBundle(hugoModules, (raw) => JSON.parse(raw) as I18nTable, { override: true }); +mergeBundle(sharedModules, (raw) => JSON.parse(raw) as I18nTable, { override: true }); function lookup(lang: Locale, key: string): string | undefined { const entry = tables[lang]?.[key]; diff --git a/astro/tsconfig.json b/astro/tsconfig.json index 3d8ede2cbbd..ecf34f256bc 100644 --- a/astro/tsconfig.json +++ b/astro/tsconfig.json @@ -6,6 +6,7 @@ "plugins": [{ "name": "@astrojs/ts-plugin" }], "paths": { "@hugo-site/*": ["../hugo/*"], + "@shared/*": ["../shared/*"], "@websites-modules/*": ["../../../../../../dd/websites-modules/*"], "@layouts/*": ["./src/layouts/*"], "@components/*": ["./src/components/*"], From b6a409416a59ffa01fb66e589e363e75ab2d7f85 Mon Sep 17 00:00:00 2001 From: Brian Deutsch Date: Tue, 1 Sep 2026 14:21:40 -0400 Subject: [PATCH 2/6] Thread lang into ApiSideNav and localize its chrome strings Co-Authored-By: Claude Opus 5 --- astro/src/components/ApiSideNav/ApiSideNav.astro | 5 +++-- astro/src/layouts/ApiLayout.astro | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/astro/src/components/ApiSideNav/ApiSideNav.astro b/astro/src/components/ApiSideNav/ApiSideNav.astro index 9864b55e1c5..a2baeb84ce4 100644 --- a/astro/src/components/ApiSideNav/ApiSideNav.astro +++ b/astro/src/components/ApiSideNav/ApiSideNav.astro @@ -6,6 +6,7 @@ import SearchBar, { type SearchBarLabels, } from "@components/SearchBar/SearchBar"; import { DEFAULT_LOCALE, localizedHref, type Locale } from "@lib/i18n/locale"; +import { i18n } from "@lib/i18n/i18n"; import ScrollActiveIntoView from "@components/ScrollActiveIntoView/ScrollActiveIntoView.astro"; import type { OverviewPage } from "@lib/api/overviewPages"; @@ -30,7 +31,7 @@ const { } = Astro.props; const cl = classListFactory(styles); -const overviewLabel = "Overview"; +const overviewLabel = i18n("overview", lang); const overviewHref = localizedHref(lang, "/api/latest/"); // TODO: replace hardcoded English with i18n() once authoritative translation @@ -45,7 +46,7 @@ const searchLabels: SearchBarLabels = {