feat(l10n): single-source localization pipeline

Add localization/ — one strings.json is the source of truth for every
user-facing string, and a Bun CLI generates the platform files:

  loc migrate   rebuild strings.json from existing platform files (one-time)
  loc validate  structural checks (plural `other`, arg refs, coverage)
  loc generate  emit .xcstrings (Apple) + per-language strings.xml (KMP)

Canonical `{name}` placeholders are converted to each platform's positional
printf tokens using declared arg types. Plurals use CLDR categories and emit
native forms: xcstrings plural variations and Compose <plurals> blocks.

Migration folds the KMP `transfer_file_count_one`/`_other` pair into a single
plural key, so `transferFileCountResource` now returns the plural resource and
callers resolve it with pluralStringResource.
This commit is contained in:
2026-07-20 10:15:28 +02:00
parent 9a3cfb55d0
commit cd6a2d66aa
19 changed files with 4882 additions and 1775 deletions

43
localization/src/types.ts Normal file
View File

@@ -0,0 +1,43 @@
/** Canonical localization data model. See ../README.md for the full spec. */
/** Output targets a string can be emitted to. */
export type Target = "kmp" | "apple";
export const ALL_TARGETS: Target[] = ["kmp", "apple"];
/** Argument types, used to pick the right platform placeholder token. */
export type ArgType = "string" | "int" | "double";
export interface Arg {
/** Name referenced inside translations as `{name}`. */
name: string;
type: ArgType;
}
/** CLDR plural categories. `other` is always required when `plural` is used. */
export type PluralCategory = "zero" | "one" | "two" | "few" | "many" | "other";
export type PluralForms = Partial<Record<PluralCategory, string>> & {
other: string;
};
export interface StringEntry {
/** Where the string appears / its purpose. Emitted as xcstrings comment + xml comment. */
context: string;
/** Defaults to all targets when omitted. */
targets?: Target[];
/** Ordered positional args referenced as `{name}` in translations. */
args?: Arg[];
/** Flat (non-plural) translations, keyed by language. Mutually exclusive with `plural`. */
translations?: Record<string, string>;
/** Plural translations, keyed by language then CLDR category. Mutually exclusive with `translations`. */
plural?: Record<string, PluralForms>;
}
export interface StringsFile {
sourceLanguage: string;
supportedLanguages: string[];
strings: Record<string, StringEntry>;
}
export function targetsOf(entry: StringEntry): Target[] {
return entry.targets ?? ALL_TARGETS;
}