|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * The options `DevPlugin` hands `I18nServicePlugin` when it auto-registers it. |
| 5 | + * |
| 6 | + * `defaultLocale` stays optional because the detected stack may declare |
| 7 | + * translations without declaring an i18n config at all — that is the common |
| 8 | + * case, and `I18nServicePlugin`'s own default is what should apply then. |
| 9 | + */ |
| 10 | +export interface DevI18nPluginOptions { |
| 11 | + defaultLocale?: string; |
| 12 | + fallbackLocale: string; |
| 13 | +} |
| 14 | + |
| 15 | +const asBag = (value: unknown): Record<string, unknown> | undefined => |
| 16 | + (value && typeof value === 'object' ? value as Record<string, unknown> : undefined); |
| 17 | + |
| 18 | +const declaresTranslationArray = (body: unknown): boolean => { |
| 19 | + const declared = asBag(body)?.translations; |
| 20 | + return Array.isArray(declared) && declared.length > 0; |
| 21 | +}; |
| 22 | + |
| 23 | +/** |
| 24 | + * Does this stack DECLARE translations? |
| 25 | + * |
| 26 | + * ⚠️ TOP LEVEL ONLY, which is the defect #15232 exists to fix — recorded here |
| 27 | + * so this intermediate commit is not mistaken for the fix. A multi-package |
| 28 | + * artifact under ADR-0130 D4's option-B shape carries `translations` inside |
| 29 | + * `packages[]` and nothing at the top level, so this answers `false` and the |
| 30 | + * dev server keeps the in-memory i18n fallback with nothing thrown and nothing |
| 31 | + * logged. The probe row landing in the same commit ledgers exactly that loss. |
| 32 | + */ |
| 33 | +export function stackDeclaresTranslations(stack: unknown): boolean { |
| 34 | + return declaresTranslationArray(stack); |
| 35 | +} |
| 36 | + |
| 37 | +/** |
| 38 | + * [#15232] The `I18nServicePlugin` options a stack implies — the ONE decision |
| 39 | + * `DevPlugin`'s i18n auto-detect makes, resolved in one place. |
| 40 | + * |
| 41 | + * `undefined` means "this stack declares no i18n content", i.e. do not register |
| 42 | + * the file-based service and leave the slot to the core in-memory fallback. |
| 43 | + * Returning the options rather than a boolean is what keeps the decision and |
| 44 | + * the values it derives from the same read — a caller cannot pair a `true` with |
| 45 | + * a locale resolved from somewhere else. |
| 46 | + * |
| 47 | + * Three declarations trigger it, exactly as they have since the auto-detect was |
| 48 | + * written: a `translations` collection (read through |
| 49 | + * {@link stackDeclaresTranslations}), an `i18n` config on the stack or its |
| 50 | + * manifest, or `manifest.translations` — the authoring manifest's glob |
| 51 | + * patterns, which are a declaration of intent even before a bundle is |
| 52 | + * assembled. |
| 53 | + * |
| 54 | + * Exported because the #15004 option-B acceptance probe measures this decision |
| 55 | + * by CALLING it. A probe that re-implemented the read would be a second copy of |
| 56 | + * the code the reader program changes, and would stay red after the reader |
| 57 | + * beside it was fixed. |
| 58 | + */ |
| 59 | +export function devI18nPluginOptions(stack: unknown): DevI18nPluginOptions | undefined { |
| 60 | + const bag = asBag(stack); |
| 61 | + if (!bag) return undefined; |
| 62 | + |
| 63 | + const manifest = asBag(bag.manifest); |
| 64 | + const hasTranslations = stackDeclaresTranslations(stack); |
| 65 | + const hasI18nConfig = !!(bag.i18n || manifest?.i18n); |
| 66 | + const hasManifestTranslations = !!( |
| 67 | + manifest && Array.isArray(manifest.translations) && manifest.translations.length > 0 |
| 68 | + ); |
| 69 | + |
| 70 | + if (!hasTranslations && !hasI18nConfig && !hasManifestTranslations) return undefined; |
| 71 | + |
| 72 | + // `stack.i18n || stack.manifest.i18n || {}`, the original expression: the |
| 73 | + // stack's own config wins, the manifest's is the fallback, and neither being |
| 74 | + // present means the service plugin's own defaults apply. |
| 75 | + const i18nConfig = (bag.i18n || manifest?.i18n || {}) as { |
| 76 | + defaultLocale?: string; |
| 77 | + fallbackLocale?: string; |
| 78 | + }; |
| 79 | + return { |
| 80 | + defaultLocale: i18nConfig.defaultLocale, |
| 81 | + fallbackLocale: i18nConfig.fallbackLocale || i18nConfig.defaultLocale || 'en', |
| 82 | + }; |
| 83 | +} |
0 commit comments