-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtranslate_strings.py
More file actions
executable file
·218 lines (179 loc) · 7.98 KB
/
Copy pathtranslate_strings.py
File metadata and controls
executable file
·218 lines (179 loc) · 7.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
#!/usr/bin/env python3
"""
translate_strings.py
Finds all Android locale strings.xml files under app/src/main/res/,
compares them to the source (values/strings.xml), translates any missing
strings via the Anthropic API, and writes the updated files back to disk.
Also writes /tmp/pr_body.md summarising what was changed for the PR.
"""
import os
import re
import sys
import json
import textwrap
import anthropic
from pathlib import Path
from lxml import etree
# ── Configuration ─────────────────────────────────────────────────────────────
SOURCE_DIR = "app/src/main/res/values"
RES_BASE_DIR = "app/src/main/res"
PR_BODY_PATH = "/tmp/pr_body.md"
# Map Android locale folder suffix → human language name for the prompt
LOCALE_MAP = {
"values-ar": "Arabic",
"values-zh-rTW": "Traditional Chinese (Taiwan)",
"values-zh-rCN": "Simplified Chinese (Mainland China)",
"values-fr": "French",
"values-de": "German",
"values-fa": "Farsi (Persian)",
"values-pa": "Punjabi",
"values-pl": "Polish",
"values-in": "Indonesian",
"values-es": "Spanish",
"values-sv": "Swedish",
"values-uk": "Ukrainian",
"values-ru": "Russian",
"values-tr": "Turkish",
"values-ja": "Japanese",
"values-ko": "Korean",
"values-hi": "Hindi",
"values-it": "Italian",
"values-pt-rBR": "Brazilian Portuguese",
}
# RTL languages that need android:textDirection="rtl" awareness (informational)
RTL_LOCALES = {"values-ar", "values-fa", "values-pa"}
BATCH_SIZE = 40 # strings per API call – keeps prompts manageable
# ── Helpers ───────────────────────────────────────────────────────────────────
def parse_strings(path: Path) -> dict[str, str]:
"""Return {name: text} for every translatable <string> in the file."""
if not path.exists():
return {}
tree = etree.parse(str(path))
result = {}
for el in tree.getroot().findall("string"):
name = el.get("name")
translatable = el.get("translatable", "true")
if name and translatable.lower() != "false":
# Preserve inner XML (handles <b>, <i>, %1$s etc.)
inner = (el.text or "") + "".join(
etree.tostring(child, encoding="unicode") for child in el
)
result[name] = inner.strip()
return result
def load_or_create_tree(path: Path):
"""Parse existing file or return a fresh <resources> tree."""
if path.exists():
return etree.parse(str(path))
root = etree.Element("resources")
root.addprevious(etree.Comment(" Auto-generated by translate_strings.py "))
return etree.ElementTree(root)
def write_tree(tree, path: Path):
path.parent.mkdir(parents=True, exist_ok=True)
tree.write(
str(path),
encoding="utf-8",
xml_declaration=True,
pretty_print=True,
)
def set_translated(tree, name: str, translated_text: str):
"""Insert or update a <string name="…"> element in the tree."""
root = tree.getroot()
existing = root.find(f'string[@name="{name}"]')
if existing is not None:
existing.text = translated_text
return
el = etree.SubElement(root, "string")
el.set("name", name)
el.text = translated_text
# ── Translation via Anthropic ─────────────────────────────────────────────────
def translate_batch(
client: anthropic.Anthropic,
language: str,
batch: dict[str, str],
) -> dict[str, str]:
"""
Send a batch of {name: english_text} to Claude and get back
{name: translated_text}.
Returns an empty dict on failure (strings will be skipped this run).
"""
items_json = json.dumps(batch, ensure_ascii=False, indent=2)
prompt = textwrap.dedent(f"""
You are a professional mobile-app localisation expert.
Translate the following Android string resource values from English into
**{language}**.
Rules:
- Preserve ALL placeholder tokens exactly as-is: %1$s %2$s %d %f etc.
- Preserve HTML tags exactly: <b> </b> <i> </i> <br/> etc.
- Preserve ampersand escapes: & < > \' etc.
- Do NOT translate the JSON keys (string resource names).
- Do NOT add explanations or markdown — return ONLY valid JSON.
- The app is a Litecoin cryptocurrency wallet called "Brainwallet".
Keep "Brainwallet", "Litecoin", "LTC", "PIN", "QR" untranslated.
- Use natural, friendly language suitable for a mobile finance app.
Input JSON (keys are resource names, values are English strings):
{items_json}
Return a JSON object with the same keys and translated values.
""").strip()
try:
message = client.messages.create(
model="claude-opus-4-5",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}],
)
raw = message.content[0].text.strip()
# Strip possible ```json fences
raw = re.sub(r"^```(?:json)?\s*", "", raw)
raw = re.sub(r"\s*```$", "", raw)
return json.loads(raw)
except Exception as exc:
print(f" ⚠️ API/parse error for {language}: {exc}", file=sys.stderr)
return {}
# ── Main ──────────────────────────────────────────────────────────────────────
def main():
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
sys.exit("ERROR: ANTHROPIC_API_KEY environment variable not set.")
client = anthropic.Anthropic(api_key=api_key)
source_path = Path(SOURCE_DIR) / "strings.xml"
if not source_path.exists():
sys.exit(f"ERROR: Source file not found: {source_path}")
source_strings = parse_strings(source_path)
print(f"✅ Source: {len(source_strings)} translatable strings loaded.")
pr_lines = [
"## 🌍 Auto-translation summary\n",
"| Language | New translations | File |",
"|---|---|---|",
]
for locale_dir, language in LOCALE_MAP.items():
target_path = Path(RES_BASE_DIR) / locale_dir / "strings.xml"
existing = parse_strings(target_path)
missing = {
k: v for k, v in source_strings.items()
if k not in existing or not existing[k].strip()
}
if not missing:
print(f" ✔ {language}: nothing to translate.")
pr_lines.append(f"| {language} | 0 (already complete) | `{target_path}` |")
continue
print(f" 🔄 {language}: {len(missing)} strings to translate …")
tree = load_or_create_tree(target_path)
translated_count = 0
keys = list(missing.keys())
for i in range(0, len(keys), BATCH_SIZE):
chunk_keys = keys[i : i + BATCH_SIZE]
batch = {k: missing[k] for k in chunk_keys}
result = translate_batch(client, language, batch)
for name, text in result.items():
if name in batch: # sanity-check key came from our batch
set_translated(tree, name, text)
translated_count += 1
write_tree(tree, target_path)
print(f" ✅ {language}: {translated_count} strings written → {target_path}")
pr_lines.append(f"| {language} | {translated_count} | `{target_path}` |")
# Write PR body
pr_body = "\n".join(pr_lines) + "\n\n---\n*Generated automatically by translate_strings.py*\n"
Path(PR_BODY_PATH).write_text(pr_body, encoding="utf-8")
print(f"\n📄 PR body written to {PR_BODY_PATH}")
print("🎉 Translation run complete.")
if __name__ == "__main__":
main()