android-localisation 1.5.0__tar.gz → 1.6.2__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.5.0/android_localisation.egg-info → android_localisation-1.6.2}/PKG-INFO +79 -12
- {android_localisation-1.5.0 → android_localisation-1.6.2}/README.md +78 -11
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation/__init__.py +1 -1
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation/cli.py +27 -3
- android_localisation-1.6.2/android_localisation/credentials.py +185 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation/store_listing.py +4 -5
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation/translate.py +43 -28
- {android_localisation-1.5.0 → android_localisation-1.6.2/android_localisation.egg-info}/PKG-INFO +79 -12
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation.egg-info/SOURCES.txt +1 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/pyproject.toml +1 -1
- {android_localisation-1.5.0 → android_localisation-1.6.2}/LICENSE +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/MANIFEST.in +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation/__main__.py +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation/fix.py +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation/java/VerifyStrings.java +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation/locales.py +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation/resources.py +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation/setup_path.py +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation/updates.py +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation/verify.py +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation.egg-info/dependency_links.txt +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation.egg-info/entry_points.txt +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation.egg-info/top_level.txt +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.2}/setup.cfg +0 -0
{android_localisation-1.5.0/android_localisation.egg-info → android_localisation-1.6.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: android-localisation
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.2
|
|
4
4
|
Summary: Zero-dependency Android strings.xml and Google Play store listing translation using LLMs (Gemini, OpenAI, Anthropic, Ollama).
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/BharathKmalviya/android-llm-localization
|
|
@@ -127,12 +127,16 @@ When you run `android-localise translate --api-key YOUR_KEY`, here's exactly wha
|
|
|
127
127
|
4. Parses the response and checks duplicate/unexpected/missing resources, attributes, inline markup, item structure, control escapes and format arguments. A valid result replaces the file atomically; new folders are created only when saving. `--skip-existing` validates existing files and skips them without API calls. `--dry-run` shows a diff without writing any files or creating folders
|
|
128
128
|
5. Waits 5 seconds between each language request to avoid hitting API rate limits
|
|
129
129
|
|
|
130
|
+
Keys resolve from `--api-key`, then provider environment variables / `API_KEY`,
|
|
131
|
+
then saved Windows credentials for built-in provider endpoints. The CLI never
|
|
132
|
+
prompts for a key during translation; use `credentials set` yourself beforehand.
|
|
133
|
+
|
|
130
134
|
**Defaults used when you don't specify anything:**
|
|
131
135
|
|
|
132
136
|
| What | Default |
|
|
133
137
|
|---|---|
|
|
134
138
|
| Provider | Gemini |
|
|
135
|
-
| Model | `gemini-3.
|
|
139
|
+
| Model | `gemini-3.5-flash-lite` |
|
|
136
140
|
| Source directory | `app/src/main/res` |
|
|
137
141
|
| Delay between requests | 5 seconds |
|
|
138
142
|
| App context | none (generic prompt) |
|
|
@@ -226,7 +230,7 @@ android-localise translate \
|
|
|
226
230
|
|
|
227
231
|
| Flag | What it does | Default |
|
|
228
232
|
|---|---|---|
|
|
229
|
-
| `--api-key` |
|
|
233
|
+
| `--api-key` | Explicit key override; omit for environment or saved Windows credentials | environment, then saved Windows key |
|
|
230
234
|
| `--provider` | Which AI to use: `gemini` `openai` `anthropic` `custom` | `gemini` |
|
|
231
235
|
| `--model` | Specific model to use | see [Providers](#providers) |
|
|
232
236
|
| `--languages` | Comma-separated Android/conventional codes and/or `all`, e.g. `all,zu` or `hi,es-ES,b+es+419` | scan existing destination locale folders |
|
|
@@ -305,7 +309,7 @@ Console import file. The CLI does not upload or publish a listing.
|
|
|
305
309
|
| `--dry-run` | Generate, validate and display diffs without files/directories being written | off; API usage applies |
|
|
306
310
|
| `--provider` | `gemini`, `openai`, `anthropic`, `custom` | `gemini` |
|
|
307
311
|
| `--model` | Pin any supported model and disable fallbacks | provider default |
|
|
308
|
-
| `--api-key` |
|
|
312
|
+
| `--api-key` | Explicit key override; omit for environment or saved Windows credentials | environment, then saved Windows key |
|
|
309
313
|
| `--base-url` | OpenAI-compatible endpoint; required for `custom` | provider endpoint |
|
|
310
314
|
| `--app-context` | Terminology context; source copy remains the source of facts | none |
|
|
311
315
|
| `--sleep` | Delay between requests, including correction/fallback requests | `5.0` seconds |
|
|
@@ -480,18 +484,18 @@ environment behavior. All commands can also run through `python -m android_local
|
|
|
480
484
|
|
|
481
485
|
## Providers
|
|
482
486
|
|
|
483
|
-
By default the tool uses Gemini
|
|
487
|
+
By default the tool uses Gemini 3.5 Flash-Lite (`gemini-3.5-flash-lite`) for both XML and store listing translation. You can switch providers with `--provider` and optionally pin a specific model with `--model`.
|
|
484
488
|
|
|
485
|
-
| Provider | Default model |
|
|
489
|
+
| Provider | Default model | Fallback | API key env var |
|
|
486
490
|
|---|---|---|---|
|
|
487
|
-
| `gemini` _(default)_ | `gemini-3.
|
|
488
|
-
| `openai` | `gpt-6-luna` | `gpt-
|
|
489
|
-
| `anthropic` | `claude-
|
|
491
|
+
| `gemini` _(default)_ | `gemini-3.5-flash-lite` | `gemini-3.5-flash` | `GEMINI_API_KEY` |
|
|
492
|
+
| `openai` | `gpt-6-luna` | `gpt-5.6-terra` | `OPENAI_API_KEY` |
|
|
493
|
+
| `anthropic` | `claude-haiku-4-5` | `claude-sonnet-5-5` | `ANTHROPIC_API_KEY` |
|
|
490
494
|
| `custom` | set with `--model` | none | `OPENAI_API_KEY` (optional) |
|
|
491
495
|
|
|
492
|
-
If the default model returns a "model not found" error (e.g. it was deprecated), the tool
|
|
496
|
+
Each hosted provider has exactly one automatic fallback. If the default model returns a "model not found" error (e.g. it was deprecated), the tool retries with that fallback. Authentication, quota and network errors do not trigger a model fallback. If you pin a model with `--model`, no fallback is used.
|
|
493
497
|
|
|
494
|
-
|
|
498
|
+
These default/fallback pairs follow the selected configuration. Gemini model IDs were checked against [Flash-Lite](https://ai.google.dev/gemini-api/docs/models/gemini-3.5-flash-lite) and [Flash](https://ai.google.dev/gemini-api/docs/models/gemini-3.5-flash) documentation; the OpenAI fallback against [GPT-5.6 Terra](https://developers.openai.com/api/docs/models/gpt-5.6-terra); and Anthropic's default/fallback against its [model migration guide](https://platform.claude.com/docs/en/models/sonnet-5-5/migration-guide) on **2026-10-05**. The existing OpenAI default was checked on **2026-10-02**. Pin `--model` to avoid automatic model changes. Explicit model selection and custom/local endpoints remain available. The catalog is bundled with each CLI release, rather than automatically discovering models at runtime.
|
|
495
499
|
|
|
496
500
|
OpenAI and local providers retain the Chat Completions request format. Provider replies indicating truncation or blocked/incomplete output are rejected. Anthropic allows up to 16,384 output tokens per request and text blocks are collected separately from thinking blocks. Large files can still exceed a model's limits; automatic batching is future work.
|
|
497
501
|
|
|
@@ -525,9 +529,72 @@ android-localise translate \
|
|
|
525
529
|
|
|
526
530
|
---
|
|
527
531
|
|
|
532
|
+
## Save a key once on Windows
|
|
533
|
+
|
|
534
|
+
Run this yourself in your terminal:
|
|
535
|
+
|
|
536
|
+
```bash
|
|
537
|
+
android-localise credentials set --provider gemini
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Enter your key at the hidden prompt. It is stored in **Windows Credential
|
|
541
|
+
Manager**, for your Windows user on this computer, and persists across terminal
|
|
542
|
+
and computer restarts. The CLI does not write a plaintext key file or modify
|
|
543
|
+
environment variables. Running `set` again replaces that provider's saved key.
|
|
544
|
+
There is no key argument or piped-input option for this command; it requires an
|
|
545
|
+
interactive terminal and refuses a prompt that cannot hide input.
|
|
546
|
+
|
|
547
|
+
Then you or an AI assistant can run ordinary commands without including a key:
|
|
548
|
+
|
|
549
|
+
```bash
|
|
550
|
+
android-localise translate --languages hi,es
|
|
551
|
+
android-localise store-listing --source listing.json --languages hi-IN,es-ES
|
|
552
|
+
android-localise credentials status --provider gemini
|
|
553
|
+
android-localise credentials remove --provider gemini
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
`status` reports only `saved` or `not saved`; no command displays the saved value.
|
|
557
|
+
`remove` deletes only this CLI's saved entry for that provider; it does not revoke
|
|
558
|
+
the provider key or clear environment-variable overrides. The same commands also
|
|
559
|
+
work with `python -m android_localisation credentials ...`.
|
|
560
|
+
|
|
561
|
+
| Argument | Description | Default |
|
|
562
|
+
|---|---|---|
|
|
563
|
+
| `set`, `status`, `remove` | Hidden entry, presence check, or deletion | required action |
|
|
564
|
+
| `--provider` | `gemini`, `openai` or `anthropic` | `gemini` |
|
|
565
|
+
|
|
566
|
+
Both translation commands use this precedence: `--api-key` → provider-specific
|
|
567
|
+
environment variable → `API_KEY` → saved Windows key. Existing overrides retain
|
|
568
|
+
their behavior; if an old environment key is set, saving a new key does not
|
|
569
|
+
override it. Saved OpenAI keys load only for its default endpoint; `custom`
|
|
570
|
+
providers and other `--base-url` endpoints continue using explicit keys or
|
|
571
|
+
environment variables. Saved keys are not automatically sent to custom hosts.
|
|
572
|
+
Provider requests reject HTTP redirects, so use a custom endpoint's final URL.
|
|
573
|
+
Gemini authentication uses a header rather than a URL query parameter, and
|
|
574
|
+
API/network error messages redact the key used for that request before display.
|
|
575
|
+
|
|
576
|
+
This keeps keys out of chat, command arguments and routine CLI output. It
|
|
577
|
+
**does not isolate secrets from an AI or other program with unrestricted access
|
|
578
|
+
under your Windows account**: Windows permits programs running as that user to
|
|
579
|
+
read their credentials. The key is also present in memory during an API request.
|
|
580
|
+
For stronger separation, use a restricted runner or separately secured proxy.
|
|
581
|
+
See [Microsoft's credential API documentation](https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credreadw).
|
|
582
|
+
|
|
583
|
+
Saved-key commands are Windows-only, with no plaintext fallback. Other platforms
|
|
584
|
+
keep using environment variables or `--api-key`.
|
|
585
|
+
|
|
586
|
+
Manual check: save a key yourself, confirm input is hidden, open a new terminal
|
|
587
|
+
and check `status`. Run a small XML and listing preview without `--api-key`,
|
|
588
|
+
review the output, then remove the entry and confirm `not saved`. Ensure any key
|
|
589
|
+
environment overrides are absent when checking saved-key behavior.
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
528
593
|
## Environment variables
|
|
529
594
|
|
|
530
|
-
|
|
595
|
+
Environment variables remain available on all platforms. For Windows saved
|
|
596
|
+
credentials, use the commands above instead of putting a key in shell history.
|
|
597
|
+
To use an environment variable:
|
|
531
598
|
|
|
532
599
|
```bash
|
|
533
600
|
# macOS / Linux
|
|
@@ -99,12 +99,16 @@ When you run `android-localise translate --api-key YOUR_KEY`, here's exactly wha
|
|
|
99
99
|
4. Parses the response and checks duplicate/unexpected/missing resources, attributes, inline markup, item structure, control escapes and format arguments. A valid result replaces the file atomically; new folders are created only when saving. `--skip-existing` validates existing files and skips them without API calls. `--dry-run` shows a diff without writing any files or creating folders
|
|
100
100
|
5. Waits 5 seconds between each language request to avoid hitting API rate limits
|
|
101
101
|
|
|
102
|
+
Keys resolve from `--api-key`, then provider environment variables / `API_KEY`,
|
|
103
|
+
then saved Windows credentials for built-in provider endpoints. The CLI never
|
|
104
|
+
prompts for a key during translation; use `credentials set` yourself beforehand.
|
|
105
|
+
|
|
102
106
|
**Defaults used when you don't specify anything:**
|
|
103
107
|
|
|
104
108
|
| What | Default |
|
|
105
109
|
|---|---|
|
|
106
110
|
| Provider | Gemini |
|
|
107
|
-
| Model | `gemini-3.
|
|
111
|
+
| Model | `gemini-3.5-flash-lite` |
|
|
108
112
|
| Source directory | `app/src/main/res` |
|
|
109
113
|
| Delay between requests | 5 seconds |
|
|
110
114
|
| App context | none (generic prompt) |
|
|
@@ -198,7 +202,7 @@ android-localise translate \
|
|
|
198
202
|
|
|
199
203
|
| Flag | What it does | Default |
|
|
200
204
|
|---|---|---|
|
|
201
|
-
| `--api-key` |
|
|
205
|
+
| `--api-key` | Explicit key override; omit for environment or saved Windows credentials | environment, then saved Windows key |
|
|
202
206
|
| `--provider` | Which AI to use: `gemini` `openai` `anthropic` `custom` | `gemini` |
|
|
203
207
|
| `--model` | Specific model to use | see [Providers](#providers) |
|
|
204
208
|
| `--languages` | Comma-separated Android/conventional codes and/or `all`, e.g. `all,zu` or `hi,es-ES,b+es+419` | scan existing destination locale folders |
|
|
@@ -277,7 +281,7 @@ Console import file. The CLI does not upload or publish a listing.
|
|
|
277
281
|
| `--dry-run` | Generate, validate and display diffs without files/directories being written | off; API usage applies |
|
|
278
282
|
| `--provider` | `gemini`, `openai`, `anthropic`, `custom` | `gemini` |
|
|
279
283
|
| `--model` | Pin any supported model and disable fallbacks | provider default |
|
|
280
|
-
| `--api-key` |
|
|
284
|
+
| `--api-key` | Explicit key override; omit for environment or saved Windows credentials | environment, then saved Windows key |
|
|
281
285
|
| `--base-url` | OpenAI-compatible endpoint; required for `custom` | provider endpoint |
|
|
282
286
|
| `--app-context` | Terminology context; source copy remains the source of facts | none |
|
|
283
287
|
| `--sleep` | Delay between requests, including correction/fallback requests | `5.0` seconds |
|
|
@@ -452,18 +456,18 @@ environment behavior. All commands can also run through `python -m android_local
|
|
|
452
456
|
|
|
453
457
|
## Providers
|
|
454
458
|
|
|
455
|
-
By default the tool uses Gemini
|
|
459
|
+
By default the tool uses Gemini 3.5 Flash-Lite (`gemini-3.5-flash-lite`) for both XML and store listing translation. You can switch providers with `--provider` and optionally pin a specific model with `--model`.
|
|
456
460
|
|
|
457
|
-
| Provider | Default model |
|
|
461
|
+
| Provider | Default model | Fallback | API key env var |
|
|
458
462
|
|---|---|---|---|
|
|
459
|
-
| `gemini` _(default)_ | `gemini-3.
|
|
460
|
-
| `openai` | `gpt-6-luna` | `gpt-
|
|
461
|
-
| `anthropic` | `claude-
|
|
463
|
+
| `gemini` _(default)_ | `gemini-3.5-flash-lite` | `gemini-3.5-flash` | `GEMINI_API_KEY` |
|
|
464
|
+
| `openai` | `gpt-6-luna` | `gpt-5.6-terra` | `OPENAI_API_KEY` |
|
|
465
|
+
| `anthropic` | `claude-haiku-4-5` | `claude-sonnet-5-5` | `ANTHROPIC_API_KEY` |
|
|
462
466
|
| `custom` | set with `--model` | none | `OPENAI_API_KEY` (optional) |
|
|
463
467
|
|
|
464
|
-
If the default model returns a "model not found" error (e.g. it was deprecated), the tool
|
|
468
|
+
Each hosted provider has exactly one automatic fallback. If the default model returns a "model not found" error (e.g. it was deprecated), the tool retries with that fallback. Authentication, quota and network errors do not trigger a model fallback. If you pin a model with `--model`, no fallback is used.
|
|
465
469
|
|
|
466
|
-
|
|
470
|
+
These default/fallback pairs follow the selected configuration. Gemini model IDs were checked against [Flash-Lite](https://ai.google.dev/gemini-api/docs/models/gemini-3.5-flash-lite) and [Flash](https://ai.google.dev/gemini-api/docs/models/gemini-3.5-flash) documentation; the OpenAI fallback against [GPT-5.6 Terra](https://developers.openai.com/api/docs/models/gpt-5.6-terra); and Anthropic's default/fallback against its [model migration guide](https://platform.claude.com/docs/en/models/sonnet-5-5/migration-guide) on **2026-10-05**. The existing OpenAI default was checked on **2026-10-02**. Pin `--model` to avoid automatic model changes. Explicit model selection and custom/local endpoints remain available. The catalog is bundled with each CLI release, rather than automatically discovering models at runtime.
|
|
467
471
|
|
|
468
472
|
OpenAI and local providers retain the Chat Completions request format. Provider replies indicating truncation or blocked/incomplete output are rejected. Anthropic allows up to 16,384 output tokens per request and text blocks are collected separately from thinking blocks. Large files can still exceed a model's limits; automatic batching is future work.
|
|
469
473
|
|
|
@@ -497,9 +501,72 @@ android-localise translate \
|
|
|
497
501
|
|
|
498
502
|
---
|
|
499
503
|
|
|
504
|
+
## Save a key once on Windows
|
|
505
|
+
|
|
506
|
+
Run this yourself in your terminal:
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
android-localise credentials set --provider gemini
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Enter your key at the hidden prompt. It is stored in **Windows Credential
|
|
513
|
+
Manager**, for your Windows user on this computer, and persists across terminal
|
|
514
|
+
and computer restarts. The CLI does not write a plaintext key file or modify
|
|
515
|
+
environment variables. Running `set` again replaces that provider's saved key.
|
|
516
|
+
There is no key argument or piped-input option for this command; it requires an
|
|
517
|
+
interactive terminal and refuses a prompt that cannot hide input.
|
|
518
|
+
|
|
519
|
+
Then you or an AI assistant can run ordinary commands without including a key:
|
|
520
|
+
|
|
521
|
+
```bash
|
|
522
|
+
android-localise translate --languages hi,es
|
|
523
|
+
android-localise store-listing --source listing.json --languages hi-IN,es-ES
|
|
524
|
+
android-localise credentials status --provider gemini
|
|
525
|
+
android-localise credentials remove --provider gemini
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
`status` reports only `saved` or `not saved`; no command displays the saved value.
|
|
529
|
+
`remove` deletes only this CLI's saved entry for that provider; it does not revoke
|
|
530
|
+
the provider key or clear environment-variable overrides. The same commands also
|
|
531
|
+
work with `python -m android_localisation credentials ...`.
|
|
532
|
+
|
|
533
|
+
| Argument | Description | Default |
|
|
534
|
+
|---|---|---|
|
|
535
|
+
| `set`, `status`, `remove` | Hidden entry, presence check, or deletion | required action |
|
|
536
|
+
| `--provider` | `gemini`, `openai` or `anthropic` | `gemini` |
|
|
537
|
+
|
|
538
|
+
Both translation commands use this precedence: `--api-key` → provider-specific
|
|
539
|
+
environment variable → `API_KEY` → saved Windows key. Existing overrides retain
|
|
540
|
+
their behavior; if an old environment key is set, saving a new key does not
|
|
541
|
+
override it. Saved OpenAI keys load only for its default endpoint; `custom`
|
|
542
|
+
providers and other `--base-url` endpoints continue using explicit keys or
|
|
543
|
+
environment variables. Saved keys are not automatically sent to custom hosts.
|
|
544
|
+
Provider requests reject HTTP redirects, so use a custom endpoint's final URL.
|
|
545
|
+
Gemini authentication uses a header rather than a URL query parameter, and
|
|
546
|
+
API/network error messages redact the key used for that request before display.
|
|
547
|
+
|
|
548
|
+
This keeps keys out of chat, command arguments and routine CLI output. It
|
|
549
|
+
**does not isolate secrets from an AI or other program with unrestricted access
|
|
550
|
+
under your Windows account**: Windows permits programs running as that user to
|
|
551
|
+
read their credentials. The key is also present in memory during an API request.
|
|
552
|
+
For stronger separation, use a restricted runner or separately secured proxy.
|
|
553
|
+
See [Microsoft's credential API documentation](https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credreadw).
|
|
554
|
+
|
|
555
|
+
Saved-key commands are Windows-only, with no plaintext fallback. Other platforms
|
|
556
|
+
keep using environment variables or `--api-key`.
|
|
557
|
+
|
|
558
|
+
Manual check: save a key yourself, confirm input is hidden, open a new terminal
|
|
559
|
+
and check `status`. Run a small XML and listing preview without `--api-key`,
|
|
560
|
+
review the output, then remove the entry and confirm `not saved`. Ensure any key
|
|
561
|
+
environment overrides are absent when checking saved-key behavior.
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
500
565
|
## Environment variables
|
|
501
566
|
|
|
502
|
-
|
|
567
|
+
Environment variables remain available on all platforms. For Windows saved
|
|
568
|
+
credentials, use the commands above instead of putting a key in shell history.
|
|
569
|
+
To use an environment variable:
|
|
503
570
|
|
|
504
571
|
```bash
|
|
505
572
|
# macOS / Linux
|
|
@@ -33,11 +33,12 @@ Other examples:
|
|
|
33
33
|
android-localise store-listing --source listing.json --languages hi,es-ES
|
|
34
34
|
android-localise translate --languages hi --missing-only --dry-run
|
|
35
35
|
android-localise models --provider openai
|
|
36
|
+
android-localise credentials set --provider gemini (Windows, hidden prompt)
|
|
36
37
|
python -m android_localisation setup-path (Windows, one-time)
|
|
37
38
|
|
|
38
39
|
Use android-localise COMMAND --help for flags, defaults and examples.
|
|
39
40
|
All commands also work with: python -m android_localisation COMMAND
|
|
40
|
-
Keys:
|
|
41
|
+
Keys: --api-key, environment variables, then saved Windows credentials.
|
|
41
42
|
Update notices: set ANDROID_LOCALISE_NO_UPDATE_CHECK=1 to disable them.""",
|
|
42
43
|
)
|
|
43
44
|
parser.add_argument("--version", action="version", version=f"android-localisation {__version__}")
|
|
@@ -71,7 +72,7 @@ Use android-localise models to see current defaults and fallbacks.""",
|
|
|
71
72
|
add_flexible_arguments(translate_parser)
|
|
72
73
|
translate_parser.add_argument("--provider", choices=["gemini", "openai", "anthropic", "custom"], default="gemini", help="AI provider (default: gemini)")
|
|
73
74
|
translate_parser.add_argument("--model", help="Pin any supported model and disable fallbacks (default: provider default; see models)")
|
|
74
|
-
translate_parser.add_argument("--api-key", help="API key
|
|
75
|
+
translate_parser.add_argument("--api-key", help="API key; otherwise use provider environment variable / API_KEY, then saved Windows key")
|
|
75
76
|
translate_parser.add_argument("--base-url", help="Custom OpenAI-compatible endpoint URL (required for 'custom' provider)")
|
|
76
77
|
translate_parser.add_argument("--app-context", help="Short description of your app for better translations")
|
|
77
78
|
translate_parser.add_argument("--sleep", type=float, default=5.0, help="Seconds between API requests (default: 5.0)")
|
|
@@ -151,6 +152,25 @@ Custom/local providers require an explicit --model.""",
|
|
|
151
152
|
models_parser.add_argument("--provider", choices=["gemini", "openai", "anthropic"], default=None,
|
|
152
153
|
help="Filter by provider (shows all if not set)")
|
|
153
154
|
|
|
155
|
+
from android_localisation.credentials import add_arguments as add_credential_arguments
|
|
156
|
+
credentials_parser = subparsers.add_parser(
|
|
157
|
+
"credentials", help="Manage saved Windows API keys without displaying them",
|
|
158
|
+
description="""Save provider keys in Windows Credential Manager for your user account.
|
|
159
|
+
Run set yourself in an interactive terminal; the key prompt is hidden.
|
|
160
|
+
Status reports presence only. Translation commands load saved keys automatically
|
|
161
|
+
after argument/environment overrides. This does not isolate keys from programs
|
|
162
|
+
or AI agents with unrestricted access under your Windows account.""",
|
|
163
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
164
|
+
epilog="""Examples:
|
|
165
|
+
android-localise credentials set --provider gemini
|
|
166
|
+
android-localise credentials status --provider gemini
|
|
167
|
+
android-localise credentials remove --provider gemini
|
|
168
|
+
|
|
169
|
+
No key argument, piped input or plaintext storage fallback is supported.
|
|
170
|
+
Custom endpoints continue using explicit keys or environment variables.""",
|
|
171
|
+
)
|
|
172
|
+
add_credential_arguments(credentials_parser)
|
|
173
|
+
|
|
154
174
|
subparsers.add_parser(
|
|
155
175
|
"setup-path", help="Add the installed Scripts folder to Windows user PATH",
|
|
156
176
|
description="""One-time Windows setup for an installed CLI that PowerShell cannot find.
|
|
@@ -183,7 +203,11 @@ Exit code 1 reports unsupported platforms, virtual environments or setup failure
|
|
|
183
203
|
|
|
184
204
|
|
|
185
205
|
def _run_command(args):
|
|
186
|
-
if args.command == "
|
|
206
|
+
if args.command == "credentials":
|
|
207
|
+
from android_localisation.credentials import main as run
|
|
208
|
+
return run(args)
|
|
209
|
+
|
|
210
|
+
elif args.command == "setup-path":
|
|
187
211
|
from android_localisation.setup_path import main as run
|
|
188
212
|
return run(args)
|
|
189
213
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Explicit, user-scoped API key storage in Windows Credential Manager."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import ctypes
|
|
5
|
+
import getpass
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import warnings
|
|
9
|
+
from ctypes import wintypes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
PROVIDERS = ("gemini", "openai", "anthropic")
|
|
13
|
+
OPENAI_ENDPOINT = "https://api.openai.com/v1/chat/completions"
|
|
14
|
+
_GENERIC = 1
|
|
15
|
+
_LOCAL_MACHINE = 2 # Persistent on this computer, still scoped to the user.
|
|
16
|
+
_NOT_FOUND = 1168
|
|
17
|
+
_MAX_BLOB = 2560
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _Credential(ctypes.Structure):
|
|
21
|
+
_fields_ = [
|
|
22
|
+
("Flags", wintypes.DWORD), ("Type", wintypes.DWORD),
|
|
23
|
+
("TargetName", wintypes.LPWSTR), ("Comment", wintypes.LPWSTR),
|
|
24
|
+
("LastWritten", wintypes.FILETIME), ("CredentialBlobSize", wintypes.DWORD),
|
|
25
|
+
("CredentialBlob", ctypes.POINTER(wintypes.BYTE)), ("Persist", wintypes.DWORD),
|
|
26
|
+
("AttributeCount", wintypes.DWORD), ("Attributes", ctypes.c_void_p),
|
|
27
|
+
("TargetAlias", wintypes.LPWSTR), ("UserName", wintypes.LPWSTR),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _target(provider):
|
|
32
|
+
if provider not in PROVIDERS:
|
|
33
|
+
raise ValueError("Saved keys support gemini, openai and anthropic only.")
|
|
34
|
+
return "android-localisation:api-key:" + provider
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _windows_api():
|
|
38
|
+
if os.name != "nt":
|
|
39
|
+
raise OSError("Saved key commands require Windows Credential Manager. Use environment variables on other platforms.")
|
|
40
|
+
api = ctypes.WinDLL("advapi32", use_last_error=True)
|
|
41
|
+
pointer = ctypes.POINTER(_Credential)
|
|
42
|
+
api.CredWriteW.argtypes = [pointer, wintypes.DWORD]
|
|
43
|
+
api.CredWriteW.restype = wintypes.BOOL
|
|
44
|
+
api.CredReadW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD,
|
|
45
|
+
ctypes.POINTER(pointer)]
|
|
46
|
+
api.CredReadW.restype = wintypes.BOOL
|
|
47
|
+
api.CredDeleteW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD]
|
|
48
|
+
api.CredDeleteW.restype = wintypes.BOOL
|
|
49
|
+
api.CredFree.argtypes = [ctypes.c_void_p]
|
|
50
|
+
api.CredFree.restype = None
|
|
51
|
+
return api
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _failure():
|
|
55
|
+
# Do not include credential values or structures in diagnostics.
|
|
56
|
+
return OSError("Windows Credential Manager failed (error {}).".format(ctypes.get_last_error()))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def save_key(provider, key):
|
|
60
|
+
target = _target(provider)
|
|
61
|
+
if not key or any(ord(char) < 33 or ord(char) > 126 for char in key):
|
|
62
|
+
raise ValueError("API key must be nonempty printable ASCII without whitespace.")
|
|
63
|
+
encoded = key.encode("ascii")
|
|
64
|
+
if len(encoded) > _MAX_BLOB:
|
|
65
|
+
raise ValueError("API key exceeds Windows Credential Manager's size limit.")
|
|
66
|
+
api = _windows_api()
|
|
67
|
+
blob = (wintypes.BYTE * len(encoded)).from_buffer_copy(encoded)
|
|
68
|
+
credential = _Credential()
|
|
69
|
+
credential.Type = _GENERIC
|
|
70
|
+
credential.TargetName = target
|
|
71
|
+
credential.Comment = "API key for android-localisation"
|
|
72
|
+
credential.CredentialBlobSize = len(encoded)
|
|
73
|
+
credential.CredentialBlob = blob
|
|
74
|
+
credential.Persist = _LOCAL_MACHINE
|
|
75
|
+
credential.UserName = provider
|
|
76
|
+
try:
|
|
77
|
+
if not api.CredWriteW(ctypes.byref(credential), 0):
|
|
78
|
+
raise _failure()
|
|
79
|
+
finally:
|
|
80
|
+
ctypes.memset(blob, 0, len(encoded))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _read_key(provider, presence_only=False):
|
|
84
|
+
target = _target(provider)
|
|
85
|
+
api = _windows_api()
|
|
86
|
+
pointer = ctypes.POINTER(_Credential)()
|
|
87
|
+
if not api.CredReadW(target, _GENERIC, 0, ctypes.byref(pointer)):
|
|
88
|
+
if ctypes.get_last_error() == _NOT_FOUND:
|
|
89
|
+
return False if presence_only else None
|
|
90
|
+
raise _failure()
|
|
91
|
+
try:
|
|
92
|
+
credential = pointer.contents
|
|
93
|
+
if presence_only:
|
|
94
|
+
return True
|
|
95
|
+
size = credential.CredentialBlobSize
|
|
96
|
+
if not 0 < size <= _MAX_BLOB or not credential.CredentialBlob:
|
|
97
|
+
raise ValueError("Saved API key is invalid; save it again with credentials set.")
|
|
98
|
+
try:
|
|
99
|
+
key = ctypes.string_at(credential.CredentialBlob, size).decode("ascii")
|
|
100
|
+
except UnicodeDecodeError:
|
|
101
|
+
raise ValueError("Saved API key is invalid; save it again with credentials set.") from None
|
|
102
|
+
if any(ord(char) < 33 or ord(char) > 126 for char in key):
|
|
103
|
+
raise ValueError("Saved API key is invalid; save it again with credentials set.")
|
|
104
|
+
return key
|
|
105
|
+
finally:
|
|
106
|
+
# The Python string still exists while needed; this is not a memory vault.
|
|
107
|
+
if pointer.contents.CredentialBlob and pointer.contents.CredentialBlobSize <= _MAX_BLOB:
|
|
108
|
+
ctypes.memset(pointer.contents.CredentialBlob, 0, pointer.contents.CredentialBlobSize)
|
|
109
|
+
api.CredFree(pointer)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def remove_key(provider):
|
|
113
|
+
target = _target(provider)
|
|
114
|
+
api = _windows_api()
|
|
115
|
+
if api.CredDeleteW(target, _GENERIC, 0):
|
|
116
|
+
return True
|
|
117
|
+
if ctypes.get_last_error() == _NOT_FOUND:
|
|
118
|
+
return False
|
|
119
|
+
raise _failure()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def resolve_api_key(provider, supplied=None, base_url=None):
|
|
123
|
+
"""Preserve argument/env precedence; use saved keys only for built-in endpoints."""
|
|
124
|
+
env_name = {"gemini": "GEMINI_API_KEY", "openai": "OPENAI_API_KEY",
|
|
125
|
+
"anthropic": "ANTHROPIC_API_KEY", "custom": "OPENAI_API_KEY"}.get(provider)
|
|
126
|
+
key = supplied or (os.environ.get(env_name) if env_name else None) or os.environ.get("API_KEY")
|
|
127
|
+
if key:
|
|
128
|
+
return key
|
|
129
|
+
if os.name != "nt" or provider not in PROVIDERS:
|
|
130
|
+
return None
|
|
131
|
+
if provider == "openai" and base_url and base_url != OPENAI_ENDPOINT:
|
|
132
|
+
return None # Never silently forward a saved OpenAI key to a custom host.
|
|
133
|
+
return _read_key(provider)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def add_arguments(parser):
|
|
137
|
+
parser.add_argument("action", choices=("set", "status", "remove"),
|
|
138
|
+
help="Save via hidden prompt, report presence, or remove the saved key")
|
|
139
|
+
parser.add_argument("--provider", choices=PROVIDERS, default="gemini",
|
|
140
|
+
help="Provider credential to manage (default: gemini)")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _parse_args(args=None):
|
|
144
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
145
|
+
add_arguments(parser)
|
|
146
|
+
return parser.parse_args(args)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def main(args=None):
|
|
150
|
+
if args is None or isinstance(args, list):
|
|
151
|
+
args = _parse_args(args)
|
|
152
|
+
try:
|
|
153
|
+
if args.action == "set":
|
|
154
|
+
if os.name != "nt":
|
|
155
|
+
raise OSError("Saved key commands require Windows Credential Manager. Use environment variables on other platforms.")
|
|
156
|
+
if not sys.stdin.isatty():
|
|
157
|
+
raise ValueError("Run credentials set yourself in an interactive terminal; piped keys are not accepted.")
|
|
158
|
+
with warnings.catch_warnings():
|
|
159
|
+
warnings.simplefilter("error", getpass.GetPassWarning)
|
|
160
|
+
key = getpass.getpass("{} API key (hidden): ".format(args.provider))
|
|
161
|
+
try:
|
|
162
|
+
save_key(args.provider, key)
|
|
163
|
+
finally:
|
|
164
|
+
key = None
|
|
165
|
+
print("Saved {} API key in Windows Credential Manager for this user.".format(args.provider))
|
|
166
|
+
elif args.action == "status":
|
|
167
|
+
present = _read_key(args.provider, presence_only=True)
|
|
168
|
+
print("{}: {}".format(args.provider, "saved" if present else "not saved"))
|
|
169
|
+
elif args.action == "remove":
|
|
170
|
+
removed = remove_key(args.provider)
|
|
171
|
+
print("{}: {}".format(args.provider, "saved key removed" if removed else "no saved key"))
|
|
172
|
+
else:
|
|
173
|
+
raise ValueError("Unknown credentials action.")
|
|
174
|
+
except (OSError, ValueError, getpass.GetPassWarning) as error:
|
|
175
|
+
# Specific errors above are safe; getpass fallback must never echo a key.
|
|
176
|
+
print("ERROR: {}".format(error))
|
|
177
|
+
return 1
|
|
178
|
+
except (EOFError, KeyboardInterrupt):
|
|
179
|
+
print("Key entry cancelled; saved credentials unchanged.")
|
|
180
|
+
return 1
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
raise SystemExit(main())
|
{android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation/store_listing.py
RENAMED
|
@@ -11,6 +11,7 @@ import time
|
|
|
11
11
|
import unicodedata
|
|
12
12
|
|
|
13
13
|
from android_localisation.resources import atomic_write
|
|
14
|
+
from android_localisation.credentials import resolve_api_key
|
|
14
15
|
from android_localisation.locales import (
|
|
15
16
|
GOOGLE_PLAY_LOCALES, language_items, normalize_play_locale, select_locales,
|
|
16
17
|
)
|
|
@@ -47,7 +48,7 @@ def add_arguments(parser):
|
|
|
47
48
|
parser.add_argument("--dry-run", action="store_true", help="Generate and validate a diff without saving (API usage applies)")
|
|
48
49
|
parser.add_argument("--provider", choices=list(PROVIDER_MODELS), default="gemini", help="AI provider (default: gemini)")
|
|
49
50
|
parser.add_argument("--model", help="Pin a model and disable automatic fallbacks (default: provider default; see models)")
|
|
50
|
-
parser.add_argument("--api-key", help="API key
|
|
51
|
+
parser.add_argument("--api-key", help="API key; otherwise provider environment variable / API_KEY, then saved Windows key")
|
|
51
52
|
parser.add_argument("--base-url", help="Custom OpenAI-compatible endpoint (required for custom provider)")
|
|
52
53
|
parser.add_argument("--app-context", help="Short app description for terminology; listing remains the source of facts")
|
|
53
54
|
parser.add_argument("--sleep", type=float, default=5.0, help="Seconds between requests, including validation retries (default: 5.0)")
|
|
@@ -176,11 +177,9 @@ def main(args=None):
|
|
|
176
177
|
if args.provider == "custom" and (not args.model or not args.base_url):
|
|
177
178
|
raise ValueError("custom provider requires --model and --base-url")
|
|
178
179
|
models = [args.model] if args.model else model_list
|
|
179
|
-
|
|
180
|
-
"anthropic": "ANTHROPIC_API_KEY", "custom": "OPENAI_API_KEY"}[args.provider]
|
|
181
|
-
api_key = args.api_key or os.environ.get(env_name) or os.environ.get("API_KEY")
|
|
180
|
+
api_key = resolve_api_key(args.provider, args.api_key, args.base_url)
|
|
182
181
|
if not api_key and args.provider != "custom":
|
|
183
|
-
raise ValueError("provide --api-key or set
|
|
182
|
+
raise ValueError("provide --api-key, a provider environment variable, or save a Windows key with credentials set; saved keys are not used with custom endpoints")
|
|
184
183
|
except (OSError, ValueError, KeyError) as exc:
|
|
185
184
|
print("ERROR: {}".format(exc))
|
|
186
185
|
return 1
|
|
@@ -5,6 +5,7 @@ import socket
|
|
|
5
5
|
import argparse
|
|
6
6
|
import urllib.request
|
|
7
7
|
import urllib.error
|
|
8
|
+
import urllib.parse
|
|
8
9
|
import json
|
|
9
10
|
import math
|
|
10
11
|
import difflib
|
|
@@ -22,6 +23,7 @@ from android_localisation.resources import (
|
|
|
22
23
|
from android_localisation.locales import (
|
|
23
24
|
all_android_locales, android_locale, language_items, normalize_play_locale, select_locales,
|
|
24
25
|
)
|
|
26
|
+
from android_localisation.credentials import resolve_api_key
|
|
25
27
|
|
|
26
28
|
DEFAULT_RES_DIR = "app/src/main/res"
|
|
27
29
|
DEFAULT_API_TIMEOUT = 180 # seconds (3 minutes) — large strings.xml files can exceed 60s
|
|
@@ -29,20 +31,19 @@ MAX_TIMEOUT_RETRIES = 2
|
|
|
29
31
|
|
|
30
32
|
# Ordered list of models per provider.
|
|
31
33
|
# First entry = default. Rest = automatic fallbacks (used only when user hasn't pinned a model).
|
|
32
|
-
#
|
|
33
|
-
# Do not add older-generation models as automatic fallbacks.
|
|
34
|
+
# User-selected defaults with one fallback each; changed model IDs checked on 2026-10-05.
|
|
34
35
|
PROVIDER_MODELS = {
|
|
35
36
|
"gemini": [
|
|
36
|
-
"gemini-3.
|
|
37
|
+
"gemini-3.5-flash-lite",
|
|
38
|
+
"gemini-3.5-flash",
|
|
37
39
|
],
|
|
38
40
|
"openai": [
|
|
39
41
|
"gpt-6-luna",
|
|
40
|
-
"gpt-6
|
|
41
|
-
"gpt-6-astra",
|
|
42
|
+
"gpt-5.6-terra",
|
|
42
43
|
],
|
|
43
44
|
"anthropic": [
|
|
45
|
+
"claude-haiku-4-5",
|
|
44
46
|
"claude-sonnet-5-5",
|
|
45
|
-
"claude-opus-5-5",
|
|
46
47
|
],
|
|
47
48
|
"custom": [], # user must specify --model
|
|
48
49
|
}
|
|
@@ -148,7 +149,7 @@ def clean_xml_response(result):
|
|
|
148
149
|
|
|
149
150
|
def _read_error_body(e):
|
|
150
151
|
try:
|
|
151
|
-
return e.read().decode("utf-8", errors="replace")
|
|
152
|
+
return e.read().decode("utf-8", errors="replace")
|
|
152
153
|
except Exception:
|
|
153
154
|
return "(could not read error body)"
|
|
154
155
|
|
|
@@ -173,27 +174,43 @@ def _is_timeout_error(exc):
|
|
|
173
174
|
)
|
|
174
175
|
|
|
175
176
|
|
|
176
|
-
def
|
|
177
|
+
def _redact_error(value, api_key):
|
|
178
|
+
text = str(value)
|
|
179
|
+
if api_key:
|
|
180
|
+
for secret in sorted({api_key, urllib.parse.quote(api_key, safe=""),
|
|
181
|
+
urllib.parse.quote_plus(api_key), json.dumps(api_key)[1:-1]},
|
|
182
|
+
key=len, reverse=True):
|
|
183
|
+
text = text.replace(secret, "[REDACTED]")
|
|
184
|
+
return text[:500]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
188
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
189
|
+
return None # Do not forward authentication headers to redirect targets.
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _urlopen_with_retries(req, provider_label, timeout, api_key=None):
|
|
177
193
|
"""
|
|
178
194
|
Execute an HTTP request with timeout/network error handling and retries on timeout.
|
|
179
195
|
Returns (response_bytes, model_not_found).
|
|
180
196
|
"""
|
|
197
|
+
opener = urllib.request.build_opener(_NoRedirect())
|
|
181
198
|
for attempt in range(MAX_TIMEOUT_RETRIES + 1):
|
|
182
199
|
if attempt > 0:
|
|
183
200
|
wait = attempt * 3
|
|
184
201
|
print(f" 🔁 {provider_label} timed out — retrying ({attempt}/{MAX_TIMEOUT_RETRIES}) in {wait}s...")
|
|
185
202
|
time.sleep(wait)
|
|
186
203
|
try:
|
|
187
|
-
with
|
|
204
|
+
with opener.open(req, timeout=timeout) as response:
|
|
188
205
|
return response.read(), False
|
|
189
206
|
except urllib.error.HTTPError as e:
|
|
190
207
|
body = _read_error_body(e)
|
|
191
208
|
model_gone = _is_model_not_found(e.code, body)
|
|
192
|
-
print(f" ❌ {provider_label} API Error: {e.code} - {body}")
|
|
209
|
+
print(f" ❌ {provider_label} API Error: {e.code} - {_redact_error(body, api_key)}")
|
|
193
210
|
return None, model_gone
|
|
194
211
|
except (TimeoutError, socket.timeout, urllib.error.URLError) as e:
|
|
195
212
|
if not _is_timeout_error(e):
|
|
196
|
-
print(f" ❌ {provider_label} network error: {e.reason}")
|
|
213
|
+
print(f" ❌ {provider_label} network error: {_redact_error(e.reason, api_key)}")
|
|
197
214
|
return None, False
|
|
198
215
|
if attempt < MAX_TIMEOUT_RETRIES:
|
|
199
216
|
continue
|
|
@@ -203,11 +220,11 @@ def _urlopen_with_retries(req, provider_label, timeout):
|
|
|
203
220
|
|
|
204
221
|
|
|
205
222
|
def call_gemini(api_key, model, prompt, timeout=DEFAULT_API_TIMEOUT):
|
|
206
|
-
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent
|
|
207
|
-
headers = {"Content-Type": "application/json"}
|
|
223
|
+
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent"
|
|
224
|
+
headers = {"Content-Type": "application/json", "x-goog-api-key": api_key}
|
|
208
225
|
data = {"contents": [{"parts": [{"text": prompt}]}]}
|
|
209
226
|
req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), headers=headers, method="POST")
|
|
210
|
-
raw, model_gone = _urlopen_with_retries(req, "Gemini", timeout)
|
|
227
|
+
raw, model_gone = _urlopen_with_retries(req, "Gemini", timeout, api_key)
|
|
211
228
|
if raw is None:
|
|
212
229
|
return None, model_gone
|
|
213
230
|
result = json.loads(raw.decode("utf-8"))
|
|
@@ -217,7 +234,7 @@ def call_gemini(api_key, model, prompt, timeout=DEFAULT_API_TIMEOUT):
|
|
|
217
234
|
return None, False
|
|
218
235
|
candidate = candidates[0]
|
|
219
236
|
if candidate.get("finishReason") not in (None, "STOP"):
|
|
220
|
-
print(" ❌ Gemini response was incomplete or blocked: {}".format(candidate.get("finishReason")))
|
|
237
|
+
print(" ❌ Gemini response was incomplete or blocked: {}".format(_redact_error(candidate.get("finishReason"), api_key)))
|
|
221
238
|
return None, False
|
|
222
239
|
return "".join(part.get("text", "") for part in candidate.get("content", {}).get("parts", [])
|
|
223
240
|
if not part.get("thought")), False
|
|
@@ -233,7 +250,7 @@ def call_openai_compatible(api_key, base_url, model, prompt, timeout=DEFAULT_API
|
|
|
233
250
|
"messages": [{"role": "user", "content": prompt}],
|
|
234
251
|
}
|
|
235
252
|
req = urllib.request.Request(base_url, data=json.dumps(data).encode("utf-8"), headers=headers, method="POST")
|
|
236
|
-
raw, model_gone = _urlopen_with_retries(req, "OpenAI (compatible)", timeout)
|
|
253
|
+
raw, model_gone = _urlopen_with_retries(req, "OpenAI (compatible)", timeout, api_key)
|
|
237
254
|
if raw is None:
|
|
238
255
|
return None, model_gone
|
|
239
256
|
result = json.loads(raw.decode("utf-8"))
|
|
@@ -242,7 +259,7 @@ def call_openai_compatible(api_key, base_url, model, prompt, timeout=DEFAULT_API
|
|
|
242
259
|
print(" ❌ OpenAI returned no choices.")
|
|
243
260
|
return None, False
|
|
244
261
|
if choices[0].get("finish_reason") not in (None, "stop"):
|
|
245
|
-
print(" ❌ OpenAI-compatible response was incomplete: {}".format(choices[0].get("finish_reason")))
|
|
262
|
+
print(" ❌ OpenAI-compatible response was incomplete: {}".format(_redact_error(choices[0].get("finish_reason"), api_key)))
|
|
246
263
|
return None, False
|
|
247
264
|
return choices[0].get("message", {}).get("content", ""), False
|
|
248
265
|
|
|
@@ -260,7 +277,7 @@ def call_anthropic(api_key, model, prompt, timeout=DEFAULT_API_TIMEOUT):
|
|
|
260
277
|
"messages": [{"role": "user", "content": prompt}],
|
|
261
278
|
}
|
|
262
279
|
req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), headers=headers, method="POST")
|
|
263
|
-
raw, model_gone = _urlopen_with_retries(req, "Anthropic", timeout)
|
|
280
|
+
raw, model_gone = _urlopen_with_retries(req, "Anthropic", timeout, api_key)
|
|
264
281
|
if raw is None:
|
|
265
282
|
return None, model_gone
|
|
266
283
|
result = json.loads(raw.decode("utf-8"))
|
|
@@ -269,7 +286,7 @@ def call_anthropic(api_key, model, prompt, timeout=DEFAULT_API_TIMEOUT):
|
|
|
269
286
|
print(" ❌ Anthropic returned no content.")
|
|
270
287
|
return None, False
|
|
271
288
|
if result.get("stop_reason") not in (None, "end_turn"):
|
|
272
|
-
print(" ❌ Anthropic response was incomplete: {}".format(result.get("stop_reason")))
|
|
289
|
+
print(" ❌ Anthropic response was incomplete: {}".format(_redact_error(result.get("stop_reason"), api_key)))
|
|
273
290
|
return None, False
|
|
274
291
|
return "".join(block.get("text", "") for block in content if block.get("type") == "text"), False
|
|
275
292
|
|
|
@@ -327,7 +344,7 @@ def _parse_args(args=None):
|
|
|
327
344
|
add_flexible_arguments(parser)
|
|
328
345
|
parser.add_argument("--provider", choices=["gemini", "openai", "anthropic", "custom"], default="gemini", help="AI provider (default: gemini)")
|
|
329
346
|
parser.add_argument("--model", help="Pin any supported model and disable fallbacks (default: provider default; see models)")
|
|
330
|
-
parser.add_argument("--api-key", help="API key
|
|
347
|
+
parser.add_argument("--api-key", help="API key; otherwise use provider environment variable / API_KEY, then saved Windows key")
|
|
331
348
|
parser.add_argument("--base-url", help="Custom OpenAI-compatible endpoint URL (required for 'custom' provider)")
|
|
332
349
|
parser.add_argument("--app-context", help="Short description of your app for better translations")
|
|
333
350
|
parser.add_argument("--sleep", type=float, default=5.0, help="Seconds between API requests (default: 5.0)")
|
|
@@ -382,16 +399,14 @@ def main(args=None):
|
|
|
382
399
|
fallback_models = model_list[1:]
|
|
383
400
|
|
|
384
401
|
# Resolve API key
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if not api_key:
|
|
391
|
-
api_key = os.environ.get("API_KEY")
|
|
402
|
+
try:
|
|
403
|
+
api_key = resolve_api_key(provider, args.api_key, args.base_url)
|
|
404
|
+
except (OSError, ValueError) as exc:
|
|
405
|
+
print("❌ ERROR: {}".format(exc))
|
|
406
|
+
return 1
|
|
392
407
|
|
|
393
408
|
if not api_key and provider != "custom":
|
|
394
|
-
print("❌ ERROR:
|
|
409
|
+
print("❌ ERROR: Provide --api-key, a provider environment variable, or save a Windows key with credentials set. Saved keys are not used with custom endpoints.")
|
|
395
410
|
return 1
|
|
396
411
|
|
|
397
412
|
if provider == "custom" and not args.base_url:
|
{android_localisation-1.5.0 → android_localisation-1.6.2/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.
|
|
3
|
+
Version: 1.6.2
|
|
4
4
|
Summary: Zero-dependency Android strings.xml and Google Play store listing translation using LLMs (Gemini, OpenAI, Anthropic, Ollama).
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/BharathKmalviya/android-llm-localization
|
|
@@ -127,12 +127,16 @@ When you run `android-localise translate --api-key YOUR_KEY`, here's exactly wha
|
|
|
127
127
|
4. Parses the response and checks duplicate/unexpected/missing resources, attributes, inline markup, item structure, control escapes and format arguments. A valid result replaces the file atomically; new folders are created only when saving. `--skip-existing` validates existing files and skips them without API calls. `--dry-run` shows a diff without writing any files or creating folders
|
|
128
128
|
5. Waits 5 seconds between each language request to avoid hitting API rate limits
|
|
129
129
|
|
|
130
|
+
Keys resolve from `--api-key`, then provider environment variables / `API_KEY`,
|
|
131
|
+
then saved Windows credentials for built-in provider endpoints. The CLI never
|
|
132
|
+
prompts for a key during translation; use `credentials set` yourself beforehand.
|
|
133
|
+
|
|
130
134
|
**Defaults used when you don't specify anything:**
|
|
131
135
|
|
|
132
136
|
| What | Default |
|
|
133
137
|
|---|---|
|
|
134
138
|
| Provider | Gemini |
|
|
135
|
-
| Model | `gemini-3.
|
|
139
|
+
| Model | `gemini-3.5-flash-lite` |
|
|
136
140
|
| Source directory | `app/src/main/res` |
|
|
137
141
|
| Delay between requests | 5 seconds |
|
|
138
142
|
| App context | none (generic prompt) |
|
|
@@ -226,7 +230,7 @@ android-localise translate \
|
|
|
226
230
|
|
|
227
231
|
| Flag | What it does | Default |
|
|
228
232
|
|---|---|---|
|
|
229
|
-
| `--api-key` |
|
|
233
|
+
| `--api-key` | Explicit key override; omit for environment or saved Windows credentials | environment, then saved Windows key |
|
|
230
234
|
| `--provider` | Which AI to use: `gemini` `openai` `anthropic` `custom` | `gemini` |
|
|
231
235
|
| `--model` | Specific model to use | see [Providers](#providers) |
|
|
232
236
|
| `--languages` | Comma-separated Android/conventional codes and/or `all`, e.g. `all,zu` or `hi,es-ES,b+es+419` | scan existing destination locale folders |
|
|
@@ -305,7 +309,7 @@ Console import file. The CLI does not upload or publish a listing.
|
|
|
305
309
|
| `--dry-run` | Generate, validate and display diffs without files/directories being written | off; API usage applies |
|
|
306
310
|
| `--provider` | `gemini`, `openai`, `anthropic`, `custom` | `gemini` |
|
|
307
311
|
| `--model` | Pin any supported model and disable fallbacks | provider default |
|
|
308
|
-
| `--api-key` |
|
|
312
|
+
| `--api-key` | Explicit key override; omit for environment or saved Windows credentials | environment, then saved Windows key |
|
|
309
313
|
| `--base-url` | OpenAI-compatible endpoint; required for `custom` | provider endpoint |
|
|
310
314
|
| `--app-context` | Terminology context; source copy remains the source of facts | none |
|
|
311
315
|
| `--sleep` | Delay between requests, including correction/fallback requests | `5.0` seconds |
|
|
@@ -480,18 +484,18 @@ environment behavior. All commands can also run through `python -m android_local
|
|
|
480
484
|
|
|
481
485
|
## Providers
|
|
482
486
|
|
|
483
|
-
By default the tool uses Gemini
|
|
487
|
+
By default the tool uses Gemini 3.5 Flash-Lite (`gemini-3.5-flash-lite`) for both XML and store listing translation. You can switch providers with `--provider` and optionally pin a specific model with `--model`.
|
|
484
488
|
|
|
485
|
-
| Provider | Default model |
|
|
489
|
+
| Provider | Default model | Fallback | API key env var |
|
|
486
490
|
|---|---|---|---|
|
|
487
|
-
| `gemini` _(default)_ | `gemini-3.
|
|
488
|
-
| `openai` | `gpt-6-luna` | `gpt-
|
|
489
|
-
| `anthropic` | `claude-
|
|
491
|
+
| `gemini` _(default)_ | `gemini-3.5-flash-lite` | `gemini-3.5-flash` | `GEMINI_API_KEY` |
|
|
492
|
+
| `openai` | `gpt-6-luna` | `gpt-5.6-terra` | `OPENAI_API_KEY` |
|
|
493
|
+
| `anthropic` | `claude-haiku-4-5` | `claude-sonnet-5-5` | `ANTHROPIC_API_KEY` |
|
|
490
494
|
| `custom` | set with `--model` | none | `OPENAI_API_KEY` (optional) |
|
|
491
495
|
|
|
492
|
-
If the default model returns a "model not found" error (e.g. it was deprecated), the tool
|
|
496
|
+
Each hosted provider has exactly one automatic fallback. If the default model returns a "model not found" error (e.g. it was deprecated), the tool retries with that fallback. Authentication, quota and network errors do not trigger a model fallback. If you pin a model with `--model`, no fallback is used.
|
|
493
497
|
|
|
494
|
-
|
|
498
|
+
These default/fallback pairs follow the selected configuration. Gemini model IDs were checked against [Flash-Lite](https://ai.google.dev/gemini-api/docs/models/gemini-3.5-flash-lite) and [Flash](https://ai.google.dev/gemini-api/docs/models/gemini-3.5-flash) documentation; the OpenAI fallback against [GPT-5.6 Terra](https://developers.openai.com/api/docs/models/gpt-5.6-terra); and Anthropic's default/fallback against its [model migration guide](https://platform.claude.com/docs/en/models/sonnet-5-5/migration-guide) on **2026-10-05**. The existing OpenAI default was checked on **2026-10-02**. Pin `--model` to avoid automatic model changes. Explicit model selection and custom/local endpoints remain available. The catalog is bundled with each CLI release, rather than automatically discovering models at runtime.
|
|
495
499
|
|
|
496
500
|
OpenAI and local providers retain the Chat Completions request format. Provider replies indicating truncation or blocked/incomplete output are rejected. Anthropic allows up to 16,384 output tokens per request and text blocks are collected separately from thinking blocks. Large files can still exceed a model's limits; automatic batching is future work.
|
|
497
501
|
|
|
@@ -525,9 +529,72 @@ android-localise translate \
|
|
|
525
529
|
|
|
526
530
|
---
|
|
527
531
|
|
|
532
|
+
## Save a key once on Windows
|
|
533
|
+
|
|
534
|
+
Run this yourself in your terminal:
|
|
535
|
+
|
|
536
|
+
```bash
|
|
537
|
+
android-localise credentials set --provider gemini
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Enter your key at the hidden prompt. It is stored in **Windows Credential
|
|
541
|
+
Manager**, for your Windows user on this computer, and persists across terminal
|
|
542
|
+
and computer restarts. The CLI does not write a plaintext key file or modify
|
|
543
|
+
environment variables. Running `set` again replaces that provider's saved key.
|
|
544
|
+
There is no key argument or piped-input option for this command; it requires an
|
|
545
|
+
interactive terminal and refuses a prompt that cannot hide input.
|
|
546
|
+
|
|
547
|
+
Then you or an AI assistant can run ordinary commands without including a key:
|
|
548
|
+
|
|
549
|
+
```bash
|
|
550
|
+
android-localise translate --languages hi,es
|
|
551
|
+
android-localise store-listing --source listing.json --languages hi-IN,es-ES
|
|
552
|
+
android-localise credentials status --provider gemini
|
|
553
|
+
android-localise credentials remove --provider gemini
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
`status` reports only `saved` or `not saved`; no command displays the saved value.
|
|
557
|
+
`remove` deletes only this CLI's saved entry for that provider; it does not revoke
|
|
558
|
+
the provider key or clear environment-variable overrides. The same commands also
|
|
559
|
+
work with `python -m android_localisation credentials ...`.
|
|
560
|
+
|
|
561
|
+
| Argument | Description | Default |
|
|
562
|
+
|---|---|---|
|
|
563
|
+
| `set`, `status`, `remove` | Hidden entry, presence check, or deletion | required action |
|
|
564
|
+
| `--provider` | `gemini`, `openai` or `anthropic` | `gemini` |
|
|
565
|
+
|
|
566
|
+
Both translation commands use this precedence: `--api-key` → provider-specific
|
|
567
|
+
environment variable → `API_KEY` → saved Windows key. Existing overrides retain
|
|
568
|
+
their behavior; if an old environment key is set, saving a new key does not
|
|
569
|
+
override it. Saved OpenAI keys load only for its default endpoint; `custom`
|
|
570
|
+
providers and other `--base-url` endpoints continue using explicit keys or
|
|
571
|
+
environment variables. Saved keys are not automatically sent to custom hosts.
|
|
572
|
+
Provider requests reject HTTP redirects, so use a custom endpoint's final URL.
|
|
573
|
+
Gemini authentication uses a header rather than a URL query parameter, and
|
|
574
|
+
API/network error messages redact the key used for that request before display.
|
|
575
|
+
|
|
576
|
+
This keeps keys out of chat, command arguments and routine CLI output. It
|
|
577
|
+
**does not isolate secrets from an AI or other program with unrestricted access
|
|
578
|
+
under your Windows account**: Windows permits programs running as that user to
|
|
579
|
+
read their credentials. The key is also present in memory during an API request.
|
|
580
|
+
For stronger separation, use a restricted runner or separately secured proxy.
|
|
581
|
+
See [Microsoft's credential API documentation](https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credreadw).
|
|
582
|
+
|
|
583
|
+
Saved-key commands are Windows-only, with no plaintext fallback. Other platforms
|
|
584
|
+
keep using environment variables or `--api-key`.
|
|
585
|
+
|
|
586
|
+
Manual check: save a key yourself, confirm input is hidden, open a new terminal
|
|
587
|
+
and check `status`. Run a small XML and listing preview without `--api-key`,
|
|
588
|
+
review the output, then remove the entry and confirm `not saved`. Ensure any key
|
|
589
|
+
environment overrides are absent when checking saved-key behavior.
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
528
593
|
## Environment variables
|
|
529
594
|
|
|
530
|
-
|
|
595
|
+
Environment variables remain available on all platforms. For Windows saved
|
|
596
|
+
credentials, use the commands above instead of putting a key in shell history.
|
|
597
|
+
To use an environment variable:
|
|
531
598
|
|
|
532
599
|
```bash
|
|
533
600
|
# macOS / Linux
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "android-localisation"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.6.2"
|
|
8
8
|
description = "Zero-dependency Android strings.xml and Google Play store listing translation 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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{android_localisation-1.5.0 → android_localisation-1.6.2}/android_localisation/setup_path.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|