storelingo 0.0.0-stage → 0.5.1

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/LICENSE +21 -0
  3. package/README.md +331 -2
  4. package/README.ru.md +332 -0
  5. package/dist/bin.js +5 -0
  6. package/dist/cli/main.js +267 -0
  7. package/dist/commands/check.js +77 -0
  8. package/dist/commands/init.js +113 -0
  9. package/dist/commands/pull.js +45 -0
  10. package/dist/commands/push.js +123 -0
  11. package/dist/commands/translate.js +151 -0
  12. package/dist/config/load.js +43 -0
  13. package/dist/config/schema.js +70 -0
  14. package/dist/errors.js +17 -0
  15. package/dist/fields/registry.js +64 -0
  16. package/dist/locales/table.js +60 -0
  17. package/dist/lock/lock.js +84 -0
  18. package/dist/log.js +14 -0
  19. package/dist/metadata/fastlane.js +120 -0
  20. package/dist/stores/app-store/auth.js +66 -0
  21. package/dist/stores/app-store/client.js +181 -0
  22. package/dist/stores/app-store/store.js +169 -0
  23. package/dist/stores/diff.js +68 -0
  24. package/dist/stores/google-play/auth.js +95 -0
  25. package/dist/stores/google-play/client.js +127 -0
  26. package/dist/stores/google-play/store.js +59 -0
  27. package/dist/stores/local.js +23 -0
  28. package/dist/stores/types.js +1 -0
  29. package/dist/translate/engine.js +82 -0
  30. package/dist/translate/keywords.js +18 -0
  31. package/dist/translate/prompt.js +45 -0
  32. package/dist/translate/providers/anthropic.js +62 -0
  33. package/dist/translate/providers/index.js +61 -0
  34. package/dist/translate/providers/openai-compatible.js +86 -0
  35. package/dist/translate/types.js +4 -0
  36. package/dist/util/chars.js +4 -0
  37. package/dist/util/pool.js +13 -0
  38. package/dist/util/retry.js +30 -0
  39. package/dist/util/text.js +4 -0
  40. package/package.json +70 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ ## 0.5.1 — 2026-11-15
4
+
5
+ - `stores.appstore.appId` is needed only for `pull` and `push`. `translate` and `check` work with `"appstore": {}` before the app exists in App Store Connect, and `init` accepts an empty Apple ID. `pull` / `push` without it stop with a clear `E_CONFIG` before any network call (found while localizing a real app).
6
+
7
+ ## 0.5.0 — 2026-11-15
8
+
9
+ - First release on npm: `npx storelingo` works without building from source.
10
+ - Releases are published from GitHub Actions with npm trusted publishing and provenance (`docs/releasing.md`).
11
+ - `npm run build` starts from a clean `dist/`, so files of removed modules never end up in the package.
12
+ - The package includes `CHANGELOG.md`.
13
+ - Google Play: concurrent requests share one token refresh, a failed refresh can be retried, and an invalid `expires_in` falls back to one hour (thanks @yonash24, #13).
14
+
15
+ ## 0.4.1 — 2026-11-14
16
+
17
+ - GitHub Action: the commit step treats config paths literally (`GIT_LITERAL_PATHSPECS`) and commits only storelingo output even if an earlier workflow step staged other files (reported by the Codex review on #11).
18
+
19
+ ## 0.4.0 — 2026-11-14
20
+
21
+ - GitHub Action (`uses: Alisher23/storelingo@v0.4.0`): runs `translate` and/or `check` in CI. Optional `commit` of the translated files: only storelingo output is committed, only on `push`, `workflow_dispatch` or `schedule` runs on a branch, and never after a failed or cancelled step. Setup guide in `docs/github-action.md`.
22
+ - Retry-After delays from servers are capped at 60 s (thanks @yonash24, #10).
23
+ - App Store: promotional text without an editable version is sent only for languages that already exist on the live version; other languages are shown as skipped.
24
+ - The final "some changes were skipped" note is store-specific.
25
+
26
+ ## 0.3.0 — unreleased
27
+
28
+ - **Breaking:** `stores.appstore.appId` must be the numeric Apple ID (for example `1234567890`), not the bundle ID.
29
+ - App Store `pull` / `push` (`--store appstore`) through the App Store Connect API with a team API key (`STORELINGO_ASC_ISSUER_ID`, `STORELINGO_ASC_KEY_ID`, `STORELINGO_ASC_PRIVATE_KEY`); setup guide in `docs/app-store-setup.md`.
30
+ - `pull` and `push` without `--store` now run every configured store (Google Play, then the App Store), each with its own diff and confirmation.
31
+ - App Store `push` is not atomic: it applies one language at a time, sends only the changed fields, and on a failure reports "already applied: ...; failed (may be partly applied): ...; not attempted: ..."; re-running only sends what still differs.
32
+ - `pull` and `push` load the credentials of every selected store before touching any of them, so a missing App Store key no longer shows up after Google Play was already pushed.
33
+ - Release notes are not sent for the first version of an app (Apple shows them only for updates); skipped lines name their real reason; several editable versions or app infos prefer `PREPARE_FOR_SUBMISSION` or fail with a clear `E_STORE`; paginated App Store Connect lists are capped and never silently truncated.
34
+ - Fields are routed to the resource that can take them: `name`, `subtitle`, `privacy_url` to the editable app info; `description`, `keywords`, `release_notes`, `marketing_url`, `support_url` to the editable version; `promotional_text` to the editable version or else the live one. Changes with no such resource are shown as skipped and never sent.
35
+ - Requests that create a localization are never retried; a lost response is reported as "status is unknown".
36
+ - New `stores.appstore.platform` option (`IOS` by default).
37
+ - Error codes `E_AUTH_ASC` and `E_NO_EDITABLE_VERSION` are now used.
38
+ - `STORELINGO_ASC_PRIVATE_KEY` is masked in logs and errors.
39
+
40
+ ## 0.2.0 — unreleased
41
+
42
+ - `pull`: download the current Google Play listings into the fastlane folders. Never commits and never touches the lock file; keeps different local files unless `--force`.
43
+ - `push`: runs `check`, shows the diff with the live Google Play listing and uploads after confirmation (`--dry-run`, `--yes`; outside a terminal it needs one of them). One Play edit per push, committed with `ERROR_IF_IN_REVIEW` so changes already in review are never cancelled.
44
+ - `push` counts as interactive only when both input and output are a terminal: with output redirected or piped (e.g. `storelingo push > log.txt`, `storelingo push | tee log.txt`) it needs `--yes` or `--dry-run`.
45
+ - The `push` diff shows the part of a long value where it actually changes, with character counts.
46
+ - Google Play service account authentication via `STORELINGO_PLAY_SERVICE_ACCOUNT`; setup guide in `docs/google-play-setup.md`.
47
+ - New error codes `E_IN_REVIEW` and `E_STORE`.
48
+ - Hardening: the OpenAI-compatible translator no longer follows redirects.
49
+ - Hardening: `init` refuses symbolic links at `storelingo.config.json` and `.gitignore`.
50
+ - `--store appstore` is rejected until v0.3.
51
+
52
+ ## 0.1.0 — unreleased
53
+
54
+ - `init`: create `storelingo.config.json` and fastlane folders.
55
+ - `translate`: AI localization with store limits, keyword rules, lock file and manual-edit protection.
56
+ - `check`: offline validation of limits, missing fields and keyword format.
57
+ - Translators: Claude API and any OpenAI-compatible endpoint.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alisher23
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,332 @@
1
- # Temporary Holding Version
1
+ # storelingo
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ AI localization for your Google Play and App Store listings. Works with fastlane metadata — no fastlane required.
4
+
5
+ [![npm](https://img.shields.io/npm/v/storelingo)](https://www.npmjs.com/package/storelingo) [![CI](https://github.com/Alisher23/storelingo/actions/workflows/ci.yml/badge.svg)](https://github.com/Alisher23/storelingo/actions/workflows/ci.yml)
6
+
7
+ > Status: early. Covered by automated tests on Windows, macOS and Linux, but not yet run against many real store accounts. Start with `pull` and `push --dry-run`, and [open an issue](https://github.com/Alisher23/storelingo/issues) if anything looks off.
8
+
9
+ [Русская версия](README.ru.md)
10
+
11
+ ## Why
12
+
13
+ - **Store limits are respected.** Every field has its character limit (counted in Unicode code points). If a translation is too long, storelingo asks the model to shorten it, up to 2 times. Text is never truncated mechanically; if it still does not fit, the file is not written and the run reports an error.
14
+ - **Keywords are localized for search, not translated literally.** App Store keywords are chosen for what people in the target market search for, then normalized (comma-separated, no spaces, no duplicates, no words already in the name or subtitle).
15
+ - **Your manual edits are protected.** If you edit a translated file by hand, storelingo notices and skips it (use `--force` to overwrite).
16
+ - **Incremental.** A lock file records what was translated, so only changed source fields are sent to the model.
17
+ - **Bring your own model.** Claude (default) or any OpenAI-compatible endpoint, including a local Ollama.
18
+
19
+ ## Quick start
20
+
21
+ ```bash
22
+ npx storelingo init
23
+ export ANTHROPIC_API_KEY=sk-ant-...
24
+ npx storelingo translate
25
+ git diff
26
+ npx storelingo check
27
+ ```
28
+
29
+ `init` asks a few questions and creates `storelingo.config.json` plus the fastlane folders. Put your source-language texts into those folders, then run `translate`. Review the result with `git diff`, and commit `storelingo.lock.json` together with the translations.
30
+
31
+ Requires Node.js >= 22.13. To pin the version for your team, add storelingo to the project with `npm install --save-dev storelingo`. To run it in CI, see the [GitHub Action](https://github.com/Alisher23/storelingo/blob/main/docs/github-action.md).
32
+
33
+ ## Folder layout
34
+
35
+ storelingo reads and writes the standard fastlane trees (paths can be changed in `paths`):
36
+
37
+ ```
38
+ fastlane/metadata/android/ # Google Play (fastlane supply)
39
+ en-US/
40
+ title.txt
41
+ short_description.txt
42
+ full_description.txt
43
+ video.txt # not translated
44
+ changelogs/<versionCode>.txt
45
+ ru-RU/ ...
46
+
47
+ fastlane/metadata/ # App Store (fastlane deliver)
48
+ en-US/
49
+ name.txt
50
+ subtitle.txt
51
+ description.txt
52
+ keywords.txt
53
+ promotional_text.txt
54
+ release_notes.txt
55
+ marketing_url.txt # urls are not translated
56
+ support_url.txt
57
+ privacy_url.txt
58
+ ru/ ...
59
+ ```
60
+
61
+ The `android`, `default`, `review_information` and `trade_representative_contact_information` folders inside `fastlane/metadata` are not treated as App Store locales. Files are written as UTF-8 without BOM with `\n` line endings (line breaks in the model's reply are normalized before writing); the source's trailing newline is preserved.
62
+
63
+ The metadata paths and `storelingo.lock.json` are resolved against the current directory, so run storelingo from the project root. `paths.play` and `paths.appstore` must stay inside the project: absolute paths and `..` escapes are rejected with a config error (exit code 2). storelingo also refuses to follow symbolic links in the metadata folders and for the lock file (exit code 2), because the config and these files can come from someone else's repository.
64
+
65
+ For Google Play, the newest changelog (highest version code) of the source locale is the one that gets translated.
66
+
67
+ ## Config reference
68
+
69
+ `storelingo.config.json` in the project root:
70
+
71
+ ```json
72
+ {
73
+ "sourceLocale": "en-US",
74
+ "targetLocales": ["ru", "de", "tr", "ar", "uz"],
75
+ "stores": {
76
+ "play": { "packageName": "com.example.app" },
77
+ "appstore": { "appId": "1234567890" }
78
+ },
79
+ "translator": {
80
+ "provider": "anthropic",
81
+ "model": "claude-opus-5-5",
82
+ "effort": "medium",
83
+ "concurrency": 4
84
+ },
85
+ "context": "Prayer times and Quran app for Muslims in Central Asia.",
86
+ "glossary": {
87
+ "doNotTranslate": ["Quran Widgets"],
88
+ "terms": { "ru": { "prayer": "намаз" } }
89
+ },
90
+ "paths": {}
91
+ }
92
+ ```
93
+
94
+ | Key | Required | Description |
95
+ |---|---|---|
96
+ | `sourceLocale` | yes | storelingo locale code of your source texts, e.g. `en-US`. |
97
+ | `targetLocales` | yes | Non-empty list of storelingo locale codes to translate into, without duplicates. |
98
+ | `stores.play.packageName` | one store required | Enables Google Play. |
99
+ | `stores.appstore` | one store required | Enables the App Store. `{}` is enough for `translate` and `check`, even before the app exists in App Store Connect. |
100
+ | `stores.appstore.appId` | for `pull` / `push` | The numeric Apple ID of the app (App Store Connect → App Information). |
101
+ | `stores.appstore.platform` | no (default `IOS`) | `IOS`, `MAC_OS`, `TV_OS` or `VISION_OS`: the platform whose version storelingo works with. |
102
+ | `translator.provider` | yes | `anthropic` or `openai-compatible`. |
103
+ | `translator.model` | `anthropic`: no (default `claude-opus-5-5`); `openai-compatible`: yes | Model name. |
104
+ | `translator.effort` | no, `anthropic` only | `low`, `medium` (default), `high`, `xhigh` or `max`. |
105
+ | `translator.baseUrl` | `openai-compatible` only | API base URL. Must be `https://` unless it points to `localhost`, `127.0.0.1` or `[::1]`. |
106
+ | `translator.concurrency` | no (default `4`) | Locales translated in parallel, 1 to 16. |
107
+ | `context` | no | Short description of your app; added to every prompt. |
108
+ | `glossary.doNotTranslate` | no | Terms kept exactly as written (brand names). |
109
+ | `glossary.terms` | no | Preferred translations per target locale: `{ "<locale>": { "<source term>": "<translation>" } }`. |
110
+ | `paths.play` | no | Play metadata root (default `fastlane/metadata/android`). A relative path inside the project. |
111
+ | `paths.appstore` | no | App Store metadata root (default `fastlane/metadata`). A relative path inside the project. |
112
+
113
+ Unknown keys are rejected, so a typo such as `glossary.doNotTranslte` is reported instead of silently ignored.
114
+
115
+ ### Translators and API keys
116
+
117
+ Keys come only from environment variables. They are never written to the config, the lock file or the logs.
118
+
119
+ | Variable | Used for |
120
+ |---|---|
121
+ | `ANTHROPIC_API_KEY` | Claude (`provider: "anthropic"`). |
122
+ | `OPENAI_API_KEY` | OpenAI-compatible, **only** when the `baseUrl` host is `api.openai.com`. |
123
+ | `STORELINGO_TRANSLATOR_API_KEY` | Any other OpenAI-compatible host (OpenRouter, Gemini endpoint, ...). |
124
+ | `STORELINGO_TRANSLATOR_API_HOST` | The host the OpenAI-compatible provider may talk to. Required for every non-local host other than `api.openai.com`, even without a key; the only host that receives `STORELINGO_TRANSLATOR_API_KEY`. |
125
+ | `STORELINGO_PLAY_SERVICE_ACCOUNT` | Google Play `pull` / `push`: the path to the service account JSON key, or the JSON itself. See [Google Play](#google-play). |
126
+ | `STORELINGO_ASC_ISSUER_ID` | App Store `pull` / `push`: the Issuer ID of the App Store Connect team API key. See [App Store](#app-store). |
127
+ | `STORELINGO_ASC_KEY_ID` | App Store `pull` / `push`: the Key ID of that key. |
128
+ | `STORELINGO_ASC_PRIVATE_KEY` | App Store `pull` / `push`: the path to the `.p8` private key, or its contents (literal `\n` sequences are accepted as line breaks). |
129
+
130
+ Rules for `openai-compatible`:
131
+
132
+ - `api.openai.com`: uses `OPENAI_API_KEY`; no other variable is needed.
133
+ - Any other non-local host: `STORELINGO_TRANSLATOR_API_HOST` must equal the hostname of `baseUrl`, **even if no key is set**; otherwise storelingo stops with a config error (exit code 2) and sends nothing. `STORELINGO_TRANSLATOR_API_KEY`, if set, is sent only to that host.
134
+ - Local servers (`localhost`, `127.0.0.1`, `[::1]`, e.g. Ollama): no variable is needed. A `STORELINGO_TRANSLATOR_API_KEY` is sent to a local server only if `STORELINGO_TRANSLATOR_API_HOST` names it; if the key is set and the host does not match, storelingo stops with a config error.
135
+ - `baseUrl` must be `https://`; plain `http://` is allowed only for `localhost`, `127.0.0.1` and `[::1]`. Other schemes (`file:`, `data:`, ...) are rejected.
136
+
137
+ Why: a `storelingo.config.json` can come from someone else's repository. Without these rules, a malicious config could set `baseUrl` to its own server and collect whatever API key is in your environment, or your unreleased listing texts. Requests and keys can only travel to the host you explicitly allowed.
138
+
139
+ OpenRouter:
140
+
141
+ ```json
142
+ {
143
+ "translator": {
144
+ "provider": "openai-compatible",
145
+ "baseUrl": "https://openrouter.ai/api/v1",
146
+ "model": "anthropic/claude-sonnet-5-5"
147
+ }
148
+ }
149
+ ```
150
+
151
+ ```bash
152
+ export STORELINGO_TRANSLATOR_API_KEY=sk-or-...
153
+ export STORELINGO_TRANSLATOR_API_HOST=openrouter.ai
154
+ npx storelingo translate
155
+ ```
156
+
157
+ Local Ollama (no key):
158
+
159
+ ```json
160
+ {
161
+ "translator": {
162
+ "provider": "openai-compatible",
163
+ "baseUrl": "http://localhost:11434/v1",
164
+ "model": "qwen3:32b"
165
+ }
166
+ }
167
+ ```
168
+
169
+ ## Commands
170
+
171
+ ```
172
+ storelingo [--config <path>] [--verbose] [--no-color] <command>
173
+ ```
174
+
175
+ | Command | What it does |
176
+ |---|---|
177
+ | `init [--force]` | Interactive setup: creates `storelingo.config.json` and the fastlane folders. `--force` overwrites an existing config. |
178
+ | `translate [--locale <code...>] [--force]` | Translates changed fields into the target locales. `--locale` limits the run to some of `targetLocales`. `--force` re-translates everything, including files you edited manually. |
179
+ | `check` | Offline validation: character limits, missing required fields, keyword format. Makes no API calls. |
180
+ | `pull [--store <store>] [--force]` | Downloads the current store listings into the fastlane folders. Local files that differ from the store are kept unless you pass `--force`. Never changes the store. |
181
+ | `push [--store <store>] [--dry-run] [--yes]` | Runs `check`, shows the diff with the live listing and uploads after confirmation. `--dry-run` only shows the diff. Outside a terminal it needs `--yes` or `--dry-run`. |
182
+
183
+ `--store` accepts `play` or `appstore`. Without it, `pull` and `push` run every configured store, Google Play first and then the App Store, each with its own diff and confirmation. With `--store`, the store must be configured (`stores.play` or `stores.appstore`), otherwise the command stops with a config error.
184
+
185
+ Global flags: `--config <path>` (default `storelingo.config.json`), `--verbose` (debug output), `--no-color`, `--version`, `--help`.
186
+
187
+ Exit codes:
188
+
189
+ | Code | Meaning |
190
+ |---|---|
191
+ | `0` | Success. |
192
+ | `1` | `check` found errors, some translations failed, `push` was blocked by `check` errors or by a listing problem, or a runtime error (for example the source locale is not supported by a configured store, a file could not be read or written, Google Play denied access (`E_AUTH_PLAY`), changes are already in review (`E_IN_REVIEW`), App Store Connect denied access (`E_AUTH_ASC`), there is no editable App Store version for any of the changes (`E_NO_EDITABLE_VERSION`) or a store API failed (`E_STORE`)). |
193
+ | `2` | Bad usage or config: invalid flag, `--locale` outside `targetLocales`, missing or invalid config, `paths` outside the project, a symbolic link in the metadata folders or at the lock file, a corrupt lock file, missing API key or a `baseUrl` host not allowed by `STORELINGO_TRANSLATOR_API_HOST`, a missing or invalid `STORELINGO_PLAY_SERVICE_ACCOUNT`, missing or invalid `STORELINGO_ASC_ISSUER_ID`, `STORELINGO_ASC_KEY_ID` or `STORELINGO_ASC_PRIVATE_KEY`, a Google Play or App Store 404 ("app not found or not accessible"), a `--store` value other than `play` or `appstore`, a `--store` for a store that is not configured, or `push` without `--yes` outside a terminal. |
194
+
195
+ ## Google Play
196
+
197
+ `pull` and `push` use the Google Play Android Publisher API with a service account. Follow the [Google Play setup guide](https://github.com/Alisher23/storelingo/blob/main/docs/google-play-setup.md) once (Google Cloud project, service account key, access in Play Console), then:
198
+
199
+ ```bash
200
+ export STORELINGO_PLAY_SERVICE_ACCOUNT=/path/to/play-service-account.json
201
+ npx storelingo pull
202
+ npx storelingo translate
203
+ npx storelingo check
204
+ npx storelingo push --dry-run
205
+ npx storelingo push
206
+ ```
207
+
208
+ How it behaves:
209
+
210
+ - `push` validates the config, `--store` and the terminal rule (outside a terminal it needs `--yes` or `--dry-run`) before any network request. It then runs `check` and stops on errors (exit code 1).
211
+ - One Play edit per push: all changed listings are uploaded and committed together. The commit uses `changesInReviewBehavior=ERROR_IF_IN_REVIEW`, so changes that are already in review are never cancelled; if there are any, `push` stops with `E_IN_REVIEW` (exit code 1) and changes nothing.
212
+ - If the final commit request gets no clear answer (a network error, a timeout, or an HTTP 429/5xx response), storelingo reports "commit status is unknown". Check Play Console, or run `push --dry-run` to see whether the listing already matches your files.
213
+ - Google reviews listing changes before they go live.
214
+ - Changelogs (release notes) are not uploaded yet; use `fastlane supply` for them.
215
+ - Languages that exist only in Google Play are never deleted. A new language needs `title`, `short_description` and `full_description`.
216
+ - `pull` never commits anything and never touches `storelingo.lock.json`. Pulled translations have no lock entry, so `translate` treats them as manual edits and skips them; re-translate them with `translate --force` (optionally with `--locale`).
217
+ - The service account key is read only from `STORELINGO_PLAY_SERVICE_ACCOUNT` (a path or the JSON itself). Tokens are requested from Google's fixed token endpoint; the `token_uri` inside the key file is ignored.
218
+
219
+ ## App Store
220
+
221
+ `pull` and `push` use the App Store Connect API with a team API key. Follow the [App Store setup guide](https://github.com/Alisher23/storelingo/blob/main/docs/app-store-setup.md) once (API key with the App Manager role, Apple ID of the app), then:
222
+
223
+ ```bash
224
+ export STORELINGO_ASC_ISSUER_ID=...
225
+ export STORELINGO_ASC_KEY_ID=...
226
+ export STORELINGO_ASC_PRIVATE_KEY=/path/to/AuthKey_XXXXXXXXXX.p8
227
+ npx storelingo pull --store appstore
228
+ npx storelingo translate
229
+ npx storelingo check
230
+ npx storelingo push --store appstore --dry-run
231
+ npx storelingo push --store appstore
232
+ ```
233
+
234
+ How it behaves:
235
+
236
+ - **Not atomic.** The API has no transaction. `push` computes the whole diff first and then applies it one language at a time. If a request fails midway, the languages already applied stay applied; the error lists three groups ("already applied: ...; failed (may be partly applied): ...; not attempted: ..."; "none" when a group is empty; the failed language may be partly applied, e.g. app info updated but the version not) and tells you to re-run `storelingo push`. This message appears for every failure of an App Store update, including one on the first language. Only changed fields are sent. Re-running is safe either way: it reads the store again and sends only what still differs.
237
+ - **Fields go to the resource that can take them.** `name`, `subtitle` and `privacy_url` go to the editable app info. `description`, `keywords`, `release_notes`, `marketing_url` and `support_url` go to the editable version. `promotional_text` goes to the editable version, or to the live version if there is no editable one. A version is editable in the states `PREPARE_FOR_SUBMISSION`, `DEVELOPER_REJECTED`, `REJECTED` or `METADATA_REJECTED` (app info: the first three); live means `READY_FOR_DISTRIBUTION`. `pull` reads the editable resources if they exist, otherwise the live ones.
238
+ - **Skipped lines.** A change that has no resource to go to is never sent. It is shown in the diff as `<locale>/<field>: skipped (<reason>)`, where the reason is "no editable version in App Store Connect", "no editable app info in App Store Connect" or "Apple shows release notes only for updates" (release notes are not sent for the first version of an app), and `push` prints a note at the start. If every change is skipped, `push` prints the skipped lines and stops with `E_NO_EDITABLE_VERSION` (exit code 1, also with `--dry-run`): create a new version in App Store Connect and push again. Without a new version only the promotional text can change, and on the live version it takes effect right away; the rest goes live with the version you prepare.
239
+ - **New languages** need `name`, `description` and `keywords` locally and both an editable app info and an editable version.
240
+ - A request that creates a localization is never retried. If it gets no clear answer, storelingo reports `E_STORE` "status is unknown"; run `push --dry-run` to see whether it was created.
241
+ - storelingo never submits anything for review, never deletes languages and never touches prices, builds, screenshots or categories.
242
+ - The key is read only from the `STORELINGO_ASC_*` variables and never appears in logs or error messages. Keep the `.p8` out of git (`init` adds `*.p8` to `.gitignore`).
243
+
244
+ ## GitHub Action
245
+
246
+ Run storelingo in CI with `uses: Alisher23/storelingo@v0.5.1` (pin a release tag or a commit SHA). It can translate on push and commit the result, or only run the offline `check` on pull requests with no secrets. See the [GitHub Action guide](https://github.com/Alisher23/storelingo/blob/main/docs/github-action.md) for workflows, inputs and security notes.
247
+
248
+ ## How translation works
249
+
250
+ 1. For every store and target locale, storelingo decides per field: skip if the source is empty or missing; translate if the target file is missing or empty (whitespace only); skip if you edited the translated file by hand; skip if the source has not changed since the last run; otherwise translate.
251
+ 2. One request per store and locale carries all selected fields, together with your `context`, `doNotTranslate` list, the locale's `terms` and each field's character limit. The prompt asks for localized, idiomatic text and preserves line breaks and bullet lists.
252
+ 3. The JSON reply is validated; an invalid reply is retried once.
253
+ 4. A field over its limit is sent back for shortening, at most 2 times. If it still exceeds the limit, nothing is written for that field and it is reported as an error.
254
+ 5. App Store `keywords` are normalized in code after the reply: trimmed, comma-separated without spaces, deduplicated, words from the name and subtitle removed (the newly translated ones, or the locale's current `name.txt` / `subtitle.txt` when those are not re-translated in this run).
255
+ 6. Translations are written to the fastlane folders. `storelingo.lock.json` stores hashes of each source and written output. A target file whose hash no longer matches the lock was edited by you and is skipped, unless you pass `--force`. The lock is written atomically after each field; commit it to git.
256
+ 7. The run continues past a single failure. At the end storelingo prints a summary (translated, unchanged, edited manually, unsupported, failed, tokens) and exits with code 1 if anything failed. Fields that succeeded are already saved.
257
+
258
+ ## Locale codes
259
+
260
+ storelingo uses its own locale codes in `sourceLocale` and `targetLocales` (for example `ru`, `de`, `es-419`, `zh-Hans`) and maps them to each store's code: `ru` is `ru-RU` on Google Play and `ru` on the App Store, `es-419` is `es-MX` on the App Store, `zh-Hans` is `zh-CN` on Google Play. The fastlane folders use the store codes.
261
+
262
+ Some languages exist on only one store. For example `ky` (Kyrgyz), `kk` (Kazakh) and `uz` (Uzbek) are Google Play only. If a store does not support a target locale, that locale is skipped for that store with a warning and counted as "unsupported" in the summary; the other store is still translated. The source locale must be supported by every configured store.
263
+
264
+ ## Cost
265
+
266
+ The default model is `claude-opus-5-5` with `effort: "medium"`. For cheaper runs set `"model": "claude-sonnet-5-5"` or `"claude-haiku-5-5"` in the `translator` block. Only changed fields are sent, so repeat runs are small. Input and output token totals are printed after each run, so you can estimate the cost with your provider's price list.
267
+
268
+ ## Supported languages
269
+
270
+ Generated from `src/locales/table.ts`. "—" means the store does not support that language.
271
+
272
+ | Code | Language | Google Play | App Store |
273
+ |---|---|---|---|
274
+ | `ar` | Arabic | `ar` | `ar-SA` |
275
+ | `az` | Azerbaijani | `az-AZ` | — |
276
+ | `ca` | Catalan | `ca` | `ca` |
277
+ | `cs` | Czech | `cs-CZ` | `cs` |
278
+ | `da` | Danish | `da-DK` | `da` |
279
+ | `de` | German | `de-DE` | `de-DE` |
280
+ | `el` | Greek | `el-GR` | `el` |
281
+ | `en-AU` | English (Australia) | `en-AU` | `en-AU` |
282
+ | `en-CA` | English (Canada) | `en-CA` | `en-CA` |
283
+ | `en-GB` | English (United Kingdom) | `en-GB` | `en-GB` |
284
+ | `en-US` | English (United States) | `en-US` | `en-US` |
285
+ | `es-ES` | Spanish (Spain) | `es-ES` | `es-ES` |
286
+ | `es-419` | Spanish (Latin America) | `es-419` | `es-MX` |
287
+ | `fa` | Persian | `fa` | — |
288
+ | `fi` | Finnish | `fi-FI` | `fi` |
289
+ | `fr-CA` | French (Canada) | `fr-CA` | `fr-CA` |
290
+ | `fr-FR` | French (France) | `fr-FR` | `fr-FR` |
291
+ | `he` | Hebrew | `iw-IL` | `he` |
292
+ | `hi` | Hindi | `hi-IN` | `hi` |
293
+ | `hr` | Croatian | `hr` | `hr` |
294
+ | `hu` | Hungarian | `hu-HU` | `hu` |
295
+ | `id` | Indonesian | `id` | `id` |
296
+ | `it` | Italian | `it-IT` | `it` |
297
+ | `ja` | Japanese | `ja-JP` | `ja` |
298
+ | `kk` | Kazakh | `kk` | — |
299
+ | `ko` | Korean | `ko-KR` | `ko` |
300
+ | `ky` | Kyrgyz | `ky-KG` | — |
301
+ | `ms` | Malay | `ms` | `ms` |
302
+ | `nl` | Dutch | `nl-NL` | `nl-NL` |
303
+ | `no` | Norwegian | `no-NO` | `no` |
304
+ | `pl` | Polish | `pl-PL` | `pl` |
305
+ | `pt-BR` | Portuguese (Brazil) | `pt-BR` | `pt-BR` |
306
+ | `pt-PT` | Portuguese (Portugal) | `pt-PT` | `pt-PT` |
307
+ | `ro` | Romanian | `ro` | `ro` |
308
+ | `ru` | Russian | `ru-RU` | `ru` |
309
+ | `sk` | Slovak | `sk` | `sk` |
310
+ | `sv` | Swedish | `sv-SE` | `sv` |
311
+ | `th` | Thai | `th` | `th` |
312
+ | `tr` | Turkish | `tr-TR` | `tr` |
313
+ | `uk` | Ukrainian | `uk` | `uk` |
314
+ | `uz` | Uzbek | `uz` | — |
315
+ | `vi` | Vietnamese | `vi` | `vi` |
316
+ | `zh-Hans` | Chinese (Simplified) | `zh-CN` | `zh-Hans` |
317
+ | `zh-Hant` | Chinese (Traditional) | `zh-TW` | `zh-Hant` |
318
+
319
+ ## Roadmap
320
+
321
+ - v0.2 (done): Google Play `pull` / `push`
322
+ - v0.3 (done): App Store `pull` / `push`
323
+ - v0.4 (done): GitHub Action
324
+ - Later: screenshots, RuStore, Huawei AppGallery
325
+
326
+ ## Contributing
327
+
328
+ See [CONTRIBUTING.md](https://github.com/Alisher23/storelingo/blob/main/CONTRIBUTING.md). To add a language or fix a character limit you usually only need to edit one data file. Repository: https://github.com/Alisher23/storelingo
329
+
330
+ ## License
331
+
332
+ MIT