app-localizer 1.0.1__tar.gz
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.
- app_localizer-1.0.1/LICENSE +21 -0
- app_localizer-1.0.1/PKG-INFO +425 -0
- app_localizer-1.0.1/README.md +402 -0
- app_localizer-1.0.1/pyproject.toml +37 -0
- app_localizer-1.0.1/setup.cfg +4 -0
- app_localizer-1.0.1/src/app_localizer/__init__.py +19 -0
- app_localizer-1.0.1/src/app_localizer/catalog.py +597 -0
- app_localizer-1.0.1/src/app_localizer/chat_client.py +243 -0
- app_localizer-1.0.1/src/app_localizer/cldr.py +93 -0
- app_localizer-1.0.1/src/app_localizer/cli.py +245 -0
- app_localizer-1.0.1/src/app_localizer/config.py +392 -0
- app_localizer-1.0.1/src/app_localizer/engine.py +99 -0
- app_localizer-1.0.1/src/app_localizer/fastlane.py +563 -0
- app_localizer-1.0.1/src/app_localizer/fileio.py +55 -0
- app_localizer-1.0.1/src/app_localizer/lockfile.py +95 -0
- app_localizer-1.0.1/src/app_localizer/xcstrings.py +635 -0
- app_localizer-1.0.1/src/app_localizer.egg-info/PKG-INFO +425 -0
- app_localizer-1.0.1/src/app_localizer.egg-info/SOURCES.txt +32 -0
- app_localizer-1.0.1/src/app_localizer.egg-info/dependency_links.txt +1 -0
- app_localizer-1.0.1/src/app_localizer.egg-info/entry_points.txt +2 -0
- app_localizer-1.0.1/src/app_localizer.egg-info/requires.txt +3 -0
- app_localizer-1.0.1/src/app_localizer.egg-info/top_level.txt +1 -0
- app_localizer-1.0.1/tests/test_catalog.py +638 -0
- app_localizer-1.0.1/tests/test_chat_client.py +340 -0
- app_localizer-1.0.1/tests/test_cldr.py +51 -0
- app_localizer-1.0.1/tests/test_cli.py +724 -0
- app_localizer-1.0.1/tests/test_config.py +557 -0
- app_localizer-1.0.1/tests/test_engine.py +103 -0
- app_localizer-1.0.1/tests/test_fastlane.py +471 -0
- app_localizer-1.0.1/tests/test_fileio.py +51 -0
- app_localizer-1.0.1/tests/test_format_contract.py +109 -0
- app_localizer-1.0.1/tests/test_init.py +19 -0
- app_localizer-1.0.1/tests/test_lockfile.py +79 -0
- app_localizer-1.0.1/tests/test_xcstrings.py +1295 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kyle Hughes
|
|
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.
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: app-localizer
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Translate Apple String Catalogs and Fastlane App Store metadata.
|
|
5
|
+
Author: Kyle Hughes
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kylehughes/app-localizer
|
|
8
|
+
Project-URL: Repository, https://github.com/kylehughes/app-localizer
|
|
9
|
+
Project-URL: Changelog, https://github.com/kylehughes/app-localizer/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: localization,i18n,xcstrings,fastlane,app-store,translation,openai
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Software Development :: Localization
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: tomli>=2; python_version < "3.11"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# App Localizer
|
|
25
|
+
|
|
26
|
+
[](https://github.com/kylehughes/app-localizer/actions/workflows/ci.yml)
|
|
27
|
+
|
|
28
|
+
*Translate Xcode String Catalogs (`.xcstrings`) and Fastlane App Store metadata from the command line with any OpenAI-compatible chat completions endpoint.*
|
|
29
|
+
|
|
30
|
+
## About
|
|
31
|
+
|
|
32
|
+
App Localizer is a development and CI tool for app repositories. It edits localization inputs in place, records what it generated, and skips unchanged work on later runs.
|
|
33
|
+
|
|
34
|
+
The output is a translation draft. It still belongs in code review, especially UI, legal text, purchase flows, and App Store copy. Do not ship App Localizer in an app target, call it from app code, or put provider API keys in mobile apps.
|
|
35
|
+
|
|
36
|
+
### Supported Inputs
|
|
37
|
+
|
|
38
|
+
Two input types are supported, configured independently:
|
|
39
|
+
|
|
40
|
+
- **String Catalogs.** Fills missing or untranslated `.xcstrings` entries. Preserves placeholders, substitutions, variation trees, translator comments, and localizations outside the current run.
|
|
41
|
+
- **Fastlane metadata.** Translates `fastlane/metadata/<locale>/*.txt` App Store copy, copies URL fields verbatim, and enforces App Store character limits.
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### Installation
|
|
46
|
+
|
|
47
|
+
Install the CLI from PyPI:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
uv tool install app-localizer
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`pipx install app-localizer` and `python3 -m pip install app-localizer` work too. Pin a version for reproducible installs:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
uv tool install "app-localizer==1.0.1"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Configuration
|
|
60
|
+
|
|
61
|
+
Add `app-localizer.toml` to the app repository root with the sections the repository needs:
|
|
62
|
+
|
|
63
|
+
```toml
|
|
64
|
+
[xcstrings]
|
|
65
|
+
sources = ["App/Resources/Localizable.xcstrings"]
|
|
66
|
+
languages = ["de", "es", "fr", "ja"]
|
|
67
|
+
|
|
68
|
+
[fastlane]
|
|
69
|
+
metadata_path = "fastlane/metadata"
|
|
70
|
+
source_locale = "en-US"
|
|
71
|
+
locales = ["de-DE", "es-ES", "fr-FR", "ja"]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Paths resolve relative to the config file, so the same config works from CI and local shells when `--config` points at it.
|
|
75
|
+
|
|
76
|
+
### First Run
|
|
77
|
+
|
|
78
|
+
Validate the config and file structure without a provider API key:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
app-localizer --check
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Preview pending translation work without writing files:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
app-localizer --dry-run
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Translate configured inputs:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
export OPENAI_API_KEY=...
|
|
94
|
+
app-localizer
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A run rewrites catalogs and metadata in place, then updates `.app-localizer.lock` files. Commit the config, generated localization files, metadata, and lockfiles. Do not commit API keys or `app-localizer.secrets.toml`.
|
|
98
|
+
|
|
99
|
+
## Command-Line Interface
|
|
100
|
+
|
|
101
|
+
### Common Commands
|
|
102
|
+
|
|
103
|
+
Run these from the app repository root unless passing `--config`:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
# Validate config, catalogs, metadata, and existing generated files.
|
|
107
|
+
app-localizer --check
|
|
108
|
+
|
|
109
|
+
# Show pending work without network access or file writes.
|
|
110
|
+
app-localizer --dry-run
|
|
111
|
+
|
|
112
|
+
# Translate every configured domain.
|
|
113
|
+
app-localizer
|
|
114
|
+
|
|
115
|
+
# Process one configured domain.
|
|
116
|
+
app-localizer --domain fastlane
|
|
117
|
+
|
|
118
|
+
# Retranslate even values already marked translated or locked.
|
|
119
|
+
app-localizer --force
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`--check` and `--dry-run` do not require an API key. `--fail-on-untranslated` makes either command exit nonzero while work is pending. It is additive: a run can enable the policy, but cannot disable `fail_on_untranslated = true` set in the config.
|
|
123
|
+
|
|
124
|
+
### Options
|
|
125
|
+
|
|
126
|
+
`--config` selects a config file. Without it, the CLI reads `app-localizer.toml` from the working directory when that file exists.
|
|
127
|
+
|
|
128
|
+
Provider overrides are run-specific: `--base-url`, `--model`, `--reasoning-effort`, `--temperature`, and `--candidates`. `--api-key` supplies a provider key directly and wins over environment variables and the secrets file. `--secrets` selects a different TOML secrets file.
|
|
129
|
+
|
|
130
|
+
`--verbose` enables debug logging. `--version` prints the installed package version. `app-localizer --help` prints the full parser output.
|
|
131
|
+
|
|
132
|
+
### Ad Hoc Catalog Runs
|
|
133
|
+
|
|
134
|
+
For one-off String Catalog translation without a config file, pass catalog paths and target languages directly:
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
app-localizer --source Localizable.xcstrings --languages de es fr
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Ad hoc runs configure only the String Catalog domain. Use a config file for repeatable app-team workflows.
|
|
141
|
+
|
|
142
|
+
## Configuration Reference
|
|
143
|
+
|
|
144
|
+
All non-secret settings live in `app-localizer.toml`. Unknown keys are rejected, so typos fail early. See [examples/app-localizer.toml](examples/app-localizer.toml) for a copyable template.
|
|
145
|
+
|
|
146
|
+
### Provider Settings
|
|
147
|
+
|
|
148
|
+
Provider settings are top-level and shared by both domains:
|
|
149
|
+
|
|
150
|
+
| Key | Default | Description |
|
|
151
|
+
| --- | --- | --- |
|
|
152
|
+
| `base_url` | `https://api.openai.com/v1` | OpenAI-compatible chat completions endpoint. |
|
|
153
|
+
| `api_key_env` | `OPENAI_API_KEY` | Environment variable holding the API key. Set to `""` for keyless local endpoints. |
|
|
154
|
+
| `model` | `gpt-5.2` | Model identifier to request from the endpoint. |
|
|
155
|
+
| `reasoning_effort` | unset | Optional reasoning effort: `none`, `minimal`, `low`, `medium`, `high`, or `xhigh`. Unsupported endpoints are retried without it for the rest of the run. |
|
|
156
|
+
| `temperature` | `0.2` | Sampling temperature for translation requests. |
|
|
157
|
+
| `candidates` | `3` | Candidate translations requested per value before the decider pass chooses one. |
|
|
158
|
+
| `fail_on_untranslated` | `false` | Makes `--check` and `--dry-run` fail while anything is pending. |
|
|
159
|
+
|
|
160
|
+
### String Catalogs
|
|
161
|
+
|
|
162
|
+
`[xcstrings]` enables `.xcstrings` translation:
|
|
163
|
+
|
|
164
|
+
| Key | Description |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `sources` | Catalog paths, resolved relative to the config file. |
|
|
167
|
+
| `languages` | Target Xcode language tags, for example `de`, `pt-BR`, `zh-Hans`. |
|
|
168
|
+
|
|
169
|
+
### Fastlane Metadata
|
|
170
|
+
|
|
171
|
+
`[fastlane]` enables App Store metadata translation:
|
|
172
|
+
|
|
173
|
+
| Key | Description |
|
|
174
|
+
| --- | --- |
|
|
175
|
+
| `metadata_path` | Fastlane metadata directory, resolved relative to the config file. |
|
|
176
|
+
| `source_locale` | Locale directory to translate from, for example `en-US`. Fastlane's `default` fallback directory also works. |
|
|
177
|
+
| `locales` | Target App Store Connect locales, for example `de-DE`, `fr-FR`. Typos like `de` fail validation. |
|
|
178
|
+
| `keywords_trim_to_fit` | Defaults to `true`. Generated `keywords.txt` translations are normalized, deduplicated case-insensitively, and trimmed from the tail until the comma-joined list fits 100 characters. Set to `false` to reject over-limit keyword translations instead. |
|
|
179
|
+
| `allow_unknown_locales` | Defaults to `false`. Set to `true` to accept App Store locales added after this package's release instead of failing validation. Unknown locales warn; malformed tags still fail. |
|
|
180
|
+
|
|
181
|
+
### Secrets
|
|
182
|
+
|
|
183
|
+
API keys do not belong in `app-localizer.toml`. Resolution order is `--api-key`, then the environment variable named by `api_key_env`, then the secrets file. `--secrets` selects a different file.
|
|
184
|
+
|
|
185
|
+
Use an environment variable for individual developer machines:
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
export OPENAI_API_KEY=...
|
|
189
|
+
app-localizer
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Teams can share `app-localizer.secrets.toml` beside the config file. Each entry maps an environment variable name to its value; one file can hold keys for every provider a team uses:
|
|
193
|
+
|
|
194
|
+
```toml
|
|
195
|
+
OPENAI_API_KEY = "sk-..."
|
|
196
|
+
OPENROUTER_API_KEY = "sk-or-..."
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Add the secrets file to the consuming repository's `.gitignore` and distribute it out of band. See [docs/ios-integration.md](docs/ios-integration.md#security-posture) for the full key-handling policy.
|
|
200
|
+
|
|
201
|
+
### Provider Examples
|
|
202
|
+
|
|
203
|
+
Any OpenAI-compatible chat completions endpoint works. The default targets OpenAI and reads `OPENAI_API_KEY`.
|
|
204
|
+
|
|
205
|
+
OpenRouter:
|
|
206
|
+
|
|
207
|
+
```toml
|
|
208
|
+
base_url = "https://openrouter.ai/api/v1"
|
|
209
|
+
api_key_env = "OPENROUTER_API_KEY"
|
|
210
|
+
model = "anthropic/claude-sonnet-4-6"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Ollama:
|
|
214
|
+
|
|
215
|
+
```toml
|
|
216
|
+
base_url = "http://localhost:11434/v1"
|
|
217
|
+
api_key_env = ""
|
|
218
|
+
model = "llama3.3"
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
LM Studio:
|
|
222
|
+
|
|
223
|
+
```toml
|
|
224
|
+
base_url = "http://localhost:1234/v1"
|
|
225
|
+
api_key_env = ""
|
|
226
|
+
model = "qwen2.5-32b-instruct"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Apple Foundation Models has no direct HTTP API. Point `base_url` at a local OpenAI-compatible bridge and set `api_key_env = ""` if the bridge is keyless.
|
|
230
|
+
|
|
231
|
+
Leave `reasoning_effort` unset unless the selected model and endpoint accept it. When an endpoint reports the field as unknown or unsupported, the run warns once, retries without it, and omits it for the rest of that run.
|
|
232
|
+
|
|
233
|
+
## Outputs
|
|
234
|
+
|
|
235
|
+
### String Catalog Behavior
|
|
236
|
+
|
|
237
|
+
For each configured catalog and target language, a run:
|
|
238
|
+
|
|
239
|
+
- Adds missing target-language localizations from the source language.
|
|
240
|
+
- Skips entries marked with `shouldTranslate: false`.
|
|
241
|
+
- Translates `stringUnit` values, nested variation trees, and substitution variations (`%#@name@` / `%arg`).
|
|
242
|
+
- Adds the plural categories the target language's CLDR rules require, even when the source language omits them.
|
|
243
|
+
- Preserves substitution metadata such as `argNum` and `formatSpecifier`, and keeps it in sync with the source language.
|
|
244
|
+
- Preserves localizations for languages outside the current run.
|
|
245
|
+
- Copies whitespace-only values verbatim instead of sending them to the provider.
|
|
246
|
+
|
|
247
|
+
Catalog writes are atomic. The writer keeps file permissions, syncs contents to disk, sorts JSON keys, and uses Xcode-style spacing before replacing the original file. Catalogs with duplicate JSON keys or unsupported catalog versions are rejected.
|
|
248
|
+
|
|
249
|
+
### Fastlane Metadata Behavior
|
|
250
|
+
|
|
251
|
+
Files in the source locale directory are handled by name:
|
|
252
|
+
|
|
253
|
+
| File | Handling | Limit |
|
|
254
|
+
| --- | --- | --- |
|
|
255
|
+
| `name.txt` | translated | 30 |
|
|
256
|
+
| `subtitle.txt` | translated | 30 |
|
|
257
|
+
| `description.txt` | translated | 4000 |
|
|
258
|
+
| `keywords.txt` | translated, kept comma-separated | 100 |
|
|
259
|
+
| `promotional_text.txt` | translated | 170 |
|
|
260
|
+
| `release_notes.txt` | translated | 4000 |
|
|
261
|
+
| `apple_tv_privacy_policy.txt` | translated | - |
|
|
262
|
+
| `marketing_url.txt`, `support_url.txt`, `privacy_url.txt` | copied verbatim, never sent to the provider | - |
|
|
263
|
+
| `review_information/` | ignored | - |
|
|
264
|
+
|
|
265
|
+
Non-localized metadata at the root of `metadata_path` (`copyright.txt`, category files, and `review_information/`) lives outside the locale directories and is never touched.
|
|
266
|
+
|
|
267
|
+
Unrecognized files are skipped with a warning. For prose metadata files, a translation over the App Store character limit is rejected like a placeholder mismatch. If no candidate fits, the file errors and the run fails.
|
|
268
|
+
|
|
269
|
+
Generated `keywords.txt` translations differ when `keywords_trim_to_fit = true`: spaces around keywords are stripped, case-insensitive duplicates are removed, and trailing keywords are dropped until the comma-joined list fits 100 characters. Dropped terms are logged in a warning. Set `keywords_trim_to_fit = false` to reject over-limit keyword translations instead. `--check` still fails when a source file exceeds its own limit.
|
|
270
|
+
|
|
271
|
+
Metadata `.txt` files have no per-value translation state, so skip decisions come from the lockfile's per-file source hashes. `--check` validates locked target files against App Store character limits. Other hand-edited target changes with an unchanged source are not detected as pending. Use `--force` to rewrite everything.
|
|
272
|
+
|
|
273
|
+
### Lockfiles and Skip Behavior
|
|
274
|
+
|
|
275
|
+
`.app-localizer.lock` is the skip cache. The String Catalog domain writes one beside each catalog. The Fastlane domain writes one at the metadata root.
|
|
276
|
+
|
|
277
|
+
Commit lockfiles. Without them, later runs have less information and translate more than they need to.
|
|
278
|
+
|
|
279
|
+
String Catalog lockfile section:
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
{
|
|
283
|
+
"catalogs" : {
|
|
284
|
+
"Localizable.xcstrings" : {
|
|
285
|
+
"de" : {
|
|
286
|
+
"baseUrl" : "https://api.openai.com/v1",
|
|
287
|
+
"config" : "sha256...",
|
|
288
|
+
"hash" : "sha256...",
|
|
289
|
+
"model" : "gpt-5.2",
|
|
290
|
+
"timestamp" : "2026-06-11T00:00:00+00:00"
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
"generatedBy" : "app-localizer",
|
|
295
|
+
"version" : 1
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Fastlane lockfile section:
|
|
300
|
+
|
|
301
|
+
```json
|
|
302
|
+
{
|
|
303
|
+
"fastlane" : {
|
|
304
|
+
"de-DE" : {
|
|
305
|
+
"marketing_url.txt" : { "config" : "verbatim", "hash" : "sha256...", "..." : "..." },
|
|
306
|
+
"name.txt" : {
|
|
307
|
+
"baseUrl" : "https://api.openai.com/v1",
|
|
308
|
+
"config" : "sha256...",
|
|
309
|
+
"hash" : "sha256...",
|
|
310
|
+
"model" : "gpt-5.2",
|
|
311
|
+
"timestamp" : "2026-06-11T00:00:00+00:00"
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
"generatedBy" : "app-localizer",
|
|
316
|
+
"version" : 1
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
For catalogs, `hash` fingerprints the source-language content per catalog. A language is skipped only when the hashes match and no units are pending.
|
|
321
|
+
|
|
322
|
+
For metadata, `hash` fingerprints each source file. Skips are per file: a file is translated again when the target is missing, the source changed, or the configuration changed.
|
|
323
|
+
|
|
324
|
+
`config` fingerprints the endpoint, model, reasoning effort, temperature, candidate count, prompts, and domain-specific validation policy. For metadata, it also includes the character limit and keyword trim policy. `baseUrl` and `model` are informational fields that record what produced an entry.
|
|
325
|
+
|
|
326
|
+
Unreadable, unsupported, or foreign lockfiles are discarded with a warning and rebuilt on the next run. Sections a domain does not own are preserved.
|
|
327
|
+
|
|
328
|
+
### Limits and Review
|
|
329
|
+
|
|
330
|
+
Translations are validated before writing:
|
|
331
|
+
|
|
332
|
+
- Catalog translations must preserve printf placeholders, Swift interpolation placeholders, and String Catalog substitution tokens.
|
|
333
|
+
- Fastlane metadata translations must fit App Store character limits.
|
|
334
|
+
- Config files must not contain unknown keys, duplicate sources or languages, malformed language tags, or invalid App Store locale codes.
|
|
335
|
+
|
|
336
|
+
Current limits:
|
|
337
|
+
|
|
338
|
+
- The catalog lockfile is catalog-level per language, not per key. Per-unit state lives in the catalog itself.
|
|
339
|
+
- Plural variations are expanded to the categories the target language's CLDR rules require (Russian `few` and `many`, for example), translated from the source's `other` form. Languages outside the built-in CLDR table fall back to mirroring the source's categories.
|
|
340
|
+
- Output is a draft. Require human review for high-risk UI, marketing copy, legal text, purchase flows, and short ambiguous strings.
|
|
341
|
+
|
|
342
|
+
## Team Workflow
|
|
343
|
+
|
|
344
|
+
### CI
|
|
345
|
+
|
|
346
|
+
Copy [examples/github-actions/app-localizer.yml](examples/github-actions/app-localizer.yml) into the app repository. Pull requests run `--check` with no API key. Manual workflow dispatch runs translation from a repository secret and opens a draft PR with the generated changes.
|
|
347
|
+
|
|
348
|
+
If the app repository needs credentials to install App Localizer, update the workflow install step to use the repository's git credential or package source.
|
|
349
|
+
|
|
350
|
+
For release branches, set `fail_on_untranslated = true` or pass `--fail-on-untranslated` so checks fail while configured targets still have untranslated content.
|
|
351
|
+
|
|
352
|
+
### App-Team Workflow
|
|
353
|
+
|
|
354
|
+
See [docs/ios-integration.md](docs/ios-integration.md) for the recommended app-team workflow: API key handling, developer checks, CI translation PRs, review policy, and release-branch behavior.
|
|
355
|
+
|
|
356
|
+
Run translation before `fastlane deliver` so uploaded metadata is current.
|
|
357
|
+
|
|
358
|
+
## Development
|
|
359
|
+
|
|
360
|
+
### Local Development
|
|
361
|
+
|
|
362
|
+
Install the package editable and run the test suite from the repository root:
|
|
363
|
+
|
|
364
|
+
```sh
|
|
365
|
+
python3 -m venv .venv
|
|
366
|
+
. .venv/bin/activate
|
|
367
|
+
python3 -m pip install -e .
|
|
368
|
+
PYTHONPATH=src python3 -m unittest discover -s tests
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
CI runs the same suite on every push and pull request against the oldest and newest supported Python versions.
|
|
372
|
+
|
|
373
|
+
### Releasing
|
|
374
|
+
|
|
375
|
+
Releases are plain git tags; there is no package registry.
|
|
376
|
+
|
|
377
|
+
1. Update `version` in `pyproject.toml` and commit to `main`.
|
|
378
|
+
2. Tag the same version with a leading `v`.
|
|
379
|
+
3. Push `main` and the tag.
|
|
380
|
+
|
|
381
|
+
```sh
|
|
382
|
+
git tag vX.Y.Z
|
|
383
|
+
git push origin main vX.Y.Z
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Pushing the tag runs the release workflow. It verifies the tag matches the package version, runs the test suite, builds the sdist and wheel, publishes to PyPI via trusted publishing, and publishes a GitHub Release with generated notes and the build files attached.
|
|
387
|
+
|
|
388
|
+
Consumers install a pinned release from PyPI:
|
|
389
|
+
|
|
390
|
+
```sh
|
|
391
|
+
python3 -m pip install "app-localizer==X.Y.Z"
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Compatibility
|
|
395
|
+
|
|
396
|
+
Versioning follows semantic versioning as of 1.0.0. The stable surface is the CLI flag set, the `app-localizer.toml` config schema, and the `.app-localizer.lock` format. A major release is required to break any of them.
|
|
397
|
+
|
|
398
|
+
The default model (`gpt-5.2`) is not part of the stable surface and may change in a minor release; pin `model` in the config to control it. Translation output is model-defined and never guaranteed to be stable. The lockfile is a disposable cache: an unreadable or future-version lockfile is discarded and rebuilt, so its format can advance without a breaking change.
|
|
399
|
+
|
|
400
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
401
|
+
|
|
402
|
+
## Contributions
|
|
403
|
+
|
|
404
|
+
App Localizer is not accepting source contributions at this time. Bug reports will be considered.
|
|
405
|
+
|
|
406
|
+
## Author
|
|
407
|
+
|
|
408
|
+
[Kyle Hughes](https://kylehugh.es)
|
|
409
|
+
|
|
410
|
+
[![Bluesky][bluesky_image]][bluesky_url]
|
|
411
|
+
[![LinkedIn][linkedin_image]][linkedin_url]
|
|
412
|
+
[![Mastodon][mastodon_image]][mastodon_url]
|
|
413
|
+
|
|
414
|
+
[bluesky_image]: https://img.shields.io/badge/Bluesky-0285FF?logo=bluesky&logoColor=fff
|
|
415
|
+
[bluesky_url]: https://bsky.app/profile/kylehugh.es
|
|
416
|
+
[linkedin_image]: https://img.shields.io/badge/LinkedIn-0A66C2?logo=linkedin&logoColor=fff
|
|
417
|
+
[linkedin_url]: https://www.linkedin.com/in/kyle-hughes
|
|
418
|
+
[mastodon_image]: https://img.shields.io/mastodon/follow/109356914477272810?domain=https%3A%2F%2Fmister.computer&style=social
|
|
419
|
+
[mastodon_url]: https://mister.computer/@kyle
|
|
420
|
+
|
|
421
|
+
## License
|
|
422
|
+
|
|
423
|
+
App Localizer is available under the MIT license.
|
|
424
|
+
|
|
425
|
+
See `LICENSE` for details.
|