android-localisation 1.0.4__tar.gz → 1.0.6__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.
Files changed (17) hide show
  1. {android_localisation-1.0.4/android_localisation.egg-info → android_localisation-1.0.6}/PKG-INFO +91 -15
  2. {android_localisation-1.0.4 → android_localisation-1.0.6}/README.md +90 -14
  3. {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation/__init__.py +1 -1
  4. {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation/cli.py +6 -1
  5. {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation/fix.py +1 -1
  6. {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation/translate.py +95 -59
  7. {android_localisation-1.0.4 → android_localisation-1.0.6/android_localisation.egg-info}/PKG-INFO +91 -15
  8. {android_localisation-1.0.4 → android_localisation-1.0.6}/pyproject.toml +1 -1
  9. {android_localisation-1.0.4 → android_localisation-1.0.6}/LICENSE +0 -0
  10. {android_localisation-1.0.4 → android_localisation-1.0.6}/MANIFEST.in +0 -0
  11. {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation/java/VerifyStrings.java +0 -0
  12. {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation/verify.py +0 -0
  13. {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation.egg-info/SOURCES.txt +0 -0
  14. {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation.egg-info/dependency_links.txt +0 -0
  15. {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation.egg-info/entry_points.txt +0 -0
  16. {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation.egg-info/top_level.txt +0 -0
  17. {android_localisation-1.0.4 → android_localisation-1.0.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: android-localisation
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: Zero-dependency Android strings.xml translation and verification using LLMs (Gemini, OpenAI, Anthropic, Ollama).
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/BharathKmalviya/android-llm-localization
@@ -26,13 +26,15 @@ Description-Content-Type: text/markdown
26
26
  License-File: LICENSE
27
27
  Dynamic: license-file
28
28
 
29
- # android-llm-localization
29
+ # android-localisation
30
30
 
31
31
  [![PyPI version](https://img.shields.io/pypi/v/android-localisation.svg)](https://pypi.org/project/android-localisation/)
32
32
  [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://pypi.org/project/android-localisation/)
33
33
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
34
34
 
35
- Translate your Android `strings.xml` into multiple languages using AI — Gemini, OpenAI, Anthropic, or a local model via Ollama. No paid service, no CSV exports, no copy-paste.
35
+ **PyPI:** [`android-localisation`](https://pypi.org/project/android-localisation/) · **CLI:** `android-localise` · **Repo:** [android-llm-localization](https://github.com/BharathKmalviya/android-llm-localization)
36
+
37
+ Translate your Android `strings.xml` into multiple languages using AI — Gemini, OpenAI, Anthropic, or a local model via Ollama. No paid translation service, no CSV exports, no copy-paste.
36
38
 
37
39
  ---
38
40
 
@@ -86,7 +88,7 @@ When you run `android-localise translate --api-key YOUR_KEY`, here's exactly wha
86
88
  | What | Default |
87
89
  |---|---|
88
90
  | Provider | Gemini |
89
- | Model | `gemini-2.5-flash` |
91
+ | Model | `gemini-3.5-flash` |
90
92
  | Source directory | `app/src/main/res` |
91
93
  | Delay between requests | 5 seconds |
92
94
  | App context | none (generic prompt) |
@@ -150,6 +152,7 @@ android-localise translate \
150
152
  | `--res-dir` | Path to your `res/` folder | `app/src/main/res` |
151
153
  | `--base-url` | API endpoint for local/custom providers | — |
152
154
  | `--sleep` | Seconds to wait between language requests | `5.0` |
155
+ | `--timeout` | Seconds to wait for each API response (up to 3 attempts on timeout) | `180` |
153
156
 
154
157
  ---
155
158
 
@@ -157,10 +160,13 @@ android-localise translate \
157
160
 
158
161
  ```bash
159
162
  android-localise fix
163
+ android-localise fix --res-dir path/to/res
160
164
  ```
161
165
 
162
166
  LLMs occasionally produce output that looks correct but breaks the Android build — curly apostrophes (`'`) instead of escaped ones (`\'`), unescaped double quotes, or mangled `%` signs. This command scans every translated `strings.xml` and corrects these silently.
163
167
 
168
+ Strings marked `formatted="false"` are skipped — their `%` signs are literal, not format specifiers.
169
+
164
170
  Always run this before `verify` and before building.
165
171
 
166
172
  ---
@@ -169,11 +175,12 @@ Always run this before `verify` and before building.
169
175
 
170
176
  ```bash
171
177
  android-localise verify
178
+ android-localise verify --res-dir path/to/res
172
179
  ```
173
180
 
174
181
  Takes every translated string that contains a format specifier (`%1$s`, `%d`, `%1$f`, etc.) and calls `String.format()` on it using Java's actual runtime. If a translated string would throw `UnknownFormatConversionException` or `MissingFormatArgumentException` in your app, this catches it before your users do.
175
182
 
176
- Requires `javac` in your PATH. If you don't have it system-wide, run this from the Terminal tab inside Android Studio — it ships with a JDK.
183
+ Strings marked `formatted="false"` are skipped. Requires `javac` in your PATH. If you don't have it system-wide, run this from the Terminal tab inside Android Studio — it ships with a JDK.
177
184
 
178
185
  ---
179
186
 
@@ -190,21 +197,21 @@ Lists every available model and fallback for each provider.
190
197
 
191
198
  ## Providers
192
199
 
193
- By default the tool uses Gemini with `gemini-2.5-flash`. You can switch providers with `--provider` and optionally pin a specific model with `--model`.
200
+ By default the tool uses Gemini with `gemini-3.5-flash`. You can switch providers with `--provider` and optionally pin a specific model with `--model`.
194
201
 
195
202
  | Provider | Default model | Fallbacks | API key env var |
196
203
  |---|---|---|---|
197
- | `gemini` _(default)_ | `gemini-2.5-flash` | `gemini-2.0-flash` → `gemini-1.5-flash` → `gemini-1.5-pro` | `GEMINI_API_KEY` |
198
- | `openai` | `gpt-4o-mini` | `gpt-4o` → `gpt-3.5-turbo` | `OPENAI_API_KEY` |
199
- | `anthropic` | `claude-3-5-haiku-latest` | `claude-3-5-sonnet-latest` → `claude-3-opus-latest` | `ANTHROPIC_API_KEY` |
200
- | `custom` | set with `--model` | none | — |
204
+ | `gemini` _(default)_ | `gemini-3.5-flash` | `gemini-3.1-flash-lite` → `gemini-2.5-flash` → `gemini-2.5-flash-lite` | `GEMINI_API_KEY` |
205
+ | `openai` | `gpt-5.4-mini` | `gpt-5-mini` → `gpt-4o-mini` | `OPENAI_API_KEY` |
206
+ | `anthropic` | `claude-haiku-4-5` | `claude-sonnet-4-6` → `claude-opus-4-8` | `ANTHROPIC_API_KEY` |
207
+ | `custom` | set with `--model` | none | `OPENAI_API_KEY` (optional) |
201
208
 
202
209
  If the default model returns a "model not found" error (e.g. it was deprecated), the tool automatically retries with the next fallback. If you pin a model with `--model`, no fallback is used.
203
210
 
204
211
  **Using OpenAI:**
205
212
  ```bash
206
213
  android-localise translate --provider openai --api-key YOUR_KEY
207
- android-localise translate --provider openai --model gpt-4o --api-key YOUR_KEY
214
+ android-localise translate --provider openai --model gpt-5.4-mini --api-key YOUR_KEY
208
215
  ```
209
216
 
210
217
  **Using Anthropic:**
@@ -239,6 +246,9 @@ export GEMINI_API_KEY=your_key
239
246
 
240
247
  # Windows PowerShell
241
248
  $env:GEMINI_API_KEY = "your_key"
249
+
250
+ # Windows CMD
251
+ set GEMINI_API_KEY=your_key
242
252
  ```
243
253
 
244
254
  Then just run:
@@ -251,19 +261,25 @@ android-localise translate
251
261
  | `GEMINI_API_KEY` | `--provider gemini` |
252
262
  | `OPENAI_API_KEY` | `--provider openai` and `--provider custom` |
253
263
  | `ANTHROPIC_API_KEY` | `--provider anthropic` |
264
+ | `API_KEY` | fallback for any provider if the provider-specific var is not set |
254
265
 
255
266
  ---
256
267
 
257
268
  ## Full workflow example
258
269
 
259
270
  ```bash
260
- # First time setup — create locale folders
271
+ # First time setup — create locale folders (macOS / Linux)
261
272
  mkdir -p app/src/main/res/values-hi
262
273
  mkdir -p app/src/main/res/values-es
263
274
  mkdir -p app/src/main/res/values-de
264
275
 
276
+ # Windows PowerShell
277
+ New-Item -ItemType Directory -Force app/src/main/res/values-hi
278
+ New-Item -ItemType Directory -Force app/src/main/res/values-es
279
+ New-Item -ItemType Directory -Force app/src/main/res/values-de
280
+
265
281
  # Set your key once
266
- export GEMINI_API_KEY=your_key
282
+ export GEMINI_API_KEY=your_key # or $env:GEMINI_API_KEY on Windows
267
283
 
268
284
  # Translate, fix, verify
269
285
  android-localise translate --app-context "a habit tracking app"
@@ -278,23 +294,83 @@ After this, whenever you add or change strings in your English `strings.xml`, ru
278
294
 
279
295
  ---
280
296
 
297
+ ## Platform support
298
+
299
+ This project is developed and **manually tested on Windows only** at the moment. It is written in pure Python (stdlib only) and should run on macOS and Linux, but those platforms have **not been verified** by the maintainer yet.
300
+
301
+ We especially need help testing on:
302
+
303
+ - **macOS** — `translate`, `fix`, `verify` (including `javac` / Android Studio terminal)
304
+ - **Linux** — same workflow, plus common CI environments
305
+
306
+ If you use another OS, please try the [quick start](#quick-start) workflow and report what you find:
307
+
308
+ - **Works?** — open a [GitHub issue](https://github.com/BharathKmalviya/android-llm-localization/issues) titled e.g. `Confirmed working on macOS 14` with your OS, Python version, and provider used
309
+ - **Broken?** — open a [bug report](https://github.com/BharathKmalviya/android-llm-localization/issues/new?template=bug_report.md) with the full error output
310
+ - **Want to help more?** — see [Contributing](#contributing) and [CONTRIBUTING.md](CONTRIBUTING.md)
311
+
312
+ Cross-platform fixes and test notes in pull requests are very welcome.
313
+
314
+ ---
315
+
316
+ ## Limitations
317
+
318
+ | Topic | Detail |
319
+ |---|---|
320
+ | **Platform testing** | Maintainer-tested on **Windows only** — macOS and Linux need community verification (see [Platform support](#platform-support)) |
321
+ | **Scope** | Translates `values/strings.xml` only — not `plurals.xml`, `arrays.xml`, or other resource files |
322
+ | **Overwrite** | Each run replaces the entire `strings.xml` in each locale folder with a fresh LLM translation |
323
+ | **Folder scan** | Without `--languages`, every `values-*` folder is treated as a locale. Qualifier-only folders like `values-night` or `values-sw600dp` may be picked up incorrectly — prefer `--languages` or keep only locale folders in `res/` |
324
+ | **Network** | `translate` requires internet access to reach the LLM API (except local `custom` providers) |
325
+ | **JDK** | `verify` requires `javac` on your PATH |
326
+
327
+ ---
328
+
329
+ ## Troubleshooting
330
+
331
+ | Problem | What to try |
332
+ |---|---|
333
+ | `Could not find English strings.xml` | Check `--res-dir` points to your `res/` folder and `values/strings.xml` exists |
334
+ | `No locale directories found` | Add `--languages hi,es,fr` or create `values-<lang>/` folders manually |
335
+ | API auth errors | Confirm your key env var or `--api-key` matches the `--provider` |
336
+ | `javac` not found | Install a JDK or run `verify` from Android Studio's terminal |
337
+ | Build fails on apostrophes | Run `android-localise fix` before building |
338
+ | `%` crashes at runtime | Run `android-localise verify` — it catches bad format specifiers before release |
339
+ | Wrong folder translated | Use `--languages` to target exact locale codes instead of folder scan |
340
+
341
+ ---
342
+
281
343
  ## Roadmap
282
344
 
283
345
  - [ ] **iOS support** — translate `Localizable.strings` and `Localizable.xcstrings` for iOS/macOS apps. The LLM prompt and provider logic is already in place — it mainly needs a parser for Apple's strings format and the right folder structure (`<lang>.lproj/`). Good first contribution if you're familiar with iOS projects.
346
+ - [ ] **Smarter locale folder detection** — skip non-locale `values-*` qualifiers (`night`, `sw600dp`, `v21`, etc.) when scanning without `--languages`
347
+ - [ ] **Automated test suite** — unit tests for `fix`, XML parsing, and format-specifier edge cases
348
+ - [ ] **Cross-platform verification** — confirm `translate`, `fix`, and `verify` on macOS and Linux (Windows is maintainer-tested today)
284
349
 
285
350
  ---
286
351
 
287
352
  ## Contributing
288
353
 
289
- Bug reports and pull requests are welcome. For larger changes, open an issue first.
354
+ Bug reports, pull requests, and **cross-platform testing** are all welcome. For larger changes, open an issue first.
355
+
356
+ **No code required** — if you are on macOS or Linux, running the tool and filing an issue (pass or fail) is a real contribution. See [Platform support](#platform-support).
357
+
358
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development workflow, branch strategy, and release process.
290
359
 
291
360
  ```bash
292
361
  git clone https://github.com/BharathKmalviya/android-llm-localization
293
362
  cd android-llm-localization
363
+ git checkout dev
294
364
  pip install -e .
295
365
  ```
296
366
 
297
- Releases are automated via GitHub Actions — bump the version in `pyproject.toml` and `__init__.py`, update `CHANGELOG.md`, and push to `master`.
367
+ Day-to-day work happens on the `dev` branch. Releases are merged to `master` via pull request, which triggers automated PyPI publishing.
368
+
369
+ ---
370
+
371
+ ## Security
372
+
373
+ To report a security vulnerability, see [SECURITY.md](SECURITY.md). Please do not open public issues for security-sensitive reports.
298
374
 
299
375
  ---
300
376
 
@@ -1,10 +1,12 @@
1
- # android-llm-localization
1
+ # android-localisation
2
2
 
3
3
  [![PyPI version](https://img.shields.io/pypi/v/android-localisation.svg)](https://pypi.org/project/android-localisation/)
4
4
  [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://pypi.org/project/android-localisation/)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
6
 
7
- Translate your Android `strings.xml` into multiple languages using AI — Gemini, OpenAI, Anthropic, or a local model via Ollama. No paid service, no CSV exports, no copy-paste.
7
+ **PyPI:** [`android-localisation`](https://pypi.org/project/android-localisation/) · **CLI:** `android-localise` · **Repo:** [android-llm-localization](https://github.com/BharathKmalviya/android-llm-localization)
8
+
9
+ Translate your Android `strings.xml` into multiple languages using AI — Gemini, OpenAI, Anthropic, or a local model via Ollama. No paid translation service, no CSV exports, no copy-paste.
8
10
 
9
11
  ---
10
12
 
@@ -58,7 +60,7 @@ When you run `android-localise translate --api-key YOUR_KEY`, here's exactly wha
58
60
  | What | Default |
59
61
  |---|---|
60
62
  | Provider | Gemini |
61
- | Model | `gemini-2.5-flash` |
63
+ | Model | `gemini-3.5-flash` |
62
64
  | Source directory | `app/src/main/res` |
63
65
  | Delay between requests | 5 seconds |
64
66
  | App context | none (generic prompt) |
@@ -122,6 +124,7 @@ android-localise translate \
122
124
  | `--res-dir` | Path to your `res/` folder | `app/src/main/res` |
123
125
  | `--base-url` | API endpoint for local/custom providers | — |
124
126
  | `--sleep` | Seconds to wait between language requests | `5.0` |
127
+ | `--timeout` | Seconds to wait for each API response (up to 3 attempts on timeout) | `180` |
125
128
 
126
129
  ---
127
130
 
@@ -129,10 +132,13 @@ android-localise translate \
129
132
 
130
133
  ```bash
131
134
  android-localise fix
135
+ android-localise fix --res-dir path/to/res
132
136
  ```
133
137
 
134
138
  LLMs occasionally produce output that looks correct but breaks the Android build — curly apostrophes (`'`) instead of escaped ones (`\'`), unescaped double quotes, or mangled `%` signs. This command scans every translated `strings.xml` and corrects these silently.
135
139
 
140
+ Strings marked `formatted="false"` are skipped — their `%` signs are literal, not format specifiers.
141
+
136
142
  Always run this before `verify` and before building.
137
143
 
138
144
  ---
@@ -141,11 +147,12 @@ Always run this before `verify` and before building.
141
147
 
142
148
  ```bash
143
149
  android-localise verify
150
+ android-localise verify --res-dir path/to/res
144
151
  ```
145
152
 
146
153
  Takes every translated string that contains a format specifier (`%1$s`, `%d`, `%1$f`, etc.) and calls `String.format()` on it using Java's actual runtime. If a translated string would throw `UnknownFormatConversionException` or `MissingFormatArgumentException` in your app, this catches it before your users do.
147
154
 
148
- Requires `javac` in your PATH. If you don't have it system-wide, run this from the Terminal tab inside Android Studio — it ships with a JDK.
155
+ Strings marked `formatted="false"` are skipped. Requires `javac` in your PATH. If you don't have it system-wide, run this from the Terminal tab inside Android Studio — it ships with a JDK.
149
156
 
150
157
  ---
151
158
 
@@ -162,21 +169,21 @@ Lists every available model and fallback for each provider.
162
169
 
163
170
  ## Providers
164
171
 
165
- By default the tool uses Gemini with `gemini-2.5-flash`. You can switch providers with `--provider` and optionally pin a specific model with `--model`.
172
+ By default the tool uses Gemini with `gemini-3.5-flash`. You can switch providers with `--provider` and optionally pin a specific model with `--model`.
166
173
 
167
174
  | Provider | Default model | Fallbacks | API key env var |
168
175
  |---|---|---|---|
169
- | `gemini` _(default)_ | `gemini-2.5-flash` | `gemini-2.0-flash` → `gemini-1.5-flash` → `gemini-1.5-pro` | `GEMINI_API_KEY` |
170
- | `openai` | `gpt-4o-mini` | `gpt-4o` → `gpt-3.5-turbo` | `OPENAI_API_KEY` |
171
- | `anthropic` | `claude-3-5-haiku-latest` | `claude-3-5-sonnet-latest` → `claude-3-opus-latest` | `ANTHROPIC_API_KEY` |
172
- | `custom` | set with `--model` | none | — |
176
+ | `gemini` _(default)_ | `gemini-3.5-flash` | `gemini-3.1-flash-lite` → `gemini-2.5-flash` → `gemini-2.5-flash-lite` | `GEMINI_API_KEY` |
177
+ | `openai` | `gpt-5.4-mini` | `gpt-5-mini` → `gpt-4o-mini` | `OPENAI_API_KEY` |
178
+ | `anthropic` | `claude-haiku-4-5` | `claude-sonnet-4-6` → `claude-opus-4-8` | `ANTHROPIC_API_KEY` |
179
+ | `custom` | set with `--model` | none | `OPENAI_API_KEY` (optional) |
173
180
 
174
181
  If the default model returns a "model not found" error (e.g. it was deprecated), the tool automatically retries with the next fallback. If you pin a model with `--model`, no fallback is used.
175
182
 
176
183
  **Using OpenAI:**
177
184
  ```bash
178
185
  android-localise translate --provider openai --api-key YOUR_KEY
179
- android-localise translate --provider openai --model gpt-4o --api-key YOUR_KEY
186
+ android-localise translate --provider openai --model gpt-5.4-mini --api-key YOUR_KEY
180
187
  ```
181
188
 
182
189
  **Using Anthropic:**
@@ -211,6 +218,9 @@ export GEMINI_API_KEY=your_key
211
218
 
212
219
  # Windows PowerShell
213
220
  $env:GEMINI_API_KEY = "your_key"
221
+
222
+ # Windows CMD
223
+ set GEMINI_API_KEY=your_key
214
224
  ```
215
225
 
216
226
  Then just run:
@@ -223,19 +233,25 @@ android-localise translate
223
233
  | `GEMINI_API_KEY` | `--provider gemini` |
224
234
  | `OPENAI_API_KEY` | `--provider openai` and `--provider custom` |
225
235
  | `ANTHROPIC_API_KEY` | `--provider anthropic` |
236
+ | `API_KEY` | fallback for any provider if the provider-specific var is not set |
226
237
 
227
238
  ---
228
239
 
229
240
  ## Full workflow example
230
241
 
231
242
  ```bash
232
- # First time setup — create locale folders
243
+ # First time setup — create locale folders (macOS / Linux)
233
244
  mkdir -p app/src/main/res/values-hi
234
245
  mkdir -p app/src/main/res/values-es
235
246
  mkdir -p app/src/main/res/values-de
236
247
 
248
+ # Windows PowerShell
249
+ New-Item -ItemType Directory -Force app/src/main/res/values-hi
250
+ New-Item -ItemType Directory -Force app/src/main/res/values-es
251
+ New-Item -ItemType Directory -Force app/src/main/res/values-de
252
+
237
253
  # Set your key once
238
- export GEMINI_API_KEY=your_key
254
+ export GEMINI_API_KEY=your_key # or $env:GEMINI_API_KEY on Windows
239
255
 
240
256
  # Translate, fix, verify
241
257
  android-localise translate --app-context "a habit tracking app"
@@ -250,23 +266,83 @@ After this, whenever you add or change strings in your English `strings.xml`, ru
250
266
 
251
267
  ---
252
268
 
269
+ ## Platform support
270
+
271
+ This project is developed and **manually tested on Windows only** at the moment. It is written in pure Python (stdlib only) and should run on macOS and Linux, but those platforms have **not been verified** by the maintainer yet.
272
+
273
+ We especially need help testing on:
274
+
275
+ - **macOS** — `translate`, `fix`, `verify` (including `javac` / Android Studio terminal)
276
+ - **Linux** — same workflow, plus common CI environments
277
+
278
+ If you use another OS, please try the [quick start](#quick-start) workflow and report what you find:
279
+
280
+ - **Works?** — open a [GitHub issue](https://github.com/BharathKmalviya/android-llm-localization/issues) titled e.g. `Confirmed working on macOS 14` with your OS, Python version, and provider used
281
+ - **Broken?** — open a [bug report](https://github.com/BharathKmalviya/android-llm-localization/issues/new?template=bug_report.md) with the full error output
282
+ - **Want to help more?** — see [Contributing](#contributing) and [CONTRIBUTING.md](CONTRIBUTING.md)
283
+
284
+ Cross-platform fixes and test notes in pull requests are very welcome.
285
+
286
+ ---
287
+
288
+ ## Limitations
289
+
290
+ | Topic | Detail |
291
+ |---|---|
292
+ | **Platform testing** | Maintainer-tested on **Windows only** — macOS and Linux need community verification (see [Platform support](#platform-support)) |
293
+ | **Scope** | Translates `values/strings.xml` only — not `plurals.xml`, `arrays.xml`, or other resource files |
294
+ | **Overwrite** | Each run replaces the entire `strings.xml` in each locale folder with a fresh LLM translation |
295
+ | **Folder scan** | Without `--languages`, every `values-*` folder is treated as a locale. Qualifier-only folders like `values-night` or `values-sw600dp` may be picked up incorrectly — prefer `--languages` or keep only locale folders in `res/` |
296
+ | **Network** | `translate` requires internet access to reach the LLM API (except local `custom` providers) |
297
+ | **JDK** | `verify` requires `javac` on your PATH |
298
+
299
+ ---
300
+
301
+ ## Troubleshooting
302
+
303
+ | Problem | What to try |
304
+ |---|---|
305
+ | `Could not find English strings.xml` | Check `--res-dir` points to your `res/` folder and `values/strings.xml` exists |
306
+ | `No locale directories found` | Add `--languages hi,es,fr` or create `values-<lang>/` folders manually |
307
+ | API auth errors | Confirm your key env var or `--api-key` matches the `--provider` |
308
+ | `javac` not found | Install a JDK or run `verify` from Android Studio's terminal |
309
+ | Build fails on apostrophes | Run `android-localise fix` before building |
310
+ | `%` crashes at runtime | Run `android-localise verify` — it catches bad format specifiers before release |
311
+ | Wrong folder translated | Use `--languages` to target exact locale codes instead of folder scan |
312
+
313
+ ---
314
+
253
315
  ## Roadmap
254
316
 
255
317
  - [ ] **iOS support** — translate `Localizable.strings` and `Localizable.xcstrings` for iOS/macOS apps. The LLM prompt and provider logic is already in place — it mainly needs a parser for Apple's strings format and the right folder structure (`<lang>.lproj/`). Good first contribution if you're familiar with iOS projects.
318
+ - [ ] **Smarter locale folder detection** — skip non-locale `values-*` qualifiers (`night`, `sw600dp`, `v21`, etc.) when scanning without `--languages`
319
+ - [ ] **Automated test suite** — unit tests for `fix`, XML parsing, and format-specifier edge cases
320
+ - [ ] **Cross-platform verification** — confirm `translate`, `fix`, and `verify` on macOS and Linux (Windows is maintainer-tested today)
256
321
 
257
322
  ---
258
323
 
259
324
  ## Contributing
260
325
 
261
- Bug reports and pull requests are welcome. For larger changes, open an issue first.
326
+ Bug reports, pull requests, and **cross-platform testing** are all welcome. For larger changes, open an issue first.
327
+
328
+ **No code required** — if you are on macOS or Linux, running the tool and filing an issue (pass or fail) is a real contribution. See [Platform support](#platform-support).
329
+
330
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development workflow, branch strategy, and release process.
262
331
 
263
332
  ```bash
264
333
  git clone https://github.com/BharathKmalviya/android-llm-localization
265
334
  cd android-llm-localization
335
+ git checkout dev
266
336
  pip install -e .
267
337
  ```
268
338
 
269
- Releases are automated via GitHub Actions — bump the version in `pyproject.toml` and `__init__.py`, update `CHANGELOG.md`, and push to `master`.
339
+ Day-to-day work happens on the `dev` branch. Releases are merged to `master` via pull request, which triggers automated PyPI publishing.
340
+
341
+ ---
342
+
343
+ ## Security
344
+
345
+ To report a security vulnerability, see [SECURITY.md](SECURITY.md). Please do not open public issues for security-sensitive reports.
270
346
 
271
347
  ---
272
348
 
@@ -2,4 +2,4 @@
2
2
  android-localisation: Zero-dependency Android strings.xml translation using LLMs.
3
3
  """
4
4
 
5
- __version__ = "1.0.4"
5
+ __version__ = "1.0.6"
@@ -31,6 +31,11 @@ def main():
31
31
  translate_parser.add_argument("--base-url", help="Custom OpenAI-compatible endpoint URL (required for 'custom' provider)")
32
32
  translate_parser.add_argument("--app-context", help="Short description of your app for better translations")
33
33
  translate_parser.add_argument("--sleep", type=float, default=5.0, help="Seconds between API requests (default: 5.0)")
34
+ from android_localisation.translate import DEFAULT_API_TIMEOUT
35
+ translate_parser.add_argument(
36
+ "--timeout", type=float, default=DEFAULT_API_TIMEOUT,
37
+ help=f"Seconds to wait for each API response, up to 3 attempts on timeout (default: {DEFAULT_API_TIMEOUT})",
38
+ )
34
39
  translate_parser.add_argument("--languages", help="Comma-separated language codes, e.g. hi,es,fr,de — creates folders and strings.xml automatically")
35
40
 
36
41
  # --- fix ---
@@ -75,7 +80,7 @@ def main():
75
80
  print(" → Any model name your local server supports (must use --model)")
76
81
  print()
77
82
  print(" Tip: use --model to pick any model, e.g:")
78
- print(" android-localise translate --provider openai --model gpt-4o --api-key KEY")
83
+ print(" android-localise translate --provider openai --model gpt-5.4-mini --api-key KEY")
79
84
  print()
80
85
 
81
86
 
@@ -67,7 +67,7 @@ def main(args=None):
67
67
  def fix_match(m):
68
68
  opening_tag = m.group(1)
69
69
  # Skip strings marked formatted="false" — their % signs are literal, not specifiers
70
- if 'formatted="false"' in opening_tag.lower() or "formatted='false'" in opening_tag.lower():
70
+ if re.search(r"\bformatted\s*=\s*['\"]false['\"]", opening_tag, flags=re.IGNORECASE):
71
71
  return m.group(0)
72
72
  return opening_tag + _fix_text(m.group(2)) + m.group(3)
73
73
 
@@ -1,32 +1,34 @@
1
1
  import os
2
2
  import re
3
3
  import time
4
+ import socket
4
5
  import argparse
5
6
  import urllib.request
6
7
  import urllib.error
7
8
  import json
8
9
 
9
10
  DEFAULT_RES_DIR = "app/src/main/res"
10
- API_TIMEOUT = 60 # seconds
11
+ DEFAULT_API_TIMEOUT = 180 # seconds (3 minutes) — large strings.xml files can exceed 60s
12
+ MAX_TIMEOUT_RETRIES = 2
11
13
 
12
14
  # Ordered list of models per provider.
13
15
  # First entry = default. Rest = automatic fallbacks (used only when user hasn't pinned a model).
14
16
  PROVIDER_MODELS = {
15
17
  "gemini": [
18
+ "gemini-3.5-flash",
19
+ "gemini-3.1-flash-lite",
16
20
  "gemini-2.5-flash",
17
- "gemini-2.0-flash",
18
- "gemini-1.5-flash",
19
- "gemini-1.5-pro",
21
+ "gemini-2.5-flash-lite",
20
22
  ],
21
23
  "openai": [
24
+ "gpt-5.4-mini",
25
+ "gpt-5-mini",
22
26
  "gpt-4o-mini",
23
- "gpt-4o",
24
- "gpt-3.5-turbo",
25
27
  ],
26
28
  "anthropic": [
27
- "claude-3-5-haiku-latest",
28
- "claude-3-5-sonnet-latest",
29
- "claude-3-opus-latest",
29
+ "claude-haiku-4-5",
30
+ "claude-sonnet-4-6",
31
+ "claude-opus-4-8",
30
32
  ],
31
33
  "custom": [], # user must specify --model
32
34
  }
@@ -77,7 +79,7 @@ Translate the English `strings.xml` below for {context_str} into the language fo
77
79
  For example, `values-hi` is Hindi, `values-es-rES` is Spanish (Spain), `values-zh-rTW` is Traditional Chinese, `values-ar` is Arabic, etc.
78
80
 
79
81
  STRICT GUIDELINES:
80
- 1. Translate only the string values — never the keys, XML tags, or attributes.
82
+ 1. Translate only the string values — preserve keys, tags, and attributes (name, translatable, formatted) exactly as in the source.
81
83
  2. Use natural, human-sounding language. Simple everyday mobile UI tone. Not robotic or word-for-word.
82
84
  3. Preserve ALL placeholders exactly as-is: %s, %d, %1$s, %1$d, %2$s, etc.
83
85
  4. Preserve ALL escape sequences exactly as-is: \\n, \\', \\", \\\\.
@@ -87,7 +89,7 @@ STRICT GUIDELINES:
87
89
  - Start with: <?xml version="1.0" encoding="utf-8"?>
88
90
  - Use plain <resources> with NO namespace attributes (no xmlns:xliff or any other xmlns)
89
91
  - Every string on its own line: <string name="key">translated value</string>
90
- - No CDATA, no extra attributes on <string> tags except name and translatable
92
+ - No CDATA, no extra attributes on <string> tags except name, translatable, and formatted
91
93
  8. Return ONLY the raw XML. No markdown, no code fences, no explanation.
92
94
 
93
95
  SOURCE XML:
@@ -109,8 +111,12 @@ def clean_xml_response(result):
109
111
  result = result[:-3]
110
112
  result = result.strip()
111
113
 
112
- # Strip any xmlns namespace attributes from <resources> tag — not valid in standard Android strings.xml
113
- result = re.sub(r'(<resources)\s+[^>]*?(>)', r'\1\2', result)
114
+ # Strip xmlns namespace declarations from <resources> (leave other attributes intact)
115
+ def _strip_xmlns(m):
116
+ attrs = re.sub(r'\s+xmlns(?::\w+)?\s*=\s*"[^"]*"', "", m.group(2))
117
+ return m.group(1) + attrs + m.group(3)
118
+
119
+ result = re.sub(r"(<resources)([^>]*)(>)", _strip_xmlns, result, count=1)
114
120
 
115
121
  return result
116
122
 
@@ -133,27 +139,61 @@ def _is_model_not_found(http_code, body):
133
139
  ])
134
140
 
135
141
 
136
- def call_gemini(api_key, model, prompt):
142
+ def _is_timeout_error(exc):
143
+ """True for direct timeouts and URLError wrappers (common from urllib.request.urlopen)."""
144
+ if isinstance(exc, (TimeoutError, socket.timeout)):
145
+ return True
146
+ return isinstance(exc, urllib.error.URLError) and isinstance(
147
+ exc.reason, (TimeoutError, socket.timeout)
148
+ )
149
+
150
+
151
+ def _urlopen_with_retries(req, provider_label, timeout):
152
+ """
153
+ Execute an HTTP request with timeout/network error handling and retries on timeout.
154
+ Returns (response_bytes, model_not_found).
155
+ """
156
+ for attempt in range(MAX_TIMEOUT_RETRIES + 1):
157
+ if attempt > 0:
158
+ wait = attempt * 3
159
+ print(f" 🔁 {provider_label} timed out — retrying ({attempt}/{MAX_TIMEOUT_RETRIES}) in {wait}s...")
160
+ time.sleep(wait)
161
+ try:
162
+ with urllib.request.urlopen(req, timeout=timeout) as response:
163
+ return response.read(), False
164
+ except urllib.error.HTTPError as e:
165
+ body = _read_error_body(e)
166
+ model_gone = _is_model_not_found(e.code, body)
167
+ print(f" ❌ {provider_label} API Error: {e.code} - {body}")
168
+ return None, model_gone
169
+ except (TimeoutError, socket.timeout, urllib.error.URLError) as e:
170
+ if not _is_timeout_error(e):
171
+ print(f" ❌ {provider_label} network error: {e.reason}")
172
+ return None, False
173
+ if attempt < MAX_TIMEOUT_RETRIES:
174
+ continue
175
+ print(f" ❌ {provider_label} API timed out after {timeout}s ({MAX_TIMEOUT_RETRIES + 1} attempts)")
176
+ return None, False
177
+ return None, False
178
+
179
+
180
+ def call_gemini(api_key, model, prompt, timeout=DEFAULT_API_TIMEOUT):
137
181
  url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
138
182
  headers = {"Content-Type": "application/json"}
139
183
  data = {"contents": [{"parts": [{"text": prompt}]}]}
140
184
  req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), headers=headers, method="POST")
141
- try:
142
- with urllib.request.urlopen(req, timeout=API_TIMEOUT) as response:
143
- result = json.loads(response.read().decode("utf-8"))
144
- candidates = result.get("candidates", [])
145
- if not candidates:
146
- print(" ❌ Gemini returned no candidates.")
147
- return None, False
148
- return result["candidates"][0].get("content", {}).get("parts", [{}])[0].get("text", ""), False
149
- except urllib.error.HTTPError as e:
150
- body = _read_error_body(e)
151
- model_gone = _is_model_not_found(e.code, body)
152
- print(f" ❌ Gemini API Error: {e.code} - {body}")
185
+ raw, model_gone = _urlopen_with_retries(req, "Gemini", timeout)
186
+ if raw is None:
153
187
  return None, model_gone
188
+ result = json.loads(raw.decode("utf-8"))
189
+ candidates = result.get("candidates", [])
190
+ if not candidates:
191
+ print(" ❌ Gemini returned no candidates.")
192
+ return None, False
193
+ return result["candidates"][0].get("content", {}).get("parts", [{}])[0].get("text", ""), False
154
194
 
155
195
 
156
- def call_openai_compatible(api_key, base_url, model, prompt):
196
+ def call_openai_compatible(api_key, base_url, model, prompt, timeout=DEFAULT_API_TIMEOUT):
157
197
  headers = {
158
198
  "Content-Type": "application/json",
159
199
  "Authorization": f"Bearer {api_key or ''}",
@@ -163,22 +203,18 @@ def call_openai_compatible(api_key, base_url, model, prompt):
163
203
  "messages": [{"role": "user", "content": prompt}],
164
204
  }
165
205
  req = urllib.request.Request(base_url, data=json.dumps(data).encode("utf-8"), headers=headers, method="POST")
166
- try:
167
- with urllib.request.urlopen(req, timeout=API_TIMEOUT) as response:
168
- result = json.loads(response.read().decode("utf-8"))
169
- choices = result.get("choices", [])
170
- if not choices:
171
- print(" ❌ OpenAI returned no choices.")
172
- return None, False
173
- return choices[0].get("message", {}).get("content", ""), False
174
- except urllib.error.HTTPError as e:
175
- body = _read_error_body(e)
176
- model_gone = _is_model_not_found(e.code, body)
177
- print(f" ❌ OpenAI (compatible) API Error: {e.code} - {body}")
206
+ raw, model_gone = _urlopen_with_retries(req, "OpenAI (compatible)", timeout)
207
+ if raw is None:
178
208
  return None, model_gone
209
+ result = json.loads(raw.decode("utf-8"))
210
+ choices = result.get("choices", [])
211
+ if not choices:
212
+ print(" ❌ OpenAI returned no choices.")
213
+ return None, False
214
+ return choices[0].get("message", {}).get("content", ""), False
179
215
 
180
216
 
181
- def call_anthropic(api_key, model, prompt):
217
+ def call_anthropic(api_key, model, prompt, timeout=DEFAULT_API_TIMEOUT):
182
218
  url = "https://api.anthropic.com/v1/messages"
183
219
  headers = {
184
220
  "Content-Type": "application/json",
@@ -191,37 +227,33 @@ def call_anthropic(api_key, model, prompt):
191
227
  "messages": [{"role": "user", "content": prompt}],
192
228
  }
193
229
  req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), headers=headers, method="POST")
194
- try:
195
- with urllib.request.urlopen(req, timeout=API_TIMEOUT) as response:
196
- result = json.loads(response.read().decode("utf-8"))
197
- content = result.get("content", [])
198
- if not content:
199
- print(" ❌ Anthropic returned no content.")
200
- return None, False
201
- return content[0].get("text", ""), False
202
- except urllib.error.HTTPError as e:
203
- body = _read_error_body(e)
204
- model_gone = _is_model_not_found(e.code, body)
205
- print(f" ❌ Anthropic API Error: {e.code} - {body}")
230
+ raw, model_gone = _urlopen_with_retries(req, "Anthropic", timeout)
231
+ if raw is None:
206
232
  return None, model_gone
233
+ result = json.loads(raw.decode("utf-8"))
234
+ content = result.get("content", [])
235
+ if not content:
236
+ print(" ❌ Anthropic returned no content.")
237
+ return None, False
238
+ return content[0].get("text", ""), False
207
239
 
208
240
 
209
- def _call_provider(provider, api_key, model, prompt, base_url=None):
241
+ def _call_provider(provider, api_key, model, prompt, base_url=None, timeout=DEFAULT_API_TIMEOUT):
210
242
  """Dispatches to the right API. Returns (text, model_not_found)."""
211
243
  if provider == "gemini":
212
- return call_gemini(api_key, model, prompt)
244
+ return call_gemini(api_key, model, prompt, timeout)
213
245
  elif provider == "openai":
214
246
  url = base_url if base_url else "https://api.openai.com/v1/chat/completions"
215
- return call_openai_compatible(api_key, url, model, prompt)
247
+ return call_openai_compatible(api_key, url, model, prompt, timeout)
216
248
  elif provider == "anthropic":
217
- return call_anthropic(api_key, model, prompt)
249
+ return call_anthropic(api_key, model, prompt, timeout)
218
250
  else:
219
251
  print(f"❌ Unknown provider: {provider}")
220
252
  return None, False
221
253
 
222
254
 
223
255
  def translate_xml(provider, api_key, model, source_xml, target_folder_name, app_context,
224
- base_url=None, fallback_models=None):
256
+ base_url=None, fallback_models=None, timeout=DEFAULT_API_TIMEOUT):
225
257
  """
226
258
  Calls the selected provider API to translate the XML.
227
259
  If the model is not found and fallback_models are provided, retries with the next one.
@@ -233,7 +265,9 @@ def translate_xml(provider, api_key, model, source_xml, target_folder_name, app_
233
265
  for attempt_model in models_to_try:
234
266
  if attempt_model != model:
235
267
  print(f" ↩️ Falling back to model: {attempt_model}")
236
- result, model_not_found = _call_provider(provider, api_key, attempt_model, prompt, base_url)
268
+ result, model_not_found = _call_provider(
269
+ provider, api_key, attempt_model, prompt, base_url, timeout
270
+ )
237
271
  if result is not None:
238
272
  return clean_xml_response(result), attempt_model
239
273
  if not model_not_found:
@@ -252,6 +286,8 @@ def _parse_args(args=None):
252
286
  parser.add_argument("--base-url")
253
287
  parser.add_argument("--app-context")
254
288
  parser.add_argument("--sleep", type=float, default=5.0)
289
+ parser.add_argument("--timeout", type=float, default=DEFAULT_API_TIMEOUT,
290
+ help=f"Seconds to wait for each API response, up to {MAX_TIMEOUT_RETRIES + 1} attempts on timeout (default: {DEFAULT_API_TIMEOUT})")
255
291
  parser.add_argument("--languages", help="Comma-separated language codes to translate into, e.g. hi,es,fr,de. Creates folders automatically if they don't exist.")
256
292
  return parser.parse_args(args)
257
293
 
@@ -333,7 +369,7 @@ def main(args=None):
333
369
 
334
370
  translated_xml, used_model = translate_xml(
335
371
  actual_provider, api_key, model, source_xml,
336
- folder, args.app_context, args.base_url, fallback_models
372
+ folder, args.app_context, args.base_url, fallback_models, args.timeout
337
373
  )
338
374
 
339
375
  if translated_xml and "<resources" in translated_xml and "</resources>" in translated_xml:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: android-localisation
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: Zero-dependency Android strings.xml translation and verification using LLMs (Gemini, OpenAI, Anthropic, Ollama).
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/BharathKmalviya/android-llm-localization
@@ -26,13 +26,15 @@ Description-Content-Type: text/markdown
26
26
  License-File: LICENSE
27
27
  Dynamic: license-file
28
28
 
29
- # android-llm-localization
29
+ # android-localisation
30
30
 
31
31
  [![PyPI version](https://img.shields.io/pypi/v/android-localisation.svg)](https://pypi.org/project/android-localisation/)
32
32
  [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://pypi.org/project/android-localisation/)
33
33
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
34
34
 
35
- Translate your Android `strings.xml` into multiple languages using AI — Gemini, OpenAI, Anthropic, or a local model via Ollama. No paid service, no CSV exports, no copy-paste.
35
+ **PyPI:** [`android-localisation`](https://pypi.org/project/android-localisation/) · **CLI:** `android-localise` · **Repo:** [android-llm-localization](https://github.com/BharathKmalviya/android-llm-localization)
36
+
37
+ Translate your Android `strings.xml` into multiple languages using AI — Gemini, OpenAI, Anthropic, or a local model via Ollama. No paid translation service, no CSV exports, no copy-paste.
36
38
 
37
39
  ---
38
40
 
@@ -86,7 +88,7 @@ When you run `android-localise translate --api-key YOUR_KEY`, here's exactly wha
86
88
  | What | Default |
87
89
  |---|---|
88
90
  | Provider | Gemini |
89
- | Model | `gemini-2.5-flash` |
91
+ | Model | `gemini-3.5-flash` |
90
92
  | Source directory | `app/src/main/res` |
91
93
  | Delay between requests | 5 seconds |
92
94
  | App context | none (generic prompt) |
@@ -150,6 +152,7 @@ android-localise translate \
150
152
  | `--res-dir` | Path to your `res/` folder | `app/src/main/res` |
151
153
  | `--base-url` | API endpoint for local/custom providers | — |
152
154
  | `--sleep` | Seconds to wait between language requests | `5.0` |
155
+ | `--timeout` | Seconds to wait for each API response (up to 3 attempts on timeout) | `180` |
153
156
 
154
157
  ---
155
158
 
@@ -157,10 +160,13 @@ android-localise translate \
157
160
 
158
161
  ```bash
159
162
  android-localise fix
163
+ android-localise fix --res-dir path/to/res
160
164
  ```
161
165
 
162
166
  LLMs occasionally produce output that looks correct but breaks the Android build — curly apostrophes (`'`) instead of escaped ones (`\'`), unescaped double quotes, or mangled `%` signs. This command scans every translated `strings.xml` and corrects these silently.
163
167
 
168
+ Strings marked `formatted="false"` are skipped — their `%` signs are literal, not format specifiers.
169
+
164
170
  Always run this before `verify` and before building.
165
171
 
166
172
  ---
@@ -169,11 +175,12 @@ Always run this before `verify` and before building.
169
175
 
170
176
  ```bash
171
177
  android-localise verify
178
+ android-localise verify --res-dir path/to/res
172
179
  ```
173
180
 
174
181
  Takes every translated string that contains a format specifier (`%1$s`, `%d`, `%1$f`, etc.) and calls `String.format()` on it using Java's actual runtime. If a translated string would throw `UnknownFormatConversionException` or `MissingFormatArgumentException` in your app, this catches it before your users do.
175
182
 
176
- Requires `javac` in your PATH. If you don't have it system-wide, run this from the Terminal tab inside Android Studio — it ships with a JDK.
183
+ Strings marked `formatted="false"` are skipped. Requires `javac` in your PATH. If you don't have it system-wide, run this from the Terminal tab inside Android Studio — it ships with a JDK.
177
184
 
178
185
  ---
179
186
 
@@ -190,21 +197,21 @@ Lists every available model and fallback for each provider.
190
197
 
191
198
  ## Providers
192
199
 
193
- By default the tool uses Gemini with `gemini-2.5-flash`. You can switch providers with `--provider` and optionally pin a specific model with `--model`.
200
+ By default the tool uses Gemini with `gemini-3.5-flash`. You can switch providers with `--provider` and optionally pin a specific model with `--model`.
194
201
 
195
202
  | Provider | Default model | Fallbacks | API key env var |
196
203
  |---|---|---|---|
197
- | `gemini` _(default)_ | `gemini-2.5-flash` | `gemini-2.0-flash` → `gemini-1.5-flash` → `gemini-1.5-pro` | `GEMINI_API_KEY` |
198
- | `openai` | `gpt-4o-mini` | `gpt-4o` → `gpt-3.5-turbo` | `OPENAI_API_KEY` |
199
- | `anthropic` | `claude-3-5-haiku-latest` | `claude-3-5-sonnet-latest` → `claude-3-opus-latest` | `ANTHROPIC_API_KEY` |
200
- | `custom` | set with `--model` | none | — |
204
+ | `gemini` _(default)_ | `gemini-3.5-flash` | `gemini-3.1-flash-lite` → `gemini-2.5-flash` → `gemini-2.5-flash-lite` | `GEMINI_API_KEY` |
205
+ | `openai` | `gpt-5.4-mini` | `gpt-5-mini` → `gpt-4o-mini` | `OPENAI_API_KEY` |
206
+ | `anthropic` | `claude-haiku-4-5` | `claude-sonnet-4-6` → `claude-opus-4-8` | `ANTHROPIC_API_KEY` |
207
+ | `custom` | set with `--model` | none | `OPENAI_API_KEY` (optional) |
201
208
 
202
209
  If the default model returns a "model not found" error (e.g. it was deprecated), the tool automatically retries with the next fallback. If you pin a model with `--model`, no fallback is used.
203
210
 
204
211
  **Using OpenAI:**
205
212
  ```bash
206
213
  android-localise translate --provider openai --api-key YOUR_KEY
207
- android-localise translate --provider openai --model gpt-4o --api-key YOUR_KEY
214
+ android-localise translate --provider openai --model gpt-5.4-mini --api-key YOUR_KEY
208
215
  ```
209
216
 
210
217
  **Using Anthropic:**
@@ -239,6 +246,9 @@ export GEMINI_API_KEY=your_key
239
246
 
240
247
  # Windows PowerShell
241
248
  $env:GEMINI_API_KEY = "your_key"
249
+
250
+ # Windows CMD
251
+ set GEMINI_API_KEY=your_key
242
252
  ```
243
253
 
244
254
  Then just run:
@@ -251,19 +261,25 @@ android-localise translate
251
261
  | `GEMINI_API_KEY` | `--provider gemini` |
252
262
  | `OPENAI_API_KEY` | `--provider openai` and `--provider custom` |
253
263
  | `ANTHROPIC_API_KEY` | `--provider anthropic` |
264
+ | `API_KEY` | fallback for any provider if the provider-specific var is not set |
254
265
 
255
266
  ---
256
267
 
257
268
  ## Full workflow example
258
269
 
259
270
  ```bash
260
- # First time setup — create locale folders
271
+ # First time setup — create locale folders (macOS / Linux)
261
272
  mkdir -p app/src/main/res/values-hi
262
273
  mkdir -p app/src/main/res/values-es
263
274
  mkdir -p app/src/main/res/values-de
264
275
 
276
+ # Windows PowerShell
277
+ New-Item -ItemType Directory -Force app/src/main/res/values-hi
278
+ New-Item -ItemType Directory -Force app/src/main/res/values-es
279
+ New-Item -ItemType Directory -Force app/src/main/res/values-de
280
+
265
281
  # Set your key once
266
- export GEMINI_API_KEY=your_key
282
+ export GEMINI_API_KEY=your_key # or $env:GEMINI_API_KEY on Windows
267
283
 
268
284
  # Translate, fix, verify
269
285
  android-localise translate --app-context "a habit tracking app"
@@ -278,23 +294,83 @@ After this, whenever you add or change strings in your English `strings.xml`, ru
278
294
 
279
295
  ---
280
296
 
297
+ ## Platform support
298
+
299
+ This project is developed and **manually tested on Windows only** at the moment. It is written in pure Python (stdlib only) and should run on macOS and Linux, but those platforms have **not been verified** by the maintainer yet.
300
+
301
+ We especially need help testing on:
302
+
303
+ - **macOS** — `translate`, `fix`, `verify` (including `javac` / Android Studio terminal)
304
+ - **Linux** — same workflow, plus common CI environments
305
+
306
+ If you use another OS, please try the [quick start](#quick-start) workflow and report what you find:
307
+
308
+ - **Works?** — open a [GitHub issue](https://github.com/BharathKmalviya/android-llm-localization/issues) titled e.g. `Confirmed working on macOS 14` with your OS, Python version, and provider used
309
+ - **Broken?** — open a [bug report](https://github.com/BharathKmalviya/android-llm-localization/issues/new?template=bug_report.md) with the full error output
310
+ - **Want to help more?** — see [Contributing](#contributing) and [CONTRIBUTING.md](CONTRIBUTING.md)
311
+
312
+ Cross-platform fixes and test notes in pull requests are very welcome.
313
+
314
+ ---
315
+
316
+ ## Limitations
317
+
318
+ | Topic | Detail |
319
+ |---|---|
320
+ | **Platform testing** | Maintainer-tested on **Windows only** — macOS and Linux need community verification (see [Platform support](#platform-support)) |
321
+ | **Scope** | Translates `values/strings.xml` only — not `plurals.xml`, `arrays.xml`, or other resource files |
322
+ | **Overwrite** | Each run replaces the entire `strings.xml` in each locale folder with a fresh LLM translation |
323
+ | **Folder scan** | Without `--languages`, every `values-*` folder is treated as a locale. Qualifier-only folders like `values-night` or `values-sw600dp` may be picked up incorrectly — prefer `--languages` or keep only locale folders in `res/` |
324
+ | **Network** | `translate` requires internet access to reach the LLM API (except local `custom` providers) |
325
+ | **JDK** | `verify` requires `javac` on your PATH |
326
+
327
+ ---
328
+
329
+ ## Troubleshooting
330
+
331
+ | Problem | What to try |
332
+ |---|---|
333
+ | `Could not find English strings.xml` | Check `--res-dir` points to your `res/` folder and `values/strings.xml` exists |
334
+ | `No locale directories found` | Add `--languages hi,es,fr` or create `values-<lang>/` folders manually |
335
+ | API auth errors | Confirm your key env var or `--api-key` matches the `--provider` |
336
+ | `javac` not found | Install a JDK or run `verify` from Android Studio's terminal |
337
+ | Build fails on apostrophes | Run `android-localise fix` before building |
338
+ | `%` crashes at runtime | Run `android-localise verify` — it catches bad format specifiers before release |
339
+ | Wrong folder translated | Use `--languages` to target exact locale codes instead of folder scan |
340
+
341
+ ---
342
+
281
343
  ## Roadmap
282
344
 
283
345
  - [ ] **iOS support** — translate `Localizable.strings` and `Localizable.xcstrings` for iOS/macOS apps. The LLM prompt and provider logic is already in place — it mainly needs a parser for Apple's strings format and the right folder structure (`<lang>.lproj/`). Good first contribution if you're familiar with iOS projects.
346
+ - [ ] **Smarter locale folder detection** — skip non-locale `values-*` qualifiers (`night`, `sw600dp`, `v21`, etc.) when scanning without `--languages`
347
+ - [ ] **Automated test suite** — unit tests for `fix`, XML parsing, and format-specifier edge cases
348
+ - [ ] **Cross-platform verification** — confirm `translate`, `fix`, and `verify` on macOS and Linux (Windows is maintainer-tested today)
284
349
 
285
350
  ---
286
351
 
287
352
  ## Contributing
288
353
 
289
- Bug reports and pull requests are welcome. For larger changes, open an issue first.
354
+ Bug reports, pull requests, and **cross-platform testing** are all welcome. For larger changes, open an issue first.
355
+
356
+ **No code required** — if you are on macOS or Linux, running the tool and filing an issue (pass or fail) is a real contribution. See [Platform support](#platform-support).
357
+
358
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development workflow, branch strategy, and release process.
290
359
 
291
360
  ```bash
292
361
  git clone https://github.com/BharathKmalviya/android-llm-localization
293
362
  cd android-llm-localization
363
+ git checkout dev
294
364
  pip install -e .
295
365
  ```
296
366
 
297
- Releases are automated via GitHub Actions — bump the version in `pyproject.toml` and `__init__.py`, update `CHANGELOG.md`, and push to `master`.
367
+ Day-to-day work happens on the `dev` branch. Releases are merged to `master` via pull request, which triggers automated PyPI publishing.
368
+
369
+ ---
370
+
371
+ ## Security
372
+
373
+ To report a security vulnerability, see [SECURITY.md](SECURITY.md). Please do not open public issues for security-sensitive reports.
298
374
 
299
375
  ---
300
376
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "android-localisation"
7
- version = "1.0.4"
7
+ version = "1.0.6"
8
8
  description = "Zero-dependency Android strings.xml translation and verification using LLMs (Gemini, OpenAI, Anthropic, Ollama)."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }