run method
Runs this command.
The return value is wrapped in a Future if necessary and returned by
CommandRunner.runCommand.
Implementation
@override
Future<int> run() async {
final currentDir = Directory.current.path;
final translationsDir = p.join(currentDir, 'lib', 'core', 'translations');
if (!Directory(translationsDir).existsSync()) {
logger.err('Translations directory not found at $translationsDir');
return ExitCode.noInput.code;
}
final sourceLang = argResults!['source'] as String;
final sourceFile = File(p.join(translationsDir, '$sourceLang.dart'));
final keysFile = File(p.join(translationsDir, 'translation_keys.dart'));
if (!sourceFile.existsSync() || !keysFile.existsSync()) {
logger.err(
'Reference files ($sourceLang.dart or translation_keys.dart) not found.',
);
return ExitCode.noInput.code;
}
final translationFiles = Directory(translationsDir)
.listSync()
.whereType<File>()
.where(
(file) =>
p.basename(file.path).endsWith('.dart') &&
p.basename(file.path) != '$sourceLang.dart' &&
p.basename(file.path) != 'translation_keys.dart' &&
p.basename(file.path) != 'app_translations.dart' &&
p.basename(file.path) != 'translations.dart',
)
.toList();
if (translationFiles.isEmpty) {
logger.info('No target translation files found to update.');
return ExitCode.success.code;
}
logger.info(
'Found ${translationFiles.length} translation files to update.',
);
// Read reference files once
final sourceContent = sourceFile.readAsStringSync();
for (final file in translationFiles) {
final fileName = p.basename(file.path);
final language = fileName.replaceAll('.dart', '').toUpperCase();
logger.info('Translating for $language ($fileName)...');
final targetContent = file.readAsStringSync();
final prompt =
'''
You are a Flutter localization expert.
I need to update the '$fileName' file for language code '$language'.
Reference '$sourceLang.dart' (Source of Truth):
```dart
$sourceContent
```
Current '$fileName' (Target):
```dart
$targetContent
```
Task:
1. Update '$fileName' so it contains all keys found in '$sourceLang.dart'.
2. Translate any missing keys from $sourceLang to $language.
3. Keep existing translations in '$fileName' if the keys still exist in '$sourceLang.dart'.
4. Remove keys in '$fileName' that are no longer in '$sourceLang.dart'.
5. Output ONLY the complete Dart code for '$fileName'.
6. Do NOT use markdown code blocks, just raw code.
''';
// Execute GEMINI CLI
// We use 'gemini -p' (no -y needed as we don't want it to run actions, just output text)
// Actually, we keep -y and --no-telemetry for consistency, but we capture stdout.
// Get model from env or use default 'gemini-flash-latest' as it is a valid, working model name.
final model =
Platform.environment['GEMINI_MODEL'] ?? 'gemini-flash-latest';
int attempts = 0;
bool success = false;
while (attempts < 3 && !success) {
if (attempts > 0) {
logger.info('Retrying... (Attempt ${attempts + 1})');
await Future.delayed(Duration(seconds: 2 * attempts));
}
final result = await Process.run('gemini', ['-m', model, '-p', prompt]);
if (result.exitCode == 0) {
final generatedCode = result.stdout.toString().trim();
// Basic cleanup if Gemini returns markdown code blocks despite instructions
final cleanCode = generatedCode
.replaceAll('```dart', '')
.replaceAll('```', '')
.trim();
if (cleanCode.isNotEmpty && cleanCode.startsWith('import')) {
file.writeAsStringSync(cleanCode);
logger.success('Successfully updated translations for $language.');
success = true;
} else {
logger.warn(
'Gemini returned invalid or empty code for $language. Output:\n$generatedCode',
);
// Consider this a failure?
attempts++;
}
} else {
final stderr = result.stderr.toString();
if (stderr.contains('429') || stderr.contains('RESOURCE_EXHAUSTED')) {
logger.warn(
'Rate limit exceeded for $language. Waiting before retry...',
);
attempts++;
} else {
logger.err('Failed to translate for $language: $stderr');
break; // Non-retriable error
}
}
}
}
return ExitCode.success.code;
}