From e5ac755681343d90032a31014764a7c550a79801 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 18 Jul 2026 07:15:41 +0100 Subject: [PATCH] fix(i18n): regenerate derived app catalogs inside native:i18n:sync (#110454) Rewriting apps/.i18n/native-source.json without regenerating the derived Android/Apple artifacts lands a stale iOS catalog and turns the repo-wide apple/android i18n checks red for every PR (this is how main broke after recent app-string additions: the inventory and translations were committed while Localizable.xcstrings was not regenerated). native-app-i18n.ts sync --write now chains syncAndroidAppI18n and a new exported syncAppleAppI18n (catalog + InfoPlist strings) so one command leaves the whole i18n tree consistent. The shared locale list moves to scripts/native-i18n-locales.ts to break the import cycle between the inventory scanner's top-level CLI await and the derived generators. --- scripts/android-app-i18n.ts | 2 +- scripts/apple-app-i18n.ts | 17 ++++++++++++-- scripts/native-app-i18n.ts | 41 +++++++++++++++------------------- scripts/native-i18n-locales.ts | 29 ++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 26 deletions(-) create mode 100644 scripts/native-i18n-locales.ts diff --git a/scripts/android-app-i18n.ts b/scripts/android-app-i18n.ts index a6e0ddc0ddf4..2f7cf65d28ae 100644 --- a/scripts/android-app-i18n.ts +++ b/scripts/android-app-i18n.ts @@ -3,7 +3,7 @@ import { readdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { expectDefined } from "../packages/normalization-core/src/expect.js"; -import { NATIVE_I18N_LOCALES } from "./native-app-i18n.ts"; +import { NATIVE_I18N_LOCALES } from "./native-i18n-locales.ts"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(HERE, ".."); diff --git a/scripts/apple-app-i18n.ts b/scripts/apple-app-i18n.ts index 2b265a6834fb..b080e54dbac8 100644 --- a/scripts/apple-app-i18n.ts +++ b/scripts/apple-app-i18n.ts @@ -858,6 +858,20 @@ export async function syncIosCatalog(write: boolean): Promise return build; } +/** + * Regenerates every Apple derived artifact (iOS catalog, contradiction report, + * InfoPlist strings). Shared by this CLI and native-app-i18n's sync so the + * inventory can never be rewritten without its derived catalogs. + */ +export async function syncAppleAppI18n(): Promise<{ + build: AppleCatalogBuild; + infoPlistFiles: number; +}> { + const build = await syncIosCatalog(true); + const infoPlistFiles = await syncIosInfoPlist(true); + return { build, infoPlistFiles }; +} + export async function checkAppleAppI18n() { await validateRuntimeInterpolationPaths(); for (const [sourcePath, contracts] of Object.entries(LOCALIZED_WRAPPER_CONTRACTS)) { @@ -956,8 +970,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.ar if (command === "check") { await checkAppleAppI18n(); } else if (command === "sync-ios" && flag === "--write") { - const build = await syncIosCatalog(true); - const infoPlistFiles = await syncIosInfoPlist(true); + const { build, infoPlistFiles } = await syncAppleAppI18n(); process.stdout.write( `apple-app-i18n: synced iOS catalog and ${infoPlistFiles} InfoPlist files; contradictions=${build.contradictions.length}\n`, ); diff --git a/scripts/native-app-i18n.ts b/scripts/native-app-i18n.ts index be38361a924d..783e5153b2eb 100644 --- a/scripts/native-app-i18n.ts +++ b/scripts/native-app-i18n.ts @@ -5,32 +5,11 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import pMap from "p-map"; import { expectDefined } from "../packages/normalization-core/src/expect.js"; import { translateNativeEntries } from "./control-ui-i18n.ts"; +import { NATIVE_I18N_LOCALES } from "./native-i18n-locales.ts"; type NativeI18nSurface = "android" | "apple"; -export const NATIVE_I18N_LOCALES = [ - "zh-CN", - "zh-TW", - "pt-BR", - "de", - "es", - "ja-JP", - "ko", - "fr", - "hi", - "ar", - "it", - "tr", - "uk", - "id", - "pl", - "th", - "vi", - "nl", - "fa", - "ru", - "sv", -] as const; +export { NATIVE_I18N_LOCALES }; export type NativeI18nEntry = { id: string; @@ -1642,6 +1621,22 @@ async function main() { if (parsed.locale) { await syncNativeLocale(parsed.locale, entries); } + if (parsed.command === "sync" && parsed.write) { + // The inventory and native/*.json feed the generated Android/Apple app + // artifacts. Regenerate them in the same write; a sync that stops at the + // inventory lands stale derived catalogs and turns the repo-wide + // android/apple i18n checks red. Lazy imports keep check/locale-only runs + // from loading the derived generators. + const [{ syncAndroidAppI18n }, { syncAppleAppI18n }] = await Promise.all([ + import("./android-app-i18n.ts"), + import("./apple-app-i18n.ts"), + ]); + await syncAndroidAppI18n(); + const apple = await syncAppleAppI18n(); + process.stdout.write( + `native-app-i18n: synced derived artifacts (android, iOS catalog, ${apple.infoPlistFiles} InfoPlist files); contradictions=${apple.build.contradictions.length}\n`, + ); + } } if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { diff --git a/scripts/native-i18n-locales.ts b/scripts/native-i18n-locales.ts new file mode 100644 index 000000000000..f4725bd7691b --- /dev/null +++ b/scripts/native-i18n-locales.ts @@ -0,0 +1,29 @@ +/** + * Shared native-app locale list. Lives outside native-app-i18n.ts so the + * derived generators (android-app-i18n, apple-app-i18n) can import it without + * creating a cycle with native-app-i18n's top-level CLI await, which chains + * those generators after rewriting the inventory. + */ +export const NATIVE_I18N_LOCALES = [ + "zh-CN", + "zh-TW", + "pt-BR", + "de", + "es", + "ja-JP", + "ko", + "fr", + "hi", + "ar", + "it", + "tr", + "uk", + "id", + "pl", + "th", + "vi", + "nl", + "fa", + "ru", + "sv", +] as const;