Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Ingco/7b88302d1e7d7faa774887b9a929dac6 to your computer and use it in GitHub Desktop.
Save Ingco/7b88302d1e7d7faa774887b9a929dac6 to your computer and use it in GitHub Desktop.
Extract language keys from your source code and merge them into existing translations.
/// This is inspired by intl_translation extract command https://github.com/dart-lang/intl_translation/blob/8a5e883d7fe07b0244adbaf3489ceb3b08385483/bin/extract_to_arb.dart
/// and is a slimed down rough version of it. Most of the code is from this package which is an excellent learning resource for anyone who wants
/// to play around with the dart analyzer.
/// This is a rough script, to extract localization keys for the easy_localization library. It will analyze the souce
/// code and find occurrences of tr and plural ( you could add other method names e.g. gender etc ) and extract the argument at index $argumentIndex
/// which should be the translation key. It then merges those keys to your current translated keys and spits the merged version where the
/// untranslated keys have a value of "MISSING".
/// Known issues
/// tr( isSomething ? "true_key" : "false_key", context ) -> will get this as key isSomething ? "true_key" : "false_key"
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
/// Example run
/// dart easy_localization_keys_extractor.dart <absolute path to director of source files> <absolute path to file of current translations> <method argument index>
/// date easy_localization_keys_extractor.dart "Users/user/project/lib" "Users/user/project/assets/langs/en.json" 1 > merged_en.json
///
/// Arguments
/// args[0]: directory of dart source files
/// args[1]: language json file containing current translations
/// args[2]: integer representing the index of the language key argument. e.g for easy_localization's
/// AppLocalizations.of(context).tr('key',) we should pass zero here
void main(List<String> args) async {
List<FileSystemEntity> dartFiles = await dirContents(Directory(args[0]));
Map<String, String> allMessages = Map<String, String>();
Map currentTranslations = json.decode(await (File(args[1])).readAsString());
int argumentIndex = args.length >= 3 ? int.parse(args[2]) : 0;
var extraction = new MessageExtraction(argumentIndex: argumentIndex);
for (var arg in dartFiles) {
var messages = extraction.parseFile(new File(arg.path), true);
allMessages.addAll(messages);
}
// merge new tr keys with existing translated ones
allMessages.forEach((key, value) {
if (!currentTranslations.containsKey(key)) {
currentTranslations[key] = value;
}
});
// write to file or print
print(json.encode(currentTranslations));
}
class MessageExtraction {
final int argumentIndex;
MessageExtraction({
this.argumentIndex = 0,
});
Map<String, String> parseFile(File file, [bool transformer = false]) {
String contents = file.readAsStringSync();
return parseContent(contents, file.path, transformer);
}
Map<String, String> parseContent(String fileContent, String filepath,
[bool transformer = false]) {
String contents = fileContent;
String origin = filepath;
CompilationUnit root;
// Optimization to avoid parsing files we're sure don't contain any messages.
if (contents.contains('tr') || contents.contains('plural')) {
// add here more validNames
root = _parseCompilationUnit(contents, origin);
} else {
return {};
}
var visitor = new MessageFindingVisitor(argumentIndex);
root.accept(visitor);
return visitor.messages;
}
CompilationUnit _parseCompilationUnit(String contents, String origin) {
var result = parseString(content: contents, throwIfDiagnostics: false);
if (result.errors.isNotEmpty) {
print("Error in parsing $origin, no messages extracted.");
throw ArgumentError('Parsing errors in $origin');
}
return result.unit;
}
}
class MessageFindingVisitor extends GeneralizingAstVisitor {
MessageFindingVisitor(this.argumentIndex);
final int argumentIndex;
final Map<String, String> messages = new Map<String, String>();
bool isMatch(MethodInvocation node) {
const validNames = const ["tr", "plural"]; // add here more validNames
return validNames.contains(node.methodName.name);
}
void visitMethodInvocation(MethodInvocation node) {
if (!isMatch(node)) return super.visitMethodInvocation(node);
addMessage(node);
}
void addMessage(MethodInvocation node) {
String trKey = node.argumentList.arguments[argumentIndex].toString();
// match 'key' and "key"
if ((trKey.startsWith("\"") || trKey.startsWith("'")) &&
(trKey.endsWith("\"") || trKey.endsWith("'"))) {
// It keeps phrases with: "..'s .." || "..'t .."
if (!trKey.startsWith("\"") && !trKey.endsWith("\"")) {
trKey = trKey.replaceAll("'", "");
}
trKey = trKey.replaceAll("\"", "");
trKey = trKey.replaceAll(" ", " ");
}
messages[trKey] = "MISSING";
}
}
Future<List<FileSystemEntity>> dirContents(Directory dir) {
var files = <FileSystemEntity>[];
var completer = Completer<List<FileSystemEntity>>();
var lister = dir.list(recursive: true);
lister.listen(
(file) => file.path.endsWith(".dart") ? files.add(file) : null,
// should also register onError
onDone: () => completer.complete(files),
);
return completer.future;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment