# Picky i18n

Picky chrome (the menu bar panel, HUD, overlays, onboarding, notifications,
voice-state text) is translated through a single `.xcstrings` catalog.
This document is the operating manual for adding strings, adding languages,
and verifying the result.

## File layout

```
Picky/
  Resources/
    Localizable.xcstrings   ← canonical catalog (en source, ko translated)
  Localization/
    PickyLanguage.swift     ← .system / .english / .korean enum + autodetect
    LocaleManager.swift     ← runtime locale source of truth
    L10n.swift              ← non-SwiftUI helper (NSAlerts, notifications, …)
    LocalizedHostingRoot.swift ← env-locale injector for every NSHostingView
```

The catalog compiles into `en.lproj` / `ko.lproj` automatically on every
build. No manual `.strings` files.

## How a string flows

| Surface                                               | API to use                                                     |
| ----------------------------------------------------- | -------------------------------------------------------------- |
| SwiftUI `Text`, `Button`, `Toggle`, `Picker`          | `Text("my.key")` — env locale resolves it                      |
| `String` field on a view model / passed as a value    | `L10n.t("my.key")`                                             |
| Format strings with arguments                         | `L10n.t("my.key", arg1, arg2)` (uses `String(format:locale:)`) |
| Non-MainActor code (background tasks, async builders) | `L10n.t(...)` — uses a thread-safe snapshot                    |

## Switching language at runtime

1. User opens **Settings → General → App language**.
2. `LocaleManager.shared.apply(.korean)` is called.
3. `effectiveLocale` and `stringsBundle` update on the main actor.
4. The same values are mirrored into a nonisolated lock-protected snapshot
   so off-actor `L10n.t(...)` callers see the new bundle.
5. SwiftUI re-evaluates every view tree under a `LocalizedHostingRoot` and
   retranslates `Text(...)` immediately.
6. `AppleLanguages` UserDefaults is also updated so macOS-owned chrome
   (app menu, system dialogs) follows on the next cold launch.

## Adding a new key

1. Add the key to `Localizable.xcstrings` with `state: "translated"` for at
   least the required languages (en + ko by default).
2. Use the key from code:
   
   - SwiftUI: `Text("section.feature.action")`
   
   - Other: `L10n.t("section.feature.action")`
3. Run `scripts/check-localizations.sh` — fails if any language is missing.

### Format-string conventions

- Positional arguments: `%1$@`, `%2$lld`. Always use positional in localized
  format strings so translators can re-order without breaking codegen.
- Comments in the catalog explain what each `%@` means. Translators read
  comments first.

### Bold / emphasis markdown

Onboarding bubbles render markdown so action words can be amber-highlighted
(see `PickyBubbleMarkdown.highlightedAttributedText`). Wrap the same
semantic phrase in every language:

```
en: "**Click and drag** anywhere on the page to mark a region."
ko: "페이지 어디서든 **클릭&드래그**해서 영역을 표시해 보세요."
```

## Adding a new language

1. Add a case to `PickyLanguage` (e.g. `case japanese = "ja"`).
2. Add a row to `PickyLanguage.systemPreferredIdentifier`'s `supported` set.
3. Add a localization entry per key in `Localizable.xcstrings`.
4. Add the language code to `Picky.xcodeproj`'s `knownRegions`.
5. Add the new code to the default required list in `check-localizations.sh`.
6. Add a `settings.language.*` picker label key (in the destination language).
7. Update `PickyLanguage.displayKey` so the new enum case returns that label key.
8. Build — Xcode auto-emits the matching `.lproj`.

That's it. No string substitutions, no view-by-view migration; every place
that already calls `Text("key")` or `L10n.t("key")` will see the new
translation automatically.

## QA checklist

Run through the following with the language toggle in both states:

- [ ] First launch: prerequisites view bullets and rows
- [ ] Onboarding (Settings → Onboarding → Replay): every cursor bubble
  
      and the TTS voice line per beat. Bold/amber highlights land on the
      right phrase.
- [ ] HUD cards: right-click menu (every entry), inline terminal toggle,
  
      "View as TUI · N more" hint, queue Clear-all
- [ ] Compose / steer flow: composer footer hint, error banners on send
  
      failure
- [ ] Archive: toast title + Undo button
- [ ] Notifications: trigger a completed Pickle, then a failed one,
  
      then a waiting-for-input request
- [ ] Settings panels: General, Picky, Pickle, Notification, Cursor &
  
      Bubbles, Voice, Shortcuts; titles and subtitles in both the index
      and the section headers
- [ ] Status tab: build line + last-checked line interpolation, channel
  
      picker + auto-check toggle
- [ ] Feedback: every field label, send/sending/sent states
- [ ] Voice: dictation no-speech-detected message
- [ ] Quick Input: error fallback when send fails
- [ ] Startled cursor exclamation: switch language, dock to a screen
  
      edge, observe the cursor; the exclamation should match the chosen
      language

## Remediation backlog

The current implementation gaps and delivery order discovered by the
application-wide audit are tracked in
[`i18n-remediation-plan.md`](i18n-remediation-plan.md). Consult that plan before
claiming a newly touched user-facing surface is fully localized.
