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.
- {android_localisation-1.0.4/android_localisation.egg-info → android_localisation-1.0.6}/PKG-INFO +91 -15
- {android_localisation-1.0.4 → android_localisation-1.0.6}/README.md +90 -14
- {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation/__init__.py +1 -1
- {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation/cli.py +6 -1
- {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation/fix.py +1 -1
- {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation/translate.py +95 -59
- {android_localisation-1.0.4 → android_localisation-1.0.6/android_localisation.egg-info}/PKG-INFO +91 -15
- {android_localisation-1.0.4 → android_localisation-1.0.6}/pyproject.toml +1 -1
- {android_localisation-1.0.4 → android_localisation-1.0.6}/LICENSE +0 -0
- {android_localisation-1.0.4 → android_localisation-1.0.6}/MANIFEST.in +0 -0
- {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation/java/VerifyStrings.java +0 -0
- {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation/verify.py +0 -0
- {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation.egg-info/SOURCES.txt +0 -0
- {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation.egg-info/dependency_links.txt +0 -0
- {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation.egg-info/entry_points.txt +0 -0
- {android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation.egg-info/top_level.txt +0 -0
- {android_localisation-1.0.4 → android_localisation-1.0.6}/setup.cfg +0 -0
{android_localisation-1.0.4/android_localisation.egg-info → android_localisation-1.0.6}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: android-localisation
|
|
3
|
-
Version: 1.0.
|
|
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-
|
|
29
|
+
# android-localisation
|
|
30
30
|
|
|
31
31
|
[](https://pypi.org/project/android-localisation/)
|
|
32
32
|
[](https://pypi.org/project/android-localisation/)
|
|
33
33
|
[](LICENSE)
|
|
34
34
|
|
|
35
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
198
|
-
| `openai` | `gpt-
|
|
199
|
-
| `anthropic` | `claude-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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-
|
|
1
|
+
# android-localisation
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/android-localisation/)
|
|
4
4
|
[](https://pypi.org/project/android-localisation/)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
170
|
-
| `openai` | `gpt-
|
|
171
|
-
| `anthropic` | `claude-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -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-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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-
|
|
28
|
-
"claude-
|
|
29
|
-
"claude-
|
|
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 —
|
|
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
|
|
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
|
|
113
|
-
|
|
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
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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(
|
|
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:
|
{android_localisation-1.0.4 → android_localisation-1.0.6/android_localisation.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: android-localisation
|
|
3
|
-
Version: 1.0.
|
|
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-
|
|
29
|
+
# android-localisation
|
|
30
30
|
|
|
31
31
|
[](https://pypi.org/project/android-localisation/)
|
|
32
32
|
[](https://pypi.org/project/android-localisation/)
|
|
33
33
|
[](LICENSE)
|
|
34
34
|
|
|
35
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
198
|
-
| `openai` | `gpt-
|
|
199
|
-
| `anthropic` | `claude-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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" }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{android_localisation-1.0.4 → android_localisation-1.0.6}/android_localisation.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|