From d61c75d33f6cadc9e9c2e9206e13bacf5701f2eb Mon Sep 17 00:00:00 2001 From: Hamatoma Date: Tue, 10 Aug 2021 16:36:00 +0200 Subject: [PATCH] i18n_text_parser, dart_tools * improvements in i18n_text_parser: subcommands "parse", "example" and "msg-init" * yaml_merger and i18n_text_parser moved to directory dart_tools --- .gitignore | 8 + bin/generator.dart | 17 +- bin/i18n_text_parser.dart | 352 -------------- bin/meta_tool.dart | 8 +- dart_tools/bin/i18n_text_parser.dart | 595 +++++++++++++++++++++++ {bin => dart_tools/bin}/yaml_merger.dart | 0 dart_tools/pubspec.yaml | 19 + dart_tools/tools/CompileI18n | 5 + {tools => dart_tools/tools}/CompileMerge | 0 data/i18n/!global.de_DE.po | 45 ++ data/i18n/Users.de_DE.po | 25 + data/i18n/project.i18n.yaml | 15 + lib/base/i18n.dart | 98 +++- lib/base/i18n_io.dart | 104 ++++ lib/meta/modules.dart | 1 - lib/meta/roles_meta.dart | 14 +- lib/meta/users_meta.dart | 16 +- rest_server/data/sql/users.sql.yaml | 29 +- test/i18n_test.dart | 109 +++++ test/i18n_text_parser_test.dart | 87 +++- test/yaml_merger_test.dart | 2 +- tools/UpdateI18n | 35 ++ 22 files changed, 1155 insertions(+), 429 deletions(-) delete mode 100644 bin/i18n_text_parser.dart create mode 100644 dart_tools/bin/i18n_text_parser.dart rename {bin => dart_tools/bin}/yaml_merger.dart (100%) create mode 100644 dart_tools/pubspec.yaml create mode 100755 dart_tools/tools/CompileI18n rename {tools => dart_tools/tools}/CompileMerge (100%) create mode 100644 data/i18n/!global.de_DE.po create mode 100644 data/i18n/Users.de_DE.po create mode 100644 data/i18n/project.i18n.yaml create mode 100644 lib/base/i18n_io.dart create mode 100644 test/i18n_test.dart create mode 100755 tools/UpdateI18n diff --git a/.gitignore b/.gitignore index e75c7bd..d3472d3 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/bin/generator.dart b/bin/generator.dart index 4cfb235..a6fa433 100644 --- a/bin/generator.dart +++ b/bin/generator.dart @@ -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 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 index c8e56c8..0000000 --- a/bin/i18n_text_parser.dart +++ /dev/null @@ -1,352 +0,0 @@ -import "dart:io"; - -import 'package:dart_bones/dart_bones.dart'; -import "package:path/path.dart"; - -void main(List args) {} - -class I18nTextParser { - final globalModule = '!global'; - final errorModule = '!error'; - final BaseLogger logger; - final mapModules = >{}; - final mapPlurals = >{}; - final foundFiles = {}; - var regExpFiles = RegExp(r'.dart$'); - String currentModule = ''; - final moduleVariables = {}; - String currentFile = ''; - String currentArguments = ''; - List 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 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] = {}; - } - 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 = {}; - for (var module in mapModules.keys) { - doneModules.add(module); - final map = mapModules[module]!; - final keys = map.keys.toList(); - Map? 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 references; - final String keyPlural; - PluralValue(this.keyPlural, this.references); -} diff --git a/bin/meta_tool.dart b/bin/meta_tool.dart index 13af6c5..ef419ea 100644 --- a/bin/meta_tool.dart +++ b/bin/meta_tool.dart @@ -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 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/dart_tools/bin/i18n_text_parser.dart b/dart_tools/bin/i18n_text_parser.dart new file mode 100644 index 0000000..a816ed2 --- /dev/null +++ b/dart_tools/bin/i18n_text_parser.dart @@ -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 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 args) { + if (args.length != 3) { + usage('wrong count of arguments'); + } else if (!Directory(args[0]).existsSync()) { + usage(' is not a directory: ${args[0]}'); + } else if (!File(join(args[0], args[1] + '.pot')).existsSync()) { + usage('wrong : missing .pot'); + } else if (RegExp(r'[a-z]{2}(_[A-Z]{2})?').firstMatch(args[2]) == null) { + usage('wrong : 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 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 [] + Parses recursively the Dart files of the and create the *.pot + files in the . + : + "": 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 + Creates the ..po file from the .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 = >{}; + final mapPlurals = >{}; + final foundFiles = {}; + var regExpFiles = RegExp(r'.dart$'); + String currentModule = ''; + final moduleVariables = {}; + String currentFile = ''; + String currentArguments = ''; + List 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: "" + version: "1.0.0" +author: "J. Hamatoma " +translator: "J. Hamatoma " +$locale: + language: "German" + name: "J. Hamatoma " +copyright: + name: "J. Hamatoma " + start: 2021 +license: "CC0 1.0 Universal" +bugResponsible: "J. Hamatoma " +'''); + } + + /// 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 \\n" +"Language-Team: LANGUAGE \\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 '; + 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 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] = {}; + } + 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 = {}; + for (var module in mapModules.keys) { + doneModules.add(module); + final map = mapModules[module]!; + final keys = map.keys.toList(); + Map? 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 references; + final String keyPlural; + PluralValue(this.keyPlural, this.references); +} diff --git a/bin/yaml_merger.dart b/dart_tools/bin/yaml_merger.dart similarity index 100% rename from bin/yaml_merger.dart rename to dart_tools/bin/yaml_merger.dart diff --git a/dart_tools/pubspec.yaml b/dart_tools/pubspec.yaml new file mode 100644 index 0000000..7aec104 --- /dev/null +++ b/dart_tools/pubspec.yaml @@ -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 index 0000000..61ae0ec --- /dev/null +++ b/dart_tools/tools/CompileI18n @@ -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/tools/CompileMerge b/dart_tools/tools/CompileMerge similarity index 100% rename from tools/CompileMerge rename to dart_tools/tools/CompileMerge diff --git a/data/i18n/!global.de_DE.po b/data/i18n/!global.de_DE.po new file mode 100644 index 0000000..ada29cf --- /dev/null +++ b/data/i18n/!global.de_DE.po @@ -0,0 +1,45 @@ +# Header entry was created by Lokalize. +# +# Texts of module !global, parsed by i18n_text_parser +# J. Hamatoma , 2021. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2021-08-11 10:55+0200\n" +"Last-Translator: J. Hamatoma \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 index 0000000..e25e844 --- /dev/null +++ b/data/i18n/Users.de_DE.po @@ -0,0 +1,25 @@ +# Header entry was created by Lokalize. +# +# Texts of module Users, parsed by i18n_text_parser +# J. Hamatoma , 2021. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"PO-Revision-Date: 2021-08-11 10:54+0200\n" +"Last-Translator: J. Hamatoma \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 index 0000000..455c856 --- /dev/null +++ b/data/i18n/project.i18n.yaml @@ -0,0 +1,15 @@ +--- +# Configuration of the I18N data: +project: + name: "Exhibition" + version: "1.0.0" +author: "J. Hamatoma " +translator: "J. Hamatoma " +de_DE: + language: "German" + name: "J. Hamatoma " +copyright: + name: "J. Hamatoma " + start: 2021 +license: "CC0 1.0 Universal" +bugResponsible: "J. Hamatoma " diff --git a/lib/base/i18n.dart b/lib/base/i18n.dart index b70b2f6..ebb313d 100644 --- a/lib/base/i18n.dart +++ b/lib/base/i18n.dart @@ -1,33 +1,88 @@ -class I18N { - static I18N? instance; - Map> modules = {}; - String locale = 'de'; - factory I18N() { +import 'package:dart_bones/dart_bones.dart'; +typedef MapModule = Map; +typedef MapPlural = Map; +class I18n { + static I18n? instance; + final BaseLogger logger; + final globalModule = '!global'; + Map mapModules = {}; + Map 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? 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 index 0000000..0f39dd2 --- /dev/null +++ b/lib/base/i18n_io.dart @@ -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 = {}; + 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 = {}; + 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 = []; + 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'); + } + } + } + } +} diff --git a/lib/meta/modules.dart b/lib/meta/modules.dart index bedf989..7ef0955 100644 --- a/lib/meta/modules.dart +++ b/lib/meta/modules.dart @@ -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]. diff --git a/lib/meta/roles_meta.dart b/lib/meta/roles_meta.dart index ef454ba..8023c45 100644 --- a/lib/meta/roles_meta.dart +++ b/lib/meta/roles_meta.dart @@ -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), ], ); diff --git a/lib/meta/users_meta.dart b/lib/meta/users_meta.dart index 2d15ff9..daec632 100644 --- a/lib/meta/users_meta.dart +++ b/lib/meta/users_meta.dart @@ -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), ], ); diff --git a/rest_server/data/sql/users.sql.yaml b/rest_server/data/sql/users.sql.yaml index d8848ab..72cfb38 100644 --- a/rest_server/data/sql/users.sql.yaml +++ b/rest_server/data/sql/users.sql.yaml @@ -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 index 0000000..4f1cac7 --- /dev/null +++ b/test/i18n_test.dart @@ -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; +} diff --git a/test/i18n_text_parser_test.dart b/test/i18n_text_parser_test.dart index fcba747..7f2de13 100644 --- a/test/i18n_text_parser_test.dart +++ b/test/i18n_text_parser_test.dart @@ -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 +# License: CC0 1.0 Universal +# J. Hamatoma , 2021. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: J. Hamatoma \n" +"POT-Creation-Date: *\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \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 +# License: CC0 1.0 Universal +# J. Hamatoma , 2021. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: J. Hamatoma \n" +"POT-Creation-Date: *\n" +"PO-Revision-Date: *\n" +"Last-Translator: J. Hamatoma \n" +"Language-Team: German J. Hamatoma \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'); diff --git a/test/yaml_merger_test.dart b/test/yaml_merger_test.dart index f214344..eb93497 100644 --- a/test/yaml_merger_test.dart +++ b/test/yaml_merger_test.dart @@ -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/UpdateI18n b/tools/UpdateI18n new file mode 100755 index 0000000..4b36920 --- /dev/null +++ b/tools/UpdateI18n @@ -0,0 +1,35 @@ +#! /bin/bash +LOCALE=de_DE +if [ -n "$1" ]; then + LOCALE=$1 +fi +function Usage() { + echo "Usage: UpdateI18n []" + echo " Creates/updates the translation data." + echo " Precondition: current directory must contain data/i18n" + echo ": 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" +else + Update $LOCALE +fi \ No newline at end of file -- 2.39.5