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.
This commit is contained in:
Peter Steinberger
2026-07-18 07:15:41 +01:00
committed by GitHub
parent 0d822a3495
commit e5ac755681
4 changed files with 63 additions and 26 deletions
+1 -1
View File
@@ -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, "..");
+15 -2
View File
@@ -858,6 +858,20 @@ export async function syncIosCatalog(write: boolean): Promise<AppleCatalogBuild>
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`,
);
+18 -23
View File
@@ -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) {
+29
View File
@@ -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;