|
2 | 2 |
|
3 | 3 | /** |
4 | 4 | * #14882 — the metadata reads hand the document translators the DECLARED |
5 | | - * fallback chain, not the resolver's literal `['en']`. |
| 5 | + * fallback chain, not a literal `en`; #15711 — and the DECLARED default |
| 6 | + * locale, so a request for it answers with the authored label. |
6 | 7 | * |
7 | 8 | * The RULE lives in `@objectstack/spec/system`: the label resolvers walk |
8 | | - * `requested locale → fallbackChain → authored label`, and honour whatever |
9 | | - * chain they are handed (pinned in `i18n-resolver.test.ts`). What can only be |
10 | | - * tested here is the PLUMBING — that every seam translating a metadata |
11 | | - * document passes `fallbackChain: [i18n.getFallbackLocale()]`, the locale the |
12 | | - * i18n service's own `t()` falls back to, which `I18nServicePlugin` receives |
13 | | - * from the stack's `i18n` config as `fallbackLocale || defaultLocale || 'en'`. |
14 | | - * Before this, every seam passed NO chain, so the declared `fallbackLocale` |
15 | | - * never reached the resolver and `en` was consulted before the authored label. |
| 9 | + * `requested locale → fallbackChain → authored label` for a non-default |
| 10 | + * request, skip the chain for a request that names `defaultLocale` (the |
| 11 | + * authored label IS the default locale's text, ruled on #15711), and invent |
| 12 | + * no chain for a caller that declares none (pinned in |
| 13 | + * `i18n-resolver.test.ts`). What can only be tested here is the PLUMBING — |
| 14 | + * that every seam translating a metadata document passes |
| 15 | + * `fallbackChain: [i18n.getFallbackLocale()]`, the locale the i18n service's |
| 16 | + * own `t()` falls back to, which `I18nServicePlugin` receives from the |
| 17 | + * stack's `i18n` config as `fallbackLocale || defaultLocale || 'en'`, and |
| 18 | + * `defaultLocale: i18n.getDefaultLocale()`, the same accessor a header-less |
| 19 | + * request already resolves its locale from. Before #14882, every seam passed |
| 20 | + * NO chain, so the declared `fallbackLocale` never reached the resolver and |
| 21 | + * `en` was consulted before the authored label. |
16 | 22 | * |
17 | 23 | * The fixture is the card's workspace: labels authored in the default locale |
18 | 24 | * (`zh-CN`), a courtesy `en` bundle for English users, and NO `zh-CN` bundle |
|
23 | 29 | * |
24 | 30 | * Seams covered: `GET /meta/:type/:name` (object and app), `GET /meta/:type` |
25 | 31 | * (list), `GET /meta` (the types listing) — and the feature-detection |
26 | | - * contract for a service that declares no fallback. |
| 32 | + * contract for a service that declares no fallback, or no default. |
27 | 33 | */ |
28 | 34 |
|
29 | 35 | import { describe, it, expect, vi } from 'vitest'; |
@@ -286,36 +292,83 @@ describe('#14882 §3 — controls', () => { |
286 | 292 | // §4 — the feature-detection contract: no declaration, no invented chain |
287 | 293 | // --------------------------------------------------------------------------- |
288 | 294 |
|
289 | | -describe('#14882 §4 — a service that declares no fallback keeps the resolver default', () => { |
290 | | - // The serving layer threads a DECLARATION; it does not derive one. An |
291 | | - // i18n provider without the accessor (or answering `undefined`) gets no |
292 | | - // chain, so the resolver's own `['en']` default applies exactly as it did |
293 | | - // before this card — the pre-#14882 answer, pinned so a later "helpful" |
294 | | - // derivation from `getDefaultLocale()` cannot land unnoticed (it would |
295 | | - // decide the contract question §5 leaves open). |
296 | | - it('a provider without getFallbackLocale answers as before (en consulted)', async () => { |
| 295 | +describe('#14882 §4 / #15711 — a service that declares no fallback gets no chain, and no en is invented', () => { |
| 296 | + // The serving layer threads DECLARATIONS; it derives nothing. An i18n |
| 297 | + // provider without `getFallbackLocale` (or answering `undefined`) gets no |
| 298 | + // chain, and since #15711 the resolver's own default is `[]`: a request |
| 299 | + // walks `requested locale → authored label`, and `en` is consulted only |
| 300 | + // when it is requested or declared. `getDefaultLocale()` IS threaded now |
| 301 | + // (#15711 ruled the question §5 used to leave open), so the `zh-CN` |
| 302 | + // request below answers authored for two independent reasons; the `fr` |
| 303 | + // request isolates the second facet, where only the `[]` default applies. |
| 304 | + it('a provider without getFallbackLocale: zh-CN (the default) and fr (not) both answer authored', async () => { |
297 | 305 | const legacy = i18nFor({ bundles: { 'zh-CN': {}, en: EN_DATA }, defaultLocale: 'zh-CN', fallbackLocale: null }); |
298 | | - expect((await readItem(makeRest(legacy), 'object', 'kpi_entry_sheet', 'zh-CN')).label).toBe('Entry Sheet'); |
| 306 | + expect(labelsOf(await readItem(makeRest(legacy), 'object', 'kpi_entry_sheet', 'zh-CN'))).toEqual(AUTHORED); |
| 307 | + expect(labelsOf(await readItem(makeRest(legacy), 'object', 'kpi_entry_sheet', 'fr'))).toEqual(AUTHORED); |
| 308 | + // Control — an `en` request still finds its own bundle. |
| 309 | + expect((await readItem(makeRest(legacy), 'object', 'kpi_entry_sheet', 'en')).label).toBe('Entry Sheet'); |
299 | 310 | }); |
300 | 311 |
|
301 | | - it('a provider answering undefined answers as before (en consulted)', async () => { |
| 312 | + it('a provider answering undefined answers the same', async () => { |
302 | 313 | const undeclared = i18nFor({ bundles: { 'zh-CN': {}, en: EN_DATA }, defaultLocale: 'zh-CN', fallbackLocale: undefined }); |
303 | | - expect((await readItem(makeRest(undeclared), 'object', 'kpi_entry_sheet', 'zh-CN')).label).toBe('Entry Sheet'); |
| 314 | + expect((await readItem(makeRest(undeclared), 'object', 'kpi_entry_sheet', 'zh-CN')).label).toBe('填报单'); |
| 315 | + expect((await readItem(makeRest(undeclared), 'object', 'kpi_entry_sheet', 'fr')).label).toBe('填报单'); |
| 316 | + }); |
| 317 | + |
| 318 | + it('a provider without getDefaultLocale gets no default: nothing is the default, the chain is all there is', async () => { |
| 319 | + // No `defaultLocale` is threaded (the seam passes nothing, never |
| 320 | + // `'en'`), so the default-locale rule is off and the DECLARED `en` |
| 321 | + // chain is walked for a `zh-CN` request exactly as #14882 pinned it. |
| 322 | + const noDefault = i18nFor({ bundles: { 'zh-CN': {}, en: EN_DATA }, defaultLocale: 'zh-CN', fallbackLocale: 'en' }); |
| 323 | + delete noDefault.getDefaultLocale; |
| 324 | + expect((await readItem(makeRest(noDefault), 'object', 'kpi_entry_sheet', 'zh-CN')).label).toBe('Entry Sheet'); |
304 | 325 | }); |
305 | 326 | }); |
306 | 327 |
|
307 | 328 | // --------------------------------------------------------------------------- |
308 | | -// §5 — a stack that DECLARES en as its fallback is honoured as it reads |
| 329 | +// §5 — a stack that DECLARES en as its fallback: honoured for every request |
| 330 | +// but the default-locale one (#15711) |
309 | 331 | // --------------------------------------------------------------------------- |
310 | 332 |
|
311 | | -describe('#14882 §5 — a declared en fallback still consults en before the authored label', () => { |
312 | | - // `defaultLocale: 'zh-CN'`, `fallbackLocale: 'en'`, no zh-CN bundle. |
313 | | - // Pinned as it answers today — the `en` bundle — because whether the |
314 | | - // authored label is the default-locale source (and so should outrank a |
315 | | - // declared fallback's bundle) is a CONTRACT question this card does not |
316 | | - // decide. #14882 changes which chain reaches the resolver, nothing else. |
317 | | - it('GET /meta/object/:name serves the en bundle for a zh-CN request', async () => { |
318 | | - const enFallback = i18nFor({ bundles: { 'zh-CN': {}, en: EN_DATA }, defaultLocale: 'zh-CN', fallbackLocale: 'en' }); |
319 | | - expect((await readItem(makeRest(enFallback), 'object', 'kpi_entry_sheet', 'zh-CN')).label).toBe('Entry Sheet'); |
| 333 | +describe('#15711 §5 — a declared en fallback never outranks the authored label for a default-locale request', () => { |
| 334 | + // `defaultLocale: 'zh-CN'`, `fallbackLocale: 'en'`, no zh-CN bundle — the |
| 335 | + // reflexive AI-authored config. #14882 pinned this as it answered then |
| 336 | + // (`Entry Sheet`) because whether the authored label is the default-locale |
| 337 | + // source was a CONTRACT question that card did not decide. #15711 ruled |
| 338 | + // it: the authored label IS the default locale's text. |
| 339 | + const enFallback = () => |
| 340 | + i18nFor({ bundles: { 'zh-CN': {}, en: EN_DATA }, defaultLocale: 'zh-CN', fallbackLocale: 'en' }); |
| 341 | + |
| 342 | + it('GET /meta/object/:name serves the authored 填报单 for a zh-CN request', async () => { |
| 343 | + expect(labelsOf(await readItem(makeRest(enFallback()), 'object', 'kpi_entry_sheet', 'zh-CN'))).toEqual(AUTHORED); |
| 344 | + }); |
| 345 | + |
| 346 | + it('GET /meta/app/:name, the list read and the types listing agree', async () => { |
| 347 | + expect((await readItem(makeRest(enFallback()), 'app', 'kpi_app', 'zh-CN')).label).toBe('KPI 考核管理'); |
| 348 | + const [sheet] = await readList(makeRest(enFallback()), 'object', 'zh-CN'); |
| 349 | + expect(labelsOf(sheet)).toEqual(AUTHORED); |
| 350 | + const body = await readTypes(makeRest(enFallback()), 'zh-CN'); |
| 351 | + expect(body.entries.find((e: any) => e.type === 'object').label).toBe('对象'); |
| 352 | + }); |
| 353 | + |
| 354 | + it('with NO Accept-Language the request falls to the default locale and answers authored', async () => { |
| 355 | + expect(labelsOf(await readItem(makeRest(enFallback()), 'object', 'kpi_entry_sheet', undefined))).toEqual(AUTHORED); |
| 356 | + }); |
| 357 | + |
| 358 | + it('a NON-default request still walks fr → en bundle → authored', async () => { |
| 359 | + // `fallbackLocale` keeps its full meaning for every other locale. |
| 360 | + expect(labelsOf(await readItem(makeRest(enFallback()), 'object', 'kpi_entry_sheet', 'fr'))).toEqual(ENGLISH); |
| 361 | + expect((await readItem(makeRest(enFallback()), 'app', 'kpi_app', 'fr')).label).toBe('KPI Assessment'); |
| 362 | + }); |
| 363 | + |
| 364 | + it('an en request on the same stack still gets the en bundle', async () => { |
| 365 | + expect(labelsOf(await readItem(makeRest(enFallback()), 'object', 'kpi_entry_sheet', 'en'))).toEqual(ENGLISH); |
| 366 | + }); |
| 367 | + |
| 368 | + it('a shipped zh-CN bundle still wins; a key it omits is authored, never en', async () => { |
| 369 | + const withZh = i18nFor({ bundles: { 'zh-CN': ZH_DATA, en: EN_DATA }, defaultLocale: 'zh-CN', fallbackLocale: 'en' }); |
| 370 | + const item = await readItem(makeRest(withZh), 'object', 'kpi_entry_sheet', 'zh-CN'); |
| 371 | + expect(item.label).toBe('填报单(bundle)'); |
| 372 | + expect(item.fields.name.label).toBe('填报单名称'); |
320 | 373 | }); |
321 | 374 | }); |
0 commit comments