dev
dart 285 lines 9.27 KB
Raw
1 import 'dart:convert';
2 import 'dart:io';
3 import 'package:http/http.dart' as http; // very_insecure_http_do_not_use
4 import 'package:args/args.dart';
5 import './print_verbose_dummy.dart';
6
7 class TranslationChecker {
8 final String ollamaBaseUrl;
9 final String model;
10
11 TranslationChecker({
12 required this.ollamaBaseUrl,
13 required this.model,
14 });
15
16 /// Check and correct translations line by line
17 Future<void> checkTranslations({
18 required String sourceArbPath,
19 required String destinationArbPath,
20 String? specificKey,
21 }) async {
22 final sourceContent = await _readArbFile(sourceArbPath);
23 final destinationContent = await _readArbFile(destinationArbPath);
24
25 final sourceMap = json.decode(sourceContent) as Map<String, dynamic>;
26 final destinationMap = json.decode(destinationContent) as Map<String, dynamic>;
27
28 print('Found ${sourceMap.length} keys in source, ${destinationMap.length} in destination');
29
30 final keysToProcess = specificKey != null
31 ? (sourceMap.containsKey(specificKey) ? [specificKey] : <String>[])
32 : sourceMap.keys.toList();
33
34 if (specificKey != null && keysToProcess.isEmpty) {
35 print('Error: Key "$specificKey" not found in source file');
36 return;
37 }
38
39 print('Processing ${keysToProcess.length} translations...');
40
41 int processed = 0;
42 int corrected = 0;
43
44 for (final key in keysToProcess) {
45 final sourceValue = sourceMap[key];
46 final destinationValue = destinationMap[key];
47
48 if (sourceValue is String && destinationValue is String) {
49 final correctedTranslation = await _checkSingleTranslation(
50 key: key,
51 sourceText: sourceValue,
52 currentTranslation: destinationValue,
53 sourceFile: sourceArbPath,
54 destinationFile: destinationArbPath,
55 );
56
57 if (correctedTranslation != destinationValue) {
58 destinationMap[key] = correctedTranslation;
59 corrected++;
60 print('Processed: "$key" -> CORRECTED');
61 print(' - eng : "$sourceValue"');
62 print(' - dst orig: "$destinationValue"');
63 print(' - dst new : "$correctedTranslation"');
64 await _writeArbFile(destinationArbPath, destinationMap);
65 } else {
66 print('Processed: "$key" -> VERIFIED');
67 }
68 } else {
69 print('Processed: "$key" -> SKIPPED (non-string)');
70 }
71
72 processed++;
73 }
74
75 await _writeArbFile(destinationArbPath, destinationMap);
76
77 print('');
78 print('Summary:');
79 print('Processed: $processed keys');
80 print('Corrected: $corrected keys');
81 print('Updated: $destinationArbPath');
82 }
83
84 Future<String> _checkSingleTranslation({
85 required String key,
86 required String sourceText,
87 required String currentTranslation,
88 required String sourceFile,
89 required String destinationFile,
90 }) async {
91 final prompt = _createTranslationCheckPrompt(
92 sourceFile: sourceFile,
93 destinationFile: destinationFile,
94 key: key,
95 sourceText: sourceText,
96 currentTranslation: currentTranslation,
97 );
98
99 try {
100 final response = await _callLLM(prompt);
101
102 // Extract JSON from the response
103 final correctedTranslation = _extractJsonFromResponse(response);
104
105 if (correctedTranslation != null && correctedTranslation != currentTranslation) {
106 return correctedTranslation;
107 }
108
109 return currentTranslation;
110 } catch (e) {
111 print('Error checking translation for key "$key": $e');
112 return currentTranslation;
113 }
114 }
115
116 String _createTranslationCheckPrompt({
117 required String key,
118 required String sourceText,
119 required String currentTranslation,
120 required String sourceFile,
121 required String destinationFile,
122 }) {
123 return '''
124 You are a professional translator checking the accuracy of a translation for a cryptocurrency wallet called Cake Wallet.
125
126 source file is: ${sourceFile}, destination file is: ${destinationFile}.
127
128 Rules you must obey:
129 - respect company branding and style (Cake Wallet, Cake Pay, Bird Pay and other brandings stay the same no matter the language)
130 - be accurate and remember that Cake Wallet lets you hold cryptocurrency, so use words that are associated with cryptocurrency and wallets more preferably than words that are associated with banks.
131 - elements with \${value} should remain non-translated as they are used by codegen to dynamically insert the value.
132 - make sure that the translation that you output improves the original translation, and doesn't change the meaning of it.
133 - if original translation contains abbreviations, make sure to translate them as well, and do not expand them.
134 - Make sure to use the same capitalization as the original translation, if it is capitalized, capitalize the translation as well to keep the UI consistent.
135 - maintain new lines with `\n` preserved in the translation.
136
137
138 KEY: "$key"
139 SOURCE TEXT (English): "${sourceText.replaceAll("\n", "\\n")}"
140 CURRENT TRANSLATION: "${currentTranslation.replaceAll("\n", "\\n")}"
141
142 Please check if the current translation is accurate and natural. If it needs correction, provide the corrected version.
143
144 Before you return the JSON object, think about the translation and make sure that it is accurate and natural.
145
146 IMPORTANT: Respond with a JSON object in the exact format, you must think about the translation and at the very end you must return the JSON object with the most correct (in destination language) translation, make sure to use proper grammar, spelling and punctuation:
147 {
148 "corrected_translation": "your corrected translation here"
149 }
150 ''';
151 }
152
153 Future<String> _callLLM(String prompt) async {
154 final headers = {
155 'Content-Type': 'application/json',
156 };
157
158 final body = json.encode({
159 'model': model,
160 'prompt': prompt,
161 'stream': false,
162 'options': {
163 'temperature': 0.3,
164 'num_predict': 1000,
165 }
166 });
167
168 final response = await http.post(
169 Uri.parse('$ollamaBaseUrl/api/generate'),
170 headers: headers,
171 body: body,
172 );
173
174 if (response.statusCode != 200) {
175 throw Exception('Ollama API error: ${response.statusCode} - ${response.body}');
176 }
177
178 final data = json.decode(response.body);
179 return data['response'] as String;
180 }
181
182 String? _extractJsonFromResponse(String response) {
183 try {
184 // Find JSON object in the response
185 final jsonStart = response.indexOf('{');
186 final jsonEnd = response.lastIndexOf('}');
187
188 if (jsonStart == -1 || jsonEnd == -1 || jsonStart >= jsonEnd) {
189 print('No valid JSON found in LLM response');
190 return null;
191 }
192
193 final jsonText = response.substring(jsonStart, jsonEnd + 1);
194 final parsed = json.decode(jsonText) as Map<String, dynamic>;
195
196 return parsed['corrected_translation'] as String?;
197 } catch (e) {
198 print('Error extracting JSON from LLM response: $e');
199 return null;
200 }
201 }
202
203 Future<String> _readArbFile(String path) async {
204 final file = File(path);
205 if (!file.existsSync()) {
206 throw Exception('ARB file not found: $path');
207 }
208 return await file.readAsString();
209 }
210
211 Future<void> _writeArbFile(String path, Map<String, dynamic> content) async {
212 final file = File(path);
213 final prettyJson = const JsonEncoder.withIndent(' ').convert(content);
214 await file.writeAsString(prettyJson, flush: true);
215 }
216 }
217
218 void main(List<String> args) async {
219 final parser = ArgParser()
220 ..addOption('source',
221 abbr: 's',
222 help: 'Path to source ARB file (e.g., ./res/values/strings_en.arb)',
223 mandatory: true)
224 ..addOption('destination',
225 abbr: 'd',
226 help: 'Path to destination ARB file (e.g., ./res/values/strings_pl.arb)',
227 mandatory: true)
228 ..addOption('key', abbr: 'k', help: 'Translate only this specific key (optional)')
229 ..addOption('ollama-url',
230 help: 'Ollama server URL (default: http://localhost:11434)',
231 defaultsTo: 'http://localhost:11434')
232 ..addOption('model',
233 help: 'Ollama model name (default: gpt-oss:120b)', defaultsTo: 'gpt-oss:120b')
234 ..addFlag('help', abbr: 'h', help: 'Show this help message', negatable: false);
235
236 try {
237 final results = parser.parse(args);
238
239 if (results['help'] as bool) {
240 print(parser.usage);
241 return;
242 }
243
244 final sourcePath = results['source'] as String;
245 final destinationPath = results['destination'] as String;
246 final specificKey = results['key'] as String?;
247 final ollamaUrl = results['ollama-url'] as String;
248 final model = results['model'] as String;
249
250 if (!File(sourcePath).existsSync()) {
251 print('Error: Source ARB file not found: $sourcePath');
252 exit(1);
253 }
254
255 if (!File(destinationPath).existsSync()) {
256 print('Error: Destination ARB file not found: $destinationPath');
257 exit(1);
258 }
259
260 print('Translation Checker');
261 print('Source: $sourcePath');
262 print('Destination: $destinationPath');
263 print('Ollama URL: $ollamaUrl');
264 print('Model: $model');
265 if (specificKey != null) {
266 print('Key: $specificKey');
267 }
268 print('');
269
270 final checker = TranslationChecker(
271 ollamaBaseUrl: ollamaUrl,
272 model: model,
273 );
274
275 await checker.checkTranslations(
276 sourceArbPath: sourcePath,
277 destinationArbPath: destinationPath,
278 specificKey: specificKey,
279 );
280 } catch (e) {
281 print('Error: $e');
282 print('Run with --help for more information.');
283 exit(1);
284 }
285 }