Files
vnidrop/localization/README.md
cdricms cd6a2d66aa 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.
2026-07-20 10:15:28 +02:00

88 lines
3.5 KiB
Markdown

# Localization
Single source of truth for every user-facing string, in [`strings.json`](strings.json).
A Bun CLI generates the platform-native files from it:
| Target | Output | Notes |
| --- | --- | --- |
| `apple` | `apple/VniDrop/Resources/Localizable.xcstrings` | one catalog, all languages nested |
| `kmp` | `shared/src/commonMain/composeResources/values[-lang]/strings.xml` | one file per language |
## Workflow
```bash
cd localization
bun run src/cli.ts validate # structural checks (run before committing)
bun run src/cli.ts generate # regenerate .xcstrings + strings.xml from strings.json
bun run src/cli.ts migrate # one-time: rebuild strings.json from existing platform files
```
**Never edit the generated `.xcstrings` / `strings.xml` by hand** — edit `strings.json` and
regenerate. Regenerated output is deterministic (sorted keys), so diffs stay small.
## `strings.json` format
```jsonc
{
"sourceLanguage": "en",
"supportedLanguages": ["en", "fr"],
"strings": {
"send_title": {
"context": "Send tab — screen title.",
"translations": { "en": "Send", "fr": "Envoyer" }
},
"send_selected_files_count": {
"context": "Send flow — number of files chosen before creating a transfer.",
"targets": ["kmp", "apple"],
"args": [{ "name": "count", "type": "int" }],
"plural": {
"en": { "one": "{count} file selected", "other": "{count} files selected" },
"fr": { "one": "{count} fichier sélectionné", "other": "{count} fichiers sélectionnés" }
}
}
}
}
```
### Fields
- **`context`** *(required)* — where the string appears and its purpose. Emitted as the
`.xcstrings` comment and an XML comment; also the note translators see.
- **`targets`** *(optional)*`["kmp", "apple"]`. Omit to mean **all** targets.
- **`args`** *(optional)* — ordered list of `{ name, type }`, `type``string | int | double`.
Referenced in text as `{name}`.
- **`translations`** — flat text per language. **Mutually exclusive with `plural`.**
- **`plural`** — per language, per [CLDR category](https://cldr.unicode.org/index/cldr-spec/plural-rules)
(`zero`, `one`, `two`, `few`, `many`, `other`). `other` is always required.
### Placeholders
Write named tokens `{count}`, `{name}` in text. The generator converts them to the right
positional token per platform, using the declared `type`:
| type | Apple | Android/KMP |
| --- | --- | --- |
| `string` | `%N$@` | `%N$s` |
| `int` | `%N$d` | `%N$d` |
| `double` | `%N$f` | `%N$f` |
A literal `%` in text is emitted as `%%` whenever the string has args.
## Adding a language
Add its code to `supportedLanguages`, fill in `translations` / `plural` for each key, then
`generate`. KMP gets a new `values-<lang>/strings.xml`; Apple gets the language inside the
single catalog. `validate` warns about any key still missing that language.
## Migration notes (from the initial import)
- Apple keys that were literal English strings (`"%@ · %@"`) were imported verbatim — rename
them to semantic keys and update the Swift call sites.
- Arg names default to `arg1`, `arg2`… (a lone int arg becomes `count`). Rename for clarity;
keep the `{token}` in text in sync.
- Folding `transfer_file_count_one` / `_other` into the plural key `transfer_file_count`
requires switching the KMP call site from `Res.string.transfer_file_count_one` to the
Compose plural API (`pluralStringResource(Res.plurals.transfer_file_count, count, count)`),
and the Apple side to automatic plural inflection.