android-localisation 1.0.2__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.
Files changed (18) hide show
  1. {android_localisation-1.0.2/android_localisation.egg-info → android_localisation-1.0.4}/PKG-INFO +1 -1
  2. {android_localisation-1.0.2 → android_localisation-1.0.4}/android_localisation/__init__.py +1 -1
  3. {android_localisation-1.0.2 → android_localisation-1.0.4}/android_localisation/fix.py +5 -1
  4. android_localisation-1.0.4/android_localisation/java/VerifyStrings.java +219 -0
  5. {android_localisation-1.0.2 → android_localisation-1.0.4}/android_localisation/translate.py +33 -13
  6. {android_localisation-1.0.2 → android_localisation-1.0.4/android_localisation.egg-info}/PKG-INFO +1 -1
  7. {android_localisation-1.0.2 → android_localisation-1.0.4}/pyproject.toml +1 -1
  8. android_localisation-1.0.2/android_localisation/java/VerifyStrings.java +0 -148
  9. {android_localisation-1.0.2 → android_localisation-1.0.4}/LICENSE +0 -0
  10. {android_localisation-1.0.2 → android_localisation-1.0.4}/MANIFEST.in +0 -0
  11. {android_localisation-1.0.2 → android_localisation-1.0.4}/README.md +0 -0
  12. {android_localisation-1.0.2 → android_localisation-1.0.4}/android_localisation/cli.py +0 -0
  13. {android_localisation-1.0.2 → android_localisation-1.0.4}/android_localisation/verify.py +0 -0
  14. {android_localisation-1.0.2 → android_localisation-1.0.4}/android_localisation.egg-info/SOURCES.txt +0 -0
  15. {android_localisation-1.0.2 → android_localisation-1.0.4}/android_localisation.egg-info/dependency_links.txt +0 -0
  16. {android_localisation-1.0.2 → android_localisation-1.0.4}/android_localisation.egg-info/entry_points.txt +0 -0
  17. {android_localisation-1.0.2 → android_localisation-1.0.4}/android_localisation.egg-info/top_level.txt +0 -0
  18. {android_localisation-1.0.2 → android_localisation-1.0.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: android-localisation
3
- Version: 1.0.2
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
@@ -2,4 +2,4 @@
2
2
  android-localisation: Zero-dependency Android strings.xml translation using LLMs.
3
3
  """
4
4
 
5
- __version__ = "1.0.2"
5
+ __version__ = "1.0.4"
@@ -65,7 +65,11 @@ def main(args=None):
65
65
  content = f.read()
66
66
 
67
67
  def fix_match(m):
68
- return m.group(1) + _fix_text(m.group(2)) + m.group(3)
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("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&");
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 mobile app localization expert.
74
+ return f"""You are a professional Android localization expert.
74
75
 
75
- I am sending you the complete English `strings.xml` for {context_str}.
76
- Your job is to translate it into the language corresponding to the 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, 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. Preserve the exact meaning and intent of the English text.
81
- 2. The language must be clear, natural, human-sounding, and understandable by all users (from rural to tier-1 cities).
82
- 3. Do not sound like a machine translation. Use simple, everyday mobile UI language.
83
- 4. NEVER modify string keys, XML structure, placeholders (like %s, %1$d), escape characters, \\n line breaks, or HTML tags.
84
- 5. Keep it short and UI friendly.
85
- 6. Return ONLY the raw updated XML content. Do not add markdown formatting like ```xml or any conversational text.
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
- return result.strip()
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>" in translated_xml and "</resources>" in translated_xml:
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)
@@ -331,7 +344,14 @@ def main(args=None):
331
344
  action = "Created" if is_new_file else "Updated"
332
345
  print(f"✅ {action} {folder}/strings.xml{suffix}")
333
346
  else:
334
- print(f"⚠️ Failed or got invalid XML for {folder}. Skipping.")
347
+ if translated_xml is None:
348
+ print(f"⚠️ [{folder}] API call failed — see error above. Skipping.")
349
+ elif "<resources" not in translated_xml:
350
+ preview = translated_xml[:200].replace("\n", " ") if translated_xml else "(empty response)"
351
+ print(f"⚠️ [{folder}] Response missing <resources> tag. Skipping.")
352
+ print(f" Response preview: {preview}")
353
+ else:
354
+ print(f"⚠️ [{folder}] Response missing </resources> closing tag. Skipping.")
335
355
 
336
356
  if args.sleep > 0:
337
357
  time.sleep(args.sleep)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: android-localisation
3
- Version: 1.0.2
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.2"
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("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&");
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
- }