localship 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: localship-extract
3
- description: Make an Android or iOS app localizable by moving text hardcoded in Kotlin, XML layouts or Swift into string resources or a String Catalog, without changing behavior. Use when an app has no translation files yet, when localship detect finds nothing, or when the user asks to find hardcoded strings.
3
+ description: Make an Android or iOS app localizable by moving text hardcoded in Kotlin, XML layouts or Swift into string resources or a String Catalog, without changing behavior. Use when the user wants text ready for translation or hardcoded strings found, however they phrase it, and as the first step when they want the app in another language but its text is still in the code.
4
4
  ---
5
5
 
6
6
  # Make an app localizable
@@ -14,9 +14,8 @@ the rules below keep it safe. Change how text is looked up, never what the app d
14
14
  `localship detect` (or `localship_detect`) gives a first estimate: how much UI text is still in the
15
15
  code, per module, with examples. Use it to size the work, then search properly as below.
16
16
 
17
- Search the UI code for user-facing literals and show the user a summary grouped by screen or
18
- feature, with counts. Ask whether to do the whole app or one feature first. For big apps, go one
19
- feature at a time and build after each.
17
+ Do the scope the user wants: the whole app unless they named particular screens or features. Search that code for user-facing literals, say in one line how
18
+ much you found, and start. Build as you go, and give a short progress update after each part.
20
19
 
21
20
  **Android (Kotlin, Compose, Views)**, look for string literals in:
22
21
  - `Text("…")`, `text = "…"`, `label = { Text("…") }`, `title = "…"`, `placeholder`, `contentDescription = "…"`
@@ -32,59 +31,136 @@ feature at a time and build after each.
32
31
  `alert`/`confirmationDialog` text passed as `String`, `Button(title)` with a `String`
33
32
  - UIKit: `label.text = "…"`, `button.setTitle("…", for:)`, `title = "…"`
34
33
  - notification content, widget text, App Intents titles
34
+ - default UI text in parameters (`title: String = "Cancel"`): not extracted until the parameter
35
+ is a `LocalizedStringResource` or the default is wrapped
36
+
37
+ **Sentences built from English grammar** (both platforms). Wrapping these in a resource does
38
+ nothing; search for them and rewrite each sentence whole:
39
+ - pieces joined around a value (`"Finished " + amount + " over your plan."`, often so the value
40
+ can be styled): one complete sentence per case with placeholders, styling the value inside it
41
+ (`AttributedString(localized:)`; Android annotations or spans on the placeholder)
42
+ - plural endings in code (`count == 1 ? "" : "s"`, `== 1 ?` near text): plural resources
43
+ - helpers that spell out numbers, write ordinals or choose "a"/"an" (names with `spell`,
44
+ `ordinal`, `article`): formatters (`.formatted(.number.notation(.spellOut))`, an ordinal
45
+ `NumberFormatter`; ICU `RuleBasedNumberFormat` and `MessageFormat` on Android) or one sentence
46
+ per case
47
+ - text surgery on UI strings (`replacingOccurrences`, `replace`, `removePrefix`): store both forms
48
+ - lists (`joined(separator: ", ")` plus "and"): `.formatted(.list(type: .and))`,
49
+ `ListFormatter`, or ICU `ListFormatter` on Android
50
+
51
+ Rewrite even when it adds variants, and keep the English output identical so tests still pass.
35
52
 
36
53
  **Leave alone:** log messages, analytics event names, preference and database keys, routes and
37
54
  deep links, test tags and accessibility identifiers, URLs, format patterns, debug-only screens,
38
- and anything never shown to users. Brand names can stay in code, or become resources marked
39
- not translatable.
55
+ and anything never shown to users. Also text that doubles as data: enum raw values, `case`
56
+ patterns, query item names, dictionary keys, and any string that's parsed back, compared or saved
57
+ (picker values parsed into intervals, values written to export files). To show those translated,
58
+ add a separate display property and keep the stored value as it is. Brand names can stay in code,
59
+ or become resources marked not translatable.
40
60
 
41
61
  ## 2. Android rules
42
62
 
43
63
  - Put strings in `res/values/strings.xml` of the module that uses them. Create the file if needed:
44
64
  `<?xml version="1.0" encoding="utf-8"?>` then `<resources>…</resources>`.
45
- - Key names: `screen_role` in snake_case (`budget_title`, `budget_save_button`). Reuse an existing
46
- key when the text *and* meaning are the same; otherwise make a new one.
47
- - Add a comment above each string saying where it appears and what placeholders mean:
48
- `<!-- Budget screen: button that saves the plan. %1$s is the month name -->`
65
+ Keep a default for every resource the app uses; a definition only in `values-en` is not a default.
66
+ Check the merged resources with Android lint for the relevant build variants, especially when
67
+ libraries, flavors or multiple source sets supply defaults.
68
+ - Key names and comments: follow the project's existing resources. If there are none, use
69
+ `screen_role` in snake_case (`settings_title`, `settings_save_button`) with a comment above each
70
+ string saying where it appears and what placeholders mean
71
+ (`<!-- Settings screen: saves changes. %1$s is the profile name -->`), and say so in your report.
72
+ Reuse an existing key when the text *and* meaning are the same; otherwise make a new one.
49
73
  - Escape `'` as `\'` and `"` as `\"`; `&` as `&amp;`.
50
74
  - **Never concatenate** text: one string with numbered placeholders (`%1$s`, `%2$d`) per sentence,
51
75
  so translators can reorder.
52
76
  - **Counts use plurals:** `<plurals name="…"><item quantity="one">%d item</item><item quantity="other">%d items</item></plurals>`.
53
77
  - Compose: `stringResource(R.string.key)`, `stringResource(R.string.key, arg)`,
54
78
  `pluralStringResource(R.plurals.key, count, count)`.
55
- - Outside composables: `context.getString(…)` / `resources.getQuantityString(…)`. Don't pass a
56
- `Context` into a ViewModel: expose `@StringRes` ids, or a small `UiText` wrapper resolved in the UI.
79
+ - Outside composables: `context.getString(…)` / `resources.getQuantityString(…)`. For text built
80
+ in view models or other non-UI code, follow the pattern the project already uses. If it has
81
+ none, expose `@StringRes` ids or a small `UiText` wrapper resolved in the UI rather than passing
82
+ a `Context`, and say so in your report.
57
83
  - Money, numbers and dates go through formatters (`NumberFormat`, `DateTimeFormatter.ofLocalizedDate`),
58
- never hand-built strings.
84
+ never hand-built strings. Keep the amount signed and set its actual currency; changing the display
85
+ language must not change the currency. Respect the user's regional and numbering preferences.
59
86
  - Brand names that must not change: `<string name="app_name" translatable="false">…</string>`.
87
+ - Check `android:supportsRtl`, start/end spacing and alignment, and directional icons. Mirror
88
+ directional navigation affordances when appropriate; do not mirror every image or chart blindly.
89
+ - Localship handles plain locale folders. Qualified string resources such as `values-ar-night`
90
+ remain separate; a coverage warning must be resolved with platform checks, not by moving those
91
+ strings into a plain locale folder or removing the qualifiers.
60
92
 
61
93
  ## 3. iOS rules
62
94
 
63
- - A String Catalog must exist. If there is none, ask the user to add one in Xcode
64
- (File › New › File from Template › String Catalog, named `Localizable`) or create
65
- `Localizable.xcstrings` in the app target's folder containing
66
- `{"sourceLanguage" : "en", "strings" : {}, "version" : "1.0"}`. In Xcode 16 synced folders it
67
- is picked up automatically; otherwise it must be added to the target in Xcode.
95
+ - **Generated projects:** when `project.yml` (XcodeGen) or `Project.swift` (Tuist) exists, make
96
+ every project change there and regenerate; never edit the `.pbxproj` or rely on changes made in
97
+ Xcode, which regeneration drops. In XcodeGen that's `options: developmentLanguage: <lang>` and,
98
+ under `settings: base:`, `SWIFT_EMIT_LOC_STRINGS: YES` and
99
+ `LOCALIZATION_PREFERS_STRING_CATALOGS: YES`, with each catalog inside a source folder of its
100
+ target.
101
+ - Every target that shows text (the app, widgets and other extensions) needs its own catalog in
102
+ its sources.
103
+ - Use String Catalogs and the project's actual development language; never assume English.
104
+ Reuse existing tables. For a new catalog, use Xcode's localization preparation tools when
105
+ available, or create `Localizable.xcstrings` in the appropriate target with `sourceLanguage`
106
+ matching the development language, an empty `strings` object and `version: "1.0"`.
107
+ Verify target membership and resource inclusion, including extensions and Swift packages.
108
+ - Verify **Use Compiler to Extract Swift Strings** (`SWIFT_EMIT_LOC_STRINGS`) for the relevant
109
+ targets. Build all relevant schemes/targets and supported platforms before translating;
110
+ building only the main app may miss platform-conditional strings and extension resources.
111
+ - **Filling the catalog:** a command-line build (`xcodebuild`) writes `.stringsdata` files into
112
+ DerivedData but leaves the `.xcstrings` unchanged; only Xcode's catalog editor merges them. An
113
+ empty catalog after a clean build doesn't mean extraction failed. Sync each catalog from its
114
+ own target's build output:
115
+ ```sh
116
+ xcrun xcstringstool sync <Target>/Localizable.xcstrings \
117
+ --stringsdata <DerivedData>/Build/Intermediates.noindex/<Project>.build/<Config>-<sdk>/<Target>.build/Objects-normal/*/*.stringsdata
118
+ ```
119
+ - **Keys with no words** (`" "`, `"·"`, `"%@%@"`, `"—"`) come from text built in pieces.
120
+ Localship skips them; mark them `"shouldTranslate" : false` in the catalog so Xcode's progress
121
+ agrees, and check whether each one is a sentence split into pieces that should be rewritten.
122
+ - For copy-heavy models (personalities, tips, onboarding pages), changing an initializer
123
+ parameter from `String` to `LocalizedStringResource` extracts all the copy passed to it.
68
124
  - `Text("literal")`, `Button("literal")`, `Label("literal", …)` are extracted by Xcode
69
- automatically. Add a comment where the meaning isn't obvious: `Text("Save", comment: "Saves the budget")`.
70
- - Text in variables: make it `String(localized: "…", comment: "…")` where it's created, or type the
71
- property as `LocalizedStringResource` / `LocalizedStringKey`.
125
+ automatically. Add a comment where the meaning isn't obvious: `Text("Save", comment: "Saves the profile")`.
126
+ - Prefer `LocalizedStringResource` for passing localizable text through models and custom views;
127
+ use `String(localized:)` where a resolved string is needed. Avoid caching resolved translations
128
+ across language changes. `LocalizedStringKey` also works for SwiftUI-specific view APIs.
129
+ - Specify the right table and bundle for non-default catalogs, frameworks and packages.
130
+ On toolchains supporting it, `#bundle` refers to the current target's resources; otherwise use
131
+ the appropriate bundle, such as `Bundle.module` in a Swift package, rather than assuming `.main`.
132
+ - Xcode 26+ can generate typed `LocalizedStringResource` symbols for manually managed keys;
133
+ respect an existing symbol-based workflow. It also offers automatic translator comments;
134
+ review generated comments and add app-specific meaning and placeholder context.
72
135
  - Use `Text(verbatim:)` for text that must never be translated (codes, user content).
73
136
  - Interpolations become placeholders automatically: `Text("\(count) items")` → key `%lld items`.
74
- Counts need plural forms: list those strings and tell the user to right-click them in the
75
- catalog › Vary by Plural, since that's done in Xcode.
137
+ Counts need plural forms: after syncing, find the keys with count placeholders (`%lld`,
138
+ `%d`) and give them plural variations (**Vary by Plural** in Xcode, or `variations.plural` in
139
+ the catalog JSON with the forms the source language needs). Whatever you can't convert, list
140
+ in your report so the user can do it in Xcode before translating.
141
+ Multiple independent counts require substitutions; device wording may require device variants.
142
+ Localship cannot translate or validate those advanced entries yet. Keep their structure and
143
+ use Xcode to handle them; never flatten them to make Localship's checks pass.
76
144
  - Money, numbers and dates: `.formatted()` and `FormatStyle`, never hand-built strings.
77
- - **Build once** (in Xcode, or `xcodebuild … build`) so Xcode collects the strings into the catalog.
145
+ - Include user-facing permission descriptions and other localizable Info.plist values through
146
+ `InfoPlist.xcstrings`, plus storyboard/xib and App Intents text where the app uses them.
147
+ - Xcode 27 has native agent localization and preview tools. When available, use them for project
148
+ preparation and visual review, keeping the same glossary and voice guidance. If they edit
149
+ translations tracked by Localship, review the resulting hand edits before adopting them.
78
150
 
79
151
  ## 4. Check nothing was missed
80
152
 
81
- - The app must build after each chunk. Don't change logic, layout or behavior.
153
+ - The app must build after each chunk. Don't change logic, layout or behavior. Run Android lint
154
+ for affected variants, or build the affected Apple targets/platforms; file validation alone
155
+ cannot verify resource merging, extraction or runtime lookups.
82
156
  - Android: enable pseudolocales (`android { buildTypes { debug { isPseudoLocalesEnabled = true } } }`),
83
157
  switch the device to "English (XA)", and look for text that isn't accented: that text is still hardcoded.
84
158
  "AR (XB)" shows right-to-left layout problems.
85
159
  - iOS: Scheme › Run › Options › App Language › "Accented Pseudolanguage" (and "Right-to-Left
86
- Pseudolanguage"). Plain English left over was missed.
87
- - Ask the user to run these if you can't; list the screens to check.
160
+ Pseudolanguage"). Plain English left over was missed. Without Xcode's UI, a scripted check works:
161
+ add a temporary language whose values wrap the English in markers (`⟦Save⟧`), launch with
162
+ `-AppleLanguages "(xx)"`, look for text without markers, then remove the temporary language.
163
+ - If you can't run these, list the screens for the user to check.
88
164
 
89
165
  ### Text the resource files can't show
90
166
 
@@ -94,21 +170,29 @@ for them while you're in it:
94
170
  - **Names stored in data.** Categories, accounts or templates seeded in English and saved to a
95
171
  database are shown as stored. Translate them at display time while they're still the seeded
96
172
  value (a map from seeded name to string resource); never translate what the user typed.
173
+ Use the translated name only for showing it: anything saved (database rows, defaults,
174
+ exports, a name copied into a new record) keeps the stored value, or it stays in whatever
175
+ language the app had when it was saved.
97
176
  - **Text built once and kept.** View models, singletons and caches that build sentences keep the
98
177
  old language after an in-app language switch. Rebuild them when the locale changes; on Android,
99
178
  view models survive the activity being recreated for the new language.
100
- - **Charts drawn by hand.** A Canvas or custom drawing doesn't mirror in right-to-left languages,
101
- but the labels laid out next to it do, so the bars end up under the wrong labels. Mirror the
102
- drawing when the layout direction is right to left.
103
- - **Signs and symbols on amounts.** `"-" + amount` comes out as `$60-` in Arabic. Keep a signed
104
- amount in one left-to-right run (wrap it in U+2066 … U+2069). Android's Arabic number
105
- format also starts with an invisible right-to-left mark (U+200F), which can cut the symbol
106
- off single-line figures; strip it.
107
- - **Dates and months** formatted with the device's locale instead of the app's: use the app's
108
- locale so they follow an in-app language switch.
179
+ - **Charts drawn by hand.** Check that custom drawing and surrounding labels agree in RTL.
180
+ Decide mirroring based on the chart's meaning and axis conventions, and test it on a device.
181
+ - **Signs and symbols on amounts.** Format the signed value as one unit with the platform's
182
+ number/currency formatter. Preserve its Unicode direction marks. Do not prepend a minus sign,
183
+ strip U+200F or force every amount to LTR. For dynamic text inserted into an Android localized
184
+ message, use context-aware `BidiFormatter.unicodeWrap()` unless already wrapped. On Apple,
185
+ use localized interpolation and natural text direction. Test positive/negative amounts,
186
+ currency symbols and mixed-script text in the actual UI before adjusting directionality.
187
+ - **Dates and months.** Check that language switching refreshes labels while formatters respect
188
+ intended regional preferences; language, region, calendar and currency are separate choices.
189
+
190
+ Use the [platform verification guide](references/platform-localization.md) for current official
191
+ references and the final test checklist.
109
192
 
110
193
  ## 5. Then
111
194
 
112
- Run `localship detect` (or `localship_detect`): it should now find the strings. Continue with the
113
- `localship-setup` skill. Commit nothing unless the user asks; suggest committing the extraction on its
114
- own before translating, so the diff is easy to review.
195
+ Run `localship detect` (or `localship_detect`): it should now find the strings. Report what you
196
+ moved, and any convention you had to pick. If the user also asked for translation, continue with
197
+ the `localship` skill. Don't create branches or commit unless the user asks; if they do commit,
198
+ suggest committing the extraction on its own, so the diff is easy to review.
@@ -0,0 +1,65 @@
1
+ # Platform verification
2
+
3
+ Guidance checked against official Android and Apple documentation on October 17, 2026.
4
+ Check the project's installed toolchain and deployment targets before applying newer APIs.
5
+
6
+ ## Android
7
+
8
+ - Keep all needed default strings in unqualified `values` resources. Verify resource merging
9
+ and references with the relevant Gradle lint/build tasks. A Localship target is a resource
10
+ directory, not a model of every dependency, flavor and source set in the APK.
11
+ - Plain locale folders are supported. Strings in other configurations are preserved and reported
12
+ as uncovered; `--strict` and `--require-approved` fail for those files. Do not remove qualifiers
13
+ or copy conditional overrides into base resources to silence this diagnostic.
14
+ - Test supported languages with the system and any in-app picker, including persistence and
15
+ switching while screens and view models remain alive.
16
+ - Format signed currency values using their actual currency and the intended display locale.
17
+ Preserve direction marks produced by formatters; use bidi-aware handling for embedded text.
18
+
19
+ Official references:
20
+
21
+ - [Localization and default resources](https://developer.android.com/guide/topics/resources/localization)
22
+ - [String resources and plurals](https://developer.android.com/guide/topics/resources/string-resource)
23
+ - [Per-app language setup and migration](https://developer.android.com/guide/topics/resources/app-languages)
24
+ - [RTL, layout mirroring and BidiFormatter](https://developer.android.com/training/basics/supporting-devices/languages)
25
+ - [Pseudolocales](https://developer.android.com/guide/topics/resources/pseudolocales)
26
+
27
+ ## Apple platforms
28
+
29
+ - Use the actual development language, correct table/bundle, and compiler extraction. Build
30
+ relevant targets and platforms before treating catalogs as complete; include extensions,
31
+ packages, Info.plist text and interface resources used by the app.
32
+ - Command-line builds write `.stringsdata` but don't update catalogs; sync them with
33
+ `xcrun xcstringstool sync`. In XcodeGen or Tuist projects, make localization settings in
34
+ `project.yml` or `Project.swift`, since regenerating drops changes made in Xcode.
35
+ - Keep device variants and substitutions intact. Localship currently models plain strings and
36
+ one level of plural string units. Advanced entries remain in the catalog but block strict
37
+ completeness checks. Handle them in Xcode and report that Localship cannot certify the whole
38
+ catalog; do not set `shouldTranslate: false` merely to bypass validation.
39
+ - Generated symbols are optional: preserve the project's chosen extraction/manual-key workflow.
40
+ Review generated comments for accurate context. Native Xcode agent tools can prepare catalogs
41
+ and preview translations; align them with the project's terminology and review their edits.
42
+
43
+ Official references:
44
+
45
+ - [String Catalogs, plurals and device variants](https://developer.apple.com/documentation/Xcode/localizing-and-varying-text-with-a-string-catalog)
46
+ - [Compiler extraction, bundles and substitutions](https://developer.apple.com/videos/play/wwdc2023/10155/)
47
+ - [Xcode 26 comments, symbols and resource bundles](https://developer.apple.com/videos/play/wwdc2025/225/)
48
+ - [Generated localizable symbols](https://developer.apple.com/documentation/xcode/using-generated-localizable-symbols-in-your-code)
49
+ - [Xcode 27 agent localization and visual review](https://developer.apple.com/videos/play/wwdc2026/213/)
50
+
51
+ ## Release verification
52
+
53
+ 1. Run Localship validation; review errors, warnings and coverage gaps. After approving the
54
+ selected locales, run `localship validate --require-approved --locale <locales>`.
55
+ 2. Run the native build and applicable Android lint tasks. Resolve missing defaults and wrong
56
+ target/bundle membership. File validation cannot replace these checks.
57
+ 3. Exercise pseudolocales and actual languages: long text, tall scripts, RTL and large system
58
+ text sizes. Look for clipping, incorrect alignment, inaccessible labels and layout changes.
59
+ 4. Check plural boundaries appropriate to each locale, signed/zero amounts, mixed-script names,
60
+ and date/calendar/number formatting. Preserve user-entered content.
61
+ 5. Test language switching, navigation, notifications, widgets and cached/seeded labels where
62
+ the app has them. Get native-speaker feedback on meaning and tone.
63
+
64
+ Report which checks ran and which could not. Approval records do not prove linguistic quality,
65
+ and a passing check for supported files does not establish correct runtime behavior.
@@ -23,7 +23,8 @@ installed, ask to install it once with `npm install -g localship`.
23
23
  localship next-batch --locale hi --states draft
24
24
  ```
25
25
  Each item shows `source` and the translation as `current`, with the comment, plural forms
26
- and glossary. Read `.localship/brief.md` and the locale's voice first.
26
+ and glossary. A `note` says how the translator read an ambiguous source: check that reading
27
+ first, and mention it to the user if you're not sure. `role` says where it appears. Read `.localship/brief.md` and the locale's voice first.
27
28
  To review translations made before Localship, use `--states untracked`; for hand edits,
28
29
  `--states edited`. Fixing those replaces a person's work, so ask before resubmitting, and
29
30
  pass `--force` only then.
@@ -49,4 +50,12 @@ installed, ask to install it once with `npm install -g localship`.
49
50
  - `--from edited` accepts hand edits; `--from untracked` adopts translations made before
50
51
  the tool.
51
52
 
52
- Run `localship validate` at the end; it must have no errors. Commit nothing unless asked.
53
+ Run `localship validate` at the end; it must have no errors. Report coverage warnings separately:
54
+ unsupported variants are not reviewed just because the supported entries passed. After approval,
55
+ use `localship validate --require-approved --locale <reviewed-locales>` for the release check.
56
+ Do not auto-approve or flatten unsupported entries to satisfy that check.
57
+
58
+ For a release, follow the [platform verification checklist](../localship-extract/references/platform-localization.md):
59
+ native build/lint, pseudolocales, large text, RTL, language switching and native-speaker review.
60
+ Use Xcode preview tools when available for visual checks, and report what still needs a device.
61
+ Commit nothing unless asked.
@@ -9,8 +9,8 @@ Localship keeps translations in the repo's own files (Android `res/values-*/stri
9
9
  gettext `.po`, Apple `.xcstrings`) and checks every translation in code. You do the parts
10
10
  that need judgment: choosing what to set up, and writing the context translators use.
11
11
 
12
- > For a direct request ("translate this app to Hindi"), start with the `localship` skill: it
13
- > plans once, asks for one confirmation, and then uses this playbook without more questions.
12
+ > For a direct request ("translate this app to Hindi"), start with the `localship` skill instead:
13
+ > it sets up only the config and translates.
14
14
 
15
15
  ## Running the tool
16
16
 
@@ -19,9 +19,6 @@ that need judgment: choosing what to set up, and writing the context translators
19
19
  `npm install -g localship` (running it through `npx` every time is slow).
20
20
  - Add `--json` when you need to read the output.
21
21
 
22
- Before writing anything, check `git status`. If there are uncommitted changes, mention it, so the
23
- user can tell your changes apart from theirs.
24
-
25
22
  ## 1. Find out where the project stands
26
23
 
27
24
  Run `localship status`. If it works, the project is already set up (if it warns that most text is still in the code, go to case C): go to step 5 (context) if
@@ -30,20 +27,19 @@ pick the case that fits:
30
27
 
31
28
  | What detect shows | Case |
32
29
  |---|---|
33
- | A warning that most of the app's text is still in the code (more UI text in code than strings in resources) | **C. Text is hardcoded**, even if a few strings exist: run `localship-extract` first, or translations will cover only part of the app. Tell the user how much is in code |
30
+ | A warning that most of the app's text is still in the code (more UI text in code than strings in resources) | **C. Text is hardcoded**, even if a few strings exist: tell the user how much is in code; translations cover only what's in resources until it's moved (the `localship-extract` skill, if they ask) |
34
31
  | Setups with existing languages (`hi, ar` …) | **A. Already localized** |
35
32
  | Setups with strings but no languages | **B. Ready to translate** |
36
- | An empty String Catalog (0 strings) | Ask the user to build the app in Xcode once, then detect again |
33
+ | An empty String Catalog (0 strings) | Check compiler extraction and target membership, build the relevant targets and platforms, and sync the catalog from the build (`xcrun xcstringstool sync`; a command-line build alone leaves it empty, see `localship-extract`), then detect again |
37
34
  | A gettext template with no translations | **B**, after confirming where new `.po` files go (detect suggests a path) |
38
- | Nothing, with a hint about Android or Xcode | **C. Text is hardcoded**: run the `localship-extract` skill first, then come back |
35
+ | Nothing, with a hint about Android or Xcode | **C. Text is hardcoded**: nothing to set up until the text is in resources; the `localship-extract` skill moves it, if the user asks |
39
36
  | Only formats listed as "not supported yet" (Flutter ARB, JSON, `.strings` …) | Say so plainly; Localship can't translate those yet |
40
37
 
41
- ## 2. Ask, then init
38
+ ## 2. Init
42
39
 
43
- Show the user what was found and ask:
44
- - which setups to include (skip debug, test and sample folders unless they want them);
45
- - **case A:** whether to keep translating the languages already there, and whether to add more;
46
- - **case B:** which languages to translate into. Suggest from their market if they don't know.
40
+ Include every setup detect found except debug, test and sample folders, unless the user said
41
+ otherwise. Languages: the ones the user named, plus (case A) the ones already there. If the user
42
+ named none and there are none, ask which; that's the one thing setup can't guess.
47
43
 
48
44
  Then run `localship init --accept <numbers> --locales <list>` (add `--source <locale>` if the detected
49
45
  source language is wrong). Language codes are BCP 47 (`hi`, `ar`, `pt-BR`, `zh-Hans`). Never pass
@@ -51,40 +47,57 @@ source language is wrong). Language codes are BCP 47 (`hi`, `ar`, `pt-BR`, `zh-H
51
47
 
52
48
  ## 3. Existing translations (case A)
53
49
 
54
- They show as `untracked`: made before Localship, so the tool doesn't know if they're good. Ask:
55
- - **Trust them:** `localship approve --from untracked`.
56
- - **Review first:** run the `localship-review` skill on them (it can fetch untracked strings).
57
- - **Leave them:** they stay as they are and are never overwritten without the user's say-so.
58
- Then `localship validate`: existing files often have real problems (missing placeholders, broken
59
- plurals). Report the errors and offer to fix them.
50
+ They show as `untracked`: made before Localship, so the tool doesn't know if they're good. Leave
51
+ them as they are; they're never overwritten without the user's say-so. In your report, mention
52
+ that the user can approve them (`localship approve --from untracked`) or have them reviewed (the
53
+ `localship-review` skill). Run `localship validate`: existing files often have real problems
54
+ (missing placeholders, broken plurals). Report the errors; fix them if the user asks.
60
55
 
61
56
  ## 4. Platform setup the tool can't do (mention what applies)
62
57
 
63
- - Android: per-app language picker — in `build.gradle(.kts)` set
64
- `androidResources { generateLocaleConfig = true }` and add
65
- `app/src/main/res/resources.properties` with `unqualifiedResLocale=en-US` (the source).
66
- - iOS: add each language under Project › Info › Localizations so Xcode keeps them.
58
+ - Android: inspect AGP, compileSdk and any existing language configuration first. Automatic
59
+ `androidResources { generateLocaleConfig = true }` needs AGP 8.1+ and compileSdk 33+.
60
+ Set `unqualifiedResLocale` in the app module's `res/resources.properties` to the actual
61
+ default locale, not an assumed `en-US`. Do not combine a manual LocaleConfig with generation;
62
+ reconcile the existing setup. Check the generated supported-language list, including library
63
+ resources, and restrict it to languages ready for release using the installed AGP's supported
64
+ locale-filter configuration.
65
+ - Android in-app selection: integrate platform `LocaleManager` on Android 13+ or compatible
66
+ AndroidX `AppCompatDelegate.setApplicationLocales`; consider the Activity/context requirements
67
+ on older devices, persisted preferences and migration from any existing picker. Test system
68
+ settings, app settings, restart and configuration changes together. Generating LocaleConfig
69
+ alone does not implement a backward-compatible picker.
70
+ - iOS: add languages through the catalog/project localization settings or available Xcode
71
+ preparation tools. Check the development language, resource bundles, compiler extraction and
72
+ all relevant targets/platforms, as described in `localship-extract`. Xcode 27's native agent
73
+ tools can assist preparation; use them when available, with the project's glossary and voice.
74
+
75
+ Read [platform verification](../localship-extract/references/platform-localization.md) for the
76
+ official references. Report unsupported catalog/resource configurations before translating;
77
+ strict validation blocks them even when supported strings are all complete.
67
78
 
68
79
  ## 5. Write the context
69
80
 
70
- Create these in `.localship/` (the `context` folder in the config). Draft them from the README, store
71
- listing, website copy and the strings themselves, then show the user and adjust.
81
+ The user asked for setup, so write these in `.localship/` (the `context` folder in the config).
82
+ Draft them from the README, store listing, website copy and the strings themselves, and show the
83
+ user what you wrote. Don't overwrite files that already exist.
72
84
 
73
85
  - `brief.md`: what the app does, who uses it, the tone of its copy (calm, playful, formal…), and
74
- anything translators get wrong without context (e.g. "a 'budget' is a monthly spending plan,
75
- not a cheap option"). Half a page at most.
86
+ anything translators get wrong without context (e.g. "a 'notebook' is a group of notes, not
87
+ a paper notebook"). Half a page at most.
76
88
  - `voice/all.md`: voice rules for every language. `voice/<locale>.md`: per language, e.g. formality
77
- (German du vs Sie, Hindi तुम vs आप), English loanwords, number and date conventions. Ask the user
78
- their preference for formality; don't guess silently.
89
+ (German du vs Sie, Hindi तुम vs आप), English loanwords, number and date conventions. Use what the
90
+ user said; otherwise the usual convention for consumer apps in that language, and name the choice
91
+ in your summary so they can change it.
79
92
  - `glossary.yaml`: brand and product names to keep (`translate: false`), and product terms that
80
93
  must be translated consistently:
81
94
  ```yaml
82
95
  terms:
83
- - term: StillMoney
96
+ - term: Jotter
84
97
  translate: false
85
- - term: safe to spend
86
- note: The amount the user can spend today without breaking their budget
87
- translations: { hi: खर्च के लिए सुरक्षित }
98
+ - term: notebook
99
+ note: A named group of notes, not a paper notebook
100
+ translations: { hi: नोटबुक }
88
101
  ```
89
102
  Keep it short: names, and terms that appear in many strings or carry product meaning.
90
103
 
@@ -93,7 +106,8 @@ Without file access, use `localship_update_context` instead.
93
106
  ## Cleaning up
94
107
 
95
108
  After strings are deleted or renamed, `localship prune --dry-run` shows leftover translations; show
96
- the user, then run `localship prune`.
109
+ the user, then run `localship prune`. First verify that the source keys were actually removed:
110
+ a missing Android default can look like a leftover translation, and should be restored instead.
97
111
 
98
112
  ## Adding languages later
99
113
 
@@ -101,5 +115,5 @@ the user, then run `localship prune`.
101
115
 
102
116
  ## Finish
103
117
 
104
- Run `localship validate` and `localship status`, and tell the user the next step: the `localship-translate` skill
105
- (or `localship translate --model …` for a large catalog). Commit nothing unless the user asks.
118
+ Run `localship validate` and `localship status`, and tell the user the next step: translating (the
119
+ `localship-translate` skill). Commit nothing unless the user asks.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: localship-translate
3
- description: Translate an app's missing or changed strings with Localship — fetch batches with context, translate them, submit them for checks, and fix what's rejected until nothing is left. Use when the user asks to translate, localize, add a language, or update translations after copy changes.
3
+ description: Translate an app's missing or changed strings with Localship — fetch batches with context, translate them, submit them for checks, and fix what's rejected until nothing is left. Use for the translating itself, once the text is in string resources; for a request to localize the app or add a language, start with the localship skill, which works out whether text has to be moved first.
4
4
  ---
5
5
 
6
6
  # Translate with Localship
@@ -10,7 +10,7 @@ The tool picks the strings, gives you the context, checks every translation in c
10
10
  You translate. Don't edit translation files by hand.
11
11
 
12
12
  > For a direct request ("translate this app to Hindi"), start with the `localship` skill: it
13
- > plans once, asks for one confirmation, and then uses this playbook without more questions.
13
+ > checks the project, then uses this playbook.
14
14
 
15
15
  ## Running the tool
16
16
 
@@ -19,23 +19,23 @@ You translate. Don't edit translation files by hand.
19
19
  `npm install -g localship`.
20
20
  - No `.localship/config.yaml`? Run the `localship-setup` skill first.
21
21
  - A language that isn't configured yet: `localship add-locale <code>` first.
22
- - Check `git status` first and mention uncommitted changes, so the translation diff stays reviewable.
23
22
 
24
23
  ## 1. Size up the work
25
24
 
26
25
  Run `localship status`. Count `missing` + `stale` across the locales to translate.
26
+ Read the status notes too. Unsupported Apple variations and Android configurations are excluded
27
+ from the counts; report them as uncovered work and preserve their structure. Zero remaining
28
+ batch entries does not mean those strings have been translated or validated.
27
29
 
28
- - **Up to a few hundred strings:** translate them yourself (step 2).
29
- - **More than that**, or the user wants it done without you: delegate to a model API with
30
- `localship translate --model <provider:model>` (or `models:` in the config). Providers:
31
- `anthropic`, `openai`, `gemini`, `openrouter` (keys from `ANTHROPIC_API_KEY`,
32
- `OPENAI_API_KEY`, `GEMINI_API_KEY`, `OPENROUTER_API_KEY`), and local `ollama`, `lmstudio`.
33
- It runs the same checks and retries rejected strings. Then review what it reports as failed
34
- and translate those yourself.
30
+ Translate them yourself (step 2), however many there are. Use a model API
31
+ (`localship translate --model <provider:model>`) only when the user asks for it: cloud providers
32
+ (`anthropic`, `openai`, `gemini`, `openrouter`) bill per use and need their API key; local
33
+ `ollama` and `lmstudio` are free. It runs the same checks; translate what it reports as failed
34
+ yourself.
35
35
 
36
- Read `.localship/brief.md`, `.localship/voice/` and `.localship/glossary.yaml` once if they exist. If
37
- there's no brief, write a short one first (see `localship-setup`): translations without context
38
- come out generic.
36
+ Read `.localship/brief.md`, `.localship/voice/` and `.localship/glossary.yaml` once if they exist.
37
+ If there's no brief, read the README for what the app does and its tone; don't write a brief
38
+ unless the user asks.
39
39
 
40
40
  ## 2. The loop
41
41
 
@@ -51,7 +51,8 @@ come out generic.
51
51
  - `editedByHand: true` — a person edited the current translation. Keep their wording where you
52
52
  can, and show the user your proposed change: `submit` refuses it until they agree (then pass
53
53
  `--force` for just those strings).
54
- The batch also carries the `brief`, each locale's `voice`, and notes per format.
54
+ The batch also carries the `brief`, each locale's `voice` (or `defaultVoice`, the usual
55
+ convention, when the project has set none), and notes per format.
55
56
 
56
57
  2. Translate every item following these rules:
57
58
  - Natural, concise UI language a native speaker would write, in the brief's tone and the
@@ -64,7 +65,14 @@ come out generic.
64
65
  one).
65
66
  - Arrays: same number of items, same order.
66
67
  - Reuse `memory` unless the context differs. Follow the glossary.
67
- - Keep it about as long as the source; buttons and tabs have little room.
68
+ - Keep it about as long as the source. Items with a `role` of button, tab, chip or menu item
69
+ have little room: prefer the shortest natural wording.
70
+ - When the source could mean two things ("Open" as a button or a status, "Balance" as money
71
+ or equilibrium) and the context doesn't settle it, add a short `note` saying how you read
72
+ it. Reviewers see it. Skip notes when the meaning is clear.
73
+ - Same source text, different meaning ("Delete account" for a sign-in account and for a
74
+ budget): translate each for its meaning and add a `note` saying how they differ, so the
75
+ consistency check doesn't flag it.
68
76
  - Android values: write plain text; apostrophes, quotes and `&` are escaped for you.
69
77
 
70
78
  3. Submit, using a file for anything non-trivial:
@@ -77,7 +85,8 @@ come out generic.
77
85
  {"target": "android", "locale": "hi", "key": "days_left", "value": {"one": "%d दिन बाकी", "other": "%d दिन बाकी"}}
78
86
  ]}
79
87
  ```
80
- Copy `target`, `locale` and `key` from the batch items. Delete the file afterwards.
88
+ Copy `target`, `locale` and `key` from the batch items. Add `"note"` to an entry only for an
89
+ ambiguous source, or one worded differently on purpose. Delete the file afterwards.
81
90
 
82
91
  4. Read the result. Rejected entries list their errors: fix exactly those and resubmit only
83
92
  them. Accepted entries may have warnings (glossary, untranslated, whitespace): fix the
@@ -88,15 +97,19 @@ come out generic.
88
97
  ## 3. Finish
89
98
 
90
99
  - Run `localship validate`. It must have no errors.
100
+ - Resolve or explicitly report coverage warnings. `validate --strict` checks completeness;
101
+ after review and approval, `validate --require-approved` also enforces approval. Unsupported
102
+ content blocks both checks; use the platform tools for that content and report the limitation.
91
103
  - Run `localship status` and report per locale: how many drafts were written, anything that
92
- failed, warnings you left and why, and terms you were unsure of.
104
+ failed, warnings you left and why, and terms you were unsure of. Say which voice you followed
105
+ (especially a `defaultVoice`) so the user can change it.
93
106
  - **Don't approve your own drafts.** Suggest the user reviews them (the `localship-review` skill),
94
107
  then `localship approve`.
95
108
  - For a right-to-left language (Arabic, Hebrew, Persian, Urdu), or any first translation, tell the
96
109
  user what still needs a device: the layout mirrored, long text cut off, plurals at 0, 1, 2, 5
97
110
  and 11, switching language while the app is open, charts, amounts with a sign, and names
98
- stored in data. Offer to look for these in the code (the `localship-extract` skill lists
99
- them); Localship's checks only cover the translation files.
111
+ stored in data. Localship's checks only cover the translation files; the `localship-extract`
112
+ skill lists where to look in the code if the user wants that checked.
100
113
  - Never pass `--force` unless the user explicitly asks: it replaces translations a person
101
114
  wrote, reviewed or approved.
102
115