You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- inheritTopLevelNavigation(): when navigation.languages[].sidebar/tabs/etc.
are omitted, copy the top-level navigation onto the entry. Lets users
define the structure once and only declare locales — translations come
from catalogs.
- extractCatalogOverrides(): catalog keys prefixed with `$` are settings
override paths, not translation keys. Pulled out at boot and merged into
the matching navigation.languages[].overrides.
- applyOverrides() in mapSettingsToProps.resolveLocaleSettings: deep-merge
per-locale overrides into the resolved settings at request time. Accepts
both nested Partial<Settings> and flat dot-keys (expanded before merge).
- Rename `i18n.translations` -> `i18n.catalogs` across types, loader,
e2e fixture, and docs.
- Unit tests for inheritTopLevelNavigation (5) and extractCatalogOverrides
(5); existing loadTranslations tests updated for the rename.
- New user-facing guide apps/docs/guides/internalization.md and updated
dev doc .dev/docs/18.features/Internationalization.md.
This page documents the i18n feature - a multi-language docs are configured in `docs.json`, how routes/files are organized, and how the framework wires it all up at runtime.
8
8
9
-
> **Status:** V1. Routing, per-locale navigation, per-locale content, the `FwLocaleSwitcher` component (auto-registered on the `nav.right` surface), and the `"i18n: <key>"` translation-key resolver all work. SEO (`hreflang`, `<html lang>`), search-locale filtering, and the prehydration script are tracked as follow-up slices — see "Future work" at the bottom.
9
+
> **Status:** V1. Routing, per-locale navigation, per-locale content, the `FwLocaleSwitcher` component (auto-registered on the `nav.right` surface), the `"i18n: <key>"` translation-key resolver, catalog-only navigation (top-level nav inherited when language entry omits `sidebar`/`tabs`/etc.), and per-locale settings overrides — both via `navigation.languages[].overrides` and via catalog `$`-prefixed keys — all work. SEO (`hreflang`, `<html lang>`), search-locale filtering, and the prehydration script are tracked as follow-up slices — see "Future work" at the bottom.
10
10
11
11
## Overview
12
12
@@ -21,18 +21,24 @@ Missing translations 404 — there is no fallback to the default locale.
`getPathname()` (in `packages/xyd-plugin-docs/src/pages/page.tsx` and `layout.tsx`) returns `{ slug, locale }`. The slug already contains the locale prefix when serving a non-default locale (because the sidebar was pre-prefixed and routes were emitted accordingly), so the mapping lookup is a direct dictionary hit. The separate `locale` is passed to `mapSettingsToProps` so it can resolve the right per-language navigation tree.
If a language entry omits `sidebar`/`tabs`/`sidebarDropdown`/`segments`/`anchors`, it inherits the matching field from the top-level `navigation`. This lets users write the structure once at `navigation.sidebar` and only declare locales — translations come from catalogs (see below).
// each language entry gets a deep-cloned copy of the top-level sidebar,
111
+
// so the prefixSidebarPages() pass below mutates per-locale copies safely.
112
+
```
113
+
114
+
Implemented in `pluginDocs` as `inheritTopLevelNavigation(settings)`, called once before `prefixSidebarPages` runs.
115
+
116
+
### `extractCatalogOverrides` (catalog `$`-keys)
117
+
118
+
Catalog keys prefixed with `$` are not translation keys — they encode per-locale settings overrides applied to the matching language entry's `overrides`:
119
+
120
+
```json
121
+
// pl.json
122
+
{
123
+
"sidebar.greet": "Cześć",
124
+
"$components.footer.footnote.props.children": "Wspierane przez LiveSession",
At boot, `pluginDocs` calls `extractCatalogOverrides(catalogs)` which:
130
+
131
+
1. Pulls out every `$`-prefixed key from each catalog and strips the prefix.
132
+
2. Mutates the catalogs to drop the `$` keys (so `"i18n: <key>"` lookup never sees them).
133
+
3. Returns a per-locale flat-key overrides map.
134
+
135
+
The map is then merged into `navigation.languages[].overrides` so the runtime `resolveLocaleSettings` deep-merges it like any declared override.
136
+
137
+
### `resolveLocaleSettings` + `applyOverrides`
95
138
96
139
```ts
97
140
function resolveLocaleSettings(settings:Settings, locale?:string):Settings {
98
141
const langs =settings?.navigation?.languages
99
142
if (!locale||!langs?.length) returnsettings
100
143
const entry =langs.find(l=>l.language===locale)
101
144
if (!entry) returnsettings
102
-
return {
145
+
146
+
const next:Settings= {
103
147
...settings,
104
148
navigation: {
105
149
...settings.navigation,
106
150
sidebar: entry.sidebar,
107
151
tabs: entry.tabs,
108
-
// … etc
152
+
sidebarDropdown: entry.sidebarDropdown,
153
+
segments: entry.segments,
154
+
anchors: entry.anchors,
109
155
}
110
156
}
157
+
158
+
if (entry.overrides) {
159
+
returnapplyOverrides(next, entry.overrides)
160
+
}
161
+
returnnext
111
162
}
112
163
```
113
164
165
+
`applyOverrides` deep-merges the override block into the resolved settings. It supports both shapes:
166
+
167
+
-**Nested objects** — standard `Partial<Settings>` (`{components: {footer: {…}}}`).
168
+
-**Flat dot-keys** — `{"components.footer.footnote.props.children": "Wspierane"}`. Expanded to nested form before merge so a single override path doesn't have to declare four levels of object literal.
169
+
170
+
Two helpers do the work: `expandDotKeys()` walks the override map and turns dot-keys into nested objects; `mergeDeep()` merges source into target without mutating either side. JSON cloning protects the result from later mutation.
171
+
114
172
This is the only locale-aware step inside `mapSettingsToProps`. Everything downstream (sidebar groups, breadcrumbs, prev/next nav links) operates on `settings.navigation.sidebar` as if there were no i18n.
115
173
116
174
### Prerender list (`docPaths`)
@@ -157,7 +215,7 @@ When the `i18n` block is present, it wins:
157
215
158
216
-`i18n.defaultLocale` overrides any `default: true` shorthand on language entries.
159
217
-`i18n.detectLanguage` — reserved for the prehydration redirect (future slice; currently unused at runtime).
160
-
-`i18n.translations` — translation catalogs used by the `"i18n: <key>"` resolver. See [Translation catalogs](#translation-catalogs) below.
218
+
-`i18n.catalogs` — translation catalogs used by the `"i18n: <key>"` resolver. See [Translation catalogs](#translation-catalogs) below.
161
219
162
220
### Translation catalogs
163
221
@@ -170,7 +228,7 @@ There are three ways to register catalogs, in priority order:
170
228
```json
171
229
{
172
230
"i18n": {
173
-
"translations": {
231
+
"catalogs": {
174
232
"en": "./locales/english.json",
175
233
"pl": "./tlumaczenia/polski.json",
176
234
"de": "./locales/de-DE.json"
@@ -184,15 +242,15 @@ There are three ways to register catalogs, in priority order:
184
242
```json
185
243
{
186
244
"i18n": {
187
-
"translations": {
245
+
"catalogs": {
188
246
"en": { "footer.copyright": "All rights reserved" },
189
247
"pl": { "footer": { "copyright": "Wszelkie prawa zastrzeżone" } }
190
248
}
191
249
}
192
250
}
193
251
```
194
252
195
-
**3. Convention fallback** — when `i18n.translations` is omitted (or a particular locale's entry is missing), the framework auto-discovers `i18n/<language>.json` at the project root.
253
+
**3. Convention fallback** — when `i18n.catalogs` is omitted (or a particular locale's entry is missing), the framework auto-discovers `i18n/<language>.json` at the project root.
196
254
197
255
#### Catalog file format
198
256
@@ -210,6 +268,27 @@ Lookup tries the exact flat key first, then walks dot-segments through nested ob
210
268
211
269
For the full field reference, see the `Settings`, `Navigation`, `LanguageNavigation`, and `I18nConfig` interfaces in `4.settings/1.SETTINGS.md` (and the source of truth: `packages/xyd-core/src/types/settings.ts`).
212
270
271
+
### Per-locale overrides
272
+
273
+
Each `navigation.languages[]` entry accepts `overrides?: Partial<Settings>` for any field that should differ per locale (e.g. translated footer link text, banner content, locale-specific component config). Two equivalent shapes are accepted:
274
+
275
+
```jsonc
276
+
// Flat dot-key (recommended for narrow overrides)
Overrides come from two sources, both ending up on the same `lang.overrides` field:
286
+
287
+
1.**Declared in `docs.json`** under `navigation.languages[].overrides`.
288
+
2.**Extracted from catalog `$`-keys** at boot — e.g. `"$components.footer.footnote.props.children"` in `pl.json` becomes `lang.overrides["components.footer.footnote.props.children"]`. See `extractCatalogOverrides` above.
289
+
290
+
At request time `resolveLocaleSettings` deep-merges `lang.overrides` into the effective settings. Flat dot-keys are expanded to nested form before the merge.
0 commit comments