fix(i18n): invalidate native artifacts on glossary changes

This commit is contained in:
Vincent Koc
2026-07-01 05:57:51 -07:00
committed by Vincent Koc
parent 1611e04fd9
commit be2c4c65ab
2 changed files with 32 additions and 7 deletions
+15 -7
View File
@@ -42,6 +42,7 @@ export type NativeI18nEntry = {
type Candidate = Omit<NativeI18nEntry, "id">;
type NativeTranslationArtifact = {
entries: Array<{ id: string; source: string; translated: string }>;
glossaryHash: string;
locale: string;
version: 1;
};
@@ -923,8 +924,15 @@ export async function syncNativeLocale(
// Native runtime resources are owned by the Android and Apple slices; these
// artifacts keep the shared translation-memory handoff current between them.
const artifactPath = path.join(options.translationsDir ?? TRANSLATIONS_DIR, `${locale}.json`);
const glossary = options.glossary ?? (await loadGlossary(locale));
const glossaryHash = createHash("sha256").update(JSON.stringify(glossary)).digest("hex");
let previousRaw = "";
let previous: NativeTranslationArtifact = { entries: [], locale, version: 1 };
let previous: NativeTranslationArtifact = {
entries: [],
glossaryHash: "",
locale,
version: 1,
};
try {
previousRaw = await readFile(artifactPath, "utf8");
previous = JSON.parse(previousRaw) as NativeTranslationArtifact;
@@ -932,10 +940,13 @@ export async function syncNativeLocale(
// The first refresh creates the locale artifact.
}
const previousById = new Map(previous.entries.map((entry) => [entry.id, entry]));
const glossaryChanged = previous.glossaryHash !== glossaryHash;
const pending = entries
.filter((entry) => {
const current = previousById.get(entry.id);
return !current || current.source !== entry.source || !current.translated.trim();
return (
glossaryChanged || !current || current.source !== entry.source || !current.translated.trim()
);
})
.map((entry) => ({
id: entry.id,
@@ -943,15 +954,12 @@ export async function syncNativeLocale(
sourcePath: entry.path,
}));
const translated = pending.length
? await (options.translate ?? translateNativeEntries)(
pending,
locale,
options.glossary ?? (await loadGlossary(locale)),
)
? await (options.translate ?? translateNativeEntries)(pending, locale, glossary)
: new Map<string, string>();
const artifact: NativeTranslationArtifact = {
version: 1,
locale,
glossaryHash,
entries: entries.map((entry) => ({
id: entry.id,
source: entry.source,
+17
View File
@@ -193,6 +193,23 @@ describe("native app i18n inventory", () => {
expect(second).toEqual({ changed: false, translated: 0 });
expect(await readFile(artifactPath, "utf8")).toBe(firstContents);
expect((await stat(artifactPath)).mtimeMs).toBe(firstModifiedAt);
const refreshed = await syncNativeLocale("sv", entries, {
glossary: [{ source: "Request", target: "Begäran" }],
translationsDir,
translate: async (pending) =>
new Map(pending.map((entry) => [entry.id, `refreshed:${entry.source}`])),
});
expect(refreshed).toEqual({ changed: true, translated: 4 });
const refreshedArtifact = JSON.parse(await readFile(artifactPath, "utf8")) as {
entries: Array<{ translated: string }>;
glossaryHash: string;
};
expect(refreshedArtifact.glossaryHash).toMatch(/^[a-f0-9]{64}$/u);
expect(
refreshedArtifact.entries.every((entry) => entry.translated.startsWith("refreshed:")),
).toBe(true);
} finally {
cleanupTempDirs(tempDirs);
}