mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
feat: French localization — community contribution by @OUARZA (#371)
The complete French dictionary contributed in #371 (1026 keys, zero empty values, zero placeholder mismatches) lands as the second lazy locale on the fr.ts + one static entry. The contributor's snapshot predated this week's keys, so the 121 additions (device inbox #29, backdrop guard #39, junction limits #331, lazy-editor toasts #353/#354, furniture #159) are translated in this commit and flagged in the issue for the author's review; 21 stale pre-#62 keys are dropped; key order follows en.json. The HA integration translations file ships as contributed (full parity). Wiring: loadFrench + __HOUSEPLAN_FR_RETRY_ASSET__ with the same 1/1 replacement guarantee as German; locale roles and localeRoots generalise to (de|fr); the stale-entry fallback panel (#353) gains its French branch — the r1 reviewer caught that this second hardcoded language list would have silently degraded French to English on a cached entry. French profiles (fr, fr-FR, fr-CA, fr-BE, fr-CH) select automatically. Proofs: the registry-driven parity suite covers fr by construction (1128/1128 keys); French analogues of both German-personal tests (product glossary + non-translation scan with a reviewed equal-to-English allow-list of 22 legitimate homographs); smoke_french_locale — fr-CA profile commits French from the real bundle, one lazy chunk per page, initial graph free of fr; smoke_entry_stale gains the French run; a registry mutant drops the fr entry and is killed by the smoke. Initial view: +0.5 KB (registry entry + fallback branch); the 23 KB dictionary is lazy. Issue: #371 User-Visible: yes
This commit is contained in:
@@ -7,6 +7,7 @@ const BUILD_FINGERPRINT_TOKEN = '__HOUSEPLAN_SOURCE_FINGERPRINT__';
|
||||
const EDITOR_RETRY_ASSET_TOKEN = '__HOUSEPLAN_EDITOR_RETRY_ASSET__';
|
||||
const ONBOARDING_RETRY_ASSET_TOKEN = '__HOUSEPLAN_ONBOARDING_RETRY_ASSET__';
|
||||
const DE_RETRY_ASSET_TOKEN = '__HOUSEPLAN_DE_RETRY_ASSET__';
|
||||
const FR_RETRY_ASSET_TOKEN = '__HOUSEPLAN_FR_RETRY_ASSET__';
|
||||
|
||||
const sha256 = (value) => createHash('sha256').update(value).digest('hex');
|
||||
|
||||
@@ -29,7 +30,7 @@ export function buildBundleManifest(bundle, fingerprint) {
|
||||
.map((chunk) => {
|
||||
const contents = Buffer.from(chunk.code, 'utf8');
|
||||
const modules = Object.keys(chunk.modules || {}).map((id) => id.replaceAll('\\', '/'));
|
||||
const role = modules.some((id) => id.endsWith('/src/i18n/de.ts'))
|
||||
const role = modules.some((id) => id.endsWith('/src/i18n/de.ts') || id.endsWith('/src/i18n/fr.ts'))
|
||||
? 'locale'
|
||||
: modules.some((id) => id.endsWith('/src/houseplan-onboarding-runtime.ts'))
|
||||
? 'onboarding'
|
||||
@@ -59,7 +60,7 @@ export function buildBundleManifest(bundle, fingerprint) {
|
||||
for (const path of reachable(root, byPath, 'imports')) if (!initial.has(path)) lazy.add(path);
|
||||
}
|
||||
const localeRoots = dynamicRoots.filter((path) => byPath.get(path)?._role === 'locale'
|
||||
|| /(?:^|\/)de-[^/]+\.js$/.test(path));
|
||||
|| /(?:^|\/)(?:de|fr)-[^/]+\.js$/.test(path));
|
||||
const onboardingRoots = dynamicRoots.filter((path) => byPath.get(path)?._role === 'onboarding'
|
||||
|| path.includes('houseplan-onboarding-runtime-'));
|
||||
const editorRoots = dynamicRoots.filter((path) => (byPath.get(path)?._role === 'editor'
|
||||
@@ -140,12 +141,16 @@ export function editorRuntimeRetryUrlPlugin() {
|
||||
.some((id) => id.replaceAll('\\', '/').endsWith('/src/houseplan-onboarding-runtime.ts')));
|
||||
const german = chunks.find((chunk) => Object.keys(chunk.modules)
|
||||
.some((id) => id.replaceAll('\\', '/').endsWith('/src/i18n/de.ts')));
|
||||
const french = chunks.find((chunk) => Object.keys(chunk.modules)
|
||||
.some((id) => id.replaceAll('\\', '/').endsWith('/src/i18n/fr.ts')));
|
||||
if (!editor) throw new Error('editor runtime chunk was not emitted');
|
||||
if (!onboarding) throw new Error('onboarding runtime chunk was not emitted');
|
||||
if (!german) throw new Error('German locale chunk was not emitted');
|
||||
if (!french) throw new Error('French locale chunk was not emitted');
|
||||
let editorReplacements = 0;
|
||||
let onboardingReplacements = 0;
|
||||
let germanReplacements = 0;
|
||||
let frenchReplacements = 0;
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.code.includes(EDITOR_RETRY_ASSET_TOKEN)) {
|
||||
let asset = posix.relative(posix.dirname(chunk.fileName), editor.fileName);
|
||||
@@ -165,10 +170,17 @@ export function editorRuntimeRetryUrlPlugin() {
|
||||
germanReplacements += chunk.code.split(DE_RETRY_ASSET_TOKEN).length - 1;
|
||||
chunk.code = chunk.code.replaceAll(DE_RETRY_ASSET_TOKEN, asset);
|
||||
}
|
||||
if (chunk.code.includes(FR_RETRY_ASSET_TOKEN)) {
|
||||
let asset = posix.relative(posix.dirname(chunk.fileName), french.fileName);
|
||||
if (!asset.startsWith('.')) asset = `./${asset}`;
|
||||
frenchReplacements += chunk.code.split(FR_RETRY_ASSET_TOKEN).length - 1;
|
||||
chunk.code = chunk.code.replaceAll(FR_RETRY_ASSET_TOKEN, asset);
|
||||
}
|
||||
}
|
||||
if (editorReplacements !== 1 || onboardingReplacements !== 1 || germanReplacements !== 1) {
|
||||
if (editorReplacements !== 1 || onboardingReplacements !== 1
|
||||
|| germanReplacements !== 1 || frenchReplacements !== 1) {
|
||||
throw new Error('lazy retry URL placeholder counts are '
|
||||
+ `${editorReplacements}/${onboardingReplacements}/${germanReplacements}, expected 1/1/1`);
|
||||
+ `${editorReplacements}/${onboardingReplacements}/${germanReplacements}/${frenchReplacements}, expected 1/1/1/1`);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -209,6 +221,8 @@ export function entryFallbackPlugin() {
|
||||
+ '?"House Plan обновился — перезагрузите страницу (Ctrl+F5)."'
|
||||
+ ':l.startsWith("de")'
|
||||
+ '?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5)."'
|
||||
+ ':l.startsWith("fr")'
|
||||
+ '?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5)."'
|
||||
+ ':"House Plan was updated — please reload the page (Ctrl+F5).";'
|
||||
+ 'customElements.define("houseplan-card",class extends HTMLElement{'
|
||||
+ 'setConfig(){}getCardSize(){return 1}connectedCallback(){'
|
||||
|
||||
@@ -680,6 +680,17 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' this._languageFailureUnsub = undefined;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'french-locale-entry-removed',
|
||||
guard: 'node demo/smoke_french_locale.mjs',
|
||||
because: 'dropping the fr registry entry silently reverts French profiles to English — the '
|
||||
+ 'community contribution must stay wired (#371)',
|
||||
patches: [{
|
||||
file: 'src/i18n/registry.ts',
|
||||
find: " { code: 'fr', nativeLabel: 'Français', loadDictionary: loadFrench },\n",
|
||||
replace: '',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'opening-light-quantum-identity',
|
||||
guard: 'node --test --test-name-pattern="#366" test/logic.test.mjs',
|
||||
|
||||
@@ -39,7 +39,7 @@ export const SMOKE_LINKS = [
|
||||
'LanguageRuntime', 'LanguageRuntimeContract', 'languageRenderGate',
|
||||
'dictionaryFor', 'ensureLanguage',
|
||||
],
|
||||
smokes: ['smoke_german_locale.mjs'],
|
||||
smokes: ['smoke_german_locale.mjs', 'smoke_french_locale.mjs'],
|
||||
because: 'the production-bundle scenario observes the lazy German chunk through network '
|
||||
+ 'requests and four root render gates: neutral cold frame, atomic German commit, page-cache '
|
||||
+ 'reuse and the bounded retry-to-English path cannot be proven from pure registry tests (#348)',
|
||||
|
||||
Reference in New Issue
Block a user