Files
vnidrop/localization
cdricms 442a533eae fix(l10n): translate the saved-device strings
22 keys carried the English text in every locale. Copied English does not
look unfinished — it looks shipped: the key is present and non-empty, so
`validate` passes and it reaches users as a French or Russian build that
silently speaks English. 21 came in with the saved-devices work; one was
added alongside the send-cancellation fix by copying its neighbours.

Translations follow the vocabulary already established in the file —
appareil enregistré, dispositivo guardado, Kopplung, Mémoriser/Ricorda/
Zapamiętaj, and European Portuguese ficheiros/guardado.

Only format_separated_pair and format_separated_triple still read the same
in every locale. They are pure punctuation ("{first} · {second}") with no
words to translate.

Documents the rule in the README, including why it is worse than a missing
key and the trap that spread it here: copying a neighbour's shape without
checking the neighbour was translated.
2026-08-13 22:33:41 +02:00
..

Localization

Single source of truth for every user-facing string, in 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

# From the repository root:
make check-localization      # structural checks (run before committing)
make localization            # regenerate .xcstrings + strings.xml from strings.json
make localization-migrate    # one-time: rebuild strings.json from 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

{
  "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 }, typestring | int | double. Referenced in text as {name}.
  • translations — flat text per language. Mutually exclusive with plural.
  • plural — per language, per CLDR category (zero, one, two, few, many, other). other is always required.

Every language gets a real translation

Never copy the English text into another language as a placeholder. A key is not done until every language in supportedLanguages has text actually written in that language.

Copied English does not look unfinished — it looks shipped. Nothing flags it, because the key is present and non-empty, so it passes validate and reaches users as a French or Russian build that silently speaks English. It is far harder to find later than a missing key would have been.

If you cannot produce a translation, say so in the PR and leave the key out of the release rather than filling it with English. Two narrow exceptions, both of which must be obvious from the text itself:

  • Pure punctuation or layout templates with no words ("{first} · {second}").
  • Proper nouns and brand names that are identical in every language.

When adding a key next to existing ones, check that the neighbours are translated before copying their shape — a placeholder tends to be copied into the keys added after it.

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.