fix(i18n): require stable IDs for translation reuse

This commit is contained in:
Vincent Koc
2026-07-12 15:07:38 +02:00
parent 7f8a647663
commit 264a45a6a3
2 changed files with 8 additions and 47 deletions
+1 -39
View File
@@ -1526,17 +1526,6 @@ export async function checkNativeLocaleArtifacts(
);
}
function choosePreviousTranslation(
source: string,
entries: readonly { translated: string }[],
): string | undefined {
const first = entries[0]?.translated;
if (!first?.trim() || first === source) {
return undefined;
}
return entries.every((entry) => entry.translated === first) ? first : undefined;
}
export async function syncNativeLocale(
locale: string,
entries: NativeI18nEntry[],
@@ -1561,38 +1550,11 @@ export async function syncNativeLocale(
// The first refresh creates the locale artifact.
}
const previousById = new Map(previous.entries.map((entry) => [entry.id, entry]));
const previousBySource = new Map<
string,
Array<{ id: string; source: string; translated: string }>
>();
for (const entry of previous.entries) {
const candidates = previousBySource.get(entry.source) ?? [];
candidates.push(entry);
previousBySource.set(entry.source, candidates);
}
const currentIds = new Set(entries.map((entry) => entry.id));
const currentSourceCounts = new Map<string, number>();
for (const entry of entries) {
currentSourceCounts.set(entry.source, (currentSourceCounts.get(entry.source) ?? 0) + 1);
}
const reusableBySource = new Map(
[...previousBySource].map(([source, candidates]) => {
const isCompleteIdChurn =
candidates.length === currentSourceCounts.get(source) &&
candidates.every((candidate) => !currentIds.has(candidate.id));
return [
source,
isCompleteIdChurn ? choosePreviousTranslation(source, candidates) : undefined,
] as const;
}),
);
const reusableById = new Map(
entries.map((entry) => {
const exact = previousById.get(entry.id);
const translated =
exact?.source === entry.source && exact.translated.trim()
? exact.translated
: reusableBySource.get(entry.source);
exact?.source === entry.source && exact.translated.trim() ? exact.translated : undefined;
return [entry.id, translated] as const;
}),
);
+7 -8
View File
@@ -663,11 +663,10 @@ describe("native app i18n inventory", () => {
const moved = await syncNativeLocale("sv", movedEntries, {
glossary: [],
translationsDir,
translate: async () => {
throw new Error("source-stable ID churn must reuse translation memory");
},
translate: async (pending) =>
new Map(pending.map((entry) => [entry.id, `moved:${entry.source}`])),
});
expect(moved).toEqual({ changed: true, translated: 0 });
expect(moved).toEqual({ changed: true, translated: 4 });
const movedArtifact = JSON.parse(await readFile(artifactPath, "utf8")) as {
entries: Array<{ id: string; source: string; translated: string }>;
};
@@ -675,10 +674,10 @@ describe("native app i18n inventory", () => {
movedEntries.map((entry) => entry.id),
);
expect(movedArtifact.entries.map((entry) => entry.translated)).toEqual([
"Hej",
"Begärans-ID: \\(requestId)",
"${apps.size} totalt, ${visibleApps.size} visas",
"Av \\(total) behörigheter har \\(granted) beviljats",
"moved:Hello",
"moved:Request ID: \\(requestId)",
"moved:Showing ${visibleApps.size} of ${apps.size}",
"moved:\\(granted) of \\(total) permissions granted",
]);
const refreshed = await syncNativeLocale("sv", entries, {