Ctrl K
ComunidadGuías…

docs/localization.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Strings and localization (developer guide)

How user-visible text works in iTerm2: how to add a localizable string, what to
leave in English, and the rules that keep translations correct. For translating
the app into a new language see `docs/adding-a-translation.md`; for the Info.plist
mechanism see `docs/info-plist-localization.md`.

## Overview

iTerm2 localizes with Apple **String Catalogs** (`.xcstrings`). The source
language is **English**; any untranslated string falls back to English, so
partial translations are safe. There are three kinds of catalog:

- **Code strings** - `xcstrings/Localizable.xcstrings`, keyed by symbolic keys
  (`Feature.Name`). This is what you edit when writing code. The build copies
  it to `sources/Localizable.xcstrings`, which is gitignored; never edit that copy.
- **XIB strings** - one `mul.lproj/<name>.xcstrings` per XIB, keyed by object ID.
  Populated automatically by the build's extraction step; you don't hand-write
  these.
- **Info.plist strings** - `sources/InfoPlist.xcstrings` (see the Info.plist doc).

## Adding a user-visible string

Wrap the English in a localized-string call with a **stable symbolic key**, an
English **default value**, and a **comment** that explains the context and every
placeholder. The key convention is `Feature.ShortName`.

- Swift:
  ```swift
  String(localized: "PseudoTerminal.CloseTabsHeading",
         defaultValue: "Close Tabs",
         comment: "Warning heading shown when closing multiple tabs")
  ```
- Objective-C:
  ```objc
  NSLocalizedStringWithDefaultValue(@"PseudoTerminal.CloseTabsHeading", nil,
      [NSBundle mainBundle], @"Close Tabs",
      @"Warning heading shown when closing multiple tabs")
  ```

The comment is the only context a translator gets - write it. Use
`[iTermUserDefaults userDefaults]`-style project conventions elsewhere, but for
strings always go through these calls so extraction picks them up.

## What to leave in English

Not every string should be localized. Mark such sites with a
`// Localization unneeded` comment (so a future reader knows it was considered)
and leave them as plain literals - they then never reach the catalog. Leave
English:

- **Stable identifiers and sentinels** - anything compared with `==`, used as a
  dictionary key, or persisted (a `NoSync…` user-defaults key, a Codable
  `rawValue`, a generated profile name that is looked up by that exact string).
  Localizing these forks identity across locales. If such a value is *also*
  displayed, keep the sentinel English and localize only at the display site.
- **Technical/debug output** - `DLog`/`RLog` and format skeletons. (Script-console
  diagnostics are localized, per commit c7f3e2d24, since users read them.)
- **Prompts sent to an AI model** (system/instruction prompts). These are tuned
  engineering artifacts; English maximizes instruction-following, and the user's
  language reaches the model through their query. Localize UI *about* the AI, not
  the instructions to it.
- **Scripting-API error reasons.** The `errorReason` returned through the Python
  scripting API (`ITMInvokeFunctionResponse.error.errorReason`, surfaced as
  `RPCException` messages) *is* shown to the user (in the Script Console and in
  error dialogs), so it **should** be localized. The built-in-function,
  function-call, and expression-parser error reasons are therefore localized like
  any other user-facing string. Do not add matching against these reason strings to
  tests or scripts; match on error codes/domains instead, which stay stable across
  locales.

## Write complete sentences, not injected fragments

Do **not** build a sentence by injecting a *translated word or phrase* into a
format frame:

```swift
// WRONG: "Clear"/"Reset" is a translated verb spliced into a frame.
String(localized: "…", defaultValue: "\(verb) all sessions?")
```

Word order, gender, case, and agreement differ by language, and a translator sees
the frame and the fragment separately, so the result is ungrammatical elsewhere.
Instead give each case its own **complete** localized sentence (one string per
verb/kind/state). Injecting **self-contained values** - names, counts, file
paths, hostnames, error text - is fine, because the value carries no grammar of
its own.

When a completed sentence still interpolates more than one value, use
**positional** specifiers (`%1$@`, `%2$@`) so translations can reorder them:

```
"Plugin has version %1$@ but iTerm2 expects %2$@."
```

A bare `%@` binds to the next argument in order and cannot be reordered; a
positional `%2$@` can move ahead of `%1$@`.

## Format specifiers

- `%@` / `%1$@` - a string; `%ld` / `%lld` / `%lu` / `%d` - an integer;
  `%%` - a literal percent.
- Keep count and type consistent with the English; never change `%ld` to `%@`.
- `String(localized:)` and `+[NSString localizedStringWithFormat:]` are
  locale-aware and apply the locale's digit grouping to integer specifiers
  ("10,001"), so you rarely need a `NumberFormatter`.
- Extraction quirk: `extractLocStrings` normalizes multi-specifier strings to
  positional form and **misreads a literal `%word`** (e.g. tmux's `%begin`) as a
  specifier. Move such literals out of the localized string and pass them as
  arguments; `tools/extract_strings.sh` has a guard that fails the build on a
  malformed positional specifier.

## Plurals

Wording that depends on a count ("1 file" vs "3 files") needs a String Catalog
plural, **not** a `count == 1 ? … : …` ternary - a ternary bakes in English's two
number forms, and other languages have more categories (CLDR: Chinese has one,
English two, Russian four, Arabic six).

**Source side** - pass the count as an argument:

- Swift: `String(localized: "Key", defaultValue: "… \(count) …", comment: …)`
- ObjC: `[NSString localizedStringWithFormat:NSLocalizedStringWithDefaultValue(@"Key", nil, [NSBundle mainBundle], @"… %ld …", @"…"), (long)count]`
  - you must use `localizedStringWithFormat:`, **not** `stringWithFormat:`, or the
  plural rule is never applied.

Keep the plural string to the **count argument alone**; append lists, names, or
other values *outside* it so the catalog entry stays a simple single-argument
plural rather than a multi-argument substitution.

**Catalog side** - after the key is extracted into `Localizable.xcstrings`,
replace its `en` `stringUnit` with a `variations.plural` block holding `one` and
`other` `stringUnit`s (state `new`) by editing the raw JSON - not through Xcode's
String Catalog editor - then re-run `make extract-strings` to renormalize (see
"`extract_strings.sh` is the only canonical way to write the catalog").
`xcstringstool sync` is a key-by-key merge,
not a regenerate, so it preserves those variations (and translations) across
future extractions. Verify with:

```
xcrun xcstringstool compile --output-directory <dir> xcstrings/Localizable.xcstrings
```

The key's compiled `en.lproj/Localizable.stringsdict` entry should have
`NSStringFormatSpecTypeKey = NSStringPluralRuleType`.

Preservation is conditional - any of these **silently un-pluralizes** the site:

1. Renaming the key in code - the old plural entry goes stale and a fresh flat
   key appears, so a rename means re-authoring the plural.
2. Dropping the key's code reference - it goes stale and is eventually pruned.
3. Changing the English `defaultValue` without hand-updating the variations -
   sync can't tell which plural category the flat source maps to, so keep the
   `other` value identical to the `defaultValue`.

### Plurals that also carry a value (substitutions)

When a string pluralizes on a count *and* interpolates another value (a path, a
name), use a **substitution**: a top-level value like `"%2$#@children@"` plus a
`substitutions` block whose `argNum`/`formatSpecifier` point at the count and
whose `plural` variations spell out the categories. Inside a variation value,
`%arg` refers to that substitution's own number; you may also reference other
arguments positionally. Prefer restructuring to a single-argument plural when you
can (append the extra value outside the plural string); reach for a substitution
only when the value sits mid-sentence.

Worked references: `ScriptsMenu.BumpUpgradeIncompleteSentence` (ObjC `%ld`) and
`NonTextPaste.PasteManyFilesPrompt` (Swift `%lld`) for simple plurals;
`ApplyLayout.SplitterTooFewChildren` and `Workgroup.ExitOneWorkgroupCloseSessions`
for substitutions. A `// TODO(l10n): plural` marker flags a site still using the
ternary anti-pattern.

## Curly quotes and punctuation

In user-visible English use curly quotation marks (`“ ”`, `‘ ’`), never straight
quotes - `"` only as an inch shorthand. Do not "fix" existing curly quotes to
straight ones. Translators use their own language's marks.

## How strings reach the catalog

Swift emits `.stringsdata` during the build (`SWIFT_EMIT_LOC_STRINGS=YES`); ObjC
is scanned by `extractLocStrings`. `tools/extract_strings.sh` (run by the Beta and
Deployment targets, and standalone via `make extract-strings`) merges both into
`xcstrings/Localizable.xcstrings` with `xcstringstool sync`. So a new code string
appears in the catalog after an extraction, not immediately.

### `extract_strings.sh` is the only canonical way to write the catalog

`tools/extract_strings.sh` (i.e. `make extract-strings`) owns the exact
serialization of `xcstrings/Localizable.xcstrings`: key ordering, Unicode escaping,
and whitespace. Treat its output as the canonical form and regenerate the catalog
only through it.

Do **not** let anything else rewrite the file. In particular, opening
`Localizable.xcstrings` in Xcode's String Catalog editor (or round-tripping it
through any other tool) reserializes the whole catalog in a different order, so
even a one-string change surfaces as a whole-file diff (tens of thousands of
lines) that is unreviewable and collides with every other catalog change in
flight. If you ever see such a diff, do not hand-reconcile the noise: run
`make extract-strings` and commit that as the new baseline. A catalog last
written by some other tool will otherwise make the next extraction churn the
entire file right back.

Manual JSON edits are occasionally required (the plural `variations` and
`substitutions` blocks below). Make them as small raw-text edits to the JSON, not
through the catalog editor, then run `make extract-strings` once to renormalize:
`xcstringstool sync` preserves your variations and existing translations while
restoring the canonical serialization.

## Testing a localization

Build, then launch forced into a language:

```
open Build/Development/iTerm2.app --args -AppleLanguages '(zh-Hans)'
```

Check the menu bar, Settings (Cmd-,), and a few alerts (with different counts to
exercise plurals). Menu actions are matched by stable identifiers, not titles, so
translating titles is safe.