android-localisation 1.0.3__tar.gz → 1.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {android_localisation-1.0.3/android_localisation.egg-info → android_localisation-1.0.4}/PKG-INFO +1 -1
- {android_localisation-1.0.3 → android_localisation-1.0.4}/android_localisation/__init__.py +1 -1
- {android_localisation-1.0.3 → android_localisation-1.0.4}/android_localisation/fix.py +5 -1
- android_localisation-1.0.4/android_localisation/java/VerifyStrings.java +219 -0
- {android_localisation-1.0.3 → android_localisation-1.0.4}/android_localisation/translate.py +26 -13
- {android_localisation-1.0.3 → android_localisation-1.0.4/android_localisation.egg-info}/PKG-INFO +1 -1
- {android_localisation-1.0.3 → android_localisation-1.0.4}/pyproject.toml +1 -1
- android_localisation-1.0.3/android_localisation/java/VerifyStrings.java +0 -148
- {android_localisation-1.0.3 → android_localisation-1.0.4}/LICENSE +0 -0
- {android_localisation-1.0.3 → android_localisation-1.0.4}/MANIFEST.in +0 -0
- {android_localisation-1.0.3 → android_localisation-1.0.4}/README.md +0 -0
- {android_localisation-1.0.3 → android_localisation-1.0.4}/android_localisation/cli.py +0 -0
- {android_localisation-1.0.3 → android_localisation-1.0.4}/android_localisation/verify.py +0 -0
- {android_localisation-1.0.3 → android_localisation-1.0.4}/android_localisation.egg-info/SOURCES.txt +0 -0
- {android_localisation-1.0.3 → android_localisation-1.0.4}/android_localisation.egg-info/dependency_links.txt +0 -0
- {android_localisation-1.0.3 → android_localisation-1.0.4}/android_localisation.egg-info/entry_points.txt +0 -0
- {android_localisation-1.0.3 → android_localisation-1.0.4}/android_localisation.egg-info/top_level.txt +0 -0
- {android_localisation-1.0.3 → android_localisation-1.0.4}/setup.cfg +0 -0
{android_localisation-1.0.3/android_localisation.egg-info → android_localisation-1.0.4}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: android-localisation
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Zero-dependency Android strings.xml translation and verification using LLMs (Gemini, OpenAI, Anthropic, Ollama).
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/BharathKmalviya/android-llm-localization
|
|
@@ -65,7 +65,11 @@ def main(args=None):
|
|
|
65
65
|
content = f.read()
|
|
66
66
|
|
|
67
67
|
def fix_match(m):
|
|
68
|
-
|
|
68
|
+
opening_tag = m.group(1)
|
|
69
|
+
# Skip strings marked formatted="false" — their % signs are literal, not specifiers
|
|
70
|
+
if 'formatted="false"' in opening_tag.lower() or "formatted='false'" in opening_tag.lower():
|
|
71
|
+
return m.group(0)
|
|
72
|
+
return opening_tag + _fix_text(m.group(2)) + m.group(3)
|
|
69
73
|
|
|
70
74
|
new_content = re.sub(
|
|
71
75
|
r'(<string[^>]*name="[^"]*"[^>]*>)(.*?)(</string>)',
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import java.io.*;
|
|
2
|
+
import java.util.*;
|
|
3
|
+
import java.util.regex.*;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Utility to verify that all translated Android strings containing format specifiers
|
|
7
|
+
* (like %1$d or %s) are syntactically valid and won't throw an UnknownFormatConversionException
|
|
8
|
+
* or MissingFormatArgumentException at runtime.
|
|
9
|
+
*/
|
|
10
|
+
public class VerifyStrings {
|
|
11
|
+
|
|
12
|
+
public static void main(String[] args) throws Exception {
|
|
13
|
+
String resDirPath = "app/src/main/res";
|
|
14
|
+
if (args.length > 0) {
|
|
15
|
+
resDirPath = args[0];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
File resDir = new File(resDirPath);
|
|
19
|
+
if (!resDir.exists()) {
|
|
20
|
+
File fallback = new File("../" + resDirPath);
|
|
21
|
+
if (fallback.exists()) resDir = fallback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!resDir.exists()) {
|
|
25
|
+
System.err.println("Fatal: Could not find resource directory at: " + resDirPath);
|
|
26
|
+
System.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
File englishDir = new File(resDir, "values");
|
|
30
|
+
File[] valuesDirs = resDir.listFiles((dir, name) -> name.startsWith("values-"));
|
|
31
|
+
|
|
32
|
+
if (valuesDirs == null || valuesDirs.length == 0) {
|
|
33
|
+
System.out.println("No values-* localized directories found.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
System.out.println("Started Android String Format Verification...");
|
|
38
|
+
System.out.println("Scanning source strings in: " + englishDir.getPath());
|
|
39
|
+
|
|
40
|
+
// Parse English strings
|
|
41
|
+
Map<String, String> englishStrings = parseStringsFile(new File(englishDir, "strings.xml"));
|
|
42
|
+
Set<String> nonFormattedKeys = getNonFormattedKeys(new File(englishDir, "strings.xml"));
|
|
43
|
+
|
|
44
|
+
// Build expected arg type list per key from English strings
|
|
45
|
+
// e.g. "User %1$s has %2$d items at $%3$.2f" -> ['s', 'd', 'f']
|
|
46
|
+
Map<String, List<Character>> expectedArgTypes = new HashMap<>();
|
|
47
|
+
Pattern formatPattern = Pattern.compile("%(\\d+\\$)?([-#+ 0,(<]*)?(\\d+)?(\\.\\d+)?([tT]?[a-zA-Z])");
|
|
48
|
+
|
|
49
|
+
for (Map.Entry<String, String> entry : englishStrings.entrySet()) {
|
|
50
|
+
if (nonFormattedKeys.contains(entry.getKey())) continue;
|
|
51
|
+
|
|
52
|
+
Map<Integer, Character> indexedTypes = new TreeMap<>();
|
|
53
|
+
int autoIndex = 1;
|
|
54
|
+
// Strip %% (escaped percent) before scanning — otherwise the second % followed by a
|
|
55
|
+
// space-flag and a letter (e.g. "%% completed") gets misread as a format specifier.
|
|
56
|
+
String valueToScan = entry.getValue().replace("%%", "");
|
|
57
|
+
Matcher m = formatPattern.matcher(valueToScan);
|
|
58
|
+
|
|
59
|
+
while (m.find()) {
|
|
60
|
+
String convStr = m.group(5);
|
|
61
|
+
if (convStr == null || convStr.equals("%") || convStr.equals("n")) continue;
|
|
62
|
+
char conv = convStr.charAt(convStr.length() - 1);
|
|
63
|
+
String indexGroup = m.group(1);
|
|
64
|
+
if (indexGroup != null) {
|
|
65
|
+
int idx = Integer.parseInt(indexGroup.replace("$", ""));
|
|
66
|
+
indexedTypes.put(idx, conv);
|
|
67
|
+
} else {
|
|
68
|
+
indexedTypes.put(autoIndex++, conv);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!indexedTypes.isEmpty()) {
|
|
73
|
+
List<Character> orderedTypes = new ArrayList<>();
|
|
74
|
+
int maxIdx = Collections.max(indexedTypes.keySet());
|
|
75
|
+
for (int i = 1; i <= maxIdx; i++) {
|
|
76
|
+
orderedTypes.add(indexedTypes.getOrDefault(i, 's'));
|
|
77
|
+
}
|
|
78
|
+
expectedArgTypes.put(entry.getKey(), orderedTypes);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
int totalErrors = 0;
|
|
83
|
+
int totalStringsVerified = 0;
|
|
84
|
+
|
|
85
|
+
for (File dir : valuesDirs) {
|
|
86
|
+
File stringsFile = new File(dir, "strings.xml");
|
|
87
|
+
if (!stringsFile.exists()) continue;
|
|
88
|
+
|
|
89
|
+
Map<String, String> localizedStrings = parseStringsFile(stringsFile);
|
|
90
|
+
Set<String> localNonFormatted = getNonFormattedKeys(stringsFile);
|
|
91
|
+
|
|
92
|
+
for (Map.Entry<String, String> entry : localizedStrings.entrySet()) {
|
|
93
|
+
String name = entry.getKey();
|
|
94
|
+
String text = entry.getValue();
|
|
95
|
+
totalStringsVerified++;
|
|
96
|
+
|
|
97
|
+
// Skip strings explicitly marked as non-formatted in either English or translation
|
|
98
|
+
if (nonFormattedKeys.contains(name) || localNonFormatted.contains(name)) continue;
|
|
99
|
+
|
|
100
|
+
List<Character> argTypes = expectedArgTypes.get(name);
|
|
101
|
+
if (argTypes == null || argTypes.isEmpty()) {
|
|
102
|
+
// No format args expected — a bare unescaped % is an error
|
|
103
|
+
String stripped = text.replace("%%", "");
|
|
104
|
+
if (stripped.contains("%")) {
|
|
105
|
+
System.err.println("ERROR: [" + dir.getName() + "] String '" + name
|
|
106
|
+
+ "' contains unescaped % but English has no format args.");
|
|
107
|
+
System.err.println(" Text: " + text);
|
|
108
|
+
totalErrors++;
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Build correctly-typed args array based on what English string expects
|
|
114
|
+
Object[] formatArgs = buildArgs(argTypes);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
String.format(text, formatArgs);
|
|
118
|
+
} catch (UnknownFormatConversionException e) {
|
|
119
|
+
System.err.println("CRASH: [" + dir.getName() + "] String '" + name
|
|
120
|
+
+ "' has an invalid conversion specifier: " + e.getConversion());
|
|
121
|
+
System.err.println(" Text: " + text);
|
|
122
|
+
totalErrors++;
|
|
123
|
+
} catch (MissingFormatArgumentException e) {
|
|
124
|
+
System.err.println("CRASH: [" + dir.getName() + "] String '" + name
|
|
125
|
+
+ "' is missing format arguments or index is wrong: " + e.getFormatSpecifier());
|
|
126
|
+
System.err.println(" Text: " + text);
|
|
127
|
+
totalErrors++;
|
|
128
|
+
} catch (IllegalFormatConversionException e) {
|
|
129
|
+
System.err.println("CRASH: [" + dir.getName() + "] String '" + name
|
|
130
|
+
+ "' uses wrong type for a format specifier: " + e.getMessage());
|
|
131
|
+
System.err.println(" Text: " + text);
|
|
132
|
+
System.err.println(" English: " + englishStrings.get(name));
|
|
133
|
+
totalErrors++;
|
|
134
|
+
} catch (Exception e) {
|
|
135
|
+
System.err.println("CRASH: [" + dir.getName() + "] String '" + name
|
|
136
|
+
+ "' failed String.format() -> " + e.getClass().getSimpleName() + ": " + e.getMessage());
|
|
137
|
+
System.err.println(" Text: " + text);
|
|
138
|
+
totalErrors++;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
System.out.println("Verification Complete!");
|
|
144
|
+
System.out.println("Total Strings Verified: " + totalStringsVerified);
|
|
145
|
+
|
|
146
|
+
if (totalErrors > 0) {
|
|
147
|
+
System.out.println("Total Errors Found: " + totalErrors);
|
|
148
|
+
System.err.println("Verification Failed!");
|
|
149
|
+
System.exit(1);
|
|
150
|
+
} else {
|
|
151
|
+
System.out.println("Status: ALL STRINGS PASSED!");
|
|
152
|
+
System.exit(0);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Builds an Object[] matching the arg types detected from the English string.
|
|
158
|
+
* 's' -> String, 'd'/'i'/'o'/'x' -> Integer, 'f'/'e'/'g'/'a' -> Double, 'b' -> Boolean, 'c' -> Character
|
|
159
|
+
*/
|
|
160
|
+
private static Object[] buildArgs(List<Character> argTypes) {
|
|
161
|
+
Object[] args = new Object[argTypes.size()];
|
|
162
|
+
for (int i = 0; i < argTypes.size(); i++) {
|
|
163
|
+
char type = Character.toLowerCase(argTypes.get(i));
|
|
164
|
+
switch (type) {
|
|
165
|
+
case 'd': case 'i': case 'o': case 'x': args[i] = 42; break;
|
|
166
|
+
case 'f': case 'e': case 'g': case 'a': args[i] = 3.14; break;
|
|
167
|
+
case 'b': args[i] = true; break;
|
|
168
|
+
case 'c': args[i] = 'A'; break;
|
|
169
|
+
default: args[i] = "test"; break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return args;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns keys that have formatted="false" — their % signs are literal, not format specifiers.
|
|
177
|
+
*/
|
|
178
|
+
private static Set<String> getNonFormattedKeys(File file) throws Exception {
|
|
179
|
+
Set<String> keys = new HashSet<>();
|
|
180
|
+
if (!file.exists()) return keys;
|
|
181
|
+
String content = readFile(file);
|
|
182
|
+
// Match both attribute orderings
|
|
183
|
+
Pattern p1 = Pattern.compile("<string[^>]+name=\"([^\"]+)\"[^>]+formatted\\s*=\\s*\"false\"",
|
|
184
|
+
Pattern.CASE_INSENSITIVE);
|
|
185
|
+
Pattern p2 = Pattern.compile("<string[^>]+formatted\\s*=\\s*\"false\"[^>]+name=\"([^\"]+)\"",
|
|
186
|
+
Pattern.CASE_INSENSITIVE);
|
|
187
|
+
for (Pattern p : new Pattern[]{p1, p2}) {
|
|
188
|
+
Matcher m = p.matcher(content);
|
|
189
|
+
while (m.find()) keys.add(m.group(1));
|
|
190
|
+
}
|
|
191
|
+
return keys;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private static Map<String, String> parseStringsFile(File file) throws Exception {
|
|
195
|
+
Map<String, String> strings = new LinkedHashMap<>();
|
|
196
|
+
if (!file.exists()) return strings;
|
|
197
|
+
String content = readFile(file);
|
|
198
|
+
Pattern stringPattern = Pattern.compile(
|
|
199
|
+
"<string\\s+[^>]*name=\"([^\"]+)\"[^>]*>(.*?)</string>", Pattern.DOTALL);
|
|
200
|
+
Matcher m = stringPattern.matcher(content);
|
|
201
|
+
while (m.find()) {
|
|
202
|
+
String name = m.group(1);
|
|
203
|
+
String text = m.group(2);
|
|
204
|
+
text = text.replace("<", "<").replace(">", ">").replace("&", "&");
|
|
205
|
+
text = text.replace("\\'", "'").replace("\\\"", "\"");
|
|
206
|
+
strings.put(name, text);
|
|
207
|
+
}
|
|
208
|
+
return strings;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private static String readFile(File file) throws Exception {
|
|
212
|
+
BufferedReader reader = new BufferedReader(new FileReader(file));
|
|
213
|
+
StringBuilder sb = new StringBuilder();
|
|
214
|
+
String line;
|
|
215
|
+
while ((line = reader.readLine()) != null) sb.append(line).append("\n");
|
|
216
|
+
reader.close();
|
|
217
|
+
return sb.toString();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import re
|
|
2
3
|
import time
|
|
3
4
|
import argparse
|
|
4
5
|
import urllib.request
|
|
@@ -70,19 +71,24 @@ def read_source_xml(source_path):
|
|
|
70
71
|
|
|
71
72
|
def build_prompt(source_xml, target_folder_name, app_context):
|
|
72
73
|
context_str = f"an Android app ({app_context})" if app_context else "an Android application"
|
|
73
|
-
return f"""You are a professional
|
|
74
|
+
return f"""You are a professional Android localization expert.
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
For example, `values-hi` is Hindi, `values-es-rES` is Spanish (Spain), `values-zh-rTW` is Traditional Chinese, etc.
|
|
76
|
+
Translate the English `strings.xml` below for {context_str} into the language for Android resource directory: `{target_folder_name}`.
|
|
77
|
+
For example, `values-hi` is Hindi, `values-es-rES` is Spanish (Spain), `values-zh-rTW` is Traditional Chinese, `values-ar` is Arabic, etc.
|
|
78
78
|
|
|
79
79
|
STRICT GUIDELINES:
|
|
80
|
-
1.
|
|
81
|
-
2.
|
|
82
|
-
3.
|
|
83
|
-
4.
|
|
84
|
-
5.
|
|
85
|
-
6.
|
|
80
|
+
1. Translate only the string values — never the keys, XML tags, or attributes.
|
|
81
|
+
2. Use natural, human-sounding language. Simple everyday mobile UI tone. Not robotic or word-for-word.
|
|
82
|
+
3. Preserve ALL placeholders exactly as-is: %s, %d, %1$s, %1$d, %2$s, etc.
|
|
83
|
+
4. Preserve ALL escape sequences exactly as-is: \\n, \\', \\", \\\\.
|
|
84
|
+
5. Preserve ALL HTML tags exactly as-is: <b>, <i>, <u>, <br/>, etc.
|
|
85
|
+
6. Apostrophes in translated text MUST be escaped as \\' — never use a raw ' or a curly apostrophe.
|
|
86
|
+
7. The output MUST use standard Android strings.xml format:
|
|
87
|
+
- Start with: <?xml version="1.0" encoding="utf-8"?>
|
|
88
|
+
- Use plain <resources> with NO namespace attributes (no xmlns:xliff or any other xmlns)
|
|
89
|
+
- Every string on its own line: <string name="key">translated value</string>
|
|
90
|
+
- No CDATA, no extra attributes on <string> tags except name and translatable
|
|
91
|
+
8. Return ONLY the raw XML. No markdown, no code fences, no explanation.
|
|
86
92
|
|
|
87
93
|
SOURCE XML:
|
|
88
94
|
{source_xml}
|
|
@@ -93,13 +99,20 @@ def clean_xml_response(result):
|
|
|
93
99
|
if not result:
|
|
94
100
|
return ""
|
|
95
101
|
result = result.strip()
|
|
102
|
+
|
|
103
|
+
# Strip markdown code fences
|
|
96
104
|
if result.startswith("```xml"):
|
|
97
105
|
result = result[6:]
|
|
98
106
|
if result.startswith("```"):
|
|
99
107
|
result = result[3:]
|
|
100
108
|
if result.endswith("```"):
|
|
101
109
|
result = result[:-3]
|
|
102
|
-
|
|
110
|
+
result = result.strip()
|
|
111
|
+
|
|
112
|
+
# Strip any xmlns namespace attributes from <resources> tag — not valid in standard Android strings.xml
|
|
113
|
+
result = re.sub(r'(<resources)\s+[^>]*?(>)', r'\1\2', result)
|
|
114
|
+
|
|
115
|
+
return result
|
|
103
116
|
|
|
104
117
|
|
|
105
118
|
def _read_error_body(e):
|
|
@@ -323,7 +336,7 @@ def main(args=None):
|
|
|
323
336
|
folder, args.app_context, args.base_url, fallback_models
|
|
324
337
|
)
|
|
325
338
|
|
|
326
|
-
if translated_xml and "<resources
|
|
339
|
+
if translated_xml and "<resources" in translated_xml and "</resources>" in translated_xml:
|
|
327
340
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
328
341
|
with open(target_path, "w", encoding="utf-8") as f:
|
|
329
342
|
f.write(translated_xml)
|
|
@@ -333,7 +346,7 @@ def main(args=None):
|
|
|
333
346
|
else:
|
|
334
347
|
if translated_xml is None:
|
|
335
348
|
print(f"⚠️ [{folder}] API call failed — see error above. Skipping.")
|
|
336
|
-
elif "<resources
|
|
349
|
+
elif "<resources" not in translated_xml:
|
|
337
350
|
preview = translated_xml[:200].replace("\n", " ") if translated_xml else "(empty response)"
|
|
338
351
|
print(f"⚠️ [{folder}] Response missing <resources> tag. Skipping.")
|
|
339
352
|
print(f" Response preview: {preview}")
|
{android_localisation-1.0.3 → android_localisation-1.0.4/android_localisation.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: android-localisation
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Zero-dependency Android strings.xml translation and verification using LLMs (Gemini, OpenAI, Anthropic, Ollama).
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/BharathKmalviya/android-llm-localization
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "android-localisation"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.4"
|
|
8
8
|
description = "Zero-dependency Android strings.xml translation and verification using LLMs (Gemini, OpenAI, Anthropic, Ollama)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import java.io.*;
|
|
2
|
-
import java.util.*;
|
|
3
|
-
import java.util.regex.*;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Utility to verify that all translated Android strings containing format specifiers
|
|
7
|
-
* (like %1$d or %s) are syntactically valid and won't throw an UnknownFormatConversionException
|
|
8
|
-
* or MissingFormatArgumentException at runtime.
|
|
9
|
-
*/
|
|
10
|
-
public class VerifyStrings {
|
|
11
|
-
|
|
12
|
-
public static void main(String[] args) throws Exception {
|
|
13
|
-
String resDirPath = "app/src/main/res";
|
|
14
|
-
if (args.length > 0) {
|
|
15
|
-
resDirPath = args[0];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
File resDir = new File(resDirPath);
|
|
19
|
-
if (!resDir.exists()) {
|
|
20
|
-
// Try relative to scripts if the absolute/relative failed
|
|
21
|
-
File fallback = new File("../" + resDirPath);
|
|
22
|
-
if (fallback.exists()) {
|
|
23
|
-
resDir = fallback;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!resDir.exists()) {
|
|
28
|
-
System.err.println("Fatal: Could not find resource directory at: " + resDirPath);
|
|
29
|
-
System.exit(1);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
File englishDir = new File(resDir, "values");
|
|
33
|
-
File[] valuesDirs = resDir.listFiles((dir, name) -> name.startsWith("values-"));
|
|
34
|
-
|
|
35
|
-
if (valuesDirs == null || valuesDirs.length == 0) {
|
|
36
|
-
System.err.println("No values- localized directories found!");
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
System.out.println("Started Android String Format Verification...");
|
|
41
|
-
System.out.println("Scanning source strings in: " + englishDir.getPath());
|
|
42
|
-
|
|
43
|
-
// 1. First parse the English strings to know the expected format arguments
|
|
44
|
-
Map<String, String> englishStrings = parseStringsFile(new File(englishDir, "strings.xml"));
|
|
45
|
-
Map<String, List<String>> expectedFormats = new HashMap<>();
|
|
46
|
-
|
|
47
|
-
// Regex for Android / Java format specifiers
|
|
48
|
-
Pattern formatPattern = Pattern.compile("%(\\d+\\$)?([-#+ 0,(<]*)?(\\d+)?(\\.\\d+)?([tT]?[a-zA-Z%])");
|
|
49
|
-
for (Map.Entry<String, String> entry : englishStrings.entrySet()) {
|
|
50
|
-
List<String> formats = new ArrayList<>();
|
|
51
|
-
Matcher m = formatPattern.matcher(entry.getValue());
|
|
52
|
-
while (m.find()) {
|
|
53
|
-
formats.add(m.group());
|
|
54
|
-
}
|
|
55
|
-
expectedFormats.put(entry.getKey(), formats);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
int totalErrors = 0;
|
|
59
|
-
int totalStringsVerified = 0;
|
|
60
|
-
|
|
61
|
-
// 2. Now verify every language against Android String formatting rules
|
|
62
|
-
for (File dir : valuesDirs) {
|
|
63
|
-
File stringsFile = new File(dir, "strings.xml");
|
|
64
|
-
if (!stringsFile.exists()) continue;
|
|
65
|
-
|
|
66
|
-
Map<String, String> localizedStrings = parseStringsFile(stringsFile);
|
|
67
|
-
|
|
68
|
-
for (Map.Entry<String, String> entry : localizedStrings.entrySet()) {
|
|
69
|
-
String name = entry.getKey();
|
|
70
|
-
String text = entry.getValue();
|
|
71
|
-
totalStringsVerified++;
|
|
72
|
-
|
|
73
|
-
List<String> englishFormats = expectedFormats.get(name);
|
|
74
|
-
if (englishFormats == null || englishFormats.isEmpty()) {
|
|
75
|
-
// This string shouldn't have any format arguments.
|
|
76
|
-
// If it has a %, it must be %%
|
|
77
|
-
if (text.contains("%") && !text.matches("^(?:[^%]|%%)*$")) {
|
|
78
|
-
System.err.println("ERROR: [" + dir.getName() + "] String '" + name + "' contains unescaped % but English doesn't use formatting.");
|
|
79
|
-
System.err.println(" Text: " + text);
|
|
80
|
-
totalErrors++;
|
|
81
|
-
}
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Try to format it via java natively
|
|
86
|
-
try {
|
|
87
|
-
// Provide generic arguments to satisfy any index like %1$s, %1$d.
|
|
88
|
-
// Our app primarily uses integers and strings up to 3 parameters.
|
|
89
|
-
String.format(text, 100, "test_string", 50);
|
|
90
|
-
} catch (UnknownFormatConversionException e) {
|
|
91
|
-
System.err.println("CRASH: [" + dir.getName() + "] String '" + name + "' has an invalid conversion specifier: " + e.getConversion());
|
|
92
|
-
System.err.println(" Text: " + text);
|
|
93
|
-
totalErrors++;
|
|
94
|
-
} catch (MissingFormatArgumentException e) {
|
|
95
|
-
System.err.println("CRASH: [" + dir.getName() + "] String '" + name + "' is missing format arguments or index is wrong: " + e.getFormatSpecifier());
|
|
96
|
-
System.err.println(" Text: " + text);
|
|
97
|
-
totalErrors++;
|
|
98
|
-
} catch (IllegalFormatConversionException e) {
|
|
99
|
-
System.err.println("CRASH: [" + dir.getName() + "] String '" + name + "' uses wrong variable type logic (e.g. used %d instead of %s): " + e.getMessage());
|
|
100
|
-
System.err.println(" Text: " + text);
|
|
101
|
-
System.err.println(" English expected: " + englishStrings.get(name));
|
|
102
|
-
totalErrors++;
|
|
103
|
-
} catch (Exception e) {
|
|
104
|
-
System.err.println("CRASH: [" + dir.getName() + "] String '" + name + "' failed native String.format() -> " + e.getClass().getSimpleName() + ": " + e.getMessage());
|
|
105
|
-
System.err.println(" Text: " + text);
|
|
106
|
-
totalErrors++;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
System.out.println("Verification Complete!");
|
|
112
|
-
System.out.println("Total Strings Verified: " + totalStringsVerified);
|
|
113
|
-
|
|
114
|
-
if (totalErrors > 0) {
|
|
115
|
-
System.out.println("Total Errors Found: " + totalErrors);
|
|
116
|
-
System.err.println("Verification Failed!");
|
|
117
|
-
System.exit(1);
|
|
118
|
-
} else {
|
|
119
|
-
System.out.println("Status: ALL STRINGS PASSED!");
|
|
120
|
-
System.exit(0);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
private static Map<String, String> parseStringsFile(File file) throws Exception {
|
|
125
|
-
Map<String, String> strings = new HashMap<>();
|
|
126
|
-
BufferedReader reader = new BufferedReader(new FileReader(file));
|
|
127
|
-
StringBuilder sb = new StringBuilder();
|
|
128
|
-
String line;
|
|
129
|
-
while ((line = reader.readLine()) != null) {
|
|
130
|
-
sb.append(line).append("\n");
|
|
131
|
-
}
|
|
132
|
-
reader.close();
|
|
133
|
-
|
|
134
|
-
// This regex ensures we only grab actual text contents inside `<string>`
|
|
135
|
-
Pattern stringPattern = Pattern.compile("<string name=\"([^\"]+)\"[^>]*>(.*?)</string>", Pattern.DOTALL);
|
|
136
|
-
Matcher m = stringPattern.matcher(sb.toString());
|
|
137
|
-
while (m.find()) {
|
|
138
|
-
String name = m.group(1);
|
|
139
|
-
String text = m.group(2);
|
|
140
|
-
// Decode standard XML entity things that AAPT2 does automatically
|
|
141
|
-
text = text.replace("<", "<").replace(">", ">").replace("&", "&");
|
|
142
|
-
// Unescape apostrophes and quotes as Java expects clean strings
|
|
143
|
-
text = text.replace("\\'", "'").replace("\\\"", "\"");
|
|
144
|
-
strings.put(name, text);
|
|
145
|
-
}
|
|
146
|
-
return strings;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{android_localisation-1.0.3 → android_localisation-1.0.4}/android_localisation.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|