android-localisation 1.5.0__tar.gz → 1.6.0__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.0}/PKG-INFO +71 -4
- {android_localisation-1.5.0 → android_localisation-1.6.0}/README.md +70 -3
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation/__init__.py +1 -1
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation/cli.py +27 -3
- android_localisation-1.6.0/android_localisation/credentials.py +185 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation/store_listing.py +4 -5
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation/translate.py +38 -22
- {android_localisation-1.5.0 → android_localisation-1.6.0/android_localisation.egg-info}/PKG-INFO +71 -4
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation.egg-info/SOURCES.txt +1 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/pyproject.toml +1 -1
- {android_localisation-1.5.0 → android_localisation-1.6.0}/LICENSE +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/MANIFEST.in +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation/__main__.py +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation/fix.py +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation/java/VerifyStrings.java +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation/locales.py +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation/resources.py +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation/setup_path.py +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation/updates.py +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation/verify.py +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation.egg-info/dependency_links.txt +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation.egg-info/entry_points.txt +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/android_localisation.egg-info/top_level.txt +0 -0
- {android_localisation-1.5.0 → android_localisation-1.6.0}/setup.cfg +0 -0
{android_localisation-1.5.0/android_localisation.egg-info → android_localisation-1.6.0}/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.0
|
|
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,6 +127,10 @@ 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 |
|
|
@@ -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 |
|
|
@@ -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,6 +99,10 @@ 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 |
|
|
@@ -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 |
|
|
@@ -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.0}/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
|
|
@@ -148,7 +150,7 @@ def clean_xml_response(result):
|
|
|
148
150
|
|
|
149
151
|
def _read_error_body(e):
|
|
150
152
|
try:
|
|
151
|
-
return e.read().decode("utf-8", errors="replace")
|
|
153
|
+
return e.read().decode("utf-8", errors="replace")
|
|
152
154
|
except Exception:
|
|
153
155
|
return "(could not read error body)"
|
|
154
156
|
|
|
@@ -173,27 +175,43 @@ def _is_timeout_error(exc):
|
|
|
173
175
|
)
|
|
174
176
|
|
|
175
177
|
|
|
176
|
-
def
|
|
178
|
+
def _redact_error(value, api_key):
|
|
179
|
+
text = str(value)
|
|
180
|
+
if api_key:
|
|
181
|
+
for secret in sorted({api_key, urllib.parse.quote(api_key, safe=""),
|
|
182
|
+
urllib.parse.quote_plus(api_key), json.dumps(api_key)[1:-1]},
|
|
183
|
+
key=len, reverse=True):
|
|
184
|
+
text = text.replace(secret, "[REDACTED]")
|
|
185
|
+
return text[:500]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
189
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
190
|
+
return None # Do not forward authentication headers to redirect targets.
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _urlopen_with_retries(req, provider_label, timeout, api_key=None):
|
|
177
194
|
"""
|
|
178
195
|
Execute an HTTP request with timeout/network error handling and retries on timeout.
|
|
179
196
|
Returns (response_bytes, model_not_found).
|
|
180
197
|
"""
|
|
198
|
+
opener = urllib.request.build_opener(_NoRedirect())
|
|
181
199
|
for attempt in range(MAX_TIMEOUT_RETRIES + 1):
|
|
182
200
|
if attempt > 0:
|
|
183
201
|
wait = attempt * 3
|
|
184
202
|
print(f" 🔁 {provider_label} timed out — retrying ({attempt}/{MAX_TIMEOUT_RETRIES}) in {wait}s...")
|
|
185
203
|
time.sleep(wait)
|
|
186
204
|
try:
|
|
187
|
-
with
|
|
205
|
+
with opener.open(req, timeout=timeout) as response:
|
|
188
206
|
return response.read(), False
|
|
189
207
|
except urllib.error.HTTPError as e:
|
|
190
208
|
body = _read_error_body(e)
|
|
191
209
|
model_gone = _is_model_not_found(e.code, body)
|
|
192
|
-
print(f" ❌ {provider_label} API Error: {e.code} - {body}")
|
|
210
|
+
print(f" ❌ {provider_label} API Error: {e.code} - {_redact_error(body, api_key)}")
|
|
193
211
|
return None, model_gone
|
|
194
212
|
except (TimeoutError, socket.timeout, urllib.error.URLError) as e:
|
|
195
213
|
if not _is_timeout_error(e):
|
|
196
|
-
print(f" ❌ {provider_label} network error: {e.reason}")
|
|
214
|
+
print(f" ❌ {provider_label} network error: {_redact_error(e.reason, api_key)}")
|
|
197
215
|
return None, False
|
|
198
216
|
if attempt < MAX_TIMEOUT_RETRIES:
|
|
199
217
|
continue
|
|
@@ -203,11 +221,11 @@ def _urlopen_with_retries(req, provider_label, timeout):
|
|
|
203
221
|
|
|
204
222
|
|
|
205
223
|
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"}
|
|
224
|
+
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent"
|
|
225
|
+
headers = {"Content-Type": "application/json", "x-goog-api-key": api_key}
|
|
208
226
|
data = {"contents": [{"parts": [{"text": prompt}]}]}
|
|
209
227
|
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)
|
|
228
|
+
raw, model_gone = _urlopen_with_retries(req, "Gemini", timeout, api_key)
|
|
211
229
|
if raw is None:
|
|
212
230
|
return None, model_gone
|
|
213
231
|
result = json.loads(raw.decode("utf-8"))
|
|
@@ -217,7 +235,7 @@ def call_gemini(api_key, model, prompt, timeout=DEFAULT_API_TIMEOUT):
|
|
|
217
235
|
return None, False
|
|
218
236
|
candidate = candidates[0]
|
|
219
237
|
if candidate.get("finishReason") not in (None, "STOP"):
|
|
220
|
-
print(" ❌ Gemini response was incomplete or blocked: {}".format(candidate.get("finishReason")))
|
|
238
|
+
print(" ❌ Gemini response was incomplete or blocked: {}".format(_redact_error(candidate.get("finishReason"), api_key)))
|
|
221
239
|
return None, False
|
|
222
240
|
return "".join(part.get("text", "") for part in candidate.get("content", {}).get("parts", [])
|
|
223
241
|
if not part.get("thought")), False
|
|
@@ -233,7 +251,7 @@ def call_openai_compatible(api_key, base_url, model, prompt, timeout=DEFAULT_API
|
|
|
233
251
|
"messages": [{"role": "user", "content": prompt}],
|
|
234
252
|
}
|
|
235
253
|
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)
|
|
254
|
+
raw, model_gone = _urlopen_with_retries(req, "OpenAI (compatible)", timeout, api_key)
|
|
237
255
|
if raw is None:
|
|
238
256
|
return None, model_gone
|
|
239
257
|
result = json.loads(raw.decode("utf-8"))
|
|
@@ -242,7 +260,7 @@ def call_openai_compatible(api_key, base_url, model, prompt, timeout=DEFAULT_API
|
|
|
242
260
|
print(" ❌ OpenAI returned no choices.")
|
|
243
261
|
return None, False
|
|
244
262
|
if choices[0].get("finish_reason") not in (None, "stop"):
|
|
245
|
-
print(" ❌ OpenAI-compatible response was incomplete: {}".format(choices[0].get("finish_reason")))
|
|
263
|
+
print(" ❌ OpenAI-compatible response was incomplete: {}".format(_redact_error(choices[0].get("finish_reason"), api_key)))
|
|
246
264
|
return None, False
|
|
247
265
|
return choices[0].get("message", {}).get("content", ""), False
|
|
248
266
|
|
|
@@ -260,7 +278,7 @@ def call_anthropic(api_key, model, prompt, timeout=DEFAULT_API_TIMEOUT):
|
|
|
260
278
|
"messages": [{"role": "user", "content": prompt}],
|
|
261
279
|
}
|
|
262
280
|
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)
|
|
281
|
+
raw, model_gone = _urlopen_with_retries(req, "Anthropic", timeout, api_key)
|
|
264
282
|
if raw is None:
|
|
265
283
|
return None, model_gone
|
|
266
284
|
result = json.loads(raw.decode("utf-8"))
|
|
@@ -269,7 +287,7 @@ def call_anthropic(api_key, model, prompt, timeout=DEFAULT_API_TIMEOUT):
|
|
|
269
287
|
print(" ❌ Anthropic returned no content.")
|
|
270
288
|
return None, False
|
|
271
289
|
if result.get("stop_reason") not in (None, "end_turn"):
|
|
272
|
-
print(" ❌ Anthropic response was incomplete: {}".format(result.get("stop_reason")))
|
|
290
|
+
print(" ❌ Anthropic response was incomplete: {}".format(_redact_error(result.get("stop_reason"), api_key)))
|
|
273
291
|
return None, False
|
|
274
292
|
return "".join(block.get("text", "") for block in content if block.get("type") == "text"), False
|
|
275
293
|
|
|
@@ -327,7 +345,7 @@ def _parse_args(args=None):
|
|
|
327
345
|
add_flexible_arguments(parser)
|
|
328
346
|
parser.add_argument("--provider", choices=["gemini", "openai", "anthropic", "custom"], default="gemini", help="AI provider (default: gemini)")
|
|
329
347
|
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
|
|
348
|
+
parser.add_argument("--api-key", help="API key; otherwise use provider environment variable / API_KEY, then saved Windows key")
|
|
331
349
|
parser.add_argument("--base-url", help="Custom OpenAI-compatible endpoint URL (required for 'custom' provider)")
|
|
332
350
|
parser.add_argument("--app-context", help="Short description of your app for better translations")
|
|
333
351
|
parser.add_argument("--sleep", type=float, default=5.0, help="Seconds between API requests (default: 5.0)")
|
|
@@ -382,16 +400,14 @@ def main(args=None):
|
|
|
382
400
|
fallback_models = model_list[1:]
|
|
383
401
|
|
|
384
402
|
# Resolve API key
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if not api_key:
|
|
391
|
-
api_key = os.environ.get("API_KEY")
|
|
403
|
+
try:
|
|
404
|
+
api_key = resolve_api_key(provider, args.api_key, args.base_url)
|
|
405
|
+
except (OSError, ValueError) as exc:
|
|
406
|
+
print("❌ ERROR: {}".format(exc))
|
|
407
|
+
return 1
|
|
392
408
|
|
|
393
409
|
if not api_key and provider != "custom":
|
|
394
|
-
print("❌ ERROR:
|
|
410
|
+
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
411
|
return 1
|
|
396
412
|
|
|
397
413
|
if provider == "custom" and not args.base_url:
|
{android_localisation-1.5.0 → android_localisation-1.6.0/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.0
|
|
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,6 +127,10 @@ 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 |
|
|
@@ -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 |
|
|
@@ -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.0"
|
|
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.0}/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
|