]> gitweb.hamatoma.de Git - exhibition.git/commitdiff
i18n_text_parser, dart_tools
authorHamatoma <author.hamatoma.de>
Tue, 10 Aug 2021 14:36:00 +0000 (16:36 +0200)
committerHamatoma <author.hamatoma.de>
Wed, 11 Aug 2021 09:15:27 +0000 (11:15 +0200)
* improvements in i18n_text_parser: subcommands "parse", "example" and "msg-init"
* yaml_merger and i18n_text_parser moved to directory dart_tools

24 files changed:
.gitignore
bin/generator.dart
bin/i18n_text_parser.dart [deleted file]
bin/meta_tool.dart
bin/yaml_merger.dart [deleted file]
dart_tools/bin/i18n_text_parser.dart [new file with mode: 0644]
dart_tools/bin/yaml_merger.dart [new file with mode: 0644]
dart_tools/pubspec.yaml [new file with mode: 0644]
dart_tools/tools/CompileI18n [new file with mode: 0755]
dart_tools/tools/CompileMerge [new file with mode: 0755]
data/i18n/!global.de_DE.po [new file with mode: 0644]
data/i18n/Users.de_DE.po [new file with mode: 0644]
data/i18n/project.i18n.yaml [new file with mode: 0644]
lib/base/i18n.dart
lib/base/i18n_io.dart [new file with mode: 0644]
lib/meta/modules.dart
lib/meta/roles_meta.dart
lib/meta/users_meta.dart
rest_server/data/sql/users.sql.yaml
test/i18n_test.dart [new file with mode: 0644]
test/i18n_text_parser_test.dart
test/yaml_merger_test.dart
tools/CompileMerge [deleted file]
tools/UpdateI18n [new file with mode: 0755]

index e75c7bdb5021119f4627828f974a64beee616dce..d3472d3cc6b04bcf47cd64e0212fdfdb2a38fc92 100644 (file)
@@ -52,3 +52,11 @@ linux/
 pubspec.lock
 tools/meta_tool
 tools/yaml_merger
+dart_tools/tools/i18n_text_parser
+dart_tools/tools/yaml_merger
+tools/i18n_text_parser
+data/i18n/*.pot
+data/i18n/exhibition.lokalize
+data/i18n/main.lqa
+*~
+data/i18n/lokalize-scripts/
index 4cfb2354d71ef1b9510c19493d94443f2041af3f..a6fa433495d672676b4c2c976b88b0ffb65411bd 100644 (file)
@@ -1,5 +1,6 @@
 import 'dart:io';
 
+import 'package:dart_bones/dart_bones.dart';
 import 'package:exhibition/meta/module_meta_data.dart';
 import 'package:exhibition/meta/modules.dart';
 import 'package:path/path.dart';
@@ -32,6 +33,8 @@ String filenameToClass(String filename) {
 }
 
 class Generator {
+  BaseLogger logger = globalLogger;
+  Generator(this.logger);
   /// Appends a [string] at the end of a [buffer].
   /// If [buffer] is null a new instance is created.
   /// If the last line in [buffer] has a length greater than [maxLength]
@@ -58,9 +61,9 @@ class Generator {
   ModuleMetaData? checkModule(List<String> args, int index) {
     ModuleMetaData? rc;
     if (index >= args.length) {
-      out('+++ too few arguments. Missing MODULE.');
+      logger.error('+++ too few arguments. Missing MODULE.');
     } else if ((rc = moduleByName(args[index])) == null) {
-      out('+++ unknown module: ${args[index]}');
+      logger.error('+++ unknown module: ${args[index]}');
       rc = null;
     }
     return rc;
@@ -290,7 +293,7 @@ update:
     if ((match = RegExp(r'class\s+(\w+)\s').firstMatch(contents)) != null) {
       rc = match!.group(1)!;
     } else {
-      print('+++ missing class in $node');
+      logger.error('+++ missing class in $node');
       rc = filenameToClass(node.replaceFirst('.dart', ''));
     }
     return rc;
@@ -298,7 +301,7 @@ update:
 
   /// Replaces print().
   void out(String line) {
-    stdout.write(line + '\n');
+    logger.log(line);
   }
   /// Generates all modules defined in the meta data.
   void updateModules(Generator generator) {
@@ -307,7 +310,7 @@ update:
     for (var name in modules) {
       ModuleMetaData? module = moduleByName(name);
       if (module == null) {
-        print('+++ unknown module: $name');
+        logger.error('+++ unknown module: $name');
       } else {
         String filename =
             classToFilename(module.moduleNameSingular) + '_data.dart';
@@ -323,7 +326,7 @@ update:
     for (var name in modules) {
       ModuleMetaData? module = moduleByName(name);
       if (module == null) {
-        print('+++ unknown module: $name');
+        logger.error('+++ unknown module: $name');
       } else {
         String filename = 'rest_server/data/sql/${name.toLowerCase()}.sql.yaml';
         writeFile(filename, generator.createSqlStatements(module));
@@ -332,7 +335,7 @@ update:
   }
 
   void writeFile(String filename, String contents) {
-    out('creating $filename ...');
+    logger.log('creating $filename ...');
     final file = File(filename);
     file.writeAsStringSync(contents);
   }
diff --git a/bin/i18n_text_parser.dart b/bin/i18n_text_parser.dart
deleted file mode 100644 (file)
index c8e56c8..0000000
+++ /dev/null
@@ -1,352 +0,0 @@
-import "dart:io";
-
-import 'package:dart_bones/dart_bones.dart';
-import "package:path/path.dart";
-
-void main(List<String> args) {}
-
-class I18nTextParser {
-  final globalModule = '!global';
-  final errorModule = '!error';
-  final BaseLogger logger;
-  final mapModules = <String, Map<String, String>>{};
-  final mapPlurals = <String, Map<String, PluralValue>>{};
-  final foundFiles = <String>{};
-  var regExpFiles = RegExp(r'.dart$');
-  String currentModule = '';
-  final moduleVariables = <String, String>{};
-  String currentFile = '';
-  String currentArguments = '';
-  List<String> lines = [];
-  int currentLineNo = 0;
-  int currentIxLines = 0;
-  // ...........................1.............1.........2
-  final regExpModule =
-      RegExp(r'''(\w+) = (I18N\(\)|i18N)\.module\(["'](.*?)['"]\);''');
-  final regExpDelimiter = RegExp(r'''["']''');
-  // ..........1.............1..2..............................2
-  final regExpText = RegExp(r'(i18N|I18N\(\))\.(tr|ntr|trMulti|trArgs)\(');
-  final regExpStringConstant = RegExp(r'''^\s*(r?)(["'])(.*?)\2''');
-  final regExpVariable = RegExp(r'^\w+');
-  final regExpEmptyString = RegExp(r'^\s*$');
-
-  RegExpMatch? currentMatch;
-
-  I18nTextParser(this.logger);
-
-  /// Handles the state if the [currentArguments] is empty: it will be updated
-  /// by the next line.
-  void handleEmptyArgument() {
-    if (regExpEmptyString.firstMatch(currentArguments) != null) {
-      currentArguments = currentIxLines >= lines.length
-          ? ''
-          : lines[currentIxLines++].trimLeft();
-    }
-  }
-
-  String handleModuleArgument() {
-    String module = globalModule;
-    if (hasComma()) {
-      handleEmptyArgument();
-      if (isStringConstant()) {
-        module = currentMatch!.group(3)!;
-      } else {
-        if ((currentMatch = regExpVariable.firstMatch(currentArguments)) !=
-            null) {
-          final variable = currentMatch!.group(0);
-          if (moduleVariables.containsKey(variable)) {
-            module = moduleVariables[variable]!;
-          } else {
-            logger.error(
-                '$currentFile-$currentLineNo: unknown module variable: $variable');
-            module = errorModule;
-          }
-        }
-      }
-    }
-    return module;
-  }
-
-  /// Gets the text (multiple lines) from the [currentArguments].
-  /// [currentArguments] is the string behind the 'trMulti('.
-  void handleMultiText() {
-    logger.error('not implemented: trMulti()');
-  }
-
-  /// Gets the text (multiple lines) from the [currentArguments] .
-  /// [currentArguments] is the string behind the 'trPlural('.
-  void handlePluralText() {
-    currentIxLines = currentLineNo;
-    handleEmptyArgument();
-    if (!isStringConstant()) {
-      logger.error(
-          '$currentFile-$currentLineNo: cannot analyse parameters of ntr().'
-          ' Missing first string constant.');
-    } else {
-      String key1 = currentMatch!.group(3)!;
-      if (!hasComma()) {
-        logger.error(
-            '$currentFile-$currentLineNo: cannot analyse parameters of ntr().'
-            ' Missing second string constant.');
-      } else {
-        handleEmptyArgument();
-        if (!isStringConstant()) {
-          logger.error(
-              '$currentFile-$currentLineNo: cannot analyse parameters of tr().'
-              ' Second argument is not a string constant.');
-        } else {
-          String key2 = currentMatch!.group(3)!;
-          String module = handleModuleArgument();
-          putPluralKey(key1, key2, module);
-        }
-      }
-    }
-  }
-
-  /// Gets the text (single line) from the [currentArguments] and store it.
-  /// [currentArguments] is the string behind the 'tr('.
-  void handleSimpleText() {
-    currentIxLines = currentLineNo;
-    handleEmptyArgument();
-    if (!isStringConstant()) {
-      logger.error(
-          '$currentFile-$currentLineNo: cannot analyse parameters of tr().'
-          ' Missing string constant.');
-    } else {
-      String module;
-      final key = currentMatch?.group(3);
-      module = handleModuleArgument();
-      putText(key!, module);
-    }
-  }
-
-  /// Gets the text (with placeholders) from the [currentArguments] and store it.
-  /// [currentArguments] is the string behind the 'trArgs('.
-  void handleTextWithArgs() {
-    currentIxLines = currentLineNo;
-    handleEmptyArgument();
-    if (!isStringConstant()) {
-      logger.error(
-          '$currentFile-$currentLineNo: cannot analyse parameters of trArgs().'
-              ' Missing string constant.');
-    } else {
-      String module;
-      final key = currentMatch?.group(3);
-      module = handleModuleArgument();
-      putText(key!, module);
-    }
-  }
-
-  /// Tests whether the [currentArguments] is preceded by a ','.
-  /// If true the comma is removed from the [currentArguments].
-  bool hasComma() {
-    final rc = currentArguments.startsWith(',');
-    if (rc) {
-      currentArguments = currentArguments.substring(1).trimLeft();
-    }
-    return rc;
-  }
-
-  /// Tests whether the [currentArguments] is preceded by a string constant.
-  /// If true the [currentMatch] contains the string content and the
-  /// string constant is removed from the [currentArguments].
-  bool isStringConstant() {
-    if ((currentMatch = regExpStringConstant.firstMatch(currentArguments)) !=
-        null) {
-      currentArguments =
-          currentArguments.substring(currentMatch!.end).trimLeft();
-    }
-    return currentMatch != null;
-  }
-
-  /// Stores the data of a plural text entry.
-  void putPluralKey(String key1, String key2, String module) {
-    if (!mapPlurals.containsKey(module)) {
-      mapPlurals[module] = <String, PluralValue>{};
-    }
-    String reference = '#: $currentFile:$currentLineNo';
-    if (module != errorModule && mapModules[module]?[key1] != null) {
-      logger.error('$currentFile-$currentLineNo: key already defined as normal '
-              'key. Modify key with the invisible tag <#1> or <#2> or ... at the '
-              'end to make it unique.\nFirst found in ' +
-          mapModules[module]![key1]!);
-      putText(key1, errorModule);
-    } else if (mapPlurals[module]!.containsKey(key1)) {
-      if (mapPlurals[module]![key1]!.keyPlural == key2) {
-        mapPlurals[module]![key1]!.references.add(reference);
-      } else {
-        logger.error('$currentFile-$currentLineNo: multiple definition of a '
-                'plural key but keyPlural is different. Modify key1 with the'
-                ' invisible tag <#1> or <#2> or ... at the end to make it '
-                'unique.\nFirst found in ' +
-            mapPlurals[module]![key1]!.references[0]);
-      }
-    } else {
-      mapPlurals[module]![key1] = PluralValue(key2, [reference]);
-    }
-  }
-
-  /// Stores the [key] in the [module] map with location info [filename] and
-  /// [lineNo].
-  void putText(String key, [String module = '']) {
-    if (module.isEmpty) {
-      module = currentModule;
-    }
-    if (!mapModules.containsKey(module)) {
-      mapModules[module] = <String, String>{};
-    }
-    if (module != errorModule && mapPlurals[module]?[key] != null) {
-      logger.error('$currentFile-$currentLineNo: key already defined as plural '
-              'key. Modify key with the invisible tag <#1> or <#2> or ... at '
-              'the end to make it unique.\nFirst found in ' +
-          mapPlurals[module]![key]!.references[0]);
-      putText(key, errorModule);
-    } else if (mapModules[module]!.containsKey(key)) {
-      mapModules[module]![key] =
-          mapModules[module]![key]! + '\n#: $currentFile:$currentLineNo';
-    } else {
-      mapModules[module]![key] = '#: $currentFile:$currentLineNo';
-    }
-  }
-
-  /// Scans recursively all dart files of the [directory].
-  void scanDirectory(
-    String directory,
-  ) {
-    final base = Directory(directory);
-    final subDirs = [];
-    try {
-      for (var file in base.listSync()) {
-        if (file is Directory) {
-          subDirs.add(file.path);
-          continue;
-        }
-        if (regExpFiles.firstMatch(file.path) == null) {
-          continue;
-        }
-        if (file is File) {
-          scanFile(file, directory);
-        } else {
-          logger.error('ignored: $directory/${file.path}');
-        }
-      }
-    } on FileSystemException catch (exc) {
-      logger.error('ignored: $directory: $exc');
-    }
-    for (var node in subDirs) {
-      scanDirectory(join(directory, node));
-    }
-  }
-
-  /// Scans the [file] for I18N texts.
-  /// [directory]: used for logging.
-  void scanFile(File file, String directory) {
-    logger.log('parsing ${file.path}', LEVEL_DETAIL);
-    try {
-      currentFile = join(directory, file.path);
-      lines = file.readAsLinesSync();
-      moduleVariables.clear();
-      currentLineNo = 0;
-      for (var line in lines) {
-        ++currentLineNo;
-        RegExpMatch? match;
-        if ((match = regExpModule.firstMatch(line)) != null) {
-          currentModule = match!.group(3)!;
-          moduleVariables[match.group(1)!] = currentModule;
-        } else {
-          for (match in regExpText.allMatches(line)) {
-            currentArguments = line.substring(match.end);
-            switch (match.group(2)!) {
-              case 'tr':
-                handleSimpleText();
-                break;
-              case 'trMulti':
-                handleMultiText();
-                break;
-              case 'ntr':
-                handlePluralText();
-                break;
-              case 'trArgs':
-                handleTextWithArgs();
-                break;
-            }
-          }
-        }
-      }
-    } on FileSystemException catch (exc) {
-      logger.error('ignored: $directory: $exc');
-    }
-  }
-
-  String convertString(String key) {
-    final rc = key.replaceAll('"', r'\"');
-    return rc;
-  }
-
-  /// Writes the *.pot files to the [directory].
-  void writePot(String directory) {
-    FileSync().ensureDirectory(directory);
-    final doneModules = <String>{};
-    for (var module in mapModules.keys) {
-      doneModules.add(module);
-      final map = mapModules[module]!;
-      final keys = map.keys.toList();
-      Map<String, PluralValue>? map2;
-      if (mapPlurals.containsKey(module)) {
-        map2 = mapPlurals[module];
-        keys.addAll(map2!.keys);
-      }
-      keys.sort();
-      final buffer = StringBuffer(
-          '# Texts of module $module, parsed by i18n_text_parser\n');
-      for (var key in keys) {
-        buffer.writeln();
-        buffer.writeln(map.containsKey(key)
-            ? map[key]
-            : map2![key]!.references.join('\n'));
-        var key2 = convertString(key);
-        buffer.writeln('msgid "$key2"');
-        if (map.containsKey(key)) {
-          buffer.writeln('msgstr ""');
-        } else {
-          key2 = convertString(map2![key]!.keyPlural);
-          buffer.writeln('msgid_plural "$key2"');
-          buffer.writeln('msgstr[0] ""');
-          buffer.writeln('msgstr[1] ""');
-        }
-      }
-      final fn = join(directory, '$module.pot');
-      logger.log('writing $fn', LEVEL_DETAIL);
-      File(fn).writeAsStringSync(buffer.toString());
-    }
-    for (var module in mapPlurals.keys) {
-      if (!doneModules.contains(module)) {
-        final map = mapPlurals[module];
-        final keys = map!.keys.toList();
-        keys.sort();
-        final buffer = StringBuffer(
-            '# Texts of module $module, parsed by i18n_text_parser\n');
-        for (var key in keys) {
-          buffer.writeln();
-          buffer.writeln(map[key]!.references.join('\n'));
-          var key2 = convertString(key);
-          buffer.writeln('msgid "$key2"');
-          key2 = convertString(map[key]!.keyPlural);
-          buffer.writeln('msgid_plural "$key2"');
-          buffer.writeln('msgstr[0] ""');
-          buffer.writeln('msgstr[1] ""');
-        }
-        final fn = join(directory, '$module.pot');
-        logger.log('writing $fn', LEVEL_DETAIL);
-        File(fn).writeAsStringSync(buffer.toString());
-      }
-    }
-  }
-}
-
-/// Stores the data of a plural text.
-class PluralValue {
-  final List<String> references;
-  final String keyPlural;
-  PluralValue(this.keyPlural, this.references);
-}
index 13af6c5f64ae28513f97781d23ecccce3017d366..ef419ea18e1f5014731720fd4d16b22377bb05eb 100644 (file)
@@ -1,12 +1,14 @@
-import 'package:exhibition/base/i18n.dart';
+import 'package:dart_bones/dart_bones.dart';
+import 'package:exhibition/base/i18n_io.dart';
 import 'package:exhibition/meta/module_meta_data.dart';
 import 'package:exhibition/meta/modules.dart';
 
 import 'generator.dart';
 
 void main(List<String> args) {
-  final generator = Generator();
-  I18N.internal('data/i18n');
+  final logger = MemoryLogger(LEVEL_DETAIL);
+  final generator = Generator(logger);
+  I18nIo.internal('data/i18n', logger);
   if (args.isEmpty) {
     usage(generator);
     generator.out('+++ missing arguments.');
diff --git a/bin/yaml_merger.dart b/bin/yaml_merger.dart
deleted file mode 100644 (file)
index 95f56e8..0000000
+++ /dev/null
@@ -1,163 +0,0 @@
-import 'dart:io';
-
-import 'package:path/path.dart';
-import 'package:dart_bones/dart_bones.dart';
-
-/// Implements a merge tool for yaml files.
-/// @see merge() for description of merge.
-class Merger {
-  RegExp regExpKey = RegExp(r'^([a-zA-Z]\w+):\s*$');
-  RegExp regExpKeyValue = RegExp(r'^ ');
-  final BaseLogger logger;
-  final precedenceKeys = <String, String>{};
-  Merger(this.logger);
-
-  /// Parses the keys of a given [file] into [this.precedenceKeys].
-  void parseKeys(File file){
-    precedenceKeys.clear();
-    final lines = file.readAsLinesSync();
-    var state = 'undef';
-    var lastKey = '';
-    final keyValue = [];
-    for (var line in lines){
-      final match = regExpKey.firstMatch(line);
-      if (match != null){
-        if (keyValue.isNotEmpty){
-          precedenceKeys[lastKey] = keyValue.join('\n');
-          keyValue.clear();
-        }
-        state = 'inKey';
-        lastKey = match.group(1)!;
-      } else {
-        if (regExpKeyValue.firstMatch(line) != null){
-          if (state == 'inKey'){
-            keyValue.add(line);
-          }
-        }
-      }
-    }
-    if (keyValue.isNotEmpty){
-      precedenceKeys[lastKey] = keyValue.join('\n');
-    }
-  }
-  /// Merges the [fileSource] and [filePreference] into the [target] file.
-  void mergeOneFile(File fileSource, File filePreference, String target) {
-    parseKeys(filePreference);
-    final output = <String>[];
-    final foundKeys = <String>{};
-    final lines = fileSource.readAsLinesSync();
-    var state = 'undef';
-    var lastKey = '';
-    final keyValue = [];
-    for (var line in lines){
-      final match = regExpKey.firstMatch(line);
-      if (match != null) {
-        if (keyValue.isNotEmpty) {
-          output.add(lastKey + ':');
-          output.add(precedenceKeys.containsKey(lastKey)
-              ? precedenceKeys[lastKey]!
-              : keyValue.join('\n'));
-        }
-        keyValue.clear();
-        state = 'inKey';
-        lastKey = match.group(1)!;
-        if (foundKeys.contains(lastKey)){
-          logger.error('key $lastKey found multiple times');
-        }
-        foundKeys.add(lastKey);
-      } else {
-        if (regExpKeyValue.firstMatch(line) != null){
-          if (state == 'inKey'){
-            keyValue.add(line);
-          }
-        } else {
-          output.add(line);
-        }
-      }
-    }
-    if (keyValue.isNotEmpty){
-      output.add(lastKey + ':');
-      if (precedenceKeys.containsKey(lastKey)) {
-        output.add(precedenceKeys[lastKey]!);
-      } else {
-        output.add(keyValue.join('\n'));
-      }
-    }
-    for (var key in precedenceKeys.keys){
-      if (! foundKeys.contains(key)){
-        output.add(key + ':');
-        output.add(precedenceKeys[key]!);
-      }
-    }
-    File(target).writeAsStringSync(output.join('\n') + '\n');
-  }
-  /// Merges or copies files from two source directories into a target directory.
-  /// [pathSource] contains files with lower priorities.
-  /// [pathPreferences] contains files with higher priorities.
-  /// [pathTarget] is the target directory.
-  /// Only files with the same node will be merged.
-  /// Merging: the two source files contains keys. If a key is part of both
-  /// files the key content of of the higher priority file is taken.
-  void merge(String pathSource, String pathPreference, String pathTarget) {
-    final dirSource = Directory(pathSource);
-    final dirPreference = Directory(pathPreference);
-    final dirTarget = Directory(pathTarget);
-    if (!dirSource.existsSync()) {
-      logger.error('source not a directory: $pathSource');
-    } else if (!dirPreference.existsSync()) {
-      logger.error('source not a directory: $pathPreference');
-    } else if (!dirTarget.existsSync()) {
-      logger.error('source not a directory: $pathTarget');
-    } else {
-      /// Copy/merge the files from dirSource:
-      for (var file in dirSource.listSync()) {
-        final node = basename(file.path);
-        if (! node.endsWith('.yaml')){
-          continue;
-        }
-        final fnPreference = join(pathPreference, node);
-        final fnTarget = join(pathTarget, node);
-        final filePreference = File(fnPreference);
-        if (!(file is File)) {
-          logger.error('not a file: ${file.path} ignoring...');
-        } else if (filePreference.existsSync()) {
-          mergeOneFile(file, filePreference, fnTarget);
-        } else {
-          logger.log('copying ${file.path}', LEVEL_DETAIL);
-          file.copy(fnTarget);
-        }
-      }
-
-      /// Copy the "single" files from preferences:
-      for (var file in dirPreference.listSync()) {
-        final node = basename(file.path);
-        if (! node.endsWith('.yaml')){
-          continue;
-        }
-        final fnSource = join(pathSource, node);
-        final fnTarget = join(pathTarget, node);
-        File fileSource = File(fnSource);
-        if (file is File && !fileSource.existsSync()) {
-          logger.log('copying ${file.path}', LEVEL_DETAIL);
-          file.copy(fnTarget);
-        }
-      }
-    }
-  }
-}
-void main(List<String> args){
-  final logger = MemoryLogger(LEVEL_DETAIL);
-  if (args.length != 3){
-    print('''Usage: yaml_merger DIR_SOURCE DIR_PREFERENCE DIR_OUTPUT
-  Merges the yaml files from DIR_SOURCE (with lower priority) and DIR_PREFERENCE
-  (with higher priority) into files in DIR_OUTPUT.
-Version: 0.1.0
-Example:
-yaml_merger data/sql data/sql/precedence /tmp/sql
-''');
-    print('+++ wrong count of arguments: ${args.length} instead of 3');
-  } else {
-    final merger = Merger(logger);
-    merger.merge(args[0], args[1], args[2]);
-  }
-}
\ No newline at end of file
diff --git a/dart_tools/bin/i18n_text_parser.dart b/dart_tools/bin/i18n_text_parser.dart
new file mode 100644 (file)
index 0000000..a816ed2
--- /dev/null
@@ -0,0 +1,595 @@
+import "dart:io";
+
+import 'package:dart_bones/dart_bones.dart';
+import "package:path/path.dart";
+import 'package:sprintf/sprintf.dart';
+
+void main(List<String> args) {
+  if (args.length == 0) {
+    usage('missing arguments');
+  } else {
+    switch (args[0]) {
+      case 'parse':
+        parse(args.sublist(1));
+        break;
+      case 'msg-init':
+        msgInit(args.sublist(1));
+        break;
+    }
+  }
+}
+
+/// Executes the "msg-init" sub command.
+void msgInit(List<String> args) {
+  if (args.length != 3) {
+    usage('wrong count of arguments');
+  } else if (!Directory(args[0]).existsSync()) {
+    usage('<TARGET_DIR> is not a directory: ${args[0]}');
+  } else if (!File(join(args[0], args[1] + '.pot')).existsSync()) {
+    usage('wrong <MODULE>: missing <MODULE>.pot');
+  } else if (RegExp(r'[a-z]{2}(_[A-Z]{2})?').firstMatch(args[2]) == null) {
+    usage('wrong <LOCALE>: xx or xx_YY expected, e.g. de_DE');
+  } else {
+    final logger = MemoryLogger(LEVEL_DETAIL);
+    final parser = I18nTextParser(logger);
+    parser.msgInit(args[0], args[1], args[2]);
+  }
+}
+
+/// Executes the "parse" sub command.
+void parse(List<String> args) {
+  if (args.length < 2) {
+    usage('wrong count of arguments');
+  } else if (!Directory(args[0]).existsSync()) {
+    usage('SOURCE_DIR is not a directory: ${args[0]}');
+  } else if (!Directory(args[1]).existsSync()) {
+    usage('TARGET_DIR is not a directory: ${args[1]}');
+  } else {
+    final logger = MemoryLogger(LEVEL_DETAIL);
+    final parser = I18nTextParser(logger);
+    String excluded = '*';
+    if (args.length == 3) {
+      excluded = args[2];
+    }
+    if (excluded.isNotEmpty) {
+      if (excluded == '*') {
+        excluded = r'linux|ios|web|\.\w.*|test|data';
+      }
+      if (excluded.contains(';')) {
+        excluded = RegExp.escape(excluded).replaceAll(';', '|');
+      }
+      parser.regExpExcluded = RegExp('^($excluded)\$');
+      logger.log('excluding: ' + parser.regExpExcluded!.pattern, LEVEL_FINE);
+    }
+    parser.scanDirectory(args[0], 0);
+    parser.writePot(args[1]);
+  }
+}
+
+void usage(String error) {
+  stderr.write('''Usage: 
+i18n_text_parser parse <SOURCE_DIR> <TARGET_DIR> [<EXCLUDED>]
+  Parses recursively the Dart files of the <SOURCE_DIR> and create the *.pot
+    files in the <TARGET_DIR>.
+  <EXCLUDED>:
+    "": exclude nothing
+    "*" exclude all standard directories: means 'linux|ios|web|\.\w.*|test|data'
+    Otherwise: a ";" delimited list of excluded directories, e.g. "ios;linux;.git"
+      or a '|' delimited list of regular expressions, e.g. 'test.*|\.[a-z]+'
+    Default value: "*"
+i18n_text_parser msg-init <TARGET_DIR> <MODULE> <LOCALE>
+  Creates the <MODULE>.<LOCALE>.po file from the <MODULE>.pot file.
+Examples:
+i18n_text_parser parse /home/ws/flutter/exhibition /tmp/i18n_data
+i18n_text_parser parse do_it /tmp/i18n_data '\..*|test"
+i18n_text_parser parse do_it /tmp/i18n_data ""
+i18n_text_parser msg-init /tmp/i18n_data Users de_DE
+''');
+  stderr.writeln('+++ $error');
+}
+
+class I18nTextParser {
+  final globalModule = '!global';
+  final errorModule = '!error';
+  final BaseLogger logger;
+  final mapModules = <String, Map<String, String>>{};
+  final mapPlurals = <String, Map<String, PluralValue>>{};
+  final foundFiles = <String>{};
+  var regExpFiles = RegExp(r'.dart$');
+  String currentModule = '';
+  final moduleVariables = <String, String>{};
+  String currentFile = '';
+  String currentArguments = '';
+  List<String> lines = [];
+  int currentLineNo = 0;
+  int currentIxLines = 0;
+  RegExp? regExpExcluded;
+  // ...........................1.............1.........2
+  final regExpModule =
+      RegExp(r'''(\w+) = (I18N\(\)|i18N)\.module\(["'](.*?)['"]\);''');
+  final regExpDelimiter = RegExp(r'''["']''');
+  // ..........1.............1..2..............................2
+  final regExpText = RegExp(r'(i18N|I18N\(\))\.(tr|ntr|trMulti|trArgs)\(');
+  final regExpStringConstant = RegExp(r'''^\s*(r?)(["'])(.*?)\2''');
+  final regExpVariable = RegExp(r'^\w+');
+  final regExpEmptyString = RegExp(r'^\s*$');
+
+  RegExpMatch? currentMatch;
+
+  I18nTextParser(this.logger);
+
+  /// Checks the completeness of the [configuration].
+  /// [location]: description of the configuration, e.g. the filename.
+  /// [locale]: null or a needed key.
+  bool checkConfiguration(
+      BaseConfiguration configuration, String location, String? locale) {
+    bool checkKey(String key) {
+      final rc = configuration.asString(key) != null;
+      if (!rc) {
+        logger.error('missing key $key in $location');
+      }
+      return rc;
+    }
+
+    bool checkSection(String key, String section) {
+      final rc = configuration.asString(key, section: section) != null;
+      if (!rc) {
+        logger.error('missing key [$section].$key in $location');
+      }
+      return rc;
+    }
+
+    var rc = checkSection('name', 'project');
+    rc &= checkSection('version', 'project');
+    rc &= checkKey('author');
+    rc &= checkKey('translator');
+    rc &= checkSection('name', 'copyright');
+    rc &= checkSection('start', 'copyright');
+    rc &= checkKey('license');
+    rc &= checkKey('bugResponsible');
+    if (locale != null) {
+      rc &= checkSection('language', locale);
+      rc &= checkSection('name', locale);
+    }
+    return rc;
+  }
+
+  String convertString(String key) {
+    final rc = key.replaceAll('"', r'\"');
+    return rc;
+  }
+
+  /// Writes an example of the configuration file into the [directory].
+  void exampleConfiguration(String directory, [String? locale]) {
+    locale ??= 'de_DE';
+    final filename = join(directory, 'project.i18n.yaml');
+    final file = File(filename);
+    file.writeAsStringSync('''---
+# Configuration of the I18N data:
+project:
+  name: "<PROJECT>"
+  version: "1.0.0"
+author: "J. Hamatoma <author@hamatoma.de>"
+translator: "J. Hamatoma <lang@hamatoma.de>"
+$locale:
+  language: "German"
+  name: "J. Hamatoma <de@hamatoma.de>"
+copyright:
+  name: "J. Hamatoma <cr@hamatoma.de>"
+  start: 2021
+license: "CC0 1.0 Universal"
+bugResponsible: "J. Hamatoma <bug@hamatoma.de>"
+''');
+  }
+
+  /// Returns the configuration file in the [directory] and test it for
+  /// completeness.
+  /// If [locale] is not null it must be a key in the configuration.
+  /// Returns null if not found or some errors has been found.
+  BaseConfiguration? fetchConfiguration(String directory, {String? locale}) {
+    final filename = join(directory, 'project.i18n.yaml');
+    final file = File(filename);
+    BaseConfiguration? rc;
+    if (!file.existsSync()) {
+      logger.error(
+          'abort: the new created configuration $filename must be edited.'
+          ' Than try again');
+      exampleConfiguration(directory);
+    } else {
+      rc = Configuration.fromFile(filename, logger);
+      if (!checkConfiguration(rc, filename, locale)) {
+        logger.error(
+            'abort: configuration $filename contains errors. Please correct '
+            'that and try again.');
+        rc = null;
+      }
+    }
+    return rc;
+  }
+
+  /// Handles the state if the [currentArguments] is empty: it will be updated
+  /// by the next line.
+  void handleEmptyArgument() {
+    if (regExpEmptyString.firstMatch(currentArguments) != null) {
+      currentArguments = currentIxLines >= lines.length
+          ? ''
+          : lines[currentIxLines++].trimLeft();
+    }
+  }
+
+  /// Handles the argument with the module (as string constant or as variable).
+  String handleModuleArgument() {
+    String module = globalModule;
+    if (hasComma()) {
+      handleEmptyArgument();
+      if (isStringConstant()) {
+        module = currentMatch!.group(3)!;
+      } else {
+        if ((currentMatch = regExpVariable.firstMatch(currentArguments)) !=
+            null) {
+          final variable = currentMatch!.group(0);
+          if (moduleVariables.containsKey(variable)) {
+            module = moduleVariables[variable]!;
+          } else {
+            logger.error(
+                '$currentFile-$currentLineNo: unknown module variable: $variable');
+            module = errorModule;
+          }
+        }
+      }
+    }
+    return module;
+  }
+
+  /// Gets the text (multiple lines) from the [currentArguments].
+  /// [currentArguments] is the string behind the 'trMulti('.
+  void handleMultiText() {
+    logger.error('not implemented: trMulti()');
+  }
+
+  /// Gets the text (multiple lines) from the [currentArguments] .
+  /// [currentArguments] is the string behind the 'trPlural('.
+  void handlePluralText() {
+    currentIxLines = currentLineNo;
+    handleEmptyArgument();
+    if (!isStringConstant()) {
+      logger.error(
+          '$currentFile-$currentLineNo: cannot analyse parameters of ntr().'
+          ' Missing first string constant.');
+    } else {
+      String key1 = currentMatch!.group(3)!;
+      if (!hasComma()) {
+        logger.error(
+            '$currentFile-$currentLineNo: cannot analyse parameters of ntr().'
+            ' Missing second string constant.');
+      } else {
+        handleEmptyArgument();
+        if (!isStringConstant()) {
+          logger.error(
+              '$currentFile-$currentLineNo: cannot analyse parameters of tr().'
+              ' Second argument is not a string constant.');
+        } else {
+          String key2 = currentMatch!.group(3)!;
+          String module = handleModuleArgument();
+          putPluralKey(key1, key2, module);
+        }
+      }
+    }
+  }
+
+  /// Gets the text (single line) from the [currentArguments] and store it.
+  /// [currentArguments] is the string behind the 'tr('.
+  void handleSimpleText() {
+    currentIxLines = currentLineNo;
+    handleEmptyArgument();
+    if (!isStringConstant()) {
+      logger.error(
+          '$currentFile-$currentLineNo: cannot analyse parameters of tr().'
+          ' Missing string constant.');
+    } else {
+      String module;
+      final key = currentMatch?.group(3);
+      module = handleModuleArgument();
+      putText(key!, module);
+    }
+  }
+
+  /// Gets the text (with placeholders) from the [currentArguments] and store it.
+  /// [currentArguments] is the string behind the 'trArgs('.
+  void handleTextWithArgs() {
+    currentIxLines = currentLineNo;
+    handleEmptyArgument();
+    if (!isStringConstant()) {
+      logger.error(
+          '$currentFile-$currentLineNo: cannot analyse parameters of trArgs().'
+          ' Missing string constant.');
+    } else {
+      String module;
+      final key = currentMatch?.group(3);
+      module = handleModuleArgument();
+      putText(key!, module);
+    }
+  }
+
+  /// Tests whether the [currentArguments] is preceded by a ','.
+  /// If true the comma is removed from the [currentArguments].
+  bool hasComma() {
+    final rc = currentArguments.startsWith(',');
+    if (rc) {
+      currentArguments = currentArguments.substring(1).trimLeft();
+    }
+    return rc;
+  }
+
+  /// Returns the introduction of a *.pot file, driven by [configuration].
+  String introduction(String module, BaseConfiguration configuration) {
+    final now = DateTime.now();
+    final year = now.year;
+    final date = '${now.year}-${now.month}-${now.day} ${now.hour}:${now.minute}'
+        '+0${now.timeZoneOffset.inHours}00';
+    final author = configuration.asString('author') ?? 'J. Hamatoma';
+    var project = configuration.asString('name', section: 'project') ??
+        '??' +
+            ' ' +
+            (configuration.asString('version', section: 'project') ?? '1.0.0');
+    final license = configuration.asString('license') ?? 'CC0 1.0 Universal';
+    var start = configuration.asString('start', section: 'copyright') ?? year;
+    var cr = configuration.asString('name', section: 'copyright') ?? author;
+    var responsible = configuration.asString('bugResponsible') ?? author;
+    final rc = '''# Texts of module $module, created by i18n_text_parser');
+# Copyright (C) $start-$year $cr
+# License: $license
+# $author, $year.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: $project\\n"
+"Report-Msgid-Bugs-To: $responsible\\n"
+"POT-Creation-Date: $date\\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
+"Language-Team: LANGUAGE <LL@li.org>\\n"
+"Language: LANGUAGE\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+''';
+    return rc;
+  }
+
+  /// Tests whether the [currentArguments] is preceded by a string constant.
+  /// If true the [currentMatch] contains the string content and the
+  /// string constant is removed from the [currentArguments].
+  bool isStringConstant() {
+    if ((currentMatch = regExpStringConstant.firstMatch(currentArguments)) !=
+        null) {
+      currentArguments =
+          currentArguments.substring(currentMatch!.end).trimLeft();
+    }
+    return currentMatch != null;
+  }
+
+  /// Creates the first *.po file from a *.pot file for the [module] with
+  /// [locale] in the [directory].
+  void msgInit(String directory, String module, String locale) {
+    final source = File(join(directory, '$module.pot'));
+    final target = File(join(directory, '$module.$locale.po'));
+    var contents = source.readAsStringSync();
+    final configuration = fetchConfiguration(directory);
+    if (configuration != null) {
+      final now = DateTime.now();
+      final date =
+          '${now.year}-${now.month}-${now.day} ${now.hour}:${now.minute}' +
+              sprintf('+%02d00', [now.timeZoneOffset.inHours]);
+      final translator = configuration.asString('translator') ?? 'J. Hamatoma';
+      final language =
+          configuration.asString('language', section: locale) ?? '$locale';
+      final team =
+          configuration.asString('name', section: locale) ?? '$locale <author@hamatoma.de>';
+      contents = contents.replaceFirst(
+          'Revision-Date: YEAR-MO-DA HO:MI+ZONE', 'Revision-Date: $date');
+      contents =
+          contents.replaceFirst(RegExp(r'tor: [^\\]*'), 'tor: $translator');
+      contents = contents.replaceFirst(RegExp(r'Team: [^\\]*'), 'Team: $language $team');
+      contents = contents.replaceFirst(RegExp(r'uage: [^\\]*'), 'uage: $language');
+      contents = contents.replaceFirst('charset=CHARSET', 'charset=UTF-8');
+      contents = contents.replaceFirst(
+          '8bit', '8bit\\n"\n"Plural-Forms: nplurals=2; plural=(n != 1);');
+      target.writeAsStringSync(contents);
+    }
+  }
+
+  /// Stores the data of a plural text entry.
+  void putPluralKey(String key1, String key2, String module) {
+    if (!mapPlurals.containsKey(module)) {
+      mapPlurals[module] = <String, PluralValue>{};
+    }
+    String reference = '#: $currentFile:$currentLineNo';
+    if (module != errorModule && mapModules[module]?[key1] != null) {
+      logger.error('$currentFile-$currentLineNo: key already defined as normal '
+              'key. Modify key with the invisible tag <#1> or <#2> or ... at the '
+              'end to make it unique.\nFirst found in ' +
+          mapModules[module]![key1]!);
+      putText(key1, errorModule);
+    } else if (mapPlurals[module]!.containsKey(key1)) {
+      if (mapPlurals[module]![key1]!.keyPlural == key2) {
+        mapPlurals[module]![key1]!.references.add(reference);
+      } else {
+        logger.error('$currentFile-$currentLineNo: multiple definition of a '
+                'plural key but keyPlural is different. Modify key1 with the'
+                ' invisible tag <#1> or <#2> or ... at the end to make it '
+                'unique.\nFirst found in ' +
+            mapPlurals[module]![key1]!.references[0]);
+      }
+    } else {
+      mapPlurals[module]![key1] = PluralValue(key2, [reference]);
+    }
+  }
+
+  /// Stores the [key] in the [module] map with location info [filename] and
+  /// [lineNo].
+  void putText(String key, [String module = '']) {
+    if (module.isEmpty) {
+      module = currentModule;
+    }
+    if (!mapModules.containsKey(module)) {
+      mapModules[module] = <String, String>{};
+    }
+    if (module != errorModule && mapPlurals[module]?[key] != null) {
+      logger.error('$currentFile-$currentLineNo: key already defined as plural '
+              'key. Modify key with the invisible tag <#1> or <#2> or ... at '
+              'the end to make it unique.\nFirst found in ' +
+          mapPlurals[module]![key]!.references[0]);
+      putText(key, errorModule);
+    } else if (mapModules[module]!.containsKey(key)) {
+      mapModules[module]![key] =
+          mapModules[module]![key]! + '\n#: $currentFile:$currentLineNo';
+    } else {
+      mapModules[module]![key] = '#: $currentFile:$currentLineNo';
+    }
+  }
+
+  /// Scans recursively all dart files of the [directory].
+  void scanDirectory(String directory, int depth) {
+    final base = Directory(directory);
+    final subDirs = [];
+    try {
+      for (var file in base.listSync()) {
+        if (file is Directory) {
+          subDirs.add(basename(file.path));
+          continue;
+        }
+        if (regExpFiles.firstMatch(file.path) == null) {
+          continue;
+        }
+        if (file is File) {
+          scanFile(file, directory);
+        } else {
+          logger.error('ignored: $directory/${file.path}');
+        }
+      }
+    } on FileSystemException catch (exc) {
+      logger.error('ignored: $directory: $exc');
+    }
+    for (var node in subDirs) {
+      if (depth == 0 &&
+          regExpExcluded != null &&
+          regExpExcluded!.firstMatch(node) != null) {
+        logger.log('ignoring $node');
+      } else {
+        scanDirectory(
+            directory == '.' ? node : join(directory, node), depth + 1);
+      }
+    }
+  }
+
+  /// Scans the [file] for I18N texts.
+  /// [directory]: used for logging.
+  void scanFile(File file, String directory) {
+    logger.log('parsing ${file.path}', LEVEL_DETAIL);
+    try {
+      currentFile = file.path;
+      lines = file.readAsLinesSync();
+      moduleVariables.clear();
+      currentLineNo = 0;
+      for (var line in lines) {
+        ++currentLineNo;
+        RegExpMatch? match;
+        if ((match = regExpModule.firstMatch(line)) != null) {
+          currentModule = match!.group(3)!;
+          moduleVariables[match.group(1)!] = currentModule;
+        } else {
+          for (match in regExpText.allMatches(line)) {
+            currentArguments = line.substring(match.end);
+            switch (match.group(2)!) {
+              case 'tr':
+                handleSimpleText();
+                break;
+              case 'trMulti':
+                handleMultiText();
+                break;
+              case 'ntr':
+                handlePluralText();
+                break;
+              case 'trArgs':
+                handleTextWithArgs();
+                break;
+            }
+          }
+        }
+      }
+    } on FileSystemException catch (exc) {
+      logger.error('ignored: $directory: $exc');
+    }
+  }
+
+  /// Writes the *.pot files to the [directory].
+  void writePot(String directory) {
+    final configuration = fetchConfiguration(directory);
+    if (configuration != null) {
+      FileSync().ensureDirectory(directory);
+      final doneModules = <String>{};
+      for (var module in mapModules.keys) {
+        doneModules.add(module);
+        final map = mapModules[module]!;
+        final keys = map.keys.toList();
+        Map<String, PluralValue>? map2;
+        if (mapPlurals.containsKey(module)) {
+          map2 = mapPlurals[module];
+          keys.addAll(map2!.keys);
+        }
+        keys.sort();
+        final buffer = StringBuffer(
+            '# Texts of module $module, parsed by i18n_text_parser\n');
+        for (var key in keys) {
+          buffer.writeln();
+          buffer.writeln(map.containsKey(key)
+              ? map[key]
+              : map2![key]!.references.join('\n'));
+          var key2 = convertString(key);
+          buffer.writeln('msgid "$key2"');
+          if (map.containsKey(key)) {
+            buffer.writeln('msgstr ""');
+          } else {
+            key2 = convertString(map2![key]!.keyPlural);
+            buffer.writeln('msgid_plural "$key2"');
+            buffer.writeln('msgstr[0] ""');
+            buffer.writeln('msgstr[1] ""');
+          }
+        }
+        final fn = join(directory, '$module.pot');
+        logger.log('writing $fn', LEVEL_DETAIL);
+        File(fn).writeAsStringSync(buffer.toString());
+      }
+      for (var module in mapPlurals.keys) {
+        if (!doneModules.contains(module)) {
+          final map = mapPlurals[module];
+          final keys = map!.keys.toList();
+          keys.sort();
+          final buffer = StringBuffer(introduction(module, configuration));
+          for (var key in keys) {
+            buffer.writeln();
+            buffer.writeln(map[key]!.references.join('\n'));
+            var key2 = convertString(key);
+            buffer.writeln('msgid "$key2"');
+            key2 = convertString(map[key]!.keyPlural);
+            buffer.writeln('msgid_plural "$key2"');
+            buffer.writeln('msgstr[0] ""');
+            buffer.writeln('msgstr[1] ""');
+          }
+          final fn = join(directory, '$module.pot');
+          logger.log('writing $fn', LEVEL_DETAIL);
+          File(fn).writeAsStringSync(buffer.toString());
+        }
+      }
+    }
+  }
+}
+
+/// Stores the data of a plural text.
+class PluralValue {
+  final List<String> references;
+  final String keyPlural;
+  PluralValue(this.keyPlural, this.references);
+}
diff --git a/dart_tools/bin/yaml_merger.dart b/dart_tools/bin/yaml_merger.dart
new file mode 100644 (file)
index 0000000..95f56e8
--- /dev/null
@@ -0,0 +1,163 @@
+import 'dart:io';
+
+import 'package:path/path.dart';
+import 'package:dart_bones/dart_bones.dart';
+
+/// Implements a merge tool for yaml files.
+/// @see merge() for description of merge.
+class Merger {
+  RegExp regExpKey = RegExp(r'^([a-zA-Z]\w+):\s*$');
+  RegExp regExpKeyValue = RegExp(r'^ ');
+  final BaseLogger logger;
+  final precedenceKeys = <String, String>{};
+  Merger(this.logger);
+
+  /// Parses the keys of a given [file] into [this.precedenceKeys].
+  void parseKeys(File file){
+    precedenceKeys.clear();
+    final lines = file.readAsLinesSync();
+    var state = 'undef';
+    var lastKey = '';
+    final keyValue = [];
+    for (var line in lines){
+      final match = regExpKey.firstMatch(line);
+      if (match != null){
+        if (keyValue.isNotEmpty){
+          precedenceKeys[lastKey] = keyValue.join('\n');
+          keyValue.clear();
+        }
+        state = 'inKey';
+        lastKey = match.group(1)!;
+      } else {
+        if (regExpKeyValue.firstMatch(line) != null){
+          if (state == 'inKey'){
+            keyValue.add(line);
+          }
+        }
+      }
+    }
+    if (keyValue.isNotEmpty){
+      precedenceKeys[lastKey] = keyValue.join('\n');
+    }
+  }
+  /// Merges the [fileSource] and [filePreference] into the [target] file.
+  void mergeOneFile(File fileSource, File filePreference, String target) {
+    parseKeys(filePreference);
+    final output = <String>[];
+    final foundKeys = <String>{};
+    final lines = fileSource.readAsLinesSync();
+    var state = 'undef';
+    var lastKey = '';
+    final keyValue = [];
+    for (var line in lines){
+      final match = regExpKey.firstMatch(line);
+      if (match != null) {
+        if (keyValue.isNotEmpty) {
+          output.add(lastKey + ':');
+          output.add(precedenceKeys.containsKey(lastKey)
+              ? precedenceKeys[lastKey]!
+              : keyValue.join('\n'));
+        }
+        keyValue.clear();
+        state = 'inKey';
+        lastKey = match.group(1)!;
+        if (foundKeys.contains(lastKey)){
+          logger.error('key $lastKey found multiple times');
+        }
+        foundKeys.add(lastKey);
+      } else {
+        if (regExpKeyValue.firstMatch(line) != null){
+          if (state == 'inKey'){
+            keyValue.add(line);
+          }
+        } else {
+          output.add(line);
+        }
+      }
+    }
+    if (keyValue.isNotEmpty){
+      output.add(lastKey + ':');
+      if (precedenceKeys.containsKey(lastKey)) {
+        output.add(precedenceKeys[lastKey]!);
+      } else {
+        output.add(keyValue.join('\n'));
+      }
+    }
+    for (var key in precedenceKeys.keys){
+      if (! foundKeys.contains(key)){
+        output.add(key + ':');
+        output.add(precedenceKeys[key]!);
+      }
+    }
+    File(target).writeAsStringSync(output.join('\n') + '\n');
+  }
+  /// Merges or copies files from two source directories into a target directory.
+  /// [pathSource] contains files with lower priorities.
+  /// [pathPreferences] contains files with higher priorities.
+  /// [pathTarget] is the target directory.
+  /// Only files with the same node will be merged.
+  /// Merging: the two source files contains keys. If a key is part of both
+  /// files the key content of of the higher priority file is taken.
+  void merge(String pathSource, String pathPreference, String pathTarget) {
+    final dirSource = Directory(pathSource);
+    final dirPreference = Directory(pathPreference);
+    final dirTarget = Directory(pathTarget);
+    if (!dirSource.existsSync()) {
+      logger.error('source not a directory: $pathSource');
+    } else if (!dirPreference.existsSync()) {
+      logger.error('source not a directory: $pathPreference');
+    } else if (!dirTarget.existsSync()) {
+      logger.error('source not a directory: $pathTarget');
+    } else {
+      /// Copy/merge the files from dirSource:
+      for (var file in dirSource.listSync()) {
+        final node = basename(file.path);
+        if (! node.endsWith('.yaml')){
+          continue;
+        }
+        final fnPreference = join(pathPreference, node);
+        final fnTarget = join(pathTarget, node);
+        final filePreference = File(fnPreference);
+        if (!(file is File)) {
+          logger.error('not a file: ${file.path} ignoring...');
+        } else if (filePreference.existsSync()) {
+          mergeOneFile(file, filePreference, fnTarget);
+        } else {
+          logger.log('copying ${file.path}', LEVEL_DETAIL);
+          file.copy(fnTarget);
+        }
+      }
+
+      /// Copy the "single" files from preferences:
+      for (var file in dirPreference.listSync()) {
+        final node = basename(file.path);
+        if (! node.endsWith('.yaml')){
+          continue;
+        }
+        final fnSource = join(pathSource, node);
+        final fnTarget = join(pathTarget, node);
+        File fileSource = File(fnSource);
+        if (file is File && !fileSource.existsSync()) {
+          logger.log('copying ${file.path}', LEVEL_DETAIL);
+          file.copy(fnTarget);
+        }
+      }
+    }
+  }
+}
+void main(List<String> args){
+  final logger = MemoryLogger(LEVEL_DETAIL);
+  if (args.length != 3){
+    print('''Usage: yaml_merger DIR_SOURCE DIR_PREFERENCE DIR_OUTPUT
+  Merges the yaml files from DIR_SOURCE (with lower priority) and DIR_PREFERENCE
+  (with higher priority) into files in DIR_OUTPUT.
+Version: 0.1.0
+Example:
+yaml_merger data/sql data/sql/precedence /tmp/sql
+''');
+    print('+++ wrong count of arguments: ${args.length} instead of 3');
+  } else {
+    final merger = Merger(logger);
+    merger.merge(args[0], args[1], args[2]);
+  }
+}
\ No newline at end of file
diff --git a/dart_tools/pubspec.yaml b/dart_tools/pubspec.yaml
new file mode 100644 (file)
index 0000000..7aec104
--- /dev/null
@@ -0,0 +1,19 @@
+name: dart_tools
+description: Some tools supporting the processes in the framework exhibition.
+version: 0.1.0
+# homepage: https://www.example.com
+
+environment:
+  sdk: '>=2.13.0 <3.0.0'
+
+dependencies:
+  http: ^0.13.0
+  args: ^2.1.0
+  path: ^1.8.0
+  yaml: ^3.1.0
+  dart_bones: ^1.2.1
+  sprintf: ^6.0.0
+
+dev_dependencies:
+  lints: ^1.0.0
+  test: ^1.16.0
diff --git a/dart_tools/tools/CompileI18n b/dart_tools/tools/CompileI18n
new file mode 100755 (executable)
index 0000000..61ae0ec
--- /dev/null
@@ -0,0 +1,5 @@
+#! /bin/bash
+APP=i18n_text_parser
+TRG=tools/$APP
+dart compile exe bin/$APP.dart -o $TRG
+ls -ld $TRG
diff --git a/dart_tools/tools/CompileMerge b/dart_tools/tools/CompileMerge
new file mode 100755 (executable)
index 0000000..e3e62dd
--- /dev/null
@@ -0,0 +1,5 @@
+#! /bin/bash
+APP=yaml_merger
+TRG=tools/$APP
+dart compile exe bin/$APP.dart -o $TRG
+ls -ld $TRG
diff --git a/data/i18n/!global.de_DE.po b/data/i18n/!global.de_DE.po
new file mode 100644 (file)
index 0000000..ada29cf
--- /dev/null
@@ -0,0 +1,45 @@
+# Header entry was created by Lokalize.
+#
+# Texts of module !global, parsed by i18n_text_parser
+# J. Hamatoma <de@hamatoma.de>, 2021.
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"PO-Revision-Date: 2021-08-11 10:55+0200\n"
+"Last-Translator: J. Hamatoma <de@hamatoma.de>\n"
+"Language-Team: German <>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Lokalize 20.12.0\n"
+
+# Texts of module !global, parsed by i18n_text_parser
+#: lib/meta/users_meta.dart:32 lib/meta/roles_meta.dart:25
+msgid "Changed"
+msgstr "Geändert"
+
+#: lib/meta/users_meta.dart:34 lib/meta/roles_meta.dart:27
+msgid "Changed by"
+msgstr "Geändert von"
+
+#: lib/meta/users_meta.dart:27 lib/meta/roles_meta.dart:20
+msgid "Created"
+msgstr "Erzeugt"
+
+#: lib/meta/users_meta.dart:29 lib/meta/roles_meta.dart:22
+msgid "Created by"
+msgstr "Erzeugt von"
+
+#: lib/meta/users_meta.dart:14 lib/meta/roles_meta.dart:15
+msgid "Id"
+msgstr "Id"
+
+#: lib/meta/users_meta.dart:16 lib/meta/roles_meta.dart:17
+msgid "Name"
+msgstr "Name"
+
+#: lib/meta/users_meta.dart:25
+msgid "Role"
+msgstr "Rolle"
diff --git a/data/i18n/Users.de_DE.po b/data/i18n/Users.de_DE.po
new file mode 100644 (file)
index 0000000..e25e844
--- /dev/null
@@ -0,0 +1,25 @@
+# Header entry was created by Lokalize.
+#
+# Texts of module Users, parsed by i18n_text_parser
+# J. Hamatoma <de@hamatoma.de>, 2021.
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"PO-Revision-Date: 2021-08-11 10:54+0200\n"
+"Last-Translator: J. Hamatoma <de@hamatoma.de>\n"
+"Language-Team: German <>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Lokalize 20.12.0\n"
+
+# Texts of module Users, parsed by i18n_text_parser
+#: lib/meta/users_meta.dart:19
+msgid "Display name"
+msgstr "Anzeigenname"
+
+#: lib/meta/users_meta.dart:22
+msgid "EMail"
+msgstr "EMail"
diff --git a/data/i18n/project.i18n.yaml b/data/i18n/project.i18n.yaml
new file mode 100644 (file)
index 0000000..455c856
--- /dev/null
@@ -0,0 +1,15 @@
+---
+# Configuration of the I18N data:
+project:
+  name: "Exhibition"
+  version: "1.0.0"
+author: "J. Hamatoma <author@hamatoma.de>"
+translator: "J. Hamatoma <lang@hamatoma.de>"
+de_DE:
+  language: "German"
+  name: "J. Hamatoma <de@hamatoma.de>"
+copyright:
+  name: "J. Hamatoma <cr@hamatoma.de>"
+  start: 2021
+license: "CC0 1.0 Universal"
+bugResponsible: "J. Hamatoma <bug@hamatoma.de>"
index b70b2f6995f852a6720e5413269ced2e39bfe22a..ebb313d9a0e4c1b948d07f1e96e228fdf2563d57 100644 (file)
@@ -1,33 +1,88 @@
-class I18N {
-  static I18N? instance;
-  Map<String, Map<String, String>> modules = {};
-  String locale = 'de';
-  factory I18N() {
+import 'package:dart_bones/dart_bones.dart';
+typedef MapModule = Map<String, String>;
+typedef MapPlural = Map<String, PluralInfo>;
+class I18n {
+  static I18n? instance;
+  final BaseLogger logger;
+  final globalModule = '!global';
+  Map<String, MapModule> mapModules = {};
+  Map<String, MapPlural> mapPlural = {};
+  String locale = 'de_DE';
+  String loadedLocale = '';
+  final regExpTag = RegExp(r'<#\d+>$');
+  factory I18n() {
     return instance!;
   }
-  I18N.internal(String directory) {
+
+  I18n.internal(this.logger) {
     instance = this;
   }
-  String module(String name) {
-    return name;
+
+  /// Defines the standard module.
+  /// Returns [name].
+  String module(String name) => name;
+
+  /// Translates the [keySingular] or [keyPlural] into the local language
+  /// using the namespace [module].
+  /// If [count] is 1 the translation of [keySingular] is returned.
+  /// Otherwise the translation of [keyPlural] is returned.
+  /// Returns the translation or (if not found) the key.
+  String ntr(String keySingular, String keyPlural, String module, int count) {
+    String? rc;
+    final map = mapPlural[module];
+    if (map == null) {
+      rc = removeInvisibleTag(count == 1 ? keySingular : keyPlural);
+    } else {
+      if (map.containsKey(keySingular)) {
+        final info = map[keySingular];
+        if (info!.list != null && count < info.list!.length) {
+          rc = info.list![count];
+        } else {
+          rc = count == 1 ? info.singular : info.plural;
+        }
+      } else {
+        if (module == globalModule) {
+          rc = removeInvisibleTag(count == 1 ? keySingular : keyPlural);
+        } else {
+          rc = ntr(keySingular, keyPlural, globalModule, count);
+        }
+      }
+    }
+    return rc;
+  }
+
+  /// If a key should be multiple times in the *.po file (same key must be
+  /// translated differently) it can be extended by an invisible tag at the end.
+  /// Than the key is unique.
+  /// Example of a tag: "<#3>".
+  /// This method removes the tag from the key.
+  String removeInvisibleTag(String key) {
+    String rc = key;
+    final match = regExpTag.firstMatch(key);
+    if (match != null) {
+      rc = key.substring(0, match.start);
+    }
+    return rc;
   }
 
   /// Translates the [key] into the local language using the namespace [module].
   /// Returns the translation or (if not found) the key.
   String tr(String key, [String module = '!global']) {
     String? rc;
-    final mapModule = modules[module];
-    if (mapModule != null) {
+    final mapModule = mapModules[module];
+    if (mapModule == null) {
+      rc = removeInvisibleTag(key);
+    } else {
       rc = mapModule[key];
       if (rc == null) {
-        if (module == '!global') {
-          rc = key;
+        if (module == globalModule) {
+          rc = removeInvisibleTag(key);
         } else {
-          rc = tr(key, '!global');
+          rc = tr(key, globalModule);
         }
       }
     }
-    return rc!;
+    return rc;
   }
 
   /// Translates the [key] into the local language using the namespace [module].
@@ -42,14 +97,11 @@ class I18N {
     }
     return rc;
   }
+}
 
-  /// Translates the [keySingular] or [keyPlural] into the local language
-  /// using the namespace [module].
-  /// If [count] is 1 the translation of [keySingular] is returned.
-  /// Otherwise the translation of [keyPlural] is returned.
-  /// Returns the translation or (if not found) the key.
-  String ntr(String keySingular, String keyPlural, String module, int count) {
-    String rc = tr(count == 1 ? keySingular : keyPlural, module);
-    return rc;
-  }
+class PluralInfo {
+  final String singular;
+  final String plural;
+  List<String>? list;
+  PluralInfo(this.singular, this.plural, this.list);
 }
diff --git a/lib/base/i18n_io.dart b/lib/base/i18n_io.dart
new file mode 100644 (file)
index 0000000..0f39dd2
--- /dev/null
@@ -0,0 +1,104 @@
+import 'dart:io';
+
+import 'package:dart_bones/dart_bones.dart';
+import 'package:path/path.dart';
+
+import 'i18n.dart';
+
+class I18nIo extends I18n {
+  String dataDirectory;
+  final regExpId = RegExp(r'^msgid "(.+?)"$');
+  final regExpTranslation = RegExp(r'^msgstr "(.+?)"$');
+  final regExpPlural = RegExp(r'^msg_plural "(.+?)"$');
+  final regExpList = RegExp(r'^msgstr\[\d+\] "(.+?)"$');
+  I18nIo.internal(this.dataDirectory, BaseLogger logger) : super.internal(logger);
+
+  /// Puts a [key] with its [translation] into the [mapModule].
+  /// Returns the [module] entry of [mapModule].
+  MapModule putKey(
+      String key, String translation, String module, MapModule? map) {
+    if (map == null) {
+      if (!mapModules.containsKey(module)) {
+        map = <String, String>{};
+        mapModules[module] = map;
+      } else {
+        map = mapModules[module];
+      }
+    }
+    map![key] = translation;
+    return map;
+  }
+
+  /// Puts a [key] with its [translation] into the [mapPlural].
+  /// Returns the [module] entry of [mapPlural].
+  MapPlural putPluralKey(String key, String translation, String plural,
+      String module, MapPlural? map) {
+    if (map == null) {
+      if (!mapPlural.containsKey(module)) {
+        map = <String, PluralInfo>{};
+        mapPlural[module] = map;
+      } else {
+        map = mapPlural[module];
+      }
+    }
+    map![key] = PluralInfo(translation, plural, null);
+    return map;
+  }
+
+  /// Converts a string given from a string constant.
+  /// Example: "a: \"b\"" returns 'a: "b"'
+  String fromConstant(String value) {
+    return value.replaceAll(r'\"', '"');
+  }
+
+  void readModule(String module, [String? locale]) {
+    locale ??= this.locale;
+    var file = File(join(dataDirectory, '$module.$locale.mo'));
+    if (!file.existsSync() && locale.length > 2) {
+      locale = locale.substring(0, 2);
+      file = File(join(dataDirectory, '$module.$locale.mo'));
+    }
+    if (!file.existsSync()) {
+      final lines = file.readAsLinesSync();
+      String lastKey = '';
+      String lastTranslation = '';
+      PluralInfo? lastEntry;
+      MapModule? map1;
+      MapPlural? map2;
+      RegExpMatch? match;
+      for (var line in lines) {
+        if (line.startsWith('#') || line.isEmpty) {
+          continue;
+        }
+        if ((match = regExpId.firstMatch(line)) != null) {
+          if (lastTranslation.isNotEmpty) {
+            map1 = putKey(lastKey, lastTranslation, module, map1);
+          }
+          lastKey = fromConstant(match!.group(1)!);
+          lastTranslation = '';
+          lastEntry = null;
+        } else if ((match = regExpTranslation.firstMatch(line)) != null) {
+          lastTranslation = fromConstant(match!.group(1)!);
+        } else if ((match = regExpPlural.firstMatch(line)) != null) {
+          map2 = putPluralKey(lastKey, lastTranslation,
+              fromConstant(match!.group(1)!), module, map2);
+          lastEntry = map2[lastKey];
+          lastKey = lastTranslation = '';
+        } else if ((match = regExpList.firstMatch(line)) != null) {
+          if (lastEntry != null) {
+            if (lastEntry.list == null) {
+              lastEntry.list = <String>[];
+              int count = lastEntry.list!.length;
+              if (count != int.parse(match!.group(1)!)){
+                logger.error('wrong index in $line. Expected: $count');
+              }
+              lastEntry.list!.add(fromConstant(match.group(2)!));
+            }
+          }
+        } else {
+          logger.error('unrecognized syntax: $line');
+        }
+      }
+    }
+  }
+}
index bedf989d4ef5cd22d988bc0c0c2da49e43aff1fc..7ef09551c0346eeec3576bb1a85d84ef6d247141 100644 (file)
@@ -1,5 +1,4 @@
 // DO NOT CHANGE. This file is created by the meta_tool
-import 'dart:io';
 import 'module_meta_data.dart';
 import 'users_meta.dart';
 /// Returns the meta data of the module given by [name].
index ef454ba3726b0ca0d15bb0875d665f6264c8dec9..8023c454045f6cfa5d95fe65afba248d69275eb4 100644 (file)
@@ -2,7 +2,7 @@ import '../base/defines.dart';
 import 'module_meta_data.dart';
 import '../base/i18n.dart';
 
-final i18N = I18N();
+final i18N = I18n();
 final M = i18N.module("Roles");
 
 class RoleMeta extends ModuleMetaData {
@@ -12,19 +12,19 @@ class RoleMeta extends ModuleMetaData {
           'Roles',
           [
             PropertyMetaData('id', DataType.reference, ':primary:', 'combo',
-                i18N.tr('Id', M)),
+                i18N.tr('Id')),
             PropertyMetaData(
-                'name', DataType.string, ':notnull:', '', i18N.tr('Name', M),
+                'name', DataType.string, ':notnull:', '', i18N.tr('Name'),
                 size: 64),
             PropertyMetaData('created', DataType.datetime, ':hidden:', '',
-                i18N.tr('Created', M)),
+                i18N.tr('Created')),
             PropertyMetaData('createdBy', DataType.string, ':hidden:', '',
-                i18N.tr('Created by', M),
+                i18N.tr('Created by'),
                 size: 32),
             PropertyMetaData('changed', DataType.datetime, ':hidden:', '',
-                i18N.tr('Changed', M)),
+                i18N.tr('Changed')),
             PropertyMetaData('changedBy', DataType.string, ':hidden:', '',
-                i18N.tr('Changed by', M),
+                i18N.tr('Changed by'),
                 size: 32),
           ],
         );
index 2d15ff94bcb20bc1e350b3d370ab414182c3f806..daec6321594024616a50c374abcc5e29d7096ac6 100644 (file)
@@ -1,7 +1,7 @@
 import '../base/defines.dart';
 import 'module_meta_data.dart';
 import '../base/i18n.dart';
-final i18N = I18N();
+final i18N = I18n();
 final M = i18N.module("Users");
 
 class UserMeta extends ModuleMetaData {
@@ -11,9 +11,9 @@ class UserMeta extends ModuleMetaData {
           'Users',
           [
             PropertyMetaData('id', DataType.reference, ':primary:', 'combo',
-                i18N.tr('Id', M)),
+                i18N.tr('Id')),
             PropertyMetaData('name', DataType.string, ':notnull:', '',
-                i18N.tr('Name', M),
+                i18N.tr('Name'),
                 size: 64),
             PropertyMetaData('displayName', DataType.string, ':unique:notnull:',
                 '', i18N.tr('Display name', M),
@@ -22,16 +22,16 @@ class UserMeta extends ModuleMetaData {
                 i18N.tr('EMail', M),
                 size: 255),
             PropertyMetaData('role', DataType.reference, ':notnull:', '',
-                i18N.tr('Role', M), reference: 'roles.role_id'),
+                i18N.tr('Role'), reference: 'roles.role_id'),
             PropertyMetaData('created', DataType.datetime, ':hidden:', '',
-                i18N.tr('Created', M)),
+                i18N.tr('Created')),
             PropertyMetaData('createdBy', DataType.string, ':hidden:', '',
-                i18N.tr('Created by', M),
+                i18N.tr('Created by'),
                 size: 32),
             PropertyMetaData('changed', DataType.datetime, ':hidden:', '',
-                i18N.tr('Changed', M)),
+                i18N.tr('Changed')),
             PropertyMetaData('changedBy', DataType.string, ':hidden:', '',
-                i18N.tr('Changed by', M),
+                i18N.tr('Changed by'),
                 size: 32),
           ],
         );
index d8848ab1b473aa06a4a72311ac0a6334c3b3a0bd..72cfb38473b52b40cb95188e01b6428861e2024e 100644 (file)
@@ -1,29 +1,30 @@
 ---
+# DO NOT CHANGE. This file is created by the meta_tool
 # SQL statements of the module "Users":
+
 module: Users
 list:
   type: list
   parameters: []
-  sql: select * from loginusers;
+  sql: "select * from Users;"
 byId:
   type: record
-  parameters: [ ":id" ]
-  sql: "select * from loginusers where user_id=:id;"
+  parameters: [ "id" ]
+  sql: "select * from Users where user_id=:id;"
 delete:
   type: delete
-  parameters: [ ":id" ]
-  sql: "delete * from loginusers where user_id=:id;"
+  parameters: [ "id" ]
+  sql: "delete * from Users where user_id=:id;"
 update:
   type: update
-  parameters: [":id", ":name", ":displayname", ":email", ":changedby"]
-  sql: "UPDATE loginusers SET
-    user_name=:name, user_displayname=:displayname, user_email=:email, user_changed=NOW(), user_changedby=:changedby
+  parameters: [":id",":name",":displayName",":email",":role",":changedBy"]
+  sql: "UPDATE Users SET
+    user_id=:id,user_name=:name,user_displayname=:displayName,user_email=:email,
+    user_role=:role,user_changedby=:changedBy,user_changed=NOW()
     WHERE user_id=:id;"
 insert:
   type: insert
-  parameters: [":name", ":displayname", ":email", ":createdby"]
-  sql: "INSERT INTO loginusers(user_name, user_displayname, user_email, user_changedby)
-    VALUES(:name, :displayname, :email, NOW(), :createdby);"
-
-
-
+  parameters: [":id",":name",":displayName",":email",":role",":createdBy"]
+  sql: "INSERT INTO Users(user_id,user_name,user_displayname,user_email,
+      user_role,user_createdby,user_created)
+    VALUES(:id,:name,:displayName,:email,:role,:createdBy,NOW());"
diff --git a/test/i18n_test.dart b/test/i18n_test.dart
new file mode 100644 (file)
index 0000000..4f1cac7
--- /dev/null
@@ -0,0 +1,109 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:path/path.dart';
+
+import '../dart_tools/bin/i18n_text_parser.dart';
+
+void main() {
+  final logger = MemoryLogger(LEVEL_FINE);
+  FileSync.initialize(logger);
+  final fileSync = FileSync();
+  final baseDir = init(logger);
+  final targetDir = join(baseDir, nodeTarget);
+  test('parse', () {
+    final parser = I18nTextParser(logger);
+    parser.scanDirectory(baseDir, 0);
+    parser.writePot(targetDir);
+    expect(parser.mapModules, isNotEmpty);
+    expect(parser.mapModules.containsKey('Example'), isTrue);
+    expect(parser.mapModules.containsKey('!global'), isTrue);
+    expect(parser.mapModules.containsKey('Sample'), isTrue);
+    expect(parser.mapModules['Example']!.containsKey('introduction'), isTrue);
+    expect(parser.mapModules['Sample']!.containsKey('info'), isTrue);
+    var fn = join(targetDir, 'Example.pot');
+    var content = fileSync.fileAsString(fn);
+    expect(content, '''# Texts of module Example, parsed by i18n_text_parser
+
+#: /tmp/unittest/i18n/lib/base/simple.dart:5
+msgid "description"
+msgstr ""
+
+#: /tmp/unittest/i18n/lib/base/simple.dart:5
+msgid "introduction"
+msgstr ""
+''');
+    fn = join(targetDir, 'Sample.pot');
+    content = fileSync.fileAsString(fn);
+    expect(content, '''# Texts of module Sample, parsed by i18n_text_parser
+
+#: /tmp/unittest/i18n/lib/base/args.dart:8
+msgid "Name: {0} Role: {1}"
+msgstr ""
+
+#: /tmp/unittest/i18n/lib/base/args.dart:8
+msgid "info"
+msgstr ""
+
+#: /tmp/unittest/i18n/lib/base/args.dart:4
+msgid "one piece"
+msgid_plural "%d pieces"
+msgstr[0] ""
+msgstr[1] ""
+''');
+    fn = join(targetDir, '!global.pot');
+    content = fileSync.fileAsString(fn);
+    expect(content, '''# Texts of module !global, parsed by i18n_text_parser
+
+#: /tmp/unittest/i18n/lib/base/simple.dart:11
+#: /tmp/unittest/i18n/lib/base/args.dart:12
+msgid "Status line"
+msgstr ""
+''');
+    fn = join(targetDir, 'Statistic.pot');
+    content = fileSync.fileAsString(fn);
+    expect(content, '''# Texts of module Statistic, parsed by i18n_text_parser
+
+#: /tmp/unittest/i18n/lib/base/simple.dart:9
+msgid "one piece"
+msgid_plural "%d pieces"
+msgstr[0] ""
+msgstr[1] ""
+''');
+  });
+}
+
+const nodeTarget = 'output';
+
+String init(MemoryLogger logger) {
+  final fileSync = FileSync();
+  final baseDir = fileSync.tempDirectory('i18n', subDirs: 'unittest');
+  final subDir = fileSync.tempDirectory('base', subDirs: 'unittest/i18n/lib');
+  fileSync.toFile(join(subDir, 'simple.dart'), r'''import 'dart:io';
+final i18N = I18N();
+final M = i18N.module('Example');
+String header(){
+  return i18N.tr('introduction', M) + " " + i18N.tr(
+    "description",
+    M);
+}
+String count() => i18N.ntr('one piece', '%d pieces', "Statistic", 3);
+String footer(){
+  return I18N().tr("Status line");
+}
+''');
+  fileSync.toFile(join(subDir, 'args.dart'), r'''import 'dart:io';
+final i18N = I18N();
+final M = i18N.module("Sample");
+String count() => i18N.ntr(
+  'one piece',
+  '%d pieces', M, countItems('any'));
+String header(String user, String role){
+  return i18N.trArgs('Name: {0} Role: {1}', M, [user, role]) + " " + i18N.tr(
+    r'info', M);
+}
+String footer(){
+  return I18N().tr('Status line');
+}
+''');
+  return baseDir;
+}
index fcba74705580566767390aac1dc1a8a0e8a74dcc..7f2de1304d2557df50b01b062d090e8cab42aff3 100644 (file)
@@ -1,8 +1,10 @@
+import 'dart:io';
+
 import 'package:dart_bones/dart_bones.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:path/path.dart';
 
-import "../bin/i18n_text_parser.dart";
+import '../dart_tools/bin/i18n_text_parser.dart';
 
 void main() {
   final logger = MemoryLogger(LEVEL_FINE);
@@ -10,9 +12,16 @@ void main() {
   final fileSync = FileSync();
   final baseDir = init(logger);
   final targetDir = join(baseDir, nodeTarget);
+  fileSync.ensureDirectory(targetDir);
+  test('example-config', (){
+    final parser = I18nTextParser(logger);
+    parser.exampleConfiguration(targetDir, 'de_DE');
+    expect(File(join(targetDir, 'project.i18n.yaml')).existsSync(), isTrue);
+    expect(parser.fetchConfiguration(targetDir, locale: 'de_DE'), isNotNull);
+  });
   test('parse', () {
     final parser = I18nTextParser(logger);
-    parser.scanDirectory(baseDir);
+    parser.scanDirectory(baseDir, 0);
     parser.writePot(targetDir);
     expect(parser.mapModules, isNotEmpty);
     expect(parser.mapModules.containsKey('Example'), isTrue);
@@ -24,11 +33,11 @@ void main() {
     var content = fileSync.fileAsString(fn);
     expect(content, '''# Texts of module Example, parsed by i18n_text_parser
 
-#: /tmp/unittest/i18n/lib/base/simple.dart:5
+#: $baseDir/lib/base/simple.dart:5
 msgid "description"
 msgstr ""
 
-#: /tmp/unittest/i18n/lib/base/simple.dart:5
+#: $baseDir/lib/base/simple.dart:5
 msgid "introduction"
 msgstr ""
 ''');
@@ -36,15 +45,15 @@ msgstr ""
     content = fileSync.fileAsString(fn);
     expect(content, '''# Texts of module Sample, parsed by i18n_text_parser
 
-#: /tmp/unittest/i18n/lib/base/args.dart:8
+#: $baseDir/lib/base/args.dart:8
 msgid "Name: {0} Role: {1}"
 msgstr ""
 
-#: /tmp/unittest/i18n/lib/base/args.dart:8
+#: $baseDir/lib/base/args.dart:8
 msgid "info"
 msgstr ""
 
-#: /tmp/unittest/i18n/lib/base/args.dart:4
+#: $baseDir/lib/base/args.dart:4
 msgid "one piece"
 msgid_plural "%d pieces"
 msgstr[0] ""
@@ -54,16 +63,67 @@ msgstr[1] ""
     content = fileSync.fileAsString(fn);
     expect(content, '''# Texts of module !global, parsed by i18n_text_parser
 
-#: /tmp/unittest/i18n/lib/base/simple.dart:11
-#: /tmp/unittest/i18n/lib/base/args.dart:12
+#: $baseDir/lib/base/simple.dart:11
+#: $baseDir/lib/base/args.dart:12
 msgid "Status line"
 msgstr ""
 ''');
     fn = join(targetDir, 'Statistic.pot');
     content = fileSync.fileAsString(fn);
-    expect(content, '''# Texts of module Statistic, parsed by i18n_text_parser
+    expect(content.replaceAll(RegExp(r'Date: 2[-+\w2: ]+'), 'Date: *'),
+        r'''# Texts of module Statistic, created by i18n_text_parser');
+# Copyright (C) 2021-2021 J. Hamatoma <cr@hamatoma.de>
+# License: CC0 1.0 Universal
+# J. Hamatoma <author@hamatoma.de>, 2021.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: <PROJECT>\n"
+"Report-Msgid-Bugs-To: J. Hamatoma <bug@hamatoma.de>\n"
+"POT-Creation-Date: *\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: LANGUAGE\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: /tmp/unittest/i18n_parser/lib/base/simple.dart:9
+msgid "one piece"
+msgid_plural "%d pieces"
+msgstr[0] ""
+msgstr[1] ""
+''');
+  });
+  test('msg-merge', (){
+    final parser = I18nTextParser(logger);
+    parser.msgInit(targetDir, 'Statistic', 'de_DE');
+    final fn = join(targetDir, 'Statistic.de_DE.po');
+    final content = fileSync.fileAsString(fn);
+    expect(content.replaceAll(RegExp(r'Date: 2[-+\w2: ]+'), 'Date: *'),
+        r'''# Texts of module Statistic, created by i18n_text_parser');
+# Copyright (C) 2021-2021 J. Hamatoma <cr@hamatoma.de>
+# License: CC0 1.0 Universal
+# J. Hamatoma <author@hamatoma.de>, 2021.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: <PROJECT>\n"
+"Report-Msgid-Bugs-To: J. Hamatoma <bug@hamatoma.de>\n"
+"POT-Creation-Date: *\n"
+"PO-Revision-Date: *\n"
+"Last-Translator: J. Hamatoma <lang@hamatoma.de>\n"
+"Language-Team: German J. Hamatoma <de@hamatoma.de>\n"
+"Language: German\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: /tmp/unittest/i18n/lib/base/simple.dart:9
+#: /tmp/unittest/i18n_parser/lib/base/simple.dart:9
 msgid "one piece"
 msgid_plural "%d pieces"
 msgstr[0] ""
@@ -76,8 +136,9 @@ const nodeTarget = 'output';
 
 String init(MemoryLogger logger) {
   final fileSync = FileSync();
-  final baseDir = fileSync.tempDirectory('i18n', subDirs: 'unittest');
-  final subDir = fileSync.tempDirectory('base', subDirs: 'unittest/i18n/lib');
+  final baseDir = fileSync.tempDirectory('i18n_parser', subDirs: 'unittest');
+  final subDir =
+      fileSync.tempDirectory('base', subDirs: 'unittest/i18n_parser/lib');
   fileSync.toFile(join(subDir, 'simple.dart'), r'''import 'dart:io';
 final i18N = I18N();
 final M = i18N.module('Example');
index f214344a64dd3aa1af0eb7ddb3b62f171f76632d..eb93497e32d34c5c5c125e641c63460901135ffa 100644 (file)
@@ -2,7 +2,7 @@ import 'package:dart_bones/dart_bones.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:path/path.dart';
 
-import "../bin/yaml_merger.dart";
+import '../dart_tools/bin/yaml_merger.dart';
 
 const nodePrecedence = 'precedence';
 const nodeTarget = 'output';
diff --git a/tools/CompileMerge b/tools/CompileMerge
deleted file mode 100755 (executable)
index e3e62dd..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-#! /bin/bash
-APP=yaml_merger
-TRG=tools/$APP
-dart compile exe bin/$APP.dart -o $TRG
-ls -ld $TRG
diff --git a/tools/UpdateI18n b/tools/UpdateI18n
new file mode 100755 (executable)
index 0000000..4b36920
--- /dev/null
@@ -0,0 +1,35 @@
+#! /bin/bash
+LOCALE=de_DE
+if [ -n "$1" ]; then
+  LOCALE=$1
+fi
+function Usage() {
+  echo "Usage: UpdateI18n [<locale>]"
+  echo "  Creates/updates the translation data."
+  echo "  Precondition: current directory must contain data/i18n"
+  echo "<locale>: the locale id, e.g. "de_DE". Default: de_DE"
+  echo "Example:"
+  echo "UpdateI18n de_DE"
+  echo "+++ $*"
+}
+function Update(){
+  local locale=$1
+  cd data/i18n
+  for potFile in *.pot; do
+    local name=${potFile/.pot/}
+    if [ ! -e $name.$LOCALE.po ]; then
+      echo "creating $name.$locale.po"
+      i18n_text_parser msg-init . $name $locale
+    else
+      echo "updating $name.$locale.po"
+      msgmerge --update $name.$locale.po $potFile
+    fi
+  done
+}
+if [ ! -d data/i18n ]; then
+  Usage "missing data/i18n. Wrong current directory?"
+elif [ $(expr $LOCALE : [a-z][a-z]) = 0 -o $(expr $LOCALE : [a-z][a-z]_[A-Z][A-Z]) = 0 ]; then
+  Usage "illegal <locale>: $LOCALE"
+else
+  Update $LOCALE
+fi
\ No newline at end of file