From 55b8ea2d42f82a418fd17f1219e1be0311ee16d3 Mon Sep 17 00:00:00 2001 From: nikaera Date: Tue, 9 Jan 2024 10:25:55 +0900 Subject: [PATCH 001/118] feat(file_utils): add support for CSV files (#185) --- slang/lib/builder/utils/file_utils.dart | 50 ++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/slang/lib/builder/utils/file_utils.dart b/slang/lib/builder/utils/file_utils.dart index 2a3fa5cb..cbccbce5 100644 --- a/slang/lib/builder/utils/file_utils.dart +++ b/slang/lib/builder/utils/file_utils.dart @@ -44,7 +44,55 @@ class FileUtils { } return json2yaml(content, yamlStyle: YamlStyle.generic); case FileType.csv: - throw UnimplementedError('CSV is not supported yet'); + String escapeRow(String value) { + final escaped = value.replaceAll('"', '""'); + if (escaped.contains(RegExp(r'[,"]'))) { + return '"$escaped"'; + } + return escaped; + } + + Map encodeRow({ + String key = '', + required dynamic value, + }) { + if (value is Map) { + final keyPrefix = key.isEmpty ? '' : '$key.'; + return value.map((k, v) { + final map = encodeRow(key: '$keyPrefix$k', value: v); + final mapEntry = map.entries.first; + + return MapEntry(mapEntry.key, mapEntry.value); + }); + } else if (value is String) { + return {key: escapeRow(value)}; + } + return {}; + } + + final Map> columns = {}; + if (content.containsKey(INFO_KEY)) { + final info = content.remove(INFO_KEY); + columns[INFO_KEY] = {INFO_KEY: escapeRow(info.join('\\n'))}; + } + content.entries.forEach((e) { + columns[e.key] = encodeRow(value: e.value); + }); + + // get all translation keys + final translationKeys = columns.values + .map((e) => e.entries.map((e) => e.key)) + .expand((e) => e) + .toSet(); + + final headers = ['key', ...columns.keys].join(','); + final rows = translationKeys.map((key) { + final values = + columns.values.map((e) => e.containsKey(key) ? e[key] : ''); + return "$key,${values.join(',')}"; + }); + + return "$headers\n${rows.join('\n')}"; case FileType.arb: // this encoder does not append \n automatically return JsonEncoder.withIndent(' ').convert(content) + '\n'; From 0fb64cec2522e65f63704b9df8935538fbda3227 Mon Sep 17 00:00:00 2001 From: Cardin Menkemeller <41063241+cmenkemeller@users.noreply.github.com> Date: Wed, 17 Jan 2024 18:48:44 -0500 Subject: [PATCH 002/118] Generated File "part of" import fix (#187) --- slang/lib/builder/builder/generate_config_builder.dart | 1 + slang/lib/builder/generator/generate_translation_map.dart | 2 +- slang/lib/builder/generator/generate_translations.dart | 2 +- slang/lib/builder/model/generate_config.dart | 2 ++ slang/lib/builder/model/raw_config.dart | 3 ++- slang/test/integration/main/json_multiple_files_test.dart | 1 + slang/test/integration/resources/main/_expected_de.output | 2 +- slang/test/integration/resources/main/_expected_en.output | 2 +- slang/test/integration/resources/main/_expected_map.output | 2 +- 9 files changed, 11 insertions(+), 6 deletions(-) diff --git a/slang/lib/builder/builder/generate_config_builder.dart b/slang/lib/builder/builder/generate_config_builder.dart index a8e54f8a..fa2446af 100644 --- a/slang/lib/builder/builder/generate_config_builder.dart +++ b/slang/lib/builder/builder/generate_config_builder.dart @@ -19,6 +19,7 @@ class GenerateConfigBuilder { baseName: baseName, baseLocale: config.baseLocale, fallbackStrategy: config.fallbackStrategy.toGenerateFallbackStrategy(), + outputFileName: config.outputFileName, outputFormat: config.outputFormat, localeHandling: config.localeHandling, flutterIntegration: config.flutterIntegration, diff --git a/slang/lib/builder/generator/generate_translation_map.dart b/slang/lib/builder/generator/generate_translation_map.dart index 07bf9572..a8a4cfa9 100644 --- a/slang/lib/builder/generator/generate_translation_map.dart +++ b/slang/lib/builder/generator/generate_translation_map.dart @@ -8,7 +8,7 @@ String generateTranslationMap( if (config.outputFormat == OutputFormat.multipleFiles) { // this is a part file - buffer.writeln('part of \'${config.baseName}.g.dart\';'); + buffer.writeln('part of \'${config.outputFileName}\';'); buffer.writeln(); } diff --git a/slang/lib/builder/generator/generate_translations.dart b/slang/lib/builder/generator/generate_translations.dart index 1d1af762..f1f6187b 100644 --- a/slang/lib/builder/generator/generate_translations.dart +++ b/slang/lib/builder/generator/generate_translations.dart @@ -28,7 +28,7 @@ String generateTranslations(GenerateConfig config, I18nData localeData) { if (config.outputFormat == OutputFormat.multipleFiles) { // this is a part file - buffer.writeln('part of \'${config.baseName}.g.dart\';'); + buffer.writeln('part of \'${config.outputFileName}\';'); } queue.add(ClassTask( diff --git a/slang/lib/builder/model/generate_config.dart b/slang/lib/builder/model/generate_config.dart index 9b1a7239..ea6928bf 100644 --- a/slang/lib/builder/model/generate_config.dart +++ b/slang/lib/builder/model/generate_config.dart @@ -13,6 +13,7 @@ class GenerateConfig { final String baseName; // name of all i18n files, like strings or messages final I18nLocale baseLocale; // defaults to 'en' final GenerateFallbackStrategy fallbackStrategy; + final String outputFileName; final OutputFormat outputFormat; final bool localeHandling; final bool flutterIntegration; @@ -35,6 +36,7 @@ class GenerateConfig { required this.baseName, required this.baseLocale, required this.fallbackStrategy, + required this.outputFileName, required this.outputFormat, required this.localeHandling, required this.flutterIntegration, diff --git a/slang/lib/builder/model/raw_config.dart b/slang/lib/builder/model/raw_config.dart index 2383a2c5..53e5037a 100644 --- a/slang/lib/builder/model/raw_config.dart +++ b/slang/lib/builder/model/raw_config.dart @@ -120,6 +120,7 @@ class RawConfig { I18nLocale? baseLocale, FallbackStrategy? fallbackStrategy, String? inputFilePattern, + String? outputFileName, OutputFormat? outputFormat, bool? localeHandling, bool? flutterIntegration, @@ -145,7 +146,7 @@ class RawConfig { inputDirectory: inputDirectory, inputFilePattern: inputFilePattern ?? this.inputFilePattern, outputDirectory: outputDirectory, - outputFileName: outputFileName, + outputFileName: outputFileName ?? this.outputFileName, outputFormat: outputFormat ?? this.outputFormat, localeHandling: localeHandling ?? this.localeHandling, flutterIntegration: flutterIntegration ?? this.flutterIntegration, diff --git a/slang/test/integration/main/json_multiple_files_test.dart b/slang/test/integration/main/json_multiple_files_test.dart index 797035d7..71267192 100644 --- a/slang/test/integration/main/json_multiple_files_test.dart +++ b/slang/test/integration/main/json_multiple_files_test.dart @@ -31,6 +31,7 @@ void main() { final result = GeneratorFacade.generate( rawConfig: RawConfigBuilder.fromYaml(buildYaml)!.copyWith( outputFormat: OutputFormat.multipleFiles, + outputFileName: 'translations.cgm.dart', ), baseName: 'translations', translationMap: TranslationMap() diff --git a/slang/test/integration/resources/main/_expected_de.output b/slang/test/integration/resources/main/_expected_de.output index ea45709d..f1b7a5fa 100644 --- a/slang/test/integration/resources/main/_expected_de.output +++ b/slang/test/integration/resources/main/_expected_de.output @@ -1,4 +1,4 @@ -part of 'translations.g.dart'; +part of 'translations.cgm.dart'; // Path: class _TranslationsDe implements Translations { diff --git a/slang/test/integration/resources/main/_expected_en.output b/slang/test/integration/resources/main/_expected_en.output index 0e4b6baa..c9591609 100644 --- a/slang/test/integration/resources/main/_expected_en.output +++ b/slang/test/integration/resources/main/_expected_en.output @@ -1,4 +1,4 @@ -part of 'translations.g.dart'; +part of 'translations.cgm.dart'; // Path: class Translations implements BaseTranslations { diff --git a/slang/test/integration/resources/main/_expected_map.output b/slang/test/integration/resources/main/_expected_map.output index 057cab17..24fd82e1 100644 --- a/slang/test/integration/resources/main/_expected_map.output +++ b/slang/test/integration/resources/main/_expected_map.output @@ -1,4 +1,4 @@ -part of 'translations.g.dart'; +part of 'translations.cgm.dart'; /// Flat map(s) containing all translations. /// Only for edge cases! For simple maps, use the map function of this library. From 0c03a57fbd95646ac6d9a3a01ac8c352f940d185 Mon Sep 17 00:00:00 2001 From: Cardin Menkemeller <41063241+cmenkemeller@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:23:38 -0500 Subject: [PATCH 003/118] Added linter ignore and coverage ignore for multiple file generation (#188) --- .../builder/generator/generate_translation_map.dart | 10 +++++++++- slang/lib/builder/generator/generate_translations.dart | 10 +++++++++- .../integration/resources/main/_expected_de.output | 6 ++++++ .../integration/resources/main/_expected_en.output | 6 ++++++ .../integration/resources/main/_expected_map.output | 6 ++++++ 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/slang/lib/builder/generator/generate_translation_map.dart b/slang/lib/builder/generator/generate_translation_map.dart index a8a4cfa9..9b49358c 100644 --- a/slang/lib/builder/generator/generate_translation_map.dart +++ b/slang/lib/builder/generator/generate_translation_map.dart @@ -8,7 +8,15 @@ String generateTranslationMap( if (config.outputFormat == OutputFormat.multipleFiles) { // this is a part file - buffer.writeln('part of \'${config.outputFileName}\';'); + + buffer.writeln(''' +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint + +part of \'${config.outputFileName}\';'''); buffer.writeln(); } diff --git a/slang/lib/builder/generator/generate_translations.dart b/slang/lib/builder/generator/generate_translations.dart index f1f6187b..f1159658 100644 --- a/slang/lib/builder/generator/generate_translations.dart +++ b/slang/lib/builder/generator/generate_translations.dart @@ -28,7 +28,15 @@ String generateTranslations(GenerateConfig config, I18nData localeData) { if (config.outputFormat == OutputFormat.multipleFiles) { // this is a part file - buffer.writeln('part of \'${config.outputFileName}\';'); + + buffer.writeln(''' +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint + +part of \'${config.outputFileName}\';'''); } queue.add(ClassTask( diff --git a/slang/test/integration/resources/main/_expected_de.output b/slang/test/integration/resources/main/_expected_de.output index f1b7a5fa..10255c36 100644 --- a/slang/test/integration/resources/main/_expected_de.output +++ b/slang/test/integration/resources/main/_expected_de.output @@ -1,3 +1,9 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint + part of 'translations.cgm.dart'; // Path: diff --git a/slang/test/integration/resources/main/_expected_en.output b/slang/test/integration/resources/main/_expected_en.output index c9591609..ae8f9c5d 100644 --- a/slang/test/integration/resources/main/_expected_en.output +++ b/slang/test/integration/resources/main/_expected_en.output @@ -1,3 +1,9 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint + part of 'translations.cgm.dart'; // Path: diff --git a/slang/test/integration/resources/main/_expected_map.output b/slang/test/integration/resources/main/_expected_map.output index 24fd82e1..4d85451d 100644 --- a/slang/test/integration/resources/main/_expected_map.output +++ b/slang/test/integration/resources/main/_expected_map.output @@ -1,3 +1,9 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint + part of 'translations.cgm.dart'; /// Flat map(s) containing all translations. From 6686787f006497dfda0f11cd2190e33fc8621e9e Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Fri, 19 Jan 2024 23:14:40 +0100 Subject: [PATCH 004/118] ci: bump flutter to 3.16.8 --- .fvm/fvm_config.json | 2 +- .github/workflows/ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index b35b02a4..9bdedb8b 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.16.5", + "flutterSdkVersion": "3.16.8", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0e603ad..70f93eec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ on: env: FLUTTER_VERSION_OLDEST: "3.0.5" - FLUTTER_VERSION_NEWEST: "3.16.5" + FLUTTER_VERSION_NEWEST: "3.16.8" jobs: format: From 4bc950929c12b4c8c677fb097ca9ebf158f7c129 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 21 Jan 2024 01:33:03 +0100 Subject: [PATCH 005/118] docs: update doc comments --- slang/lib/builder/builder/translation_map_builder.dart | 4 ++-- slang/lib/builder/model/slang_file_collection.dart | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/slang/lib/builder/builder/translation_map_builder.dart b/slang/lib/builder/builder/translation_map_builder.dart index 353704d9..267ace8d 100644 --- a/slang/lib/builder/builder/translation_map_builder.dart +++ b/slang/lib/builder/builder/translation_map_builder.dart @@ -8,8 +8,8 @@ import 'package:slang/builder/model/translation_map.dart'; class TranslationMapBuilder { /// This method transforms files to an intermediate model [TranslationMap]. /// After this step, - /// - we ignore the environment (i.e. dart:io, build_runner) - /// - we ignore the file type (JSON, YAML, CSV) because everything is a map now + /// - we removed the environment (i.e. dart:io, build_runner) + /// - we removed the file type (JSON, YAML, CSV) because everything is a map now /// /// The resulting map is in a unmodified state, so no actual i18n handling (plural, rich text) has been applied. static Future build({ diff --git a/slang/lib/builder/model/slang_file_collection.dart b/slang/lib/builder/model/slang_file_collection.dart index ae9ade1f..e960a0b1 100644 --- a/slang/lib/builder/model/slang_file_collection.dart +++ b/slang/lib/builder/model/slang_file_collection.dart @@ -4,6 +4,9 @@ import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/raw_config.dart'; import 'package:slang/builder/utils/path_utils.dart'; +/// A collection of translation files that can be read in a later step. +/// This is an abstraction to support build_runner and the custom CLI by +/// providing a common [FileReader] interface. class SlangFileCollection { final RawConfig config; final List files; From a1cbe1e99fe346613d931f135a59b08aadde16a4 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 21 Jan 2024 01:40:28 +0100 Subject: [PATCH 006/118] refactor: TranslationModelBuilder should return PopulatedContextType --- .../lib/builder/builder/generate_config_builder.dart | 10 ++-------- .../builder/builder/translation_model_builder.dart | 12 +++++++++--- slang/lib/builder/generator_facade.dart | 2 +- slang/lib/builder/model/i18n_data.dart | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/slang/lib/builder/builder/generate_config_builder.dart b/slang/lib/builder/builder/generate_config_builder.dart index fa2446af..d8193276 100644 --- a/slang/lib/builder/builder/generate_config_builder.dart +++ b/slang/lib/builder/builder/generate_config_builder.dart @@ -10,7 +10,7 @@ class GenerateConfigBuilder { required String baseName, required RawConfig config, required String inputDirectoryHint, - required List contexts, + required List contexts, required List interfaces, }) { return GenerateConfig( @@ -31,13 +31,7 @@ class GenerateConfigBuilder { translationOverrides: config.translationOverrides, renderTimestamp: config.renderTimestamp, renderStatistics: config.renderStatistics, - contexts: contexts.map((c) { - return PopulatedContextType( - enumName: c.enumName, - enumValues: c.enumValues!, - generateEnum: c.generateEnum, - ); - }).toList(), + contexts: contexts, interface: interfaces, obfuscation: config.obfuscation, imports: config.imports, diff --git a/slang/lib/builder/builder/translation_model_builder.dart b/slang/lib/builder/builder/translation_model_builder.dart index 5f053624..5c2528f0 100644 --- a/slang/lib/builder/builder/translation_model_builder.dart +++ b/slang/lib/builder/builder/translation_model_builder.dart @@ -14,7 +14,7 @@ import 'package:slang/builder/utils/string_extensions.dart'; class BuildModelResult { final ObjectNode root; // the actual strings final List interfaces; // detected interfaces - final List contexts; // detected context types + final List contexts; // detected context types BuildModelResult({ required this.root, @@ -190,8 +190,14 @@ class TranslationModelBuilder { return BuildModelResult( root: root, interfaces: interfaceCollection.resultInterfaces.values.toList(), - contexts: - contextCollection.values.where((c) => c.enumValues != null).toList(), + contexts: contextCollection.values + .where((c) => c.enumValues != null) + .map((c) => PopulatedContextType( + enumName: c.enumName, + enumValues: c.enumValues!, + generateEnum: c.generateEnum, + )) + .toList(), ); } } diff --git a/slang/lib/builder/generator_facade.dart b/slang/lib/builder/generator_facade.dart index 2de4c1fe..9d6af2dc 100644 --- a/slang/lib/builder/generator_facade.dart +++ b/slang/lib/builder/generator_facade.dart @@ -26,7 +26,7 @@ class GeneratorFacade { // combine all contexts of all locales // if one context appears on more than one locale, then the context of // the base locale will have precedence - final contextMap = {}; + final contextMap = {}; for (final locale in translationModelList) { for (final context in locale.contexts) { if (!contextMap.containsKey(context.enumName)) { diff --git a/slang/lib/builder/model/i18n_data.dart b/slang/lib/builder/model/i18n_data.dart index 7854c630..05978fef 100644 --- a/slang/lib/builder/model/i18n_data.dart +++ b/slang/lib/builder/model/i18n_data.dart @@ -10,7 +10,7 @@ class I18nData { final bool base; // whether or not this is the base locale final I18nLocale locale; // the locale (the part after the underscore) final ObjectNode root; // the actual strings - final List contexts; // detected context types + final List contexts; // detected context types final List interfaces; // detected interfaces I18nData({ From 3414630c9c87724c3a9fbd5ac343dbdaf0640bb1 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 21 Jan 2024 02:53:45 +0100 Subject: [PATCH 007/118] fix: context enums should use fallback --- .../builder/translation_model_builder.dart | 78 +++++- .../translation_model_list_builder.dart | 51 +++- slang/lib/builder/model/raw_config.dart | 2 +- .../main/fallback_base_locale_test.dart | 34 ++- ...pected_fallback_base_locale_special.output | 235 ++++++++++++++++++ .../resources/main/build_config.yaml | 2 +- .../resources/main/fallback_de.json | 5 + .../resources/main/fallback_en.json | 6 + slang/test/integration/update.dart | 30 +++ 9 files changed, 427 insertions(+), 16 deletions(-) create mode 100644 slang/test/integration/resources/main/_expected_fallback_base_locale_special.output create mode 100644 slang/test/integration/resources/main/fallback_de.json create mode 100644 slang/test/integration/resources/main/fallback_en.json diff --git a/slang/lib/builder/builder/translation_model_builder.dart b/slang/lib/builder/builder/translation_model_builder.dart index 5c2528f0..eb90e71e 100644 --- a/slang/lib/builder/builder/translation_model_builder.dart +++ b/slang/lib/builder/builder/translation_model_builder.dart @@ -26,9 +26,12 @@ class BuildModelResult { class TranslationModelBuilder { /// Builds the i18n model for ONE locale /// - /// The map must be of type Map and all children may of type + /// The [map] must be of type Map and all children may of type /// String, num, List or Map. /// + /// If [baseData] is set and [BuildModelConfig.fallbackStrategy] is [FallbackStrategy.baseLocale], + /// then the base translations will be added to contexts where the translation is missing. + /// /// [handleLinks] can be set false to ignore links and leave them as is /// e.g. ${_root.greet(name: name} will be ${_root.greet} /// This is used for "Translation Overrides" where the links are resolved @@ -40,6 +43,7 @@ class TranslationModelBuilder { static BuildModelResult build({ required BuildModelConfig buildConfig, required Map map, + BuildModelResult? baseData, bool handleLinks = true, bool shouldEscapeText = true, required String localeDebug, @@ -47,6 +51,20 @@ class TranslationModelBuilder { // flat map for leaves (TextNode, PluralNode, ContextNode) final Map leavesMap = {}; + // base contexts to be used for fallback + final Map? baseContexts = baseData == null || + baseData.contexts.isEmpty || + buildConfig.fallbackStrategy == FallbackStrategy.none + ? null + : { + for (final c in baseData.contexts) + c.enumName: PopulatedContextType( + enumName: c.enumName, + enumValues: c.enumValues, + generateEnum: c.generateEnum, + ), + }; + final contextCollection = { for (final context in buildConfig.contexts) context.enumName: context, }; @@ -66,6 +84,8 @@ class TranslationModelBuilder { keyCase: buildConfig.keyCase, leavesMap: leavesMap, contextCollection: contextCollection, + baseData: baseData, + baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, ); @@ -213,6 +233,8 @@ Map _parseMapNode({ required CaseStyle? keyCase, required Map leavesMap, required Map contextCollection, + required BuildModelResult? baseData, + required Map? baseContexts, required bool shouldEscapeText, }) { final Map resultNodeTree = {}; @@ -284,6 +306,8 @@ Map _parseMapNode({ keyCase: config.keyCase, leavesMap: leavesMap, contextCollection: contextCollection, + baseData: baseData, + baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, ); @@ -312,6 +336,8 @@ Map _parseMapNode({ : config.keyCase, leavesMap: leavesMap, contextCollection: contextCollection, + baseData: baseData, + baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, ); @@ -365,6 +391,8 @@ Map _parseMapNode({ keyCase: config.keyCase, leavesMap: leavesMap, contextCollection: contextCollection, + baseData: baseData, + baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, ).cast(); } @@ -385,6 +413,21 @@ Map _parseMapNode({ contextCollection[context.enumName] = context; } + if (config.fallbackStrategy == FallbackStrategy.baseLocale || + config.fallbackStrategy == + FallbackStrategy.baseLocaleEmptyString) { + // add base context values if necessary + final baseContext = baseContexts?[context.enumName]; + if (baseContext != null) { + digestedMap = _digestContextEntries( + baseTranslation: baseData!.root, + baseContext: baseContext, + path: '$currPath', + entries: digestedMap, + ); + } + } + finalNode = ContextNode( path: currPath, rawPath: currRawPath, @@ -780,6 +823,39 @@ void _fixEmptyLists({ }); } +/// Makes sure that every enum value in [baseContext] is also present in [entries]. +/// If a value is missing, the base translation is used. +Map _digestContextEntries({ + required ObjectNode baseTranslation, + required PopulatedContextType baseContext, + required String path, + required Map entries, +}) { + // Using "late" keyword because we are optimistic that all values are present + late ContextNode baseContextNode = + _findContextNode(baseTranslation, path.split('.')); + return { + for (final value in baseContext.enumValues) + value: entries[value] ?? baseContextNode.entries[value]!, + }; +} + +/// Recursively find the [ContextNode] using the given [path]. +ContextNode _findContextNode(ObjectNode node, List path) { + final child = node.entries[path[0]]; + if (path.length == 1) { + if (child is ContextNode) { + return child; + } else { + throw 'Parent node is not a ContextNode but a ${node.runtimeType} at path $path'; + } + } else if (child is ObjectNode) { + return _findContextNode(child, path.sublist(1)); + } else { + throw 'Cannot find base ContextNode'; + } +} + enum _DetectionType { classType, map, diff --git a/slang/lib/builder/builder/translation_model_list_builder.dart b/slang/lib/builder/builder/translation_model_list_builder.dart index b3d826f0..46f2af6d 100644 --- a/slang/lib/builder/builder/translation_model_list_builder.dart +++ b/slang/lib/builder/builder/translation_model_list_builder.dart @@ -16,22 +16,49 @@ class TranslationModelListBuilder { ) { final buildConfig = rawConfig.toBuildModelConfig(); + final baseEntry = translationMap.getInternalMap().entries.firstWhere( + (entry) => entry.key == rawConfig.baseLocale, + orElse: () => throw Exception('Base locale not found'), + ); + + // Create the base data first. + final namespaces = baseEntry.value; + final baseResult = TranslationModelBuilder.build( + buildConfig: buildConfig, + map: rawConfig.namespaces ? namespaces : namespaces.values.first, + localeDebug: baseEntry.key.languageTag, + ); + return translationMap.getInternalMap().entries.map((localeEntry) { final locale = localeEntry.key; final namespaces = localeEntry.value; - final result = TranslationModelBuilder.build( - buildConfig: buildConfig, - map: rawConfig.namespaces ? namespaces : namespaces.values.first, - localeDebug: locale.languageTag, - ); + final base = locale == rawConfig.baseLocale; + + if (base) { + // Use the already computed base data + return I18nData( + base: true, + locale: locale, + root: baseResult.root, + contexts: baseResult.contexts, + interfaces: baseResult.interfaces, + ); + } else { + final result = TranslationModelBuilder.build( + buildConfig: buildConfig, + map: rawConfig.namespaces ? namespaces : namespaces.values.first, + baseData: baseResult, + localeDebug: locale.languageTag, + ); - return I18nData( - base: rawConfig.baseLocale == locale, - locale: locale, - root: result.root, - contexts: result.contexts, - interfaces: result.interfaces, - ); + return I18nData( + base: rawConfig.baseLocale == locale, + locale: locale, + root: result.root, + contexts: result.contexts, + interfaces: result.interfaces, + ); + } }).toList() ..sort(I18nData.generationComparator); } diff --git a/slang/lib/builder/model/raw_config.dart b/slang/lib/builder/model/raw_config.dart index 53e5037a..076b4a57 100644 --- a/slang/lib/builder/model/raw_config.dart +++ b/slang/lib/builder/model/raw_config.dart @@ -4,7 +4,7 @@ import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/interface.dart'; import 'package:slang/builder/model/obfuscation_config.dart'; -/// represents a build.yaml +/// represents a build.yaml or a slang.yaml file class RawConfig { static const String defaultBaseLocale = 'en'; static const FallbackStrategy defaultFallbackStrategy = FallbackStrategy.none; diff --git a/slang/test/integration/main/fallback_base_locale_test.dart b/slang/test/integration/main/fallback_base_locale_test.dart index b5e2e792..7db8c558 100644 --- a/slang/test/integration/main/fallback_base_locale_test.dart +++ b/slang/test/integration/main/fallback_base_locale_test.dart @@ -1,5 +1,6 @@ import 'package:slang/builder/builder/raw_config_builder.dart'; import 'package:slang/builder/decoder/csv_decoder.dart'; +import 'package:slang/builder/decoder/json_decoder.dart'; import 'package:slang/builder/generator_facade.dart'; import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/i18n_locale.dart'; @@ -13,15 +14,25 @@ void main() { late String buildYaml; late String expectedOutput; + late String specialEnInput; + late String specialDeInput; + late String specialExpectedOutput; + setUp(() { compactInput = loadResource('main/csv_compact.csv'); buildYaml = loadResource('main/build_config.yaml'); expectedOutput = loadResource( 'main/_expected_fallback_base_locale.output', ); + + specialEnInput = loadResource('main/fallback_en.json'); + specialDeInput = loadResource('main/fallback_de.json'); + specialExpectedOutput = loadResource( + 'main/_expected_fallback_base_locale_special.output', + ); }); - test('translation overrides', () { + test('fallback with generic integration data', () { final parsed = CsvDecoder().decode(compactInput); final result = GeneratorFacade.generate( @@ -43,4 +54,25 @@ void main() { expect(result.joinAsSingleOutput(), expectedOutput); }); + + test('fallback with special integration data', () { + final result = GeneratorFacade.generate( + rawConfig: RawConfigBuilder.fromYaml(buildYaml)!.copyWith( + fallbackStrategy: FallbackStrategy.baseLocale, + ), + baseName: 'translations', + translationMap: TranslationMap() + ..addTranslations( + locale: I18nLocale.fromString('en'), + translations: JsonDecoder().decode(specialEnInput), + ) + ..addTranslations( + locale: I18nLocale.fromString('de'), + translations: JsonDecoder().decode(specialDeInput), + ), + inputDirectoryHint: 'fake/path/integration', + ); + + expect(result.joinAsSingleOutput(), specialExpectedOutput); + }); } diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_special.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_special.output new file mode 100644 index 00000000..3d5c4d05 --- /dev/null +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_special.output @@ -0,0 +1,235 @@ +/// Generated file. Do not edit. +/// +/// Original: fake/path/integration +/// To regenerate, run: `dart run slang` +/// +/// Locales: 2 +/// Strings: 4 (2 per locale) + +// coverage:ignore-file +// ignore_for_file: type=lint + +import 'package:flutter/widgets.dart'; +import 'package:slang/builder/model/node.dart'; +import 'package:slang_flutter/slang_flutter.dart'; +export 'package:slang_flutter/slang_flutter.dart'; + +const AppLocale _baseLocale = AppLocale.en; + +/// Supported locales, see extension methods below. +/// +/// Usage: +/// - LocaleSettings.setLocale(AppLocale.en) // set locale +/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum +/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check +enum AppLocale with BaseAppLocale { + en(languageCode: 'en', build: Translations.build), + de(languageCode: 'de', build: _TranslationsDe.build); + + const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element + + @override final String languageCode; + @override final String? scriptCode; + @override final String? countryCode; + @override final TranslationBuilder build; + + /// Gets current instance managed by [LocaleSettings]. + Translations get translations => LocaleSettings.instance.translationMap[this]!; +} + +/// Method A: Simple +/// +/// No rebuild after locale change. +/// Translation happens during initialization of the widget (call of t). +/// Configurable via 'translate_var'. +/// +/// Usage: +/// String a = t.someKey.anotherKey; +/// String b = t['someKey.anotherKey']; // Only for edge cases! +Translations get t => LocaleSettings.instance.currentTranslations; + +/// Method B: Advanced +/// +/// All widgets using this method will trigger a rebuild when locale changes. +/// Use this if you have e.g. a settings page where the user can select the locale during runtime. +/// +/// Step 1: +/// wrap your App with +/// TranslationProvider( +/// child: MyApp() +/// ); +/// +/// Step 2: +/// final t = Translations.of(context); // Get t variable. +/// String a = t.someKey.anotherKey; // Use t variable. +/// String b = t['someKey.anotherKey']; // Only for edge cases! +class TranslationProvider extends BaseTranslationProvider { + TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance); + + static InheritedLocaleData of(BuildContext context) => InheritedLocaleData.of(context); +} + +/// Method B shorthand via [BuildContext] extension method. +/// Configurable via 'translate_var'. +/// +/// Usage (e.g. in a widget's build method): +/// context.t.someKey.anotherKey +extension BuildContextTranslationsExtension on BuildContext { + Translations get t => TranslationProvider.of(this).translations; +} + +/// Manages all translation instances and the current locale +class LocaleSettings extends BaseFlutterLocaleSettings { + LocaleSettings._() : super(utils: AppLocaleUtils.instance); + + static final instance = LocaleSettings._(); + + // static aliases (checkout base methods for documentation) + static AppLocale get currentLocale => instance.currentLocale; + static Stream getLocaleStream() => instance.getLocaleStream(); + static AppLocale setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale useDeviceLocale() => instance.useDeviceLocale(); + @Deprecated('Use [AppLocaleUtils.supportedLocales]') static List get supportedLocales => instance.supportedLocales; + @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') static List get supportedLocalesRaw => instance.supportedLocalesRaw; + static void setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); +} + +/// Provides utility functions without any side effects. +class AppLocaleUtils extends BaseAppLocaleUtils { + AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values); + + static final instance = AppLocaleUtils._(); + + // static aliases (checkout base methods for documentation) + static AppLocale parse(String rawLocale) => instance.parse(rawLocale); + static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode); + static AppLocale findDeviceLocale() => instance.findDeviceLocale(); + static List get supportedLocales => instance.supportedLocales; + static List get supportedLocalesRaw => instance.supportedLocalesRaw; +} + +// context enums + +enum Gender { + male, + female, +} + +// translations + +// Path: +class Translations implements BaseTranslations { + /// Returns the current translations of the given [context]. + /// + /// Usage: + /// final t = Translations.of(context); + static Translations of(BuildContext context) => InheritedLocaleData.of(context).translations; + + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + Translations.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.en, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + dynamic operator[](String key) => $meta.getTranslation(key); + + late final Translations _root = this; // ignore: unused_field + + // Translations + String greet({required Gender context}) { + switch (context) { + case Gender.male: + return 'Hello Mr'; + case Gender.female: + return 'Hello Mrs'; + } + } +} + +// Path: +class _TranslationsDe extends Translations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + _TranslationsDe.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.de, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ), + super.build(cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver) { + super.$meta.setFlatMapFunction($meta.getTranslation); // copy base translations to super.$meta + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + @override dynamic operator[](String key) => $meta.getTranslation(key) ?? super.$meta.getTranslation(key); + + @override late final _TranslationsDe _root = this; // ignore: unused_field + + // Translations + @override String greet({required Gender context}) { + switch (context) { + case Gender.male: + return 'Hallo Herr'; + case Gender.female: + return 'Hello Mrs'; + } + } +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. + +extension on Translations { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'greet': return ({required Gender context}) { + switch (context) { + case Gender.male: + return 'Hello Mr'; + case Gender.female: + return 'Hello Mrs'; + } + }; + default: return null; + } + } +} + +extension on _TranslationsDe { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'greet': return ({required Gender context}) { + switch (context) { + case Gender.male: + return 'Hallo Herr'; + case Gender.female: + return 'Hello Mrs'; + } + }; + default: return null; + } + } +} diff --git a/slang/test/integration/resources/main/build_config.yaml b/slang/test/integration/resources/main/build_config.yaml index 5bd59550..b28471b6 100644 --- a/slang/test/integration/resources/main/build_config.yaml +++ b/slang/test/integration/resources/main/build_config.yaml @@ -6,7 +6,7 @@ targets: options: base_locale: en input_file_pattern: .i18n.json # will be ignored anyways because we put in manually - output_file_name: translations.g.dart # currently set manually for each test + output_file_name: translations.cgm.dart # currently set manually for each test output_format: single_file # may get changed programmatically locale_handling: true # may get changed programmatically string_interpolation: braces diff --git a/slang/test/integration/resources/main/fallback_de.json b/slang/test/integration/resources/main/fallback_de.json new file mode 100644 index 00000000..0a6245a0 --- /dev/null +++ b/slang/test/integration/resources/main/fallback_de.json @@ -0,0 +1,5 @@ +{ + "greet(context=Gender)": { + "male": "Hallo Herr" + } +} \ No newline at end of file diff --git a/slang/test/integration/resources/main/fallback_en.json b/slang/test/integration/resources/main/fallback_en.json new file mode 100644 index 00000000..e2e57e66 --- /dev/null +++ b/slang/test/integration/resources/main/fallback_en.json @@ -0,0 +1,6 @@ +{ + "greet(context=Gender)": { + "male": "Hello Mr", + "female": "Hello Mrs" + } +} \ No newline at end of file diff --git a/slang/test/integration/update.dart b/slang/test/integration/update.dart index 7d25112a..20435b42 100644 --- a/slang/test/integration/update.dart +++ b/slang/test/integration/update.dart @@ -23,6 +23,8 @@ void main() { final en = loadResource('main/json_en.json'); final de = loadResource('main/json_de.json'); final simple = loadResource('main/json_simple.json'); + final fallbackEn = loadResource('main/fallback_en.json'); + final fallbackDe = loadResource('main/fallback_de.json'); final buildConfig = RawConfigBuilder.fromYaml(loadResource('main/build_config.yaml'))!; generateMainIntegration(buildConfig, en, de); @@ -31,6 +33,7 @@ void main() { generateNoLocaleHandling(buildConfig, simple); generateTranslationOverrides(buildConfig, en, de); generateFallbackBaseLocale(buildConfig, en, de); + generateFallbackBaseLocaleSpecial(buildConfig, fallbackEn, fallbackDe); generateObfuscation(buildConfig, en, de); generateRichText(); @@ -192,6 +195,33 @@ void generateFallbackBaseLocale(RawConfig buildConfig, String en, String de) { ); } +void generateFallbackBaseLocaleSpecial( + RawConfig buildConfig, + String en, + String de, +) { + final result = _generate( + rawConfig: buildConfig.copyWith( + fallbackStrategy: FallbackStrategy.baseLocale, + ), + baseName: 'translations', + translationMap: TranslationMap() + ..addTranslations( + locale: I18nLocale.fromString('en'), + translations: JsonDecoder().decode(en), + ) + ..addTranslations( + locale: I18nLocale.fromString('de'), + translations: JsonDecoder().decode(de), + ), + ).joinAsSingleOutput(); + + _write( + path: 'main/_expected_fallback_base_locale_special', + content: result, + ); +} + void generateObfuscation(RawConfig buildConfig, String en, String de) { final result = _generate( rawConfig: buildConfig.copyWith( From d5070c8a7f9e9f9b107a79bf1f8a5ebe70623713 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 21 Jan 2024 03:16:09 +0100 Subject: [PATCH 008/118] release: 3.29.0 --- slang/CHANGELOG.md | 7 +++++++ .../builder/builder/translation_model_list_builder.dart | 2 +- slang/pubspec.yaml | 4 +++- slang_build_runner/CHANGELOG.md | 4 ++++ slang_build_runner/pubspec.yaml | 4 ++-- slang_flutter/CHANGELOG.md | 4 ++++ slang_flutter/pubspec.yaml | 4 ++-- 7 files changed, 23 insertions(+), 6 deletions(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index fc371e73..9e7a9108 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,10 @@ +## 3.29.0 + +- feat: `dart run slang analyze` supports csv files (#185) @nikaera +- feat: also add linter and coverage ignore to part files (#188) @cmenkemeller +- fix: generate base translations as fallback when using context enums where some enum values are missing (#182) @Tienisto +- fix: generate correct `part of` directive when using a custom dart file extension (#187) @cmenkemeller + ## 3.28.0 - feat: add `fallback_strategy: base_locale_empty_string` to also treat empty strings as missing translations (#180) diff --git a/slang/lib/builder/builder/translation_model_list_builder.dart b/slang/lib/builder/builder/translation_model_list_builder.dart index 46f2af6d..09a765ac 100644 --- a/slang/lib/builder/builder/translation_model_list_builder.dart +++ b/slang/lib/builder/builder/translation_model_list_builder.dart @@ -52,7 +52,7 @@ class TranslationModelListBuilder { ); return I18nData( - base: rawConfig.baseLocale == locale, + base: false, locale: locale, root: result.root, contexts: result.contexts, diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index 840b456c..b7dcd5ea 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -1,6 +1,6 @@ name: slang description: Localization / Internationalization (i18n) solution. Use JSON, YAML, CSV, or ARB files to create typesafe translations via source generation. -version: 3.28.0 +version: 3.29.0 repository: https://github.com/slang-i18n/slang topics: - i18n @@ -10,6 +10,8 @@ topics: screenshots: - description: The slang logo. path: resources/icon.png +funding: + - https://github.com/sponsors/Tienisto/ environment: sdk: ">=2.17.0 <4.0.0" diff --git a/slang_build_runner/CHANGELOG.md b/slang_build_runner/CHANGELOG.md index 358a89ef..1294db54 100644 --- a/slang_build_runner/CHANGELOG.md +++ b/slang_build_runner/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.29.0 + +- Bump `slang` to `3.29.0` + ## 3.28.0 - Bump `slang` to `3.28.0` diff --git a/slang_build_runner/pubspec.yaml b/slang_build_runner/pubspec.yaml index 148c1deb..d6d388ba 100644 --- a/slang_build_runner/pubspec.yaml +++ b/slang_build_runner/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_build_runner description: build_runner integration for slang. This library ensures that slang is recognized by build_runner. -version: 3.28.0 +version: 3.29.0 repository: https://github.com/slang-i18n/slang environment: @@ -11,4 +11,4 @@ dependencies: glob: ^2.0.2 # Use a tight version to ensure that all features are available - slang: '>=3.28.0 <3.29.0' + slang: '>=3.29.0 <3.30.0' diff --git a/slang_flutter/CHANGELOG.md b/slang_flutter/CHANGELOG.md index 754b870d..e4d17c74 100644 --- a/slang_flutter/CHANGELOG.md +++ b/slang_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.29.0 + +- Bump `slang` to `3.29.0` + ## 3.28.0 - Bump `slang` to `3.28.0` diff --git a/slang_flutter/pubspec.yaml b/slang_flutter/pubspec.yaml index dc61b054..be11fc47 100644 --- a/slang_flutter/pubspec.yaml +++ b/slang_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_flutter description: Flutter support for slang. This library provides helpful Flutter API. -version: 3.28.0 +version: 3.29.0 repository: https://github.com/slang-i18n/slang environment: @@ -12,7 +12,7 @@ dependencies: sdk: flutter # Use a tight version to ensure that all features are available - slang: '>=3.28.0 <3.29.0' + slang: '>=3.29.0 <3.30.0' dev_dependencies: flutter_test: From 7a50769cc0e4ec2186bd203e1dbbcb0b5a82ebb8 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Wed, 24 Jan 2024 13:56:16 +0100 Subject: [PATCH 009/118] fix: "slang clean" crashes when map is empty --- slang/lib/src/runner/clean.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/slang/lib/src/runner/clean.dart b/slang/lib/src/runner/clean.dart index 55296b5b..7d3a3db1 100644 --- a/slang/lib/src/runner/clean.dart +++ b/slang/lib/src/runner/clean.dart @@ -38,6 +38,11 @@ Future runClean({ for (final entry in unusedTranslationsMap.entries) { final locale = entry.key; final map = entry.value; + + if (map.isEmpty) { + continue; + } + final entries = MapUtils.getFlatMap(map); print(' -> Cleaning <${locale.languageTag}>...'); @@ -103,6 +108,8 @@ Future _deleteEntriesForLocale({ ); } + // Final step: Write the result + if (config.namespaces) { for (final entry in outputMap.entries) { final namespace = entry.key; @@ -118,7 +125,8 @@ Future _deleteEntriesForLocale({ } } else { if (fileMap.isEmpty) { - throw 'No file found for locale <$locale>'; + // All specified namespaces might not exist + return; } final file = fileMap.values.first; @@ -133,6 +141,8 @@ Future _deleteEntriesForLocale({ } } +/// Returns the first file in the collection +/// that matches the given [locale] and [namespace]. TranslationFile? _findFileInCollection({ required SlangFileCollection fileCollection, required I18nLocale locale, From b79e93a51bfb5803663b44a4fa6e15b3eeb86e5c Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Thu, 25 Jan 2024 17:45:20 +0100 Subject: [PATCH 010/118] feat: do not generate quotes if not necessary --- slang/lib/builder/generator/helper.dart | 8 +++++++- slang/lib/builder/model/node.dart | 14 ++++++++++---- .../integration/resources/main/_expected_de.output | 1 + .../integration/resources/main/_expected_en.output | 1 + .../main/_expected_fallback_base_locale.output | 6 +++++- .../resources/main/_expected_main.output | 2 +- .../resources/main/_expected_map.output | 2 ++ .../resources/main/_expected_obfuscation.output | 6 +++++- .../resources/main/_expected_single.output | 6 +++++- .../main/_expected_translation_overrides.output | 6 +++++- .../integration/resources/main/csv_compact.csv | 1 + slang/test/integration/resources/main/csv_de.csv | 1 + slang/test/integration/resources/main/csv_en.csv | 1 + slang/test/integration/resources/main/json_de.json | 1 + slang/test/integration/resources/main/json_en.json | 1 + slang/test/integration/resources/main/yaml_de.yaml | 1 + slang/test/integration/resources/main/yaml_en.yaml | 1 + 17 files changed, 49 insertions(+), 10 deletions(-) diff --git a/slang/lib/builder/generator/helper.dart b/slang/lib/builder/generator/helper.dart index ac56a437..3dff7e06 100644 --- a/slang/lib/builder/generator/helper.dart +++ b/slang/lib/builder/generator/helper.dart @@ -43,7 +43,13 @@ const _NULL_FLAG = '\u0000'; String getStringLiteral(String value, ObfuscationConfig config) { if (!config.enabled || value.isEmpty) { // Return the plain version - return "'$value'"; + if (value.startsWith(r'${') && value.indexOf('}') == value.length - 1) { + // We can just remove the ${ and } since it's already a string + return value.substring(2, value.length - 1).trim(); + } else { + // We need to add quotes + return "'$value'"; + } } // Return the obfuscated version diff --git a/slang/lib/builder/model/node.dart b/slang/lib/builder/model/node.dart index d07f8580..e7af35a2 100644 --- a/slang/lib/builder/model/node.dart +++ b/slang/lib/builder/model/node.dart @@ -417,8 +417,8 @@ class RichTextNode extends TextNode { ); _links.addAll(parsedLinksResult.links); return FunctionSpan( - parsed.paramName, - parsedLinksResult.parsedContent, + functionName: parsed.paramName, + arg: parsedLinksResult.parsedContent, ); } else { return VariableSpan(parsed.paramName); @@ -615,14 +615,20 @@ class LiteralSpan extends BaseSpan { final String literal; final bool isConstant; - LiteralSpan({required this.literal, required this.isConstant}); + LiteralSpan({ + required this.literal, + required this.isConstant, + }); } class FunctionSpan extends BaseSpan { final String functionName; final String arg; - FunctionSpan(this.functionName, this.arg); + FunctionSpan({ + required this.functionName, + required this.arg, + }); } class VariableSpan extends BaseSpan { diff --git a/slang/test/integration/resources/main/_expected_de.output b/slang/test/integration/resources/main/_expected_de.output index 10255c36..fea4028f 100644 --- a/slang/test/integration/resources/main/_expected_de.output +++ b/slang/test/integration/resources/main/_expected_de.output @@ -55,6 +55,7 @@ class _TranslationsOnboardingDe implements _TranslationsOnboardingEn { // Translations @override String welcome({required Object fullName}) => 'Willkommen ${fullName}'; + @override String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); /// Bye text @override String bye({required Object firstName}) => 'Tschüss ${firstName}'; diff --git a/slang/test/integration/resources/main/_expected_en.output b/slang/test/integration/resources/main/_expected_en.output index ae8f9c5d..88cc12a7 100644 --- a/slang/test/integration/resources/main/_expected_en.output +++ b/slang/test/integration/resources/main/_expected_en.output @@ -61,6 +61,7 @@ class _TranslationsOnboardingEn { // Translations String welcome({required Object fullName}) => 'Welcome ${fullName}'; + String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); /// Bye text String bye({required Object firstName}) => 'Bye ${firstName}'; diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale.output b/slang/test/integration/resources/main/_expected_fallback_base_locale.output index cee50f27..a1a96ae0 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 54 (27 per locale) +/// Strings: 56 (28 per locale) // coverage:ignore-file // ignore_for_file: type=lint @@ -213,6 +213,7 @@ class _TranslationsOnboardingEn { // Translations String welcome({required Object fullName}) => 'Welcome ${fullName}'; + String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); /// Bye text String bye({required Object firstName}) => 'Bye ${firstName}'; @@ -381,6 +382,7 @@ class _TranslationsOnboardingDe extends _TranslationsOnboardingEn { // Translations @override String welcome({required Object fullName}) => 'Willkommen ${fullName}'; + @override String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); /// Bye text @override String bye({required Object firstName}) => 'Tschüss ${firstName}'; @@ -505,6 +507,7 @@ extension on Translations { dynamic _flatMapFunction(String path) { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => 'Welcome ${fullName}'; + case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); case 'onboarding.bye': return ({required Object firstName}) => 'Bye ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), @@ -568,6 +571,7 @@ extension on _TranslationsDe { dynamic _flatMapFunction(String path) { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => 'Willkommen ${fullName}'; + case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); case 'onboarding.bye': return ({required Object firstName}) => 'Tschüss ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), diff --git a/slang/test/integration/resources/main/_expected_main.output b/slang/test/integration/resources/main/_expected_main.output index 79b175ca..9b289514 100644 --- a/slang/test/integration/resources/main/_expected_main.output +++ b/slang/test/integration/resources/main/_expected_main.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 54 (27 per locale) +/// Strings: 56 (28 per locale) // coverage:ignore-file // ignore_for_file: type=lint diff --git a/slang/test/integration/resources/main/_expected_map.output b/slang/test/integration/resources/main/_expected_map.output index 4d85451d..4ff278a8 100644 --- a/slang/test/integration/resources/main/_expected_map.output +++ b/slang/test/integration/resources/main/_expected_map.output @@ -13,6 +13,7 @@ extension on Translations { dynamic _flatMapFunction(String path) { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => 'Welcome ${fullName}'; + case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); case 'onboarding.bye': return ({required Object firstName}) => 'Bye ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), @@ -76,6 +77,7 @@ extension on _TranslationsDe { dynamic _flatMapFunction(String path) { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => 'Willkommen ${fullName}'; + case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); case 'onboarding.bye': return ({required Object firstName}) => 'Tschüss ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), diff --git a/slang/test/integration/resources/main/_expected_obfuscation.output b/slang/test/integration/resources/main/_expected_obfuscation.output index 372790f8..239b2952 100644 --- a/slang/test/integration/resources/main/_expected_obfuscation.output +++ b/slang/test/integration/resources/main/_expected_obfuscation.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 54 (27 per locale) +/// Strings: 56 (28 per locale) // coverage:ignore-file // ignore_for_file: type=lint @@ -215,6 +215,7 @@ class _TranslationsOnboardingEn { // Translations String welcome({required Object fullName}) => _root.$meta.d([12, 62, 55, 56, 52, 54, 62, 123]) + fullName.toString(); + String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); /// Bye text String bye({required Object firstName}) => _root.$meta.d([25, 34, 62, 123]) + firstName.toString(); @@ -382,6 +383,7 @@ class _TranslationsOnboardingDe implements _TranslationsOnboardingEn { // Translations @override String welcome({required Object fullName}) => _root.$meta.d([12, 50, 55, 55, 48, 52, 54, 54, 62, 53, 123]) + fullName.toString(); + @override String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); /// Bye text @override String bye({required Object firstName}) => _root.$meta.d([15, 40, 56, 51, 167, 40, 40, 123]) + firstName.toString(); @@ -506,6 +508,7 @@ extension on Translations { dynamic _flatMapFunction(String path) { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => _root.$meta.d([12, 62, 55, 56, 52, 54, 62, 123]) + fullName.toString(); + case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); case 'onboarding.bye': return ({required Object firstName}) => _root.$meta.d([25, 34, 62, 123]) + firstName.toString(); case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ TextSpan(text: _root.$meta.d([19, 50, 123])), @@ -569,6 +572,7 @@ extension on _TranslationsDe { dynamic _flatMapFunction(String path) { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => _root.$meta.d([12, 50, 55, 55, 48, 52, 54, 54, 62, 53, 123]) + fullName.toString(); + case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); case 'onboarding.bye': return ({required Object firstName}) => _root.$meta.d([15, 40, 56, 51, 167, 40, 40, 123]) + firstName.toString(); case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ TextSpan(text: _root.$meta.d([19, 50, 123])), diff --git a/slang/test/integration/resources/main/_expected_single.output b/slang/test/integration/resources/main/_expected_single.output index 5621ea2f..ef3bdfe8 100644 --- a/slang/test/integration/resources/main/_expected_single.output +++ b/slang/test/integration/resources/main/_expected_single.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 54 (27 per locale) +/// Strings: 56 (28 per locale) // coverage:ignore-file // ignore_for_file: type=lint @@ -213,6 +213,7 @@ class _TranslationsOnboardingEn { // Translations String welcome({required Object fullName}) => 'Welcome ${fullName}'; + String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); /// Bye text String bye({required Object firstName}) => 'Bye ${firstName}'; @@ -379,6 +380,7 @@ class _TranslationsOnboardingDe implements _TranslationsOnboardingEn { // Translations @override String welcome({required Object fullName}) => 'Willkommen ${fullName}'; + @override String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); /// Bye text @override String bye({required Object firstName}) => 'Tschüss ${firstName}'; @@ -503,6 +505,7 @@ extension on Translations { dynamic _flatMapFunction(String path) { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => 'Welcome ${fullName}'; + case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); case 'onboarding.bye': return ({required Object firstName}) => 'Bye ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), @@ -566,6 +569,7 @@ extension on _TranslationsDe { dynamic _flatMapFunction(String path) { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => 'Willkommen ${fullName}'; + case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); case 'onboarding.bye': return ({required Object firstName}) => 'Tschüss ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), diff --git a/slang/test/integration/resources/main/_expected_translation_overrides.output b/slang/test/integration/resources/main/_expected_translation_overrides.output index 846b6e8c..6b0f383a 100644 --- a/slang/test/integration/resources/main/_expected_translation_overrides.output +++ b/slang/test/integration/resources/main/_expected_translation_overrides.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 54 (27 per locale) +/// Strings: 56 (28 per locale) // coverage:ignore-file // ignore_for_file: type=lint @@ -239,6 +239,7 @@ class _TranslationsOnboardingEn { // Translations String welcome({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcome', {'fullName': fullName}) ?? 'Welcome ${fullName}'; + String welcomeAlias({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeAlias', {'fullName': fullName}) ?? _root.onboarding.welcome(fullName: fullName); /// Bye text String bye({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Bye ${firstName}'; @@ -413,6 +414,7 @@ class _TranslationsOnboardingDe implements _TranslationsOnboardingEn { // Translations @override String welcome({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcome', {'fullName': fullName}) ?? 'Willkommen ${fullName}'; + @override String welcomeAlias({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeAlias', {'fullName': fullName}) ?? _root.onboarding.welcome(fullName: fullName); /// Bye text @override String bye({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Tschüss ${firstName}'; @@ -545,6 +547,7 @@ extension on Translations { dynamic _flatMapFunction(String path) { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcome', {'fullName': fullName}) ?? 'Welcome ${fullName}'; + case 'onboarding.welcomeAlias': return ({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeAlias', {'fullName': fullName}) ?? _root.onboarding.welcome(fullName: fullName); case 'onboarding.bye': return ({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Bye ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TranslationOverridesFlutter.rich(_root.$meta, 'onboarding.hi', {'name': name, 'lastName': lastName, 'context': context, 'fullName': fullName, 'firstName': firstName}) ?? TextSpan(children: [ const TextSpan(text: 'Hi '), @@ -616,6 +619,7 @@ extension on _TranslationsDe { dynamic _flatMapFunction(String path) { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcome', {'fullName': fullName}) ?? 'Willkommen ${fullName}'; + case 'onboarding.welcomeAlias': return ({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeAlias', {'fullName': fullName}) ?? _root.onboarding.welcome(fullName: fullName); case 'onboarding.bye': return ({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Tschüss ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TranslationOverridesFlutter.rich(_root.$meta, 'onboarding.hi', {'name': name, 'lastName': lastName, 'context': context, 'fullName': fullName, 'firstName': firstName}) ?? TextSpan(children: [ const TextSpan(text: 'Hi '), diff --git a/slang/test/integration/resources/main/csv_compact.csv b/slang/test/integration/resources/main/csv_compact.csv index 58ecace4..0f740449 100644 --- a/slang/test/integration/resources/main/csv_compact.csv +++ b/slang/test/integration/resources/main/csv_compact.csv @@ -1,5 +1,6 @@ key,(comments),en,de onboarding.welcome,,Welcome {fullName},Willkommen {fullName} +onboarding.welcomeAlias,,@:onboarding.welcome,@:onboarding.welcome onboarding.bye,Bye text,Bye {firstName},Tschüss {firstName} onboarding.hi(rich),,Hi {name} and @:onboarding.greet,Hi {name} und @:onboarding.greet onboarding.pages.0.title,,First Page,Erste Seite diff --git a/slang/test/integration/resources/main/csv_de.csv b/slang/test/integration/resources/main/csv_de.csv index a4dd5b2c..cb15b9d2 100644 --- a/slang/test/integration/resources/main/csv_de.csv +++ b/slang/test/integration/resources/main/csv_de.csv @@ -1,4 +1,5 @@ onboarding.welcome(!!!this file is in LF which is part of the integration test!!!),Willkommen {fullName} +onboarding.welcomeAlias,@:onboarding.welcome onboarding.bye,Tschüss {firstName} onboarding.@bye,Bye text onboarding.hi(rich),Hi {name} und @:onboarding.greet diff --git a/slang/test/integration/resources/main/csv_en.csv b/slang/test/integration/resources/main/csv_en.csv index 1eda4804..3e3a6329 100644 --- a/slang/test/integration/resources/main/csv_en.csv +++ b/slang/test/integration/resources/main/csv_en.csv @@ -1,4 +1,5 @@ onboarding.welcome,Welcome {fullName} +onboarding.welcomeAlias,@:onboarding.welcome onboarding.bye,Bye {firstName} onboarding.@bye,Bye text onboarding.hi(rich),Hi {name} and @:onboarding.greet diff --git a/slang/test/integration/resources/main/json_de.json b/slang/test/integration/resources/main/json_de.json index b74b822e..84c6b34f 100644 --- a/slang/test/integration/resources/main/json_de.json +++ b/slang/test/integration/resources/main/json_de.json @@ -1,6 +1,7 @@ { "onboarding": { "welcome": "Willkommen {fullName}", + "welcomeAlias": "@:onboarding.welcome", "bye": "Tschüss {firstName}", "@bye": "Bye text", "hi(rich)": "Hi {name} und @:onboarding.greet", diff --git a/slang/test/integration/resources/main/json_en.json b/slang/test/integration/resources/main/json_en.json index daa03cc1..e0860db3 100644 --- a/slang/test/integration/resources/main/json_en.json +++ b/slang/test/integration/resources/main/json_en.json @@ -1,6 +1,7 @@ { "onboarding": { "welcome": "Welcome {fullName}", + "welcomeAlias": "@:onboarding.welcome", "bye": "Bye {firstName}", "@bye": { "this should be ignored": "ignored", diff --git a/slang/test/integration/resources/main/yaml_de.yaml b/slang/test/integration/resources/main/yaml_de.yaml index 1dafd030..acbf0134 100644 --- a/slang/test/integration/resources/main/yaml_de.yaml +++ b/slang/test/integration/resources/main/yaml_de.yaml @@ -1,5 +1,6 @@ onboarding: welcome: Willkommen {fullName} + welcomeAlias: "@:onboarding.welcome" bye: Tschüss {firstName} "@bye": Bye text hi(rich): Hi {name} und @:onboarding.greet diff --git a/slang/test/integration/resources/main/yaml_en.yaml b/slang/test/integration/resources/main/yaml_en.yaml index f95ad860..96c95af4 100644 --- a/slang/test/integration/resources/main/yaml_en.yaml +++ b/slang/test/integration/resources/main/yaml_en.yaml @@ -1,5 +1,6 @@ onboarding: welcome: Welcome {fullName} + welcomeAlias: "@:onboarding.welcome" bye: Bye {firstName} "@bye": Bye text hi(rich): Hi {name} and @:onboarding.greet From 75c040b49060d43d314c54a5e29b6afbc79716a5 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Thu, 25 Jan 2024 18:14:10 +0100 Subject: [PATCH 011/118] test: add compilation tests --- slang/pubspec.yaml | 1 + .../integration/main/compilation_test.dart | 48 +++++++++++++++++++ .../test/util/expect_error_anchor.dart | 3 ++ 3 files changed, 52 insertions(+) create mode 100644 slang/test/integration/main/compilation_test.dart create mode 100644 slang_flutter/test/util/expect_error_anchor.dart diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index b7dcd5ea..e6d92a10 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -24,5 +24,6 @@ dependencies: watcher: ^1.0.2 dev_dependencies: + expect_error: ^1.0.7 lints: ^2.0.0 test: ^1.21.0 diff --git a/slang/test/integration/main/compilation_test.dart b/slang/test/integration/main/compilation_test.dart new file mode 100644 index 00000000..61c4d39a --- /dev/null +++ b/slang/test/integration/main/compilation_test.dart @@ -0,0 +1,48 @@ +import 'package:expect_error/expect_error.dart'; +import 'package:test/test.dart'; + +import '../../util/resources_utils.dart'; + +/// These tests are used to ensure that the generated code compiles. +void main() { + late Library library; + + setUp(() async { + // A workaround so we have Flutter available in the analyzer. + // See: https://pub.dev/packages/expect_error#flutter-support + library = await Library.custom( + packageName: 'slang_flutter', + path: 'not used', + packageRoot: '../slang_flutter', + ); + }); + + Future expectCompiles(String path) { + final output = loadResource(path); + return expectLater(library.withCode(output), compiles); + } + + test('fallback base locale', () { + expectCompiles('main/_expected_fallback_base_locale.output'); + }); + + test('no flutter', () { + expectCompiles('main/_expected_no_flutter.output'); + }); + + test('obfuscation', () { + expectCompiles('main/_expected_obfuscation.output'); + }); + + test('rich text', () { + expectCompiles('main/_expected_rich_text.output'); + }); + + test('single output', () { + expectCompiles('main/_expected_single.output'); + }); + + test('translation overrides', () { + expectCompiles('main/_expected_translation_overrides.output'); + }); +} diff --git a/slang_flutter/test/util/expect_error_anchor.dart b/slang_flutter/test/util/expect_error_anchor.dart new file mode 100644 index 00000000..9abfaa41 --- /dev/null +++ b/slang_flutter/test/util/expect_error_anchor.dart @@ -0,0 +1,3 @@ +/// An anchor for the compilation_test.dart tests. +/// This ensures that theses tests have Flutter available in the analyzer. +library expect_error_anchor; From 9f4f3169cec7e56d745f187be53fdba514f06f97 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Thu, 25 Jan 2024 18:15:28 +0100 Subject: [PATCH 012/118] fix: remove trim --- slang/lib/builder/generator/helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slang/lib/builder/generator/helper.dart b/slang/lib/builder/generator/helper.dart index 3dff7e06..0e951d77 100644 --- a/slang/lib/builder/generator/helper.dart +++ b/slang/lib/builder/generator/helper.dart @@ -45,7 +45,7 @@ String getStringLiteral(String value, ObfuscationConfig config) { // Return the plain version if (value.startsWith(r'${') && value.indexOf('}') == value.length - 1) { // We can just remove the ${ and } since it's already a string - return value.substring(2, value.length - 1).trim(); + return value.substring(2, value.length - 1); } else { // We need to add quotes return "'$value'"; From 928babeeeff091db1a3588066f4b3f38ee3484a5 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Thu, 25 Jan 2024 18:17:12 +0100 Subject: [PATCH 013/118] ci: test flutter first --- .github/workflows/ci.yml | 32 +++++++++++-------- .../integration/main/compilation_test.dart | 2 +- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70f93eec..42b593ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,13 +58,6 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION_NEWEST }} channel: "stable" - - name: Dependencies (core) - working-directory: slang - run: dart pub get - - name: Test (core) - working-directory: slang - run: dart test - - name: Dependencies (flutter) working-directory: slang_flutter run: flutter pub get @@ -72,6 +65,15 @@ jobs: working-directory: slang_flutter run: flutter test + # The compilation test needs "pub get" to be run for Flutter first. + # That's why we run the core tests after the Flutter tests. + - name: Dependencies (core) + working-directory: slang + run: dart pub get + - name: Test (core) + working-directory: slang + run: dart test + - name: Dependencies (gpt) working-directory: slang_gpt run: dart pub get @@ -90,13 +92,6 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION_OLDEST }} channel: "stable" - - name: Dependencies (core) - working-directory: slang - run: dart pub get - - name: Test (core) - working-directory: slang - run: dart test - - name: Dependencies (flutter) working-directory: slang_flutter run: flutter pub get @@ -104,6 +99,15 @@ jobs: working-directory: slang_flutter run: flutter test + # The compilation test needs "pub get" to be run for Flutter first. + # That's why we run the core tests after the Flutter tests. + - name: Dependencies (core) + working-directory: slang + run: dart pub get + - name: Test (core) + working-directory: slang + run: dart test + - name: Dependencies (gpt) working-directory: slang_gpt run: dart pub get diff --git a/slang/test/integration/main/compilation_test.dart b/slang/test/integration/main/compilation_test.dart index 61c4d39a..6ffb0134 100644 --- a/slang/test/integration/main/compilation_test.dart +++ b/slang/test/integration/main/compilation_test.dart @@ -3,7 +3,7 @@ import 'package:test/test.dart'; import '../../util/resources_utils.dart'; -/// These tests are used to ensure that the generated code compiles. +/// These tests ensure that the generated code compiles. void main() { late Library library; From 0991de7b36f8766e6dad4570bc1763216a312274 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Thu, 25 Jan 2024 18:35:44 +0100 Subject: [PATCH 014/118] test: update rich text tests --- .../resources/main/_expected_rich_text.output | 44 ++++++++++++++++++- .../resources/main/json_rich_text.json | 7 +++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/slang/test/integration/resources/main/_expected_rich_text.output b/slang/test/integration/resources/main/_expected_rich_text.output index 45415b23..9a9ddaa8 100644 --- a/slang/test/integration/resources/main/_expected_rich_text.output +++ b/slang/test/integration/resources/main/_expected_rich_text.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 1 -/// Strings: 16 +/// Strings: 23 // coverage:ignore-file // ignore_for_file: type=lint @@ -152,6 +152,27 @@ class Translations implements BaseTranslations { late final Translations _root = this; // ignore: unused_field // Translations + String get simple => 'Simple'; + String simpleParam({required Object simpleParam}) => 'Simple ${simpleParam}'; + TextSpan get rich => TextSpan(children: [ + const TextSpan(text: 'Rich'), + ]); + TextSpan richParam({required InlineSpan param}) => TextSpan(children: [ + const TextSpan(text: 'Rich '), + param, + ]); + TextSpan richDefaultParam({required InlineSpanBuilder param}) => TextSpan(children: [ + const TextSpan(text: 'Rich '), + param('Hello'), + ]); + TextSpan richDefaultLinkedParam({required InlineSpanBuilder param}) => TextSpan(children: [ + const TextSpan(text: 'Rich '), + param(_root.simple), + ]); + TextSpan richDefaultLinkedParamTransitiveParam({required InlineSpanBuilder param, required Object simpleParam}) => TextSpan(children: [ + const TextSpan(text: 'Rich '), + param(_root.simpleParam(simpleParam: simpleParam)), + ]); String simplePlural({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: 'one item', other: 'multiple items', @@ -230,6 +251,27 @@ class Translations implements BaseTranslations { extension on Translations { dynamic _flatMapFunction(String path) { switch (path) { + case 'simple': return 'Simple'; + case 'simpleParam': return ({required Object simpleParam}) => 'Simple ${simpleParam}'; + case 'rich': return TextSpan(children: [ + const TextSpan(text: 'Rich'), + ]); + case 'richParam': return ({required InlineSpan param}) => TextSpan(children: [ + const TextSpan(text: 'Rich '), + param, + ]); + case 'richDefaultParam': return ({required InlineSpanBuilder param}) => TextSpan(children: [ + const TextSpan(text: 'Rich '), + param('Hello'), + ]); + case 'richDefaultLinkedParam': return ({required InlineSpanBuilder param}) => TextSpan(children: [ + const TextSpan(text: 'Rich '), + param(_root.simple), + ]); + case 'richDefaultLinkedParamTransitiveParam': return ({required InlineSpanBuilder param, required Object simpleParam}) => TextSpan(children: [ + const TextSpan(text: 'Rich '), + param(_root.simpleParam(simpleParam: simpleParam)), + ]); case 'simplePlural': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: 'one item', other: 'multiple items', diff --git a/slang/test/integration/resources/main/json_rich_text.json b/slang/test/integration/resources/main/json_rich_text.json index b9bb0ebd..95ad8387 100644 --- a/slang/test/integration/resources/main/json_rich_text.json +++ b/slang/test/integration/resources/main/json_rich_text.json @@ -1,4 +1,11 @@ { + "simple": "Simple", + "simpleParam": "Simple ${simpleParam}", + "rich(rich)": "Rich", + "richParam(rich)": "Rich ${param}", + "richDefaultParam(rich)": "Rich ${param(Hello)}", + "richDefaultLinkedParam(rich)": "Rich ${param(@:simple)}", + "richDefaultLinkedParamTransitiveParam(rich)": "Rich ${param(@:simpleParam)}", "simplePlural": { "one": "one item", "other": "multiple items" From c2bd79e4986e61d1ba13b303dd04885ef5c022b0 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Thu, 25 Jan 2024 18:49:54 +0100 Subject: [PATCH 015/118] test: remove unnecessary anchor file --- slang_flutter/test/util/expect_error_anchor.dart | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 slang_flutter/test/util/expect_error_anchor.dart diff --git a/slang_flutter/test/util/expect_error_anchor.dart b/slang_flutter/test/util/expect_error_anchor.dart deleted file mode 100644 index 9abfaa41..00000000 --- a/slang_flutter/test/util/expect_error_anchor.dart +++ /dev/null @@ -1,3 +0,0 @@ -/// An anchor for the compilation_test.dart tests. -/// This ensures that theses tests have Flutter available in the analyzer. -library expect_error_anchor; From b704d750e33d65a7c4deb5a1a5d7af00de040a3d Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Tue, 30 Jan 2024 21:22:04 +0100 Subject: [PATCH 016/118] docs: update example, readme, license --- LICENSE | 2 +- slang/LICENSE | 2 +- slang/README.md | 20 +++++++++----------- slang/example/ios/Runner/Info.plist | 7 +++++++ slang_build_runner/LICENSE | 2 +- slang_flutter/LICENSE | 2 +- slang_gpt/LICENSE | 2 +- 7 files changed, 21 insertions(+), 16 deletions(-) diff --git a/LICENSE b/LICENSE index 6b658912..ce8e0dd7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020-2023 Tien Do Nam +Copyright (c) 2020-2024 Tien Do Nam Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/slang/LICENSE b/slang/LICENSE index 6b658912..ce8e0dd7 100644 --- a/slang/LICENSE +++ b/slang/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020-2023 Tien Do Nam +Copyright (c) 2020-2024 Tien Do Nam Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/slang/README.md b/slang/README.md index ba2d8380..7dd8349a 100644 --- a/slang/README.md +++ b/slang/README.md @@ -8,8 +8,6 @@ Type-safe i18n solution using JSON, YAML, CSV, or ARB files. -The official successor of [fast_i18n](https://pub.dev/packages/fast_i18n). - ## About this library - 🚀 Minimal setup, create JSON files and get started! No configuration needed. @@ -167,19 +165,15 @@ lib/ **Step 3: Generate the dart code** -Built-in: +Built-in (recommended during development): ```text -# Recommended during development. It runs much faster than build_runner. - dart run slang ``` -Alternative (requires [slang_build_runner](https://pub.dev/packages/slang_build_runner)): +Alternative (useful for CI and initial git checkout, requires [slang_build_runner](https://pub.dev/packages/slang_build_runner)): ```text -# Useful for CI and initial git checkout. - dart run build_runner build -d ``` @@ -249,6 +243,10 @@ MaterialApp( **Step 4b: iOS configuration** +Add the supported locales to your `Info.plist` file. + +In this example, we support English (`en`) and German (`de`). + ``` File: ios/Runner/Info.plist @@ -262,9 +260,9 @@ File: ios/Runner/Info.plist **Step 5: Use your translations** ```dart -import 'package:my_app/i18n/strings.g.dart'; // import +import 'package:my_app/i18n/strings.g.dart'; // (1) import -String a = t.login.success; // get translation +String a = t.login.success; // (2) get translation ``` ## Configuration @@ -1896,7 +1894,7 @@ Feel free to extend this list :) MIT License -Copyright (c) 2020-2023 Tien Do Nam +Copyright (c) 2020-2024 Tien Do Nam Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/slang/example/ios/Runner/Info.plist b/slang/example/ios/Runner/Info.plist index a060db61..6749247f 100644 --- a/slang/example/ios/Runner/Info.plist +++ b/slang/example/ios/Runner/Info.plist @@ -41,5 +41,12 @@ UIViewControllerBasedStatusBarAppearance + + + CFBundleLocalizations + + en + de + diff --git a/slang_build_runner/LICENSE b/slang_build_runner/LICENSE index 6b658912..ce8e0dd7 100644 --- a/slang_build_runner/LICENSE +++ b/slang_build_runner/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020-2023 Tien Do Nam +Copyright (c) 2020-2024 Tien Do Nam Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/slang_flutter/LICENSE b/slang_flutter/LICENSE index 6b658912..ce8e0dd7 100644 --- a/slang_flutter/LICENSE +++ b/slang_flutter/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020-2023 Tien Do Nam +Copyright (c) 2020-2024 Tien Do Nam Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/slang_gpt/LICENSE b/slang_gpt/LICENSE index 6b658912..ce8e0dd7 100644 --- a/slang_gpt/LICENSE +++ b/slang_gpt/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020-2023 Tien Do Nam +Copyright (c) 2020-2024 Tien Do Nam Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 1a3ab946ecbd608a081bdb24a0b331428ea37dd7 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Wed, 31 Jan 2024 14:13:40 +0100 Subject: [PATCH 017/118] docs: update production app list --- slang/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/slang/README.md b/slang/README.md index 7dd8349a..a08b0535 100644 --- a/slang/README.md +++ b/slang/README.md @@ -1879,8 +1879,10 @@ Feel free to extend this list :) Open source: - [LocalSend (file sharing app)](https://github.com/localsend/localsend) +- [Hiddify](https://github.com/hiddify/hiddify-next) - [Saber (notes app)](https://github.com/adil192/saber) - [Boorusphere (booru viewer)](https://github.com/nullxception/boorusphere) +- [Alist Helper](https://github.com/Xmarmalade/alisthelper) - [Digitale Ehrenamtskarte (German volunteer app)](https://github.com/digitalfabrik/entitlementcard) - [Flutter Advanced Boilerplate (boilerplate project)](https://github.com/fikretsengul/flutter_advanced_boilerplate) From a56c642cf1a4980c80f691ad9163e63f31a3c95e Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Wed, 21 Feb 2024 13:25:40 +0100 Subject: [PATCH 018/118] test: add translation overrides new line test --- slang/test/unit/api/translation_overrides_test.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/slang/test/unit/api/translation_overrides_test.dart b/slang/test/unit/api/translation_overrides_test.dart index 825919db..63c5284c 100644 --- a/slang/test/unit/api/translation_overrides_test.dart +++ b/slang/test/unit/api/translation_overrides_test.dart @@ -15,6 +15,14 @@ void main() { expect(parsed, 'About'); }); + test('Should not escape new line', () { + final meta = _buildMetaWithOverrides({ + 'aboutPage.title': 'About\nPage', + }); + final parsed = TranslationOverrides.string(meta, 'aboutPage.title', {}); + expect(parsed, 'About\nPage'); + }); + test('Should return a plain string without escaping', () { final meta = _buildMetaWithOverrides({ 'aboutPage.title': 'About \' \$ {arg}', From 8a44d2fa0d98ea6158d5a1041dfc6c29d0368410 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Wed, 21 Feb 2024 13:26:09 +0100 Subject: [PATCH 019/118] ci: bump flutter to 3.19.0 --- .fvm/fvm_config.json | 2 +- .github/workflows/ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index 9bdedb8b..ccdae4db 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.16.8", + "flutterSdkVersion": "3.19.0", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42b593ab..b2200bf7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ on: env: FLUTTER_VERSION_OLDEST: "3.0.5" - FLUTTER_VERSION_NEWEST: "3.16.8" + FLUTTER_VERSION_NEWEST: "3.19.0" jobs: format: From ba487b798427d8982cb630dca46b3cf572b0499e Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Tue, 27 Feb 2024 22:25:05 +0100 Subject: [PATCH 020/118] fix: handle nested interfaces --- .../builder/translation_model_builder.dart | 11 +- .../translation_model_builder_test.dart | 122 ++++++++++++++++++ 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/slang/lib/builder/builder/translation_model_builder.dart b/slang/lib/builder/builder/translation_model_builder.dart index eb90e71e..6f061f33 100644 --- a/slang/lib/builder/builder/translation_model_builder.dart +++ b/slang/lib/builder/builder/translation_model_builder.dart @@ -594,7 +594,8 @@ void _applyInterfaceAndGenericsRecursive({ if (interface != null) { curr.setInterface(interface); - // in case this interface is new + // Save the interface in the collection + // (might override existing interface of the same name) interfaceCollection.resultInterfaces[interface.name] = interface; } } @@ -783,7 +784,13 @@ Set _parseAttributes(ObjectNode node) { returnType = 'List<${child.genericType}>'; parameters = {}; // lists never have parameters } else if (child is ObjectNode) { - returnType = 'Map'; + if (child.interface != null) { + returnType = child.interface!.name; + } else if (child.isMap) { + returnType = 'Map'; + } else { + returnType = 'UnsupportedType'; + } parameters = {}; // objects never have parameters } else if (child is PluralNode) { returnType = child.rich ? 'TextSpan' : 'String'; diff --git a/slang/test/unit/builder/translation_model_builder_test.dart b/slang/test/unit/builder/translation_model_builder_test.dart index 24119c08..3160de8c 100644 --- a/slang/test/unit/builder/translation_model_builder_test.dart +++ b/slang/test/unit/builder/translation_model_builder_test.dart @@ -209,5 +209,127 @@ void main() { expect(result.contexts, []); }); + + test('Should handle nested interfaces specified via modifier', () { + final resultUsingModifiers = TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), + map: { + 'myContainer(interface=MyInterface)': { + 'firstItem': { + 'a': 'A1', + 'nestedItem(singleInterface=MyNestedInterface)': { + 'z': 'Z', + }, + }, + 'secondItem': { + 'a': 'A2', + 'nestedItem(singleInterface=MyNestedInterface)': { + 'z': 'Z', + }, + }, + 'thirdItem': { + 'a': 'A3', + 'nestedItem(singleInterface=MyNestedInterface)': { + 'z': 'Z', + }, + }, + } + }, + localeDebug: RawConfig.defaultBaseLocale, + ); + + _checkInterfaceResult(resultUsingModifiers); + }); + + test('Should handle nested interface specified via config', () { + final resultUsingConfig = TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig.copyWith( + interfaces: [ + InterfaceConfig( + name: 'MyNestedInterface', + paths: [], + attributes: { + InterfaceAttribute( + attributeName: 'z', + returnType: 'String', + parameters: {}, + optional: false, + ), + }, + ), + ], + ).toBuildModelConfig(), + map: { + 'myContainer(interface=MyInterface)': { + 'firstItem': { + 'a': 'A1', + 'nestedItem': { + 'z': 'Z', + }, + }, + 'secondItem': { + 'a': 'A2', + 'nestedItem': { + 'z': 'Z', + }, + }, + 'thirdItem': { + 'a': 'A3', + 'nestedItem': { + 'z': 'Z', + }, + }, + } + }, + localeDebug: RawConfig.defaultBaseLocale, + ); + + _checkInterfaceResult(resultUsingConfig); + }); }); } + +void _checkInterfaceResult(BuildModelResult result) { + final interfaces = result.interfaces; + expect(interfaces.length, 2); + expect(interfaces[0].name, 'MyNestedInterface'); + expect(interfaces[0].attributes.length, 1); + expect(interfaces[0].attributes.first.attributeName, 'z'); + expect(interfaces[1].name, 'MyInterface'); + expect(interfaces[1].attributes.length, 2); + expect(interfaces[1].attributes.first.attributeName, 'a'); + expect(interfaces[1].attributes.last.attributeName, 'nestedItem'); + + final objectNode = result.root.entries['myContainer'] as ObjectNode; + expect(objectNode.interface, isNull); + + expect(objectNode.entries['firstItem'], isA()); + expect(objectNode.entries['secondItem'], isA()); + expect(objectNode.entries['thirdItem'], isA()); + + expect((objectNode.entries['firstItem'] as ObjectNode).interface?.name, + 'MyInterface'); + expect((objectNode.entries['secondItem'] as ObjectNode).interface?.name, + 'MyInterface'); + expect((objectNode.entries['thirdItem'] as ObjectNode).interface?.name, + 'MyInterface'); + + expect( + ((objectNode.entries['firstItem'] as ObjectNode).entries['nestedItem'] + as ObjectNode) + .interface + ?.name, + 'MyNestedInterface'); + expect( + ((objectNode.entries['secondItem'] as ObjectNode).entries['nestedItem'] + as ObjectNode) + .interface + ?.name, + 'MyNestedInterface'); + expect( + ((objectNode.entries['thirdItem'] as ObjectNode).entries['nestedItem'] + as ObjectNode) + .interface + ?.name, + 'MyNestedInterface'); +} From 6b860ae3d69f86a437a7fde9d901f3e52d30ed47 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Fri, 1 Mar 2024 02:26:07 +0100 Subject: [PATCH 021/118] feat: add parameter type hints --- slang/lib/builder/model/node.dart | 80 ++++++++++++------- slang/lib/src/text_parser.dart | 33 ++++++++ slang_flutter/lib/slang_flutter.dart | 2 + .../lib/translation_overrides_flutter.dart | 2 - 4 files changed, 88 insertions(+), 29 deletions(-) create mode 100644 slang/lib/src/text_parser.dart diff --git a/slang/lib/builder/model/node.dart b/slang/lib/builder/model/node.dart index e7af35a2..1f43c779 100644 --- a/slang/lib/builder/model/node.dart +++ b/slang/lib/builder/model/node.dart @@ -5,6 +5,7 @@ import 'package:slang/builder/model/pluralization.dart'; import 'package:slang/builder/utils/string_extensions.dart'; import 'package:slang/builder/utils/regex_utils.dart'; import 'package:slang/builder/utils/string_interpolation_extensions.dart'; +import 'package:slang/src/text_parser.dart'; class NodeModifiers { static const rich = 'rich'; @@ -288,9 +289,11 @@ class StringTextNode extends TextNode { final parsedResult = _parseInterpolation( raw: shouldEscape ? _escapeContent(raw, interpolation) : raw, interpolation: interpolation, + defaultType: 'Object', paramCase: paramCase, ); - _params = parsedResult.params; + _params = parsedResult.params.keys.toSet(); + _paramTypeMap.addAll(parsedResult.params); if (linkParamMap != null) { _params.addAll(linkParamMap.values.expand((e) => e)); @@ -373,22 +376,25 @@ class RichTextNode extends TextNode { final rawParsedResult = _parseInterpolation( raw: shouldEscape ? _escapeContent(raw, interpolation) : raw, interpolation: interpolation, + defaultType: '', // types are ignored paramCase: null, // param case will be applied later ); - final parsedParams = rawParsedResult.params - .map((p) => _parseParamWithArg(input: p, paramCase: paramCase)) - .toList(); - _params = parsedParams.map((e) => e.paramName).toSet(); + _params = {}; + for (final key in rawParsedResult.params.keys) { + final parsedParam = _parseParamWithArg( + rawParam: key, + paramCase: paramCase, + ); + _params.add(parsedParam.paramName); + _paramTypeMap[parsedParam.paramName] = + parsedParam.arg == null ? 'InlineSpan' : 'InlineSpanBuilder'; + } + if (linkParamMap != null) { _params.addAll(linkParamMap.values.expand((e) => e)); } - _paramTypeMap = { - for (final p in parsedParams) - p.paramName: (p.arg != null ? 'InlineSpanBuilder' : 'InlineSpan'), - }; - _links = {}; _spans = _splitWithMatchAndNonMatch( rawParsedResult.parsedContent, @@ -406,7 +412,7 @@ class RichTextNode extends TextNode { }, onMatch: (match) { final parsed = _parseParamWithArg( - input: (match.group(1) ?? match.group(2))!, + rawParam: (match.group(1) ?? match.group(2))!, paramCase: paramCase, ); final parsedArg = parsed.arg; @@ -485,7 +491,9 @@ String _escapeContent(String raw, StringInterpolation interpolation) { class _ParseInterpolationResult { final String parsedContent; - final Set params; + + /// Map of parameter name -> parameter type + final Map params; _ParseInterpolationResult(this.parsedContent, this.params); @@ -497,10 +505,11 @@ class _ParseInterpolationResult { _ParseInterpolationResult _parseInterpolation({ required String raw, required StringInterpolation interpolation, + required String defaultType, required CaseStyle? paramCase, }) { final String parsedContent; - final params = Set(); + final params = Map(); switch (interpolation) { case StringInterpolation.dart: @@ -508,23 +517,37 @@ _ParseInterpolationResult _parseInterpolation({ final rawParam = match.startsWith(r'${') ? match.substring(2, match.length - 1) : match.substring(1, match.length); - final param = rawParam.toCase(paramCase); - params.add(param); - return '\${$param}'; + final parsedParam = parseParam( + rawParam: rawParam, + defaultType: defaultType, + caseStyle: paramCase, + ); + params[parsedParam.paramName] = parsedParam.paramType; + return '\${${parsedParam.paramName}}'; }); break; case StringInterpolation.braces: parsedContent = raw.replaceBracesInterpolation(replacer: (match) { - final param = match.substring(1, match.length - 1).toCase(paramCase); - params.add(param); - return '\${$param}'; + final rawParam = match.substring(1, match.length - 1); + final parsedParam = parseParam( + rawParam: rawParam, + defaultType: defaultType, + caseStyle: paramCase, + ); + params[parsedParam.paramName] = parsedParam.paramType; + return '\${${parsedParam.paramName}}'; }); break; case StringInterpolation.doubleBraces: parsedContent = raw.replaceDoubleBracesInterpolation(replacer: (match) { - final param = match.substring(2, match.length - 2).toCase(paramCase); - params.add(param); - return '\${$param}'; + final rawParam = match.substring(2, match.length - 2); + final parsedParam = parseParam( + rawParam: rawParam, + defaultType: defaultType, + caseStyle: paramCase, + ); + params[parsedParam.paramName] = parsedParam.paramType; + return '\${${parsedParam.paramName}}'; }); } @@ -589,14 +612,17 @@ Iterable _splitWithMatchAndNonMatch( } _ParamWithArg _parseParamWithArg({ - required String input, + required String rawParam, required CaseStyle? paramCase, }) { - final match = RegexUtils.paramWithArg.firstMatch(input); - if (match == null) { - throw 'Rich text parameters must follow the syntax: "param(default text)"\nGot instead: "$input"\n'; + final end = rawParam.lastIndexOf(')'); + if (end == -1) { + return _ParamWithArg(rawParam.toCase(paramCase), null); } - return _ParamWithArg(match.group(1)!.toCase(paramCase), match.group(3)); + + final start = rawParam.indexOf('('); + final parameterName = rawParam.substring(0, start).toCase(paramCase); + return _ParamWithArg(parameterName, rawParam.substring(start + 1, end)); } class _ParamWithArg { diff --git a/slang/lib/src/text_parser.dart b/slang/lib/src/text_parser.dart new file mode 100644 index 00000000..f7cf3db4 --- /dev/null +++ b/slang/lib/src/text_parser.dart @@ -0,0 +1,33 @@ +import 'package:slang/builder/model/enums.dart'; +import 'package:slang/builder/utils/string_extensions.dart'; + +class ParseParamResult { + final String paramName; + final String paramType; + + ParseParamResult(this.paramName, this.paramType); + + @override + String toString() => + '_ParseParamResult(paramName: $paramName, paramType: $paramType)'; +} + +ParseParamResult parseParam({ + required String rawParam, + required String defaultType, + required CaseStyle? caseStyle, +}) { + if (rawParam.endsWith(')')) { + // rich text parameter with default value + // this will be parsed by another method + return ParseParamResult( + rawParam, + '', + ); + } + final split = rawParam.split(':'); + if (split.length == 1) { + return ParseParamResult(split[0].toCase(caseStyle), defaultType); + } + return ParseParamResult(split[0].trim().toCase(caseStyle), split[1].trim()); +} diff --git a/slang_flutter/lib/slang_flutter.dart b/slang_flutter/lib/slang_flutter.dart index 87bb9f4a..d557eb02 100644 --- a/slang_flutter/lib/slang_flutter.dart +++ b/slang_flutter/lib/slang_flutter.dart @@ -8,6 +8,8 @@ export 'package:slang/slang.dart'; part 'translation_overrides_flutter.dart'; +typedef InlineSpanBuilder = InlineSpan Function(String); + extension ExtAppLocaleUtils, T extends BaseTranslations> on BaseAppLocaleUtils { /// Returns the locale of the device. diff --git a/slang_flutter/lib/translation_overrides_flutter.dart b/slang_flutter/lib/translation_overrides_flutter.dart index aa0d4264..5f5745f4 100644 --- a/slang_flutter/lib/translation_overrides_flutter.dart +++ b/slang_flutter/lib/translation_overrides_flutter.dart @@ -1,7 +1,5 @@ part of 'slang_flutter.dart'; -typedef InlineSpanBuilder = InlineSpan Function(String); - class TranslationOverridesFlutter { /// Handler for overridden rich text. /// Returns a [TextSpan] if the [path] was successfully overridden. From 35c15037c9d6c22838faab5d8e74c32d678ed40b Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sat, 2 Mar 2024 19:07:29 +0100 Subject: [PATCH 022/118] refactor: move more files to src, handle lints --- slang/analysis_options.yaml | 9 +++- slang/bin/add.dart | 4 +- slang/bin/analyze.dart | 4 +- slang/bin/apply.dart | 4 +- slang/bin/clean.dart | 4 +- slang/bin/edit.dart | 4 +- slang/bin/outdated.dart | 4 +- slang/bin/slang.dart | 8 ++-- slang/bin/stats.dart | 4 +- slang/bin/watch.dart | 4 +- slang/lib/api/singleton.dart | 10 ++--- slang/lib/api/translation_overrides.dart | 6 +-- .../builder/generate_config_builder.dart | 2 +- .../builder/builder/raw_config_builder.dart | 22 +++++----- .../slang_file_collection_builder.dart | 6 +-- .../builder/translation_map_builder.dart | 4 +- .../builder/translation_model_builder.dart | 42 ++++++++++--------- .../lib/builder/model/build_model_config.dart | 4 +- slang/lib/builder/model/generate_config.dart | 2 +- slang/lib/builder/model/i18n_locale.dart | 4 +- slang/lib/builder/model/interface.dart | 2 +- slang/lib/builder/model/node.dart | 32 +++++++------- slang/lib/builder/model/raw_config.dart | 5 +-- .../builder/model/slang_file_collection.dart | 8 ++-- .../{ => builder/builder}/text_parser.dart | 2 +- .../builder/decoder/arb_decoder.dart | 12 +++--- .../builder/decoder/base_decoder.dart | 8 ++-- .../builder/decoder/csv_decoder.dart | 4 +- .../builder/decoder/json_decoder.dart | 2 +- .../builder/decoder/yaml_decoder.dart | 4 +- .../builder/generator/generate_header.dart | 4 +- .../generator/generate_translation_map.dart | 10 ++--- .../generator/generate_translations.dart | 12 +++--- .../builder/generator/generator.dart | 4 +- .../{ => src}/builder/generator/helper.dart | 8 ++-- .../{ => src}/builder/generator_facade.dart | 6 +-- .../builder/utils/brackets_utils.dart | 2 +- .../builder/utils/encryption_utils.dart | 2 +- .../{ => src}/builder/utils/file_utils.dart | 8 ++-- .../{ => src}/builder/utils/map_utils.dart | 4 +- .../{ => src}/builder/utils/node_utils.dart | 8 ++-- .../{ => src}/builder/utils/path_utils.dart | 4 +- .../{ => src}/builder/utils/regex_utils.dart | 0 .../builder/utils/string_extensions.dart | 6 +-- .../string_interpolation_extensions.dart | 0 slang/lib/src/runner/analyze.dart | 30 +++++++------ slang/lib/src/runner/apply.dart | 6 +-- slang/lib/src/runner/clean.dart | 4 +- slang/lib/src/runner/edit.dart | 6 +-- slang/lib/src/runner/migrate_arb.dart | 22 +++++----- slang/lib/src/runner/stats.dart | 6 +-- .../src/runner/utils/read_analysis_file.dart | 8 ++-- slang/pubspec.yaml | 2 +- .../integration/main/csv_compact_test.dart | 4 +- slang/test/integration/main/csv_test.dart | 4 +- .../main/fallback_base_locale_test.dart | 6 +-- .../main/json_multiple_files_test.dart | 4 +- slang/test/integration/main/json_test.dart | 4 +- .../integration/main/no_flutter_test.dart | 4 +- .../main/no_locale_handling_test.dart | 4 +- .../integration/main/obfuscation_test.dart | 4 +- .../test/integration/main/rich_text_test.dart | 4 +- .../main/translation_overrides_test.dart | 4 +- slang/test/integration/main/yaml_test.dart | 4 +- slang/test/integration/update.dart | 8 ++-- slang/test/unit/api/secret_test.dart | 2 +- .../unit/api/translation_overrides_test.dart | 4 +- .../translation_model_builder_test.dart | 2 +- slang/test/unit/decoder/arb_decoder_test.dart | 32 +++++++------- slang/test/unit/decoder/csv_decoder_test.dart | 2 +- .../test/unit/utils/brackets_utils_test.dart | 2 +- slang/test/unit/utils/map_utils_test.dart | 2 +- slang/test/unit/utils/node_utils_test.dart | 2 +- slang/test/unit/utils/path_utils_test.dart | 4 +- slang/test/unit/utils/regex_utils_test.dart | 2 +- slang/test/unit/utils/secret_test.dart | 2 +- .../unit/utils/string_extensions_test.dart | 2 +- .../string_interpolation_extensions_test.dart | 2 +- slang/test/util/resources_utils.dart | 7 ++-- slang_build_runner/analysis_options.yaml | 7 ++++ .../lib/slang_build_runner.dart | 17 +++++--- slang_build_runner/pubspec.yaml | 3 ++ slang_gpt/lib/runner.dart | 12 ++++-- slang_gpt/lib/util/logger.dart | 6 ++- slang_gpt/lib/util/maps.dart | 3 +- 85 files changed, 294 insertions(+), 267 deletions(-) rename slang/lib/src/{ => builder/builder}/text_parser.dart (92%) rename slang/lib/{ => src}/builder/decoder/arb_decoder.dart (91%) rename slang/lib/{ => src}/builder/decoder/base_decoder.dart (78%) rename slang/lib/{ => src}/builder/decoder/csv_decoder.dart (96%) rename slang/lib/{ => src}/builder/decoder/json_decoder.dart (71%) rename slang/lib/{ => src}/builder/decoder/yaml_decoder.dart (60%) rename slang/lib/{ => src}/builder/generator/generate_header.dart (99%) rename slang/lib/{ => src}/builder/generator/generate_translation_map.dart (95%) rename slang/lib/{ => src}/builder/generator/generate_translations.dart (98%) rename slang/lib/{ => src}/builder/generator/generator.dart (85%) rename slang/lib/{ => src}/builder/generator/helper.dart (93%) rename slang/lib/{ => src}/builder/generator_facade.dart (97%) rename slang/lib/{ => src}/builder/utils/brackets_utils.dart (96%) rename slang/lib/{ => src}/builder/utils/encryption_utils.dart (92%) rename slang/lib/{ => src}/builder/utils/file_utils.dart (94%) rename slang/lib/{ => src}/builder/utils/map_utils.dart (99%) rename slang/lib/{ => src}/builder/utils/node_utils.dart (94%) rename slang/lib/{ => src}/builder/utils/path_utils.dart (97%) rename slang/lib/{ => src}/builder/utils/regex_utils.dart (100%) rename slang/lib/{ => src}/builder/utils/string_extensions.dart (93%) rename slang/lib/{ => src}/builder/utils/string_interpolation_extensions.dart (100%) create mode 100644 slang_build_runner/analysis_options.yaml diff --git a/slang/analysis_options.yaml b/slang/analysis_options.yaml index 8a484de8..ba94c960 100644 --- a/slang/analysis_options.yaml +++ b/slang/analysis_options.yaml @@ -1 +1,8 @@ -include: package:lints/core.yaml \ No newline at end of file +include: package:lints/recommended.yaml +linter: + rules: + prefer_single_quotes: true + sort_pub_dependencies: true + always_use_package_imports: true + directives_ordering: true + constant_identifier_names: false diff --git a/slang/bin/add.dart b/slang/bin/add.dart index 3ce86b3e..3ef2970f 100644 --- a/slang/bin/add.dart +++ b/slang/bin/add.dart @@ -1,5 +1,5 @@ -import 'slang.dart' as mainRunner; +import 'slang.dart' as main_runner; void main(List arguments) async { - mainRunner.main(['add', ...arguments]); + main_runner.main(['add', ...arguments]); } diff --git a/slang/bin/analyze.dart b/slang/bin/analyze.dart index 22bfcdc6..e1f6b4d3 100644 --- a/slang/bin/analyze.dart +++ b/slang/bin/analyze.dart @@ -1,5 +1,5 @@ -import 'slang.dart' as mainRunner; +import 'slang.dart' as main_runner; void main(List arguments) async { - mainRunner.main(['analyze', ...arguments]); + main_runner.main(['analyze', ...arguments]); } diff --git a/slang/bin/apply.dart b/slang/bin/apply.dart index b6667d88..bbf5384b 100644 --- a/slang/bin/apply.dart +++ b/slang/bin/apply.dart @@ -1,5 +1,5 @@ -import 'slang.dart' as mainRunner; +import 'slang.dart' as main_runner; void main(List arguments) async { - mainRunner.main(['apply', ...arguments]); + main_runner.main(['apply', ...arguments]); } diff --git a/slang/bin/clean.dart b/slang/bin/clean.dart index ebbc3231..43595701 100644 --- a/slang/bin/clean.dart +++ b/slang/bin/clean.dart @@ -1,5 +1,5 @@ -import 'slang.dart' as mainRunner; +import 'slang.dart' as main_runner; void main(List arguments) async { - mainRunner.main(['clean', ...arguments]); + main_runner.main(['clean', ...arguments]); } diff --git a/slang/bin/edit.dart b/slang/bin/edit.dart index 4b918a78..ebe1dfd5 100644 --- a/slang/bin/edit.dart +++ b/slang/bin/edit.dart @@ -1,5 +1,5 @@ -import 'slang.dart' as mainRunner; +import 'slang.dart' as main_runner; void main(List arguments) async { - mainRunner.main(['edit', ...arguments]); + main_runner.main(['edit', ...arguments]); } diff --git a/slang/bin/outdated.dart b/slang/bin/outdated.dart index 5bd667d0..290a0cdb 100644 --- a/slang/bin/outdated.dart +++ b/slang/bin/outdated.dart @@ -1,5 +1,5 @@ -import 'slang.dart' as mainRunner; +import 'slang.dart' as main_runner; void main(List arguments) async { - mainRunner.main(['outdated', ...arguments]); + main_runner.main(['outdated', ...arguments]); } diff --git a/slang/bin/slang.dart b/slang/bin/slang.dart index 8a01066c..dc20c926 100644 --- a/slang/bin/slang.dart +++ b/slang/bin/slang.dart @@ -3,18 +3,18 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:slang/builder/builder/slang_file_collection_builder.dart'; import 'package:slang/builder/builder/translation_map_builder.dart'; -import 'package:slang/builder/generator_facade.dart'; import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/raw_config.dart'; import 'package:slang/builder/model/slang_file_collection.dart'; +import 'package:slang/src/builder/generator_facade.dart'; +import 'package:slang/src/builder/utils/file_utils.dart'; +import 'package:slang/src/builder/utils/path_utils.dart'; import 'package:slang/src/runner/analyze.dart'; import 'package:slang/src/runner/apply.dart'; import 'package:slang/src/runner/clean.dart'; import 'package:slang/src/runner/edit.dart'; import 'package:slang/src/runner/migrate.dart'; import 'package:slang/src/runner/stats.dart'; -import 'package:slang/builder/utils/file_utils.dart'; -import 'package:slang/builder/utils/path_utils.dart'; import 'package:watcher/watcher.dart'; /// Determines what the runner will do @@ -372,7 +372,7 @@ Future generateTranslations({ .firstWhereOrNull((c) => c.enumValues != null || c.paths.isNotEmpty); if (deprecatedContext != null) { print( - '${_YELLOW}[Deprecated] Use explicit context modifiers instead of populating the config: ${deprecatedContext.enumName} (see: https://github.com/slang-i18n/slang/blob/main/slang/MIGRATION.md#use-context-modifier-since-3190)$_RESET'); + '$_YELLOW[Deprecated] Use explicit context modifiers instead of populating the config: ${deprecatedContext.enumName} (see: https://github.com/slang-i18n/slang/blob/main/slang/MIGRATION.md#use-context-modifier-since-3190)$_RESET'); } } } diff --git a/slang/bin/stats.dart b/slang/bin/stats.dart index 9f544c1e..f44d0647 100644 --- a/slang/bin/stats.dart +++ b/slang/bin/stats.dart @@ -1,5 +1,5 @@ -import 'slang.dart' as mainRunner; +import 'slang.dart' as main_runner; void main(List arguments) async { - mainRunner.main(['stats', ...arguments]); + main_runner.main(['stats', ...arguments]); } diff --git a/slang/bin/watch.dart b/slang/bin/watch.dart index 72b5f9ed..e524281c 100644 --- a/slang/bin/watch.dart +++ b/slang/bin/watch.dart @@ -1,5 +1,5 @@ -import 'slang.dart' as mainRunner; +import 'slang.dart' as main_runner; void main(List arguments) async { - mainRunner.main(['watch', ...arguments]); + main_runner.main(['watch', ...arguments]); } diff --git a/slang/lib/api/singleton.dart b/slang/lib/api/singleton.dart index 68f7daa2..8855f163 100644 --- a/slang/lib/api/singleton.dart +++ b/slang/lib/api/singleton.dart @@ -3,12 +3,12 @@ import 'package:slang/api/locale.dart'; import 'package:slang/api/pluralization.dart'; import 'package:slang/api/state.dart'; import 'package:slang/builder/builder/translation_model_builder.dart'; -import 'package:slang/builder/decoder/base_decoder.dart'; import 'package:slang/builder/model/build_model_config.dart'; import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/utils/map_utils.dart'; -import 'package:slang/builder/utils/node_utils.dart'; -import 'package:slang/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/decoder/base_decoder.dart'; +import 'package:slang/src/builder/utils/map_utils.dart'; +import 'package:slang/src/builder/utils/node_utils.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; /// Provides utility functions without any side effects. abstract class BaseAppLocaleUtils, @@ -190,7 +190,7 @@ abstract class BaseLocaleSettings, BaseLocaleSettings({ required this.utils, - }) : this.translationMap = _buildMap(utils.locales); + }) : translationMap = _buildMap(utils.locales); /// Updates the provider state and therefore triggers a rebuild /// on all widgets listening to this provider. diff --git a/slang/lib/api/translation_overrides.dart b/slang/lib/api/translation_overrides.dart index a3f44152..3981dc4c 100644 --- a/slang/lib/api/translation_overrides.dart +++ b/slang/lib/api/translation_overrides.dart @@ -1,10 +1,10 @@ import 'package:slang/api/locale.dart'; import 'package:slang/api/pluralization.dart'; -import 'package:slang/builder/generator/helper.dart'; import 'package:slang/builder/model/node.dart'; import 'package:slang/builder/model/pluralization.dart'; -import 'package:slang/builder/utils/regex_utils.dart'; -import 'package:slang/builder/utils/string_interpolation_extensions.dart'; +import 'package:slang/src/builder/generator/helper.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/string_interpolation_extensions.dart'; /// Utility class handling overridden translations class TranslationOverrides { diff --git a/slang/lib/builder/builder/generate_config_builder.dart b/slang/lib/builder/builder/generate_config_builder.dart index d8193276..a5f1d435 100644 --- a/slang/lib/builder/builder/generate_config_builder.dart +++ b/slang/lib/builder/builder/generate_config_builder.dart @@ -1,9 +1,9 @@ import 'package:slang/builder/builder/build_model_config_builder.dart'; import 'package:slang/builder/model/context_type.dart'; import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/raw_config.dart'; import 'package:slang/builder/model/generate_config.dart'; import 'package:slang/builder/model/interface.dart'; +import 'package:slang/builder/model/raw_config.dart'; class GenerateConfigBuilder { static GenerateConfig build({ diff --git a/slang/lib/builder/builder/raw_config_builder.dart b/slang/lib/builder/builder/raw_config_builder.dart index 8952f5cd..3a30243c 100644 --- a/slang/lib/builder/builder/raw_config_builder.dart +++ b/slang/lib/builder/builder/raw_config_builder.dart @@ -1,12 +1,12 @@ -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/obfuscation_config.dart'; -import 'package:slang/builder/model/raw_config.dart'; import 'package:slang/builder/model/context_type.dart'; +import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/interface.dart'; -import 'package:slang/builder/utils/map_utils.dart'; -import 'package:slang/builder/utils/regex_utils.dart'; -import 'package:slang/builder/utils/string_extensions.dart'; +import 'package:slang/builder/model/obfuscation_config.dart'; +import 'package:slang/builder/model/raw_config.dart'; +import 'package:slang/src/builder/utils/map_utils.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/string_extensions.dart'; import 'package:yaml/yaml.dart'; class RawConfigBuilder { @@ -117,7 +117,7 @@ class RawConfigBuilder { extension on Map { /// Parses the 'contexts' config List toContextTypes() { - return this.entries.map((e) { + return entries.map((e) { final enumName = e.key.toCase(CaseStyle.pascal); final config = e.value as Map; @@ -139,7 +139,7 @@ extension on Map { /// Parses the 'interfaces' config List toInterfaces() { - return this.entries.map((e) { + return entries.map((e) { final interfaceName = e.key.toCase(CaseStyle.pascal); final Set attributes = {}; final List paths; @@ -157,7 +157,7 @@ extension on Map { // parse attributes final attributesConfig = interfaceConfig['attributes'] as List? ?? {}; - attributesConfig.forEach((attribute) { + for (final attribute in attributesConfig) { final match = RegexUtils.attributeRegex.firstMatch(attribute); if (match == null) { throw 'Interface "$interfaceName" has invalid attributes. "$attribute" could not be parsed.'; @@ -190,7 +190,7 @@ extension on Map { ); attributes.add(parsedAttribute); - }); + } // parse paths final pathsConfig = interfaceConfig['paths'] as List? ?? []; @@ -223,6 +223,6 @@ extension on Map { extension on String { String removeTrailingSlash() { - return this.endsWith('/') ? this.substring(0, this.length - 1) : this; + return endsWith('/') ? substring(0, length - 1) : this; } } diff --git a/slang/lib/builder/builder/slang_file_collection_builder.dart b/slang/lib/builder/builder/slang_file_collection_builder.dart index 48632327..828e3d02 100644 --- a/slang/lib/builder/builder/slang_file_collection_builder.dart +++ b/slang/lib/builder/builder/slang_file_collection_builder.dart @@ -6,8 +6,8 @@ import 'package:slang/builder/builder/raw_config_builder.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/raw_config.dart'; import 'package:slang/builder/model/slang_file_collection.dart'; -import 'package:slang/builder/utils/path_utils.dart'; -import 'package:slang/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/path_utils.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; class SlangFileCollectionBuilder { static SlangFileCollection readFromFileSystem({ @@ -101,7 +101,7 @@ class SlangFileCollectionBuilder { // could also be a non-base locale when directory name is a locale // directory name could be a locale - I18nLocale? directoryLocale = null; + I18nLocale? directoryLocale; if (config.namespaces) { directoryLocale = PathUtils.findDirectoryLocale( filePath: f.path, diff --git a/slang/lib/builder/builder/translation_map_builder.dart b/slang/lib/builder/builder/translation_map_builder.dart index 267ace8d..e0f6aee6 100644 --- a/slang/lib/builder/builder/translation_map_builder.dart +++ b/slang/lib/builder/builder/translation_map_builder.dart @@ -1,9 +1,9 @@ -import 'package:slang/builder/decoder/base_decoder.dart'; -import 'package:slang/builder/decoder/csv_decoder.dart'; import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/slang_file_collection.dart'; import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/decoder/base_decoder.dart'; +import 'package:slang/src/builder/decoder/csv_decoder.dart'; class TranslationMapBuilder { /// This method transforms files to an intermediate model [TranslationMap]. diff --git a/slang/lib/builder/builder/translation_model_builder.dart b/slang/lib/builder/builder/translation_model_builder.dart index 6f061f33..5a3ed99e 100644 --- a/slang/lib/builder/builder/translation_model_builder.dart +++ b/slang/lib/builder/builder/translation_model_builder.dart @@ -2,14 +2,14 @@ import 'dart:collection'; import 'package:collection/collection.dart'; import 'package:slang/builder/model/build_model_config.dart'; -import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/context_type.dart'; +import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/interface.dart'; import 'package:slang/builder/model/node.dart'; import 'package:slang/builder/model/pluralization.dart'; -import 'package:slang/builder/utils/node_utils.dart'; -import 'package:slang/builder/utils/regex_utils.dart'; -import 'package:slang/builder/utils/string_extensions.dart'; +import 'package:slang/src/builder/utils/node_utils.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/string_extensions.dart'; class BuildModelResult { final ObjectNode root; // the actual strings @@ -101,7 +101,7 @@ class TranslationModelBuilder { final linkParamMap = >{}; final paramTypeMap = {}; - value.links.forEach((link) { + for (final link in value.links) { final paramSet = {}; final visitedLinks = {}; final pathQueue = Queue(); @@ -121,11 +121,11 @@ class TranslationModelBuilder { paramTypeMap.addAll(linkedNode.paramTypeMap); // lookup links - linkedNode.links.forEach((child) { + for (final child in linkedNode.links) { if (!visitedLinks.contains(child)) { pathQueue.add(child); } - }); + } } else if (linkedNode is PluralNode || linkedNode is ContextNode) { final Iterable textNodes = linkedNode is PluralNode ? linkedNode.quantities.values @@ -164,20 +164,20 @@ class TranslationModelBuilder { paramSet.addAll(linkedParamSet); // lookup links of children - textNodes.forEach((element) { - element.links.forEach((child) { + for (final element in textNodes) { + for (final child in element.links) { if (!visitedLinks.contains(child)) { pathQueue.add(child); } - }); - }); + } + } } else { throw '"$key" is linked to "$currLink" which is a ${linkedNode.runtimeType} (must be $TextNode or $ObjectNode).'; } } linkParamMap[link] = paramSet; - }); + } if (linkParamMap.values.any((params) => params.isNotEmpty)) { // rebuild TextNode because its linked translations have parameters @@ -422,7 +422,7 @@ Map _parseMapNode({ digestedMap = _digestContextEntries( baseTranslation: baseData!.root, baseContext: baseContext, - path: '$currPath', + path: currPath, entries: digestedMap, ); } @@ -498,7 +498,9 @@ String? _parseCommentNode(dynamic node) { } void _setParent(Node parent, Iterable children) { - children.forEach((child) => child.setParent(parent)); + for (final child in children) { + child.setParent(parent); + } } _DetectionResult _determineNodeType( @@ -576,14 +578,14 @@ void _applyInterfaceAndGenericsRecursive({ required InterfaceCollection interfaceCollection, }) { // first calculate for children (post order!) - curr.values.forEach((child) { + for (final child in curr.values) { if (child is IterableNode) { _applyInterfaceAndGenericsRecursive( curr: child, interfaceCollection: interfaceCollection, ); } - }); + } if (curr is ObjectNode) { // check if this node itself is an interface @@ -818,7 +820,7 @@ void _fixEmptyLists({ required ObjectNode node, required Interface interface, }) { - interface.attributes.forEach((attribute) { + for (final attribute in interface.attributes) { final child = node.entries[attribute.attributeName]; if (child != null && child is ListNode && child.entries.isEmpty) { final match = RegexUtils.genericRegex.firstMatch(attribute.returnType); @@ -827,7 +829,7 @@ void _fixEmptyLists({ child.setGenericType(generic); } } - }); + } } /// Makes sure that every enum value in [baseContext] is also present in [entries]. @@ -908,13 +910,13 @@ extension on BuildModelConfig { if (interfaceConfig.paths.isEmpty && interface != null) { globalInterfaces[interface.name] = interface; } else { - interfaceConfig.paths.forEach((path) { + for (final path in interfaceConfig.paths) { if (path.isContainer) { pathInterfaceContainerMap[path.path] = interfaceConfig.name; } else { pathInterfaceNameMap[path.path] = interfaceConfig.name; } - }); + } } } return InterfaceCollection( diff --git a/slang/lib/builder/model/build_model_config.dart b/slang/lib/builder/model/build_model_config.dart index 096e4a78..af04fadb 100644 --- a/slang/lib/builder/model/build_model_config.dart +++ b/slang/lib/builder/model/build_model_config.dart @@ -1,7 +1,7 @@ -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/raw_config.dart'; import 'package:slang/builder/model/context_type.dart'; +import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/interface.dart'; +import 'package:slang/builder/model/raw_config.dart'; /// Config to generate the model. /// A subset of [RawConfig]. diff --git a/slang/lib/builder/model/generate_config.dart b/slang/lib/builder/model/generate_config.dart index ea6928bf..1d603d21 100644 --- a/slang/lib/builder/model/generate_config.dart +++ b/slang/lib/builder/model/generate_config.dart @@ -1,6 +1,6 @@ import 'package:slang/builder/model/build_model_config.dart'; -import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/context_type.dart'; +import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/interface.dart'; import 'package:slang/builder/model/obfuscation_config.dart'; diff --git a/slang/lib/builder/model/i18n_locale.dart b/slang/lib/builder/model/i18n_locale.dart index d6b419d1..9100ea41 100644 --- a/slang/lib/builder/model/i18n_locale.dart +++ b/slang/lib/builder/model/i18n_locale.dart @@ -1,6 +1,6 @@ import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/utils/string_extensions.dart'; -import 'package:slang/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/string_extensions.dart'; /// own Locale type to decouple from dart:ui package class I18nLocale { diff --git a/slang/lib/builder/model/interface.dart b/slang/lib/builder/model/interface.dart index 215f3c31..df7970c2 100644 --- a/slang/lib/builder/model/interface.dart +++ b/slang/lib/builder/model/interface.dart @@ -172,7 +172,7 @@ class InterfaceAttribute { @override String toString() { - return '$returnType${optional ? '?' : ''} $attributeName${parameters.isNotEmpty ? '(' + parameters.join(', ') + ')' : ''}'; + return '$returnType${optional ? '?' : ''} $attributeName${parameters.isNotEmpty ? '(${parameters.join(', ')})' : ''}'; } } diff --git a/slang/lib/builder/model/node.dart b/slang/lib/builder/model/node.dart index 1f43c779..11fdbfd6 100644 --- a/slang/lib/builder/model/node.dart +++ b/slang/lib/builder/model/node.dart @@ -1,11 +1,11 @@ -import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/context_type.dart'; +import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/interface.dart'; import 'package:slang/builder/model/pluralization.dart'; -import 'package:slang/builder/utils/string_extensions.dart'; -import 'package:slang/builder/utils/regex_utils.dart'; -import 'package:slang/builder/utils/string_interpolation_extensions.dart'; -import 'package:slang/src/text_parser.dart'; +import 'package:slang/src/builder/builder/text_parser.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/string_extensions.dart'; +import 'package:slang/src/builder/utils/string_interpolation_extensions.dart'; class NodeModifiers { static const rich = 'rich'; @@ -304,8 +304,8 @@ class StringTextNode extends TextNode { linkParamMap: linkParamMap, ); - this._links = parsedLinksResult.links; - this._content = parsedLinksResult.parsedContent; + _links = parsedLinksResult.links; + _content = parsedLinksResult.parsedContent; } @override @@ -313,8 +313,8 @@ class StringTextNode extends TextNode { required Map> linkParamMap, required Map paramTypeMap, }) { - this._paramTypeMap = paramTypeMap; - this._params.addAll(linkParamMap.values.expand((e) => e)); + _paramTypeMap = paramTypeMap; + _params.addAll(linkParamMap.values.expand((e) => e)); // build a temporary TextNode to get the updated content final temp = StringTextNode( @@ -329,7 +329,7 @@ class StringTextNode extends TextNode { linkParamMap: linkParamMap, ); - this._content = temp.content; + _content = temp.content; } @override @@ -357,7 +357,7 @@ class RichTextNode extends TextNode { @override Set get links => _links; - Map _paramTypeMap = {}; + final Map _paramTypeMap = {}; @override Map get paramTypeMap => _paramTypeMap; @@ -438,8 +438,8 @@ class RichTextNode extends TextNode { required Map> linkParamMap, required Map paramTypeMap, }) { - this._paramTypeMap.addAll(paramTypeMap); - this._params.addAll(linkParamMap.values.expand((e) => e)); + _paramTypeMap.addAll(paramTypeMap); + _params.addAll(linkParamMap.values.expand((e) => e)); // build a temporary TextNode to get the updated content final temp = RichTextNode( @@ -454,7 +454,7 @@ class RichTextNode extends TextNode { linkParamMap: linkParamMap, ); - this._spans = temp.spans; + _spans = temp.spans; } } @@ -509,7 +509,7 @@ _ParseInterpolationResult _parseInterpolation({ required CaseStyle? paramCase, }) { final String parsedContent; - final params = Map(); + final params = {}; switch (interpolation) { case StringInterpolation.dart: @@ -569,7 +569,7 @@ _ParseLinksResult _parseLinks({ required String input, required Map>? linkParamMap, }) { - final links = Set(); + final links = {}; final parsedContent = input.replaceAllMapped(RegexUtils.linkedRegex, (match) { final linkedPath = match.group(1)!; links.add(linkedPath); diff --git a/slang/lib/builder/model/raw_config.dart b/slang/lib/builder/model/raw_config.dart index 076b4a57..617df817 100644 --- a/slang/lib/builder/model/raw_config.dart +++ b/slang/lib/builder/model/raw_config.dart @@ -201,11 +201,10 @@ class RawConfig { print(' -> fileType: ${fileType.name}'); print(' -> baseLocale: ${baseLocale.languageTag}'); print(' -> fallbackStrategy: ${fallbackStrategy.name}'); - print( - ' -> inputDirectory: ${inputDirectory != null ? inputDirectory : 'null (everywhere)'}'); + print(' -> inputDirectory: ${inputDirectory ?? 'null (everywhere)'}'); print(' -> inputFilePattern: $inputFilePattern'); print( - ' -> outputDirectory: ${outputDirectory != null ? outputDirectory : 'null (directory of input)'}'); + ' -> outputDirectory: ${outputDirectory ?? 'null (directory of input)'}'); print(' -> outputFileName: $outputFileName'); print(' -> outputFileFormat: ${outputFormat.name}'); print(' -> localeHandling: $localeHandling'); diff --git a/slang/lib/builder/model/slang_file_collection.dart b/slang/lib/builder/model/slang_file_collection.dart index e960a0b1..eb5a1600 100644 --- a/slang/lib/builder/model/slang_file_collection.dart +++ b/slang/lib/builder/model/slang_file_collection.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/decoder/base_decoder.dart'; import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/raw_config.dart'; -import 'package:slang/builder/utils/path_utils.dart'; +import 'package:slang/src/builder/decoder/base_decoder.dart'; +import 'package:slang/src/builder/utils/path_utils.dart'; /// A collection of translation files that can be read in a later step. /// This is an abstraction to support build_runner and the custom CLI by @@ -19,7 +19,7 @@ class SlangFileCollection { String determineOutputPath() { if (config.outputDirectory != null) { // output directory specified, use this path instead - return config.outputDirectory! + '/' + config.outputFileName; + return '${config.outputDirectory!}/${config.outputFileName}'; } else { // use the directory of the first (random) translation file final tempPath = files.first.path; @@ -82,7 +82,7 @@ class PlainTranslationFile { return BaseDecoder.decodeWithFileType(fileType, content); } on FormatException catch (e) { print(''); - throw 'File: ${path}\n$e'; + throw 'File: $path\n$e'; } } } diff --git a/slang/lib/src/text_parser.dart b/slang/lib/src/builder/builder/text_parser.dart similarity index 92% rename from slang/lib/src/text_parser.dart rename to slang/lib/src/builder/builder/text_parser.dart index f7cf3db4..4ab2fc2d 100644 --- a/slang/lib/src/text_parser.dart +++ b/slang/lib/src/builder/builder/text_parser.dart @@ -1,5 +1,5 @@ import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/utils/string_extensions.dart'; +import 'package:slang/src/builder/utils/string_extensions.dart'; class ParseParamResult { final String paramName; diff --git a/slang/lib/builder/decoder/arb_decoder.dart b/slang/lib/src/builder/decoder/arb_decoder.dart similarity index 91% rename from slang/lib/builder/decoder/arb_decoder.dart rename to slang/lib/src/builder/decoder/arb_decoder.dart index cfb5dedf..41f867d2 100644 --- a/slang/lib/builder/decoder/arb_decoder.dart +++ b/slang/lib/src/builder/decoder/arb_decoder.dart @@ -1,12 +1,12 @@ import 'dart:convert'; -import 'package:slang/builder/decoder/base_decoder.dart'; import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/utils/brackets_utils.dart'; -import 'package:slang/builder/utils/map_utils.dart'; -import 'package:slang/builder/utils/regex_utils.dart'; -import 'package:slang/builder/utils/string_extensions.dart'; -import 'package:slang/builder/utils/string_interpolation_extensions.dart'; +import 'package:slang/src/builder/decoder/base_decoder.dart'; +import 'package:slang/src/builder/utils/brackets_utils.dart'; +import 'package:slang/src/builder/utils/map_utils.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/string_extensions.dart'; +import 'package:slang/src/builder/utils/string_interpolation_extensions.dart'; class ArbDecoder extends BaseDecoder { @override diff --git a/slang/lib/builder/decoder/base_decoder.dart b/slang/lib/src/builder/decoder/base_decoder.dart similarity index 78% rename from slang/lib/builder/decoder/base_decoder.dart rename to slang/lib/src/builder/decoder/base_decoder.dart index df55c734..8d83fec9 100644 --- a/slang/lib/builder/decoder/base_decoder.dart +++ b/slang/lib/src/builder/decoder/base_decoder.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/decoder/arb_decoder.dart'; -import 'package:slang/builder/decoder/csv_decoder.dart'; -import 'package:slang/builder/decoder/json_decoder.dart'; -import 'package:slang/builder/decoder/yaml_decoder.dart'; import 'package:slang/builder/model/enums.dart'; +import 'package:slang/src/builder/decoder/arb_decoder.dart'; +import 'package:slang/src/builder/decoder/csv_decoder.dart'; +import 'package:slang/src/builder/decoder/json_decoder.dart'; +import 'package:slang/src/builder/decoder/yaml_decoder.dart'; abstract class BaseDecoder { /// Transforms the raw string (json, yaml, csv) diff --git a/slang/lib/builder/decoder/csv_decoder.dart b/slang/lib/src/builder/decoder/csv_decoder.dart similarity index 96% rename from slang/lib/builder/decoder/csv_decoder.dart rename to slang/lib/src/builder/decoder/csv_decoder.dart index 3cb79378..0d6aa4d3 100644 --- a/slang/lib/builder/decoder/csv_decoder.dart +++ b/slang/lib/src/builder/decoder/csv_decoder.dart @@ -1,7 +1,7 @@ import 'package:csv/csv.dart'; import 'package:csv/csv_settings_autodetection.dart'; -import 'package:slang/builder/decoder/base_decoder.dart'; -import 'package:slang/builder/utils/map_utils.dart'; +import 'package:slang/src/builder/decoder/base_decoder.dart'; +import 'package:slang/src/builder/utils/map_utils.dart'; final _csvConverter = CsvToListConverter( // Allow both \r\n and \n diff --git a/slang/lib/builder/decoder/json_decoder.dart b/slang/lib/src/builder/decoder/json_decoder.dart similarity index 71% rename from slang/lib/builder/decoder/json_decoder.dart rename to slang/lib/src/builder/decoder/json_decoder.dart index 9f47c667..d67d151f 100644 --- a/slang/lib/builder/decoder/json_decoder.dart +++ b/slang/lib/src/builder/decoder/json_decoder.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:slang/builder/decoder/base_decoder.dart'; +import 'package:slang/src/builder/decoder/base_decoder.dart'; class JsonDecoder extends BaseDecoder { @override diff --git a/slang/lib/builder/decoder/yaml_decoder.dart b/slang/lib/src/builder/decoder/yaml_decoder.dart similarity index 60% rename from slang/lib/builder/decoder/yaml_decoder.dart rename to slang/lib/src/builder/decoder/yaml_decoder.dart index f7b350d7..94c0a679 100644 --- a/slang/lib/builder/decoder/yaml_decoder.dart +++ b/slang/lib/src/builder/decoder/yaml_decoder.dart @@ -1,5 +1,5 @@ -import 'package:slang/builder/decoder/base_decoder.dart'; -import 'package:slang/builder/utils/map_utils.dart'; +import 'package:slang/src/builder/decoder/base_decoder.dart'; +import 'package:slang/src/builder/utils/map_utils.dart'; import 'package:yaml/yaml.dart'; class YamlDecoder extends BaseDecoder { diff --git a/slang/lib/builder/generator/generate_header.dart b/slang/lib/src/builder/generator/generate_header.dart similarity index 99% rename from slang/lib/builder/generator/generate_header.dart rename to slang/lib/src/builder/generator/generate_header.dart index a8bffeb6..c9bfefe3 100644 --- a/slang/lib/builder/generator/generate_header.dart +++ b/slang/lib/src/builder/generator/generate_header.dart @@ -1,11 +1,11 @@ -import 'package:slang/builder/generator/helper.dart'; import 'package:slang/builder/model/build_model_config.dart'; import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/generate_config.dart'; import 'package:slang/builder/model/i18n_data.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/node.dart'; -import 'package:slang/builder/utils/path_utils.dart'; +import 'package:slang/src/builder/generator/helper.dart'; +import 'package:slang/src/builder/utils/path_utils.dart'; String generateHeader( GenerateConfig config, diff --git a/slang/lib/builder/generator/generate_translation_map.dart b/slang/lib/src/builder/generator/generate_translation_map.dart similarity index 95% rename from slang/lib/builder/generator/generate_translation_map.dart rename to slang/lib/src/builder/generator/generate_translation_map.dart index 9b49358c..71bcd683 100644 --- a/slang/lib/builder/generator/generate_translation_map.dart +++ b/slang/lib/src/builder/generator/generate_translation_map.dart @@ -16,7 +16,7 @@ String generateTranslationMap( // coverage:ignore-file // ignore_for_file: type=lint -part of \'${config.outputFileName}\';'''); +part of '${config.outputFileName}';'''); buffer.writeln(); } @@ -80,24 +80,24 @@ _generateTranslationMapRecursive({ ); } else if (curr is ListNode) { // recursive - curr.entries.forEach((child) { + for (final child in curr.entries) { _generateTranslationMapRecursive( buffer: buffer, curr: child, config: config, language: language, ); - }); + } } else if (curr is ObjectNode) { // recursive - curr.entries.values.forEach((child) { + for (final child in curr.entries.values) { _generateTranslationMapRecursive( buffer: buffer, curr: child, config: config, language: language, ); - }); + } } else if (curr is PluralNode) { buffer.write('\t\t\tcase \'${curr.path}\': return '); _addPluralizationCall( diff --git a/slang/lib/builder/generator/generate_translations.dart b/slang/lib/src/builder/generator/generate_translations.dart similarity index 98% rename from slang/lib/builder/generator/generate_translations.dart rename to slang/lib/src/builder/generator/generate_translations.dart index f1159658..65b716d5 100644 --- a/slang/lib/builder/generator/generate_translations.dart +++ b/slang/lib/src/builder/generator/generate_translations.dart @@ -1,13 +1,13 @@ import 'dart:collection'; -import 'package:slang/builder/generator/helper.dart'; import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/generate_config.dart'; import 'package:slang/builder/model/i18n_data.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/node.dart'; import 'package:slang/builder/model/pluralization.dart'; -import 'package:slang/builder/utils/encryption_utils.dart'; +import 'package:slang/src/builder/generator/helper.dart'; +import 'package:slang/src/builder/utils/encryption_utils.dart'; part 'generate_translation_map.dart'; @@ -36,7 +36,7 @@ String generateTranslations(GenerateConfig config, I18nData localeData) { // coverage:ignore-file // ignore_for_file: type=lint -part of \'${config.outputFileName}\';'''); +part of '${config.outputFileName}';'''); } queue.add(ClassTask( @@ -532,8 +532,8 @@ void _generateList({ depth: depth + 1, ); } else if (value is ObjectNode) { - final String key = r'$' + - '${listName ?? ''}\$' + + // ignore: prefer_interpolation_to_compose_strings + final String key = r'$' '${listName ?? ''}\$' + depth.toString() + 'i' + i.toString() + @@ -764,7 +764,7 @@ void _addRichTextCall({ } } else if (span is FunctionSpan) { buffer.write( - "${span.functionName}(${getStringLiteral(span.arg, config.obfuscation)})", + '${span.functionName}(${getStringLiteral(span.arg, config.obfuscation)})', ); } buffer.writeln(','); diff --git a/slang/lib/builder/generator/generator.dart b/slang/lib/src/builder/generator/generator.dart similarity index 85% rename from slang/lib/builder/generator/generator.dart rename to slang/lib/src/builder/generator/generator.dart index 423ffe11..c6837ed1 100644 --- a/slang/lib/builder/generator/generator.dart +++ b/slang/lib/src/builder/generator/generator.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/generator/generate_header.dart'; -import 'package:slang/builder/generator/generate_translations.dart'; import 'package:slang/builder/model/build_result.dart'; import 'package:slang/builder/model/generate_config.dart'; import 'package:slang/builder/model/i18n_data.dart'; +import 'package:slang/src/builder/generator/generate_header.dart'; +import 'package:slang/src/builder/generator/generate_translations.dart'; class Generator { /// main generate function diff --git a/slang/lib/builder/generator/helper.dart b/slang/lib/src/builder/generator/helper.dart similarity index 93% rename from slang/lib/builder/generator/helper.dart rename to slang/lib/src/builder/generator/helper.dart index 0e951d77..47ea0321 100644 --- a/slang/lib/builder/generator/helper.dart +++ b/slang/lib/src/builder/generator/helper.dart @@ -1,9 +1,9 @@ import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/obfuscation_config.dart'; -import 'package:slang/builder/utils/encryption_utils.dart'; -import 'package:slang/builder/utils/string_extensions.dart'; -import 'package:slang/builder/utils/string_interpolation_extensions.dart'; +import 'package:slang/src/builder/utils/encryption_utils.dart'; +import 'package:slang/src/builder/utils/string_extensions.dart'; +import 'package:slang/src/builder/utils/string_interpolation_extensions.dart'; /// Pragmatic way to detect links within interpolations. const String characteristicLinkPrefix = '_root.'; @@ -18,7 +18,7 @@ String getClassNameRoot({ (locale != null ? locale.languageTag.toCaseOfLocale(CaseStyle.pascal) : ''); - if (visibility == TranslationClassVisibility.private) result = '_' + result; + if (visibility == TranslationClassVisibility.private) result = '_$result'; return result; } diff --git a/slang/lib/builder/generator_facade.dart b/slang/lib/src/builder/generator_facade.dart similarity index 97% rename from slang/lib/builder/generator_facade.dart rename to slang/lib/src/builder/generator_facade.dart index 9d6af2dc..e615a95a 100644 --- a/slang/lib/builder/generator_facade.dart +++ b/slang/lib/src/builder/generator_facade.dart @@ -1,11 +1,11 @@ import 'package:slang/builder/builder/generate_config_builder.dart'; import 'package:slang/builder/builder/translation_model_list_builder.dart'; -import 'package:slang/builder/generator/generator.dart'; -import 'package:slang/builder/model/context_type.dart'; -import 'package:slang/builder/model/raw_config.dart'; import 'package:slang/builder/model/build_result.dart'; +import 'package:slang/builder/model/context_type.dart'; import 'package:slang/builder/model/interface.dart'; +import 'package:slang/builder/model/raw_config.dart'; import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/generator/generator.dart'; class GeneratorFacade { /// Common step used by custom runner and builder to get the .g.dart content diff --git a/slang/lib/builder/utils/brackets_utils.dart b/slang/lib/src/builder/utils/brackets_utils.dart similarity index 96% rename from slang/lib/builder/utils/brackets_utils.dart rename to slang/lib/src/builder/utils/brackets_utils.dart index fd843495..6d843617 100644 --- a/slang/lib/builder/utils/brackets_utils.dart +++ b/slang/lib/src/builder/utils/brackets_utils.dart @@ -68,7 +68,7 @@ class BracketRange { } @override - int get hashCode => this.start.hashCode * this.end.hashCode; + int get hashCode => start.hashCode * end.hashCode; @override String toString() { diff --git a/slang/lib/builder/utils/encryption_utils.dart b/slang/lib/src/builder/utils/encryption_utils.dart similarity index 92% rename from slang/lib/builder/utils/encryption_utils.dart rename to slang/lib/src/builder/utils/encryption_utils.dart index 5e882315..b09efd89 100644 --- a/slang/lib/builder/utils/encryption_utils.dart +++ b/slang/lib/src/builder/utils/encryption_utils.dart @@ -1,7 +1,7 @@ extension StringEncryptionExt on String { /// Encrypts the string using the provided secret. List encrypt(int secret) { - final chars = [...this.codeUnits]; + final chars = [...codeUnits]; for (var i = 0; i < chars.length; i++) { chars[i] = chars[i] ^ secret; } diff --git a/slang/lib/builder/utils/file_utils.dart b/slang/lib/src/builder/utils/file_utils.dart similarity index 94% rename from slang/lib/builder/utils/file_utils.dart rename to slang/lib/src/builder/utils/file_utils.dart index cbccbce5..01e4452b 100644 --- a/slang/lib/builder/utils/file_utils.dart +++ b/slang/lib/src/builder/utils/file_utils.dart @@ -32,7 +32,7 @@ class FileUtils { switch (fileType) { case FileType.json: // this encoder does not append \n automatically - return JsonEncoder.withIndent(' ').convert(content) + '\n'; + return '${JsonEncoder.withIndent(' ').convert(content)}\n'; case FileType.yaml: if (content.containsKey(INFO_KEY)) { // workaround @@ -75,9 +75,9 @@ class FileUtils { final info = content.remove(INFO_KEY); columns[INFO_KEY] = {INFO_KEY: escapeRow(info.join('\\n'))}; } - content.entries.forEach((e) { + for (final e in content.entries) { columns[e.key] = encodeRow(value: e.value); - }); + } // get all translation keys final translationKeys = columns.values @@ -95,7 +95,7 @@ class FileUtils { return "$headers\n${rows.join('\n')}"; case FileType.arb: // this encoder does not append \n automatically - return JsonEncoder.withIndent(' ').convert(content) + '\n'; + return '${JsonEncoder.withIndent(' ').convert(content)}\n'; } } diff --git a/slang/lib/builder/utils/map_utils.dart b/slang/lib/src/builder/utils/map_utils.dart similarity index 99% rename from slang/lib/builder/utils/map_utils.dart rename to slang/lib/src/builder/utils/map_utils.dart index f8e3c2e5..d90a48ec 100644 --- a/slang/lib/builder/utils/map_utils.dart +++ b/slang/lib/src/builder/utils/map_utils.dart @@ -1,5 +1,5 @@ import 'package:collection/collection.dart'; -import 'package:slang/builder/utils/node_utils.dart'; +import 'package:slang/src/builder/utils/node_utils.dart'; class MapUtils { /// converts Map to Map for all children @@ -108,7 +108,7 @@ class MapUtils { throw 'The leaf "$destinationPath" cannot be added because there are missing indices.'; } } else { - if (!(curr is Map)) { + if (curr is! Map) { throw 'The leaf "$destinationPath" cannot be added because the parent of "$subPath" is not a map.'; } curr[subPath] = item; diff --git a/slang/lib/builder/utils/node_utils.dart b/slang/lib/src/builder/utils/node_utils.dart similarity index 94% rename from slang/lib/builder/utils/node_utils.dart rename to slang/lib/src/builder/utils/node_utils.dart index 3e4c6c8f..854084b0 100644 --- a/slang/lib/builder/utils/node_utils.dart +++ b/slang/lib/src/builder/utils/node_utils.dart @@ -1,5 +1,5 @@ import 'package:slang/builder/model/node.dart'; -import 'package:slang/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; class NodeUtils { /// Returns a map containing modifiers @@ -80,7 +80,7 @@ class NodePathInfo { extension StringModifierExt on String { /// Returns the key without modifiers. String get withoutModifiers { - return this.split('(').first; + return split('(').first; } String withModifier(String modifierKey, [String? modifierValue]) { @@ -99,9 +99,9 @@ extension NodeFlatter on Node { final curr = this; if (curr is ObjectNode && !curr.isMap) { // recursive - curr.entries.values.forEach((child) { + for (final child in curr.entries.values) { result.addAll(child.toFlatMap()); - }); + } } else { result[path] = curr; } diff --git a/slang/lib/builder/utils/path_utils.dart b/slang/lib/src/builder/utils/path_utils.dart similarity index 97% rename from slang/lib/builder/utils/path_utils.dart rename to slang/lib/src/builder/utils/path_utils.dart index ed698f2f..a7d7ab40 100644 --- a/slang/lib/builder/utils/path_utils.dart +++ b/slang/lib/src/builder/utils/path_utils.dart @@ -1,6 +1,6 @@ import 'package:collection/collection.dart'; import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; /// Operations on paths class PathUtils { @@ -46,7 +46,7 @@ class PathUtils { List segments = PathUtils.getPathSegments(filePath); // either first directory after inputDirectory, or last directory - RegExpMatch? match = null; + RegExpMatch? match; if (inputDirectory != null) { final inputDirectorySegments = PathUtils.getPathSegments(inputDirectory); diff --git a/slang/lib/builder/utils/regex_utils.dart b/slang/lib/src/builder/utils/regex_utils.dart similarity index 100% rename from slang/lib/builder/utils/regex_utils.dart rename to slang/lib/src/builder/utils/regex_utils.dart diff --git a/slang/lib/builder/utils/string_extensions.dart b/slang/lib/src/builder/utils/string_extensions.dart similarity index 93% rename from slang/lib/builder/utils/string_extensions.dart rename to slang/lib/src/builder/utils/string_extensions.dart index efb9cfd0..d197a477 100644 --- a/slang/lib/builder/utils/string_extensions.dart +++ b/slang/lib/src/builder/utils/string_extensions.dart @@ -7,8 +7,8 @@ extension StringExtensions on String { /// 'Hello' => 'Hello' /// '' => '' String capitalize() { - if (this.isEmpty) return ''; - return "${this[0].toUpperCase()}${this.substring(1).toLowerCase()}"; + if (isEmpty) return ''; + return '${this[0].toUpperCase()}${substring(1).toLowerCase()}'; } /// transforms the string to the specified case @@ -35,7 +35,7 @@ extension StringExtensions on String { /// de-DE will be interpreted as [de,DE] /// normally, it would be [de,D,E] which we do not want String toCaseOfLocale(CaseStyle style) { - return this.toLowerCase().toCase(style); + return toLowerCase().toCase(style); } /// get word list from string input diff --git a/slang/lib/builder/utils/string_interpolation_extensions.dart b/slang/lib/src/builder/utils/string_interpolation_extensions.dart similarity index 100% rename from slang/lib/builder/utils/string_interpolation_extensions.dart rename to slang/lib/src/builder/utils/string_interpolation_extensions.dart diff --git a/slang/lib/src/runner/analyze.dart b/slang/lib/src/runner/analyze.dart index 1418ed0b..45643640 100644 --- a/slang/lib/src/runner/analyze.dart +++ b/slang/lib/src/runner/analyze.dart @@ -8,10 +8,10 @@ import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/node.dart'; import 'package:slang/builder/model/raw_config.dart'; import 'package:slang/builder/model/translation_map.dart'; -import 'package:slang/builder/utils/file_utils.dart'; -import 'package:slang/builder/utils/map_utils.dart'; -import 'package:slang/builder/utils/node_utils.dart'; -import 'package:slang/builder/utils/path_utils.dart'; +import 'package:slang/src/builder/utils/file_utils.dart'; +import 'package:slang/src/builder/utils/map_utils.dart'; +import 'package:slang/src/builder/utils/node_utils.dart'; +import 'package:slang/src/builder/utils/path_utils.dart'; final _setEquality = SetEquality(); @@ -225,37 +225,37 @@ void _addNodeRecursive({ ); } else { if (node is ListNode) { - node.entries.forEach((child) { + for (final child in node.entries) { _addNodeRecursive( node: child, resultMap: resultMap, addOutdatedModifier: false, ); - }); + } } else if (node is ObjectNode) { - node.entries.values.forEach((child) { + for (final child in node.entries.values) { _addNodeRecursive( node: child, resultMap: resultMap, addOutdatedModifier: false, ); - }); + } } else if (node is PluralNode) { - node.quantities.values.forEach((child) { + for (final child in node.quantities.values) { _addNodeRecursive( node: child, resultMap: resultMap, addOutdatedModifier: false, ); - }); + } } else if (node is ContextNode) { - node.entries.values.forEach((child) { + for (final child in node.entries.values) { _addNodeRecursive( node: child, resultMap: resultMap, addOutdatedModifier: false, ); - }); + } } else { throw 'This should not happen'; } @@ -377,10 +377,8 @@ void _writeMap({ for (final entry in result.entries) { final path = PathUtils.withFileName( directoryPath: outDir, - fileName: fileNamePrefix + - '_' + - entry.key.languageTag.replaceAll('-', '_') + - '.${fileType.name}', + fileName: + '${fileNamePrefix}_${entry.key.languageTag.replaceAll('-', '_')}.${fileType.name}', pathSeparator: Platform.pathSeparator, ); diff --git a/slang/lib/src/runner/apply.dart b/slang/lib/src/runner/apply.dart index c01d0cdc..f15785c4 100644 --- a/slang/lib/src/runner/apply.dart +++ b/slang/lib/src/runner/apply.dart @@ -7,9 +7,9 @@ import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/node.dart'; import 'package:slang/builder/model/slang_file_collection.dart'; -import 'package:slang/builder/utils/file_utils.dart'; -import 'package:slang/builder/utils/node_utils.dart'; -import 'package:slang/builder/utils/path_utils.dart'; +import 'package:slang/src/builder/utils/file_utils.dart'; +import 'package:slang/src/builder/utils/node_utils.dart'; +import 'package:slang/src/builder/utils/path_utils.dart'; import 'package:slang/src/runner/analyze.dart'; import 'package:slang/src/runner/utils/read_analysis_file.dart'; diff --git a/slang/lib/src/runner/clean.dart b/slang/lib/src/runner/clean.dart index 7d3a3db1..08057e19 100644 --- a/slang/lib/src/runner/clean.dart +++ b/slang/lib/src/runner/clean.dart @@ -2,8 +2,8 @@ import 'dart:io'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/slang_file_collection.dart'; -import 'package:slang/builder/utils/file_utils.dart'; -import 'package:slang/builder/utils/map_utils.dart'; +import 'package:slang/src/builder/utils/file_utils.dart'; +import 'package:slang/src/builder/utils/map_utils.dart'; import 'package:slang/src/runner/utils/read_analysis_file.dart'; /// Reads the "_unused_translations" file and removes the specified keys diff --git a/slang/lib/src/runner/edit.dart b/slang/lib/src/runner/edit.dart index 23d82950..4d9cb997 100644 --- a/slang/lib/src/runner/edit.dart +++ b/slang/lib/src/runner/edit.dart @@ -6,9 +6,9 @@ import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/node.dart'; import 'package:slang/builder/model/slang_file_collection.dart'; -import 'package:slang/builder/utils/file_utils.dart'; -import 'package:slang/builder/utils/map_utils.dart'; -import 'package:slang/builder/utils/node_utils.dart'; +import 'package:slang/src/builder/utils/file_utils.dart'; +import 'package:slang/src/builder/utils/map_utils.dart'; +import 'package:slang/src/builder/utils/node_utils.dart'; import 'package:slang/src/runner/apply.dart'; const _supportedFiles = [FileType.json, FileType.yaml]; diff --git a/slang/lib/src/runner/migrate_arb.dart b/slang/lib/src/runner/migrate_arb.dart index 781277bb..8ce553f9 100644 --- a/slang/lib/src/runner/migrate_arb.dart +++ b/slang/lib/src/runner/migrate_arb.dart @@ -3,12 +3,12 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/utils/brackets_utils.dart'; -import 'package:slang/builder/utils/file_utils.dart'; -import 'package:slang/builder/utils/map_utils.dart'; -import 'package:slang/builder/utils/regex_utils.dart'; -import 'package:slang/builder/utils/string_extensions.dart'; -import 'package:slang/builder/utils/string_interpolation_extensions.dart'; +import 'package:slang/src/builder/utils/brackets_utils.dart'; +import 'package:slang/src/builder/utils/file_utils.dart'; +import 'package:slang/src/builder/utils/map_utils.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/string_extensions.dart'; +import 'package:slang/src/builder/utils/string_interpolation_extensions.dart'; final _setEquality = SetEquality(); @@ -85,14 +85,14 @@ Map migrateArb(String raw, [bool verbose = true]) { ); } else { final contextResult = _digestEntry(keyParts, value, resultMap); - contextResult.forEach((c) { + for (final c in contextResult) { if (detectedContexts .every((c2) => !_setEquality.equals(c2, c.contextEnum))) { // detected new context detectedContexts.add(c.contextEnum); detectedContextNames.add(c.contextName); } - }); + } } }); @@ -119,9 +119,9 @@ Map migrateArb(String raw, [bool verbose = true]) { print('[$contextName] ... or ${additionalNames.join(', ')}'); } - detectedContexts[i].forEach((enumValue) { + for (final enumValue in detectedContexts[i]) { print(' - $enumValue'); - }); + } } } @@ -296,7 +296,7 @@ extension on List { /// Replace last element with another, returns the new list List replaceLast(String replace) { final copy = [...this]; - copy[this.length - 1] = replace; + copy[length - 1] = replace; return copy; } } diff --git a/slang/lib/src/runner/stats.dart b/slang/lib/src/runner/stats.dart index fc9b3e0f..bba65617 100644 --- a/slang/lib/src/runner/stats.dart +++ b/slang/lib/src/runner/stats.dart @@ -3,7 +3,7 @@ import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/node.dart'; import 'package:slang/builder/model/raw_config.dart'; import 'package:slang/builder/model/translation_map.dart'; -import 'package:slang/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; StatsResult getStats({ required RawConfig rawConfig, @@ -17,7 +17,7 @@ StatsResult getStats({ // use translation model and calculate statistics Map result = {}; - translationModelList.forEach((localeData) { + for (final localeData in translationModelList) { final keyCount = _countKeys(localeData.root) - 1; // don't include root final translationCount = _countTranslations(localeData.root); final wordCount = _countWords(localeData.root); @@ -28,7 +28,7 @@ StatsResult getStats({ wordCount: wordCount, characterCount: characterCount, ); - }); + } return StatsResult( localeStats: result, diff --git a/slang/lib/src/runner/utils/read_analysis_file.dart b/slang/lib/src/runner/utils/read_analysis_file.dart index f8cee271..2d6da14c 100644 --- a/slang/lib/src/runner/utils/read_analysis_file.dart +++ b/slang/lib/src/runner/utils/read_analysis_file.dart @@ -1,12 +1,12 @@ import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:slang/builder/decoder/base_decoder.dart'; import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/utils/file_utils.dart'; -import 'package:slang/builder/utils/path_utils.dart'; -import 'package:slang/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/decoder/base_decoder.dart'; +import 'package:slang/src/builder/utils/file_utils.dart'; +import 'package:slang/src/builder/utils/path_utils.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; import 'package:slang/src/runner/apply.dart'; const _supportedFiles = [FileType.json, FileType.yaml]; diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index e6d92a10..f254aed5 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -19,9 +19,9 @@ environment: dependencies: collection: ^1.15.0 csv: ^5.0.1 - yaml: ^3.1.0 json2yaml: ^3.0.0 watcher: ^1.0.2 + yaml: ^3.1.0 dev_dependencies: expect_error: ^1.0.7 diff --git a/slang/test/integration/main/csv_compact_test.dart b/slang/test/integration/main/csv_compact_test.dart index b572d827..42a03e3f 100644 --- a/slang/test/integration/main/csv_compact_test.dart +++ b/slang/test/integration/main/csv_compact_test.dart @@ -1,8 +1,8 @@ import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/decoder/csv_decoder.dart'; -import 'package:slang/builder/generator_facade.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/decoder/csv_decoder.dart'; +import 'package:slang/src/builder/generator_facade.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; diff --git a/slang/test/integration/main/csv_test.dart b/slang/test/integration/main/csv_test.dart index b21b5bc7..b9fc0e37 100644 --- a/slang/test/integration/main/csv_test.dart +++ b/slang/test/integration/main/csv_test.dart @@ -1,8 +1,8 @@ import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/decoder/csv_decoder.dart'; -import 'package:slang/builder/generator_facade.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/decoder/csv_decoder.dart'; +import 'package:slang/src/builder/generator_facade.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; diff --git a/slang/test/integration/main/fallback_base_locale_test.dart b/slang/test/integration/main/fallback_base_locale_test.dart index 7db8c558..f01446d3 100644 --- a/slang/test/integration/main/fallback_base_locale_test.dart +++ b/slang/test/integration/main/fallback_base_locale_test.dart @@ -1,10 +1,10 @@ import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/decoder/csv_decoder.dart'; -import 'package:slang/builder/decoder/json_decoder.dart'; -import 'package:slang/builder/generator_facade.dart'; import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/decoder/csv_decoder.dart'; +import 'package:slang/src/builder/decoder/json_decoder.dart'; +import 'package:slang/src/builder/generator_facade.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; diff --git a/slang/test/integration/main/json_multiple_files_test.dart b/slang/test/integration/main/json_multiple_files_test.dart index 71267192..3ff9b011 100644 --- a/slang/test/integration/main/json_multiple_files_test.dart +++ b/slang/test/integration/main/json_multiple_files_test.dart @@ -1,9 +1,9 @@ import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/decoder/json_decoder.dart'; -import 'package:slang/builder/generator_facade.dart'; import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/decoder/json_decoder.dart'; +import 'package:slang/src/builder/generator_facade.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; diff --git a/slang/test/integration/main/json_test.dart b/slang/test/integration/main/json_test.dart index 11a4ebd5..8e6b9580 100644 --- a/slang/test/integration/main/json_test.dart +++ b/slang/test/integration/main/json_test.dart @@ -1,8 +1,8 @@ import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/decoder/json_decoder.dart'; -import 'package:slang/builder/generator_facade.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/decoder/json_decoder.dart'; +import 'package:slang/src/builder/generator_facade.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; diff --git a/slang/test/integration/main/no_flutter_test.dart b/slang/test/integration/main/no_flutter_test.dart index 68ccee94..b4ea8fa4 100644 --- a/slang/test/integration/main/no_flutter_test.dart +++ b/slang/test/integration/main/no_flutter_test.dart @@ -1,8 +1,8 @@ import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/decoder/json_decoder.dart'; -import 'package:slang/builder/generator_facade.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/decoder/json_decoder.dart'; +import 'package:slang/src/builder/generator_facade.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; diff --git a/slang/test/integration/main/no_locale_handling_test.dart b/slang/test/integration/main/no_locale_handling_test.dart index 7a79c818..b6be7cb4 100644 --- a/slang/test/integration/main/no_locale_handling_test.dart +++ b/slang/test/integration/main/no_locale_handling_test.dart @@ -1,9 +1,9 @@ import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/decoder/json_decoder.dart'; -import 'package:slang/builder/generator_facade.dart'; import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/decoder/json_decoder.dart'; +import 'package:slang/src/builder/generator_facade.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; diff --git a/slang/test/integration/main/obfuscation_test.dart b/slang/test/integration/main/obfuscation_test.dart index 976c7f5e..ba1387d2 100644 --- a/slang/test/integration/main/obfuscation_test.dart +++ b/slang/test/integration/main/obfuscation_test.dart @@ -1,9 +1,9 @@ import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/decoder/csv_decoder.dart'; -import 'package:slang/builder/generator_facade.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/obfuscation_config.dart'; import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/decoder/csv_decoder.dart'; +import 'package:slang/src/builder/generator_facade.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; diff --git a/slang/test/integration/main/rich_text_test.dart b/slang/test/integration/main/rich_text_test.dart index c5c9274b..20fa3ca9 100644 --- a/slang/test/integration/main/rich_text_test.dart +++ b/slang/test/integration/main/rich_text_test.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/decoder/json_decoder.dart'; -import 'package:slang/builder/generator_facade.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/raw_config.dart'; import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/decoder/json_decoder.dart'; +import 'package:slang/src/builder/generator_facade.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; diff --git a/slang/test/integration/main/translation_overrides_test.dart b/slang/test/integration/main/translation_overrides_test.dart index cb238f5d..572a29b1 100644 --- a/slang/test/integration/main/translation_overrides_test.dart +++ b/slang/test/integration/main/translation_overrides_test.dart @@ -1,8 +1,8 @@ import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/decoder/csv_decoder.dart'; -import 'package:slang/builder/generator_facade.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/decoder/csv_decoder.dart'; +import 'package:slang/src/builder/generator_facade.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; diff --git a/slang/test/integration/main/yaml_test.dart b/slang/test/integration/main/yaml_test.dart index 4bd5454a..4d419bb5 100644 --- a/slang/test/integration/main/yaml_test.dart +++ b/slang/test/integration/main/yaml_test.dart @@ -1,8 +1,8 @@ import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/decoder/yaml_decoder.dart'; -import 'package:slang/builder/generator_facade.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/decoder/yaml_decoder.dart'; +import 'package:slang/src/builder/generator_facade.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; diff --git a/slang/test/integration/update.dart b/slang/test/integration/update.dart index 20435b42..120cfa40 100644 --- a/slang/test/integration/update.dart +++ b/slang/test/integration/update.dart @@ -1,13 +1,13 @@ import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/decoder/json_decoder.dart'; -import 'package:slang/builder/generator_facade.dart'; import 'package:slang/builder/model/build_result.dart'; import 'package:slang/builder/model/enums.dart'; +import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/obfuscation_config.dart'; import 'package:slang/builder/model/raw_config.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/translation_map.dart'; -import 'package:slang/builder/utils/file_utils.dart'; +import 'package:slang/src/builder/decoder/json_decoder.dart'; +import 'package:slang/src/builder/generator_facade.dart'; +import 'package:slang/src/builder/utils/file_utils.dart'; import '../util/resources_utils.dart'; diff --git a/slang/test/unit/api/secret_test.dart b/slang/test/unit/api/secret_test.dart index 3b291563..b5483a71 100644 --- a/slang/test/unit/api/secret_test.dart +++ b/slang/test/unit/api/secret_test.dart @@ -1,5 +1,5 @@ import 'package:slang/api/secret.dart'; -import 'package:slang/builder/utils/encryption_utils.dart'; +import 'package:slang/src/builder/utils/encryption_utils.dart'; import 'package:test/test.dart'; void main() { diff --git a/slang/test/unit/api/translation_overrides_test.dart b/slang/test/unit/api/translation_overrides_test.dart index 63c5284c..b15e3003 100644 --- a/slang/test/unit/api/translation_overrides_test.dart +++ b/slang/test/unit/api/translation_overrides_test.dart @@ -33,7 +33,7 @@ void main() { test('Should return an interpolated string', () { final meta = _buildMetaWithOverrides({ - 'aboutPage.title': r"About ${arg}", + 'aboutPage.title': r'About ${arg}', }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { 'arg': 'Page', @@ -43,7 +43,7 @@ void main() { test('Should return an interpolated string with dollar only', () { final meta = _buildMetaWithOverrides({ - 'aboutPage.title': r"About $arg", + 'aboutPage.title': r'About $arg', }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { 'arg': 'Page', diff --git a/slang/test/unit/builder/translation_model_builder_test.dart b/slang/test/unit/builder/translation_model_builder_test.dart index 3160de8c..b0508881 100644 --- a/slang/test/unit/builder/translation_model_builder_test.dart +++ b/slang/test/unit/builder/translation_model_builder_test.dart @@ -1,7 +1,7 @@ import 'package:slang/builder/builder/build_model_config_builder.dart'; import 'package:slang/builder/builder/translation_model_builder.dart'; -import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/context_type.dart'; +import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/interface.dart'; import 'package:slang/builder/model/node.dart'; import 'package:slang/builder/model/raw_config.dart'; diff --git a/slang/test/unit/decoder/arb_decoder_test.dart b/slang/test/unit/decoder/arb_decoder_test.dart index 0fbb08a6..5202a060 100644 --- a/slang/test/unit/decoder/arb_decoder_test.dart +++ b/slang/test/unit/decoder/arb_decoder_test.dart @@ -1,27 +1,27 @@ import 'dart:convert'; -import 'package:slang/builder/decoder/arb_decoder.dart'; +import 'package:slang/src/builder/decoder/arb_decoder.dart'; import 'package:test/test.dart'; void main() { group('decode', () { test('Should decode single string', () { expect( - _decodeArb({"hello": "world"}), + _decodeArb({'hello': 'world'}), {'hello': 'world'}, ); }); test('Should decode with named parameter', () { expect( - _decodeArb({"hello": "hello {name}"}), + _decodeArb({'hello': 'hello {name}'}), {'hello': 'hello {name}'}, ); }); test('Should decode with positional parameter', () { expect( - _decodeArb({"hello": "hello {0}"}), + _decodeArb({'hello': 'hello {0}'}), {'hello': 'hello {arg0}'}, ); }); @@ -29,8 +29,8 @@ void main() { test('Should decode with meta', () { expect( _decodeArb({ - "hello": "world", - "@hello": {"description": "This is a description"}, + 'hello': 'world', + '@hello': {'description': 'This is a description'}, }), { 'hello': 'world', @@ -42,8 +42,8 @@ void main() { test('Should decode plural string identifiers', () { expect( _decodeArb({ - "inboxCount": - "{count, plural, zero{You have no new messages} one{You have 1 new message} other{You have {count} new messages}}" + 'inboxCount': + '{count, plural, zero{You have no new messages} one{You have 1 new message} other{You have {count} new messages}}' }), { 'inboxCount(plural, param=count)': { @@ -58,8 +58,8 @@ void main() { test('Should decode plural with number identifiers', () { expect( _decodeArb({ - "inboxCount": - "{count, plural, =0{You have no new messages} =1{You have 1 new message} other{You have {count} new messages}}" + 'inboxCount': + '{count, plural, =0{You have no new messages} =1{You have 1 new message} other{You have {count} new messages}}' }), { 'inboxCount(plural, param=count)': { @@ -74,8 +74,8 @@ void main() { test('Should decode custom context', () { expect( _decodeArb({ - "hello": - "{gender, select, male{Hello Mr {name}} female{Hello Mrs {name}} other{Hello {name}}}" + 'hello': + '{gender, select, male{Hello Mr {name}} female{Hello Mrs {name}} other{Hello {name}}}' }), { 'hello(context=Gender, param=gender)': { @@ -90,8 +90,8 @@ void main() { test('Should decode multiple plurals', () { expect( _decodeArb({ - "inboxCount": - "{count, plural, zero{You have no new messages} one{You have 1 new message} other{You have {count} new messages}} {appleCount, plural, zero{You have no new apples} one{You have 1 new apple} other{You have {appleCount} new apples}}" + 'inboxCount': + '{count, plural, zero{You have no new messages} one{You have 1 new message} other{You have {count} new messages}} {appleCount, plural, zero{You have no new apples} one{You have 1 new apple} other{You have {appleCount} new apples}}' }), { 'inboxCount__count(plural, param=count)': { @@ -112,8 +112,8 @@ void main() { test('Should decode multiple plurals with same parameter', () { expect( _decodeArb({ - "inboxCount": - "{count, plural, zero{You have no new messages} one{You have 1 new message} other{You have {count} new messages}} {count, plural, zero{You have no new apples} one{You have 1 new apple} other{You have {count} new apples}}" + 'inboxCount': + '{count, plural, zero{You have no new messages} one{You have 1 new message} other{You have {count} new messages}} {count, plural, zero{You have no new apples} one{You have 1 new apple} other{You have {count} new apples}}' }), { 'inboxCount__count(plural, param=count)': { diff --git a/slang/test/unit/decoder/csv_decoder_test.dart b/slang/test/unit/decoder/csv_decoder_test.dart index c593ee2f..5d1a94c2 100644 --- a/slang/test/unit/decoder/csv_decoder_test.dart +++ b/slang/test/unit/decoder/csv_decoder_test.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/decoder/csv_decoder.dart'; +import 'package:slang/src/builder/decoder/csv_decoder.dart'; import 'package:test/test.dart'; void main() { diff --git a/slang/test/unit/utils/brackets_utils_test.dart b/slang/test/unit/utils/brackets_utils_test.dart index b0ffe65b..111a42ce 100644 --- a/slang/test/unit/utils/brackets_utils_test.dart +++ b/slang/test/unit/utils/brackets_utils_test.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/utils/brackets_utils.dart'; +import 'package:slang/src/builder/utils/brackets_utils.dart'; import 'package:test/test.dart'; void main() { diff --git a/slang/test/unit/utils/map_utils_test.dart b/slang/test/unit/utils/map_utils_test.dart index 0ab61dc0..d7583ca9 100644 --- a/slang/test/unit/utils/map_utils_test.dart +++ b/slang/test/unit/utils/map_utils_test.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/utils/map_utils.dart'; +import 'package:slang/src/builder/utils/map_utils.dart'; import 'package:test/test.dart'; void main() { diff --git a/slang/test/unit/utils/node_utils_test.dart b/slang/test/unit/utils/node_utils_test.dart index c36cf644..52433a25 100644 --- a/slang/test/unit/utils/node_utils_test.dart +++ b/slang/test/unit/utils/node_utils_test.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/utils/node_utils.dart'; +import 'package:slang/src/builder/utils/node_utils.dart'; import 'package:test/test.dart'; void main() { diff --git a/slang/test/unit/utils/path_utils_test.dart b/slang/test/unit/utils/path_utils_test.dart index 1459f3df..d2bcde6e 100644 --- a/slang/test/unit/utils/path_utils_test.dart +++ b/slang/test/unit/utils/path_utils_test.dart @@ -1,5 +1,5 @@ import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/utils/path_utils.dart'; +import 'package:slang/src/builder/utils/path_utils.dart'; import 'package:test/test.dart'; void main() { @@ -20,7 +20,7 @@ void main() { }); group('findDirectoryLocale', () { - final f = (String filePath, [String? inputDirectory]) => + f(String filePath, [String? inputDirectory]) => PathUtils.findDirectoryLocale( filePath: filePath, inputDirectory: inputDirectory, diff --git a/slang/test/unit/utils/regex_utils_test.dart b/slang/test/unit/utils/regex_utils_test.dart index 3d582d3c..cf5c2e8d 100644 --- a/slang/test/unit/utils/regex_utils_test.dart +++ b/slang/test/unit/utils/regex_utils_test.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; import 'package:test/test.dart'; void main() { diff --git a/slang/test/unit/utils/secret_test.dart b/slang/test/unit/utils/secret_test.dart index d1bf853d..dadb0343 100644 --- a/slang/test/unit/utils/secret_test.dart +++ b/slang/test/unit/utils/secret_test.dart @@ -1,5 +1,5 @@ -import 'package:slang/builder/generator/helper.dart'; import 'package:slang/builder/model/obfuscation_config.dart'; +import 'package:slang/src/builder/generator/helper.dart'; import 'package:test/test.dart'; void main() { diff --git a/slang/test/unit/utils/string_extensions_test.dart b/slang/test/unit/utils/string_extensions_test.dart index 1c66ab5a..b2ff784f 100644 --- a/slang/test/unit/utils/string_extensions_test.dart +++ b/slang/test/unit/utils/string_extensions_test.dart @@ -1,5 +1,5 @@ import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/utils/string_extensions.dart'; +import 'package:slang/src/builder/utils/string_extensions.dart'; import 'package:test/test.dart'; void main() { diff --git a/slang/test/unit/utils/string_interpolation_extensions_test.dart b/slang/test/unit/utils/string_interpolation_extensions_test.dart index 07b5640e..b7978563 100644 --- a/slang/test/unit/utils/string_interpolation_extensions_test.dart +++ b/slang/test/unit/utils/string_interpolation_extensions_test.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/utils/string_interpolation_extensions.dart'; +import 'package:slang/src/builder/utils/string_interpolation_extensions.dart'; import 'package:test/test.dart'; String _replacer(String s) { diff --git a/slang/test/util/resources_utils.dart b/slang/test/util/resources_utils.dart index 3ac0b9c8..0f84b0cb 100644 --- a/slang/test/util/resources_utils.dart +++ b/slang/test/util/resources_utils.dart @@ -1,10 +1,9 @@ import 'dart:io'; String loadResource(String path) { - return File("$_testDirectory/integration/resources/$path").readAsStringSync(); + return File('$_testDirectory/integration/resources/$path').readAsStringSync(); } // From https://github.com/flutter/flutter/issues/20907#issuecomment-466185328 -final _testDirectory = Directory.current.path.replaceAll('\\', '/') + - '/' + - (Directory.current.path.endsWith('test') ? '' : 'test'); +final _testDirectory = + '${Directory.current.path.replaceAll('\\', '/')}/${Directory.current.path.endsWith('test') ? '' : 'test'}'; diff --git a/slang_build_runner/analysis_options.yaml b/slang_build_runner/analysis_options.yaml new file mode 100644 index 00000000..3734fcd8 --- /dev/null +++ b/slang_build_runner/analysis_options.yaml @@ -0,0 +1,7 @@ +include: package:lints/recommended.yaml +linter: + rules: + prefer_single_quotes: true + sort_pub_dependencies: true + always_use_package_imports: true + directives_ordering: true diff --git a/slang_build_runner/lib/slang_build_runner.dart b/slang_build_runner/lib/slang_build_runner.dart index 68594f24..4b99b80e 100644 --- a/slang_build_runner/lib/slang_build_runner.dart +++ b/slang_build_runner/lib/slang_build_runner.dart @@ -1,17 +1,22 @@ import 'dart:async'; import 'package:build/build.dart'; +import 'package:glob/glob.dart'; import 'package:slang/builder/builder/raw_config_builder.dart'; import 'package:slang/builder/builder/slang_file_collection_builder.dart'; import 'package:slang/builder/builder/translation_map_builder.dart'; -import 'package:slang/builder/generator_facade.dart'; +// ignore: implementation_imports import 'package:slang/builder/model/enums.dart'; import 'package:slang/builder/model/raw_config.dart'; import 'package:slang/builder/model/slang_file_collection.dart'; -import 'package:slang/builder/utils/file_utils.dart'; -import 'package:slang/builder/utils/map_utils.dart'; -import 'package:slang/builder/utils/path_utils.dart'; -import 'package:glob/glob.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/generator_facade.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/utils/file_utils.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/utils/map_utils.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/utils/path_utils.dart'; /// Static entry point for build_runner Builder i18nBuilder(BuilderOptions options) { @@ -27,7 +32,7 @@ class I18nBuilder implements Builder { bool _generated = false; I18nBuilder({required this.config}) - : this.outputFilePattern = config.outputFileName.getFileExtension(); + : outputFilePattern = config.outputFileName.getFileExtension(); @override FutureOr build(BuildStep buildStep) async { diff --git a/slang_build_runner/pubspec.yaml b/slang_build_runner/pubspec.yaml index d6d388ba..ed1fad06 100644 --- a/slang_build_runner/pubspec.yaml +++ b/slang_build_runner/pubspec.yaml @@ -12,3 +12,6 @@ dependencies: # Use a tight version to ensure that all features are available slang: '>=3.29.0 <3.30.0' + +dev_dependencies: + lints: ^2.0.0 diff --git a/slang_gpt/lib/runner.dart b/slang_gpt/lib/runner.dart index ef0ff8ff..3c651273 100644 --- a/slang_gpt/lib/runner.dart +++ b/slang_gpt/lib/runner.dart @@ -3,12 +3,16 @@ import 'dart:io'; import 'package:http/http.dart' as http; import 'package:slang/builder/builder/slang_file_collection_builder.dart'; -import 'package:slang/builder/decoder/base_decoder.dart'; import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/builder/model/slang_file_collection.dart'; -import 'package:slang/builder/utils/file_utils.dart'; -import 'package:slang/builder/utils/map_utils.dart'; -import 'package:slang/builder/utils/path_utils.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/decoder/base_decoder.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/utils/file_utils.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/utils/map_utils.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/utils/path_utils.dart'; // ignore: implementation_imports import 'package:slang/src/runner/apply.dart'; import 'package:slang_gpt/model/gpt_config.dart'; diff --git a/slang_gpt/lib/util/logger.dart b/slang_gpt/lib/util/logger.dart index ba65289c..e723293e 100644 --- a/slang_gpt/lib/util/logger.dart +++ b/slang_gpt/lib/util/logger.dart @@ -2,8 +2,10 @@ import 'dart:convert'; import 'dart:io'; import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/utils/file_utils.dart'; -import 'package:slang/builder/utils/path_utils.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/utils/file_utils.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/utils/path_utils.dart'; import 'package:slang_gpt/model/gpt_prompt.dart'; import 'package:slang_gpt/model/gpt_response.dart'; diff --git a/slang_gpt/lib/util/maps.dart b/slang_gpt/lib/util/maps.dart index 3659911b..3cd79504 100644 --- a/slang_gpt/lib/util/maps.dart +++ b/slang_gpt/lib/util/maps.dart @@ -1,4 +1,5 @@ -import 'package:slang/builder/utils/node_utils.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/utils/node_utils.dart'; const ignoreGpt = 'ignoreGpt'; From 4ce2617c117f60d89ef5f507f9d0c9f564f1868d Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 3 Mar 2024 00:27:10 +0100 Subject: [PATCH 023/118] test: parseParam, parseParamWithArg --- slang/lib/builder/model/node.dart | 29 +------ .../lib/src/builder/builder/text_parser.dart | 28 ++++++- slang/lib/src/builder/utils/regex_utils.dart | 3 - slang/test/unit/builder/text_parser_test.dart | 76 +++++++++++++++++++ 4 files changed, 104 insertions(+), 32 deletions(-) create mode 100644 slang/test/unit/builder/text_parser_test.dart diff --git a/slang/lib/builder/model/node.dart b/slang/lib/builder/model/node.dart index 11fdbfd6..af29815e 100644 --- a/slang/lib/builder/model/node.dart +++ b/slang/lib/builder/model/node.dart @@ -4,7 +4,6 @@ import 'package:slang/builder/model/interface.dart'; import 'package:slang/builder/model/pluralization.dart'; import 'package:slang/src/builder/builder/text_parser.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; -import 'package:slang/src/builder/utils/string_extensions.dart'; import 'package:slang/src/builder/utils/string_interpolation_extensions.dart'; class NodeModifiers { @@ -382,7 +381,7 @@ class RichTextNode extends TextNode { _params = {}; for (final key in rawParsedResult.params.keys) { - final parsedParam = _parseParamWithArg( + final parsedParam = parseParamWithArg( rawParam: key, paramCase: paramCase, ); @@ -411,7 +410,7 @@ class RichTextNode extends TextNode { ); }, onMatch: (match) { - final parsed = _parseParamWithArg( + final parsed = parseParamWithArg( rawParam: (match.group(1) ?? match.group(2))!, paramCase: paramCase, ); @@ -611,30 +610,6 @@ Iterable _splitWithMatchAndNonMatch( } } -_ParamWithArg _parseParamWithArg({ - required String rawParam, - required CaseStyle? paramCase, -}) { - final end = rawParam.lastIndexOf(')'); - if (end == -1) { - return _ParamWithArg(rawParam.toCase(paramCase), null); - } - - final start = rawParam.indexOf('('); - final parameterName = rawParam.substring(0, start).toCase(paramCase); - return _ParamWithArg(parameterName, rawParam.substring(start + 1, end)); -} - -class _ParamWithArg { - final String paramName; - final String? arg; - - _ParamWithArg(this.paramName, this.arg); - - @override - String toString() => '_ParamWithArg{paramName: $paramName, arg: $arg}'; -} - abstract class BaseSpan {} class LiteralSpan extends BaseSpan { diff --git a/slang/lib/src/builder/builder/text_parser.dart b/slang/lib/src/builder/builder/text_parser.dart index 4ab2fc2d..933c86f1 100644 --- a/slang/lib/src/builder/builder/text_parser.dart +++ b/slang/lib/src/builder/builder/text_parser.dart @@ -9,7 +9,7 @@ class ParseParamResult { @override String toString() => - '_ParseParamResult(paramName: $paramName, paramType: $paramType)'; + 'ParseParamResult(paramName: $paramName, paramType: $paramType)'; } ParseParamResult parseParam({ @@ -19,7 +19,7 @@ ParseParamResult parseParam({ }) { if (rawParam.endsWith(')')) { // rich text parameter with default value - // this will be parsed by another method + // this will be parsed by parseParamWithArg return ParseParamResult( rawParam, '', @@ -31,3 +31,27 @@ ParseParamResult parseParam({ } return ParseParamResult(split[0].trim().toCase(caseStyle), split[1].trim()); } + +class ParamWithArg { + final String paramName; + final String? arg; + + ParamWithArg(this.paramName, this.arg); + + @override + String toString() => 'ParamWithArg(paramName: $paramName, arg: $arg)'; +} + +ParamWithArg parseParamWithArg({ + required String rawParam, + required CaseStyle? paramCase, +}) { + final end = rawParam.lastIndexOf(')'); + if (end == -1) { + return ParamWithArg(rawParam.toCase(paramCase), null); + } + + final start = rawParam.indexOf('('); + final parameterName = rawParam.substring(0, start).toCase(paramCase); + return ParamWithArg(parameterName, rawParam.substring(start + 1, end)); +} diff --git a/slang/lib/src/builder/utils/regex_utils.dart b/slang/lib/src/builder/utils/regex_utils.dart index f3dfc98f..e504196d 100644 --- a/slang/lib/src/builder/utils/regex_utils.dart +++ b/slang/lib/src/builder/utils/regex_utils.dart @@ -13,9 +13,6 @@ class RegexUtils { /// matches only $ but not \$ static RegExp dollarOnlyRegex = RegExp(r'([^\\]|^)\$( |$)'); - /// matches `param(arg)` - static RegExp paramWithArg = RegExp(r'^(\w+)(\((.+)\))?$'); - /// locale regex static const LOCALE_REGEX_RAW = r'([a-z]{2,3})(?:[_-]([A-Za-z]{4}))?(?:[_-]([A-Z]{2}|[0-9]{3}))?'; diff --git a/slang/test/unit/builder/text_parser_test.dart b/slang/test/unit/builder/text_parser_test.dart new file mode 100644 index 00000000..912f1418 --- /dev/null +++ b/slang/test/unit/builder/text_parser_test.dart @@ -0,0 +1,76 @@ +import 'package:slang/builder/model/enums.dart'; +import 'package:slang/src/builder/builder/text_parser.dart'; +import 'package:test/test.dart'; + +void main() { + group('parseParam', () { + test('Should parse without type', () { + final result = parseParam( + rawParam: 'myName', + caseStyle: null, + defaultType: 'DefaultType', + ); + expect(result.paramName, 'myName'); + expect(result.paramType, 'DefaultType'); + }); + + test('Should parse with type', () { + final result = parseParam( + rawParam: 'myName: MyType', + caseStyle: null, + defaultType: 'DefaultType', + ); + expect(result.paramName, 'myName'); + expect(result.paramType, 'MyType'); + }); + + test('Should recase', () { + final result = parseParam( + rawParam: 'my_name', + caseStyle: CaseStyle.pascal, + defaultType: 'DefaultType', + ); + expect(result.paramName, 'MyName'); + expect(result.paramType, 'DefaultType'); + }); + + test('Should ignore rich text default parameter', () { + final result = parseParam( + rawParam: 'hello(Hi)', + caseStyle: null, + defaultType: 'DefaultType', + ); + expect(result.paramName, 'hello(Hi)'); + expect(result.paramType, ''); + }); + }); + + group('parseParamWithArg', () { + test('Should parse without arg', () { + final result = parseParamWithArg( + rawParam: 'myName', + paramCase: null, + ); + expect(result.paramName, 'myName'); + expect(result.arg, null); + }); + + test('Should parse with arg', () { + final result = parseParamWithArg( + rawParam: 'myName(Hello!)', + paramCase: null, + ); + expect(result.paramName, 'myName'); + expect(result.arg, 'Hello!'); + }); + + test('Should recase', () { + final result = parseParamWithArg( + rawParam: 'my_name(Hello!)', + paramCase: CaseStyle.pascal, + ); + expect(result.paramName, 'MyName'); + expect(result.arg, 'Hello!'); + }); + }); +} From 3b54c35461ee865d26d3ead12a4a45a012a31c31 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 3 Mar 2024 00:38:31 +0100 Subject: [PATCH 024/118] docs: add parameter types --- slang/CHANGELOG.md | 6 ++++++ slang/README.md | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 9e7a9108..9f027047 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.30.0 + +- feat: add parameter types (e.g. `Hello {name: String}, you are {age: int} years old`); is `Object` by default @Tienisto +- fix: handle nested interfaces (#191) @Tienisto +- refactor: move code to src folder @Tienisto + ## 3.29.0 - feat: `dart run slang analyze` supports csv files (#185) @nikaera diff --git a/slang/README.md b/slang/README.md index a08b0535..2c3c1e70 100644 --- a/slang/README.md +++ b/slang/README.md @@ -63,6 +63,7 @@ dart run slang migrate arb src.arb dest.json # migrate arb to json - [Linked Translations](#-linked-translations) - [Pluralization](#-pluralization) - [Custom Contexts / Enums](#-custom-contexts--enums) + - [Typed Parameters](#-typed-parameters) - [Interfaces](#-interfaces) - [Modifiers](#-modifiers) - [Locale Enum](#-locale-enum) @@ -883,6 +884,18 @@ contexts: generate_enum: false # turn off enum generation ``` +### ➤ Typed Parameters + +Parameters are typed as `Object` by default. This is handy because it offers maximum flexibility. + +You can specify the type using the `name: type` syntax to increase type safety. + +```json +{ + "greet": "Hello {name: String}, you are {age: int} years old" +} +``` + ### ➤ Interfaces Often, multiple objects have the same attributes. You can create a common super class for that. From 32a5ddfdd10286e856f82f3e9540f9179456f438 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 3 Mar 2024 01:04:03 +0100 Subject: [PATCH 025/118] test: translation overrides + parameter type --- slang/test/unit/api/translation_overrides_test.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/slang/test/unit/api/translation_overrides_test.dart b/slang/test/unit/api/translation_overrides_test.dart index b15e3003..fac1f379 100644 --- a/slang/test/unit/api/translation_overrides_test.dart +++ b/slang/test/unit/api/translation_overrides_test.dart @@ -41,6 +41,16 @@ void main() { expect(parsed, 'About Page'); }); + test('Should ignore type in interpolated string', () { + final meta = _buildMetaWithOverrides({ + 'aboutPage.title': r'About ${arg: int}', + }); + final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { + 'arg': 'Page', + }); + expect(parsed, 'About Page'); + }); + test('Should return an interpolated string with dollar only', () { final meta = _buildMetaWithOverrides({ 'aboutPage.title': r'About $arg', From d0bb3600594502f676c19cb5827c9c96db98f318 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 3 Mar 2024 01:06:35 +0100 Subject: [PATCH 026/118] ci: bump flutter to 3.19.2 --- .fvm/fvm_config.json | 2 +- .github/workflows/ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index ccdae4db..7691f753 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.19.0", + "flutterSdkVersion": "3.19.2", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2200bf7..538b0428 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ on: env: FLUTTER_VERSION_OLDEST: "3.0.5" - FLUTTER_VERSION_NEWEST: "3.19.0" + FLUTTER_VERSION_NEWEST: "3.19.2" jobs: format: From 7e189ee8f6ae5c92c8b28dc580171e1e5617feb7 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 3 Mar 2024 01:09:43 +0100 Subject: [PATCH 027/118] release: 3.30.0 --- slang/pubspec.yaml | 2 +- slang_build_runner/CHANGELOG.md | 4 ++++ slang_build_runner/pubspec.yaml | 4 ++-- slang_flutter/CHANGELOG.md | 4 ++++ slang_flutter/pubspec.yaml | 4 ++-- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index f254aed5..5068fa21 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -1,6 +1,6 @@ name: slang description: Localization / Internationalization (i18n) solution. Use JSON, YAML, CSV, or ARB files to create typesafe translations via source generation. -version: 3.29.0 +version: 3.30.0 repository: https://github.com/slang-i18n/slang topics: - i18n diff --git a/slang_build_runner/CHANGELOG.md b/slang_build_runner/CHANGELOG.md index 1294db54..a6582d64 100644 --- a/slang_build_runner/CHANGELOG.md +++ b/slang_build_runner/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.30.0 + +- Bump `slang` to `3.30.0` + ## 3.29.0 - Bump `slang` to `3.29.0` diff --git a/slang_build_runner/pubspec.yaml b/slang_build_runner/pubspec.yaml index ed1fad06..52d14f8f 100644 --- a/slang_build_runner/pubspec.yaml +++ b/slang_build_runner/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_build_runner description: build_runner integration for slang. This library ensures that slang is recognized by build_runner. -version: 3.29.0 +version: 3.30.0 repository: https://github.com/slang-i18n/slang environment: @@ -11,7 +11,7 @@ dependencies: glob: ^2.0.2 # Use a tight version to ensure that all features are available - slang: '>=3.29.0 <3.30.0' + slang: '>=3.30.0 <3.31.0' dev_dependencies: lints: ^2.0.0 diff --git a/slang_flutter/CHANGELOG.md b/slang_flutter/CHANGELOG.md index e4d17c74..5a7b8973 100644 --- a/slang_flutter/CHANGELOG.md +++ b/slang_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.30.0 + +- Bump `slang` to `3.30.0` + ## 3.29.0 - Bump `slang` to `3.29.0` diff --git a/slang_flutter/pubspec.yaml b/slang_flutter/pubspec.yaml index be11fc47..8f87c115 100644 --- a/slang_flutter/pubspec.yaml +++ b/slang_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_flutter description: Flutter support for slang. This library provides helpful Flutter API. -version: 3.29.0 +version: 3.30.0 repository: https://github.com/slang-i18n/slang environment: @@ -12,7 +12,7 @@ dependencies: sdk: flutter # Use a tight version to ensure that all features are available - slang: '>=3.29.0 <3.30.0' + slang: '>=3.30.0 <3.31.0' dev_dependencies: flutter_test: From 5b89f828ab9aaef5de6cef29172b28d9695860b2 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 3 Mar 2024 02:47:13 +0100 Subject: [PATCH 028/118] fix: duplicate keys when there are modifiers --- slang/CHANGELOG.md | 4 +++ slang/lib/src/builder/utils/node_utils.dart | 6 ++++- slang/lib/src/runner/apply.dart | 11 +++++--- slang/pubspec.yaml | 2 +- slang/test/unit/runner/apply_test.dart | 30 +++++++++++++++++---- slang_gpt/CHANGELOG.md | 5 ++++ slang_gpt/lib/util/maps.dart | 2 +- slang_gpt/pubspec.yaml | 2 +- 8 files changed, 50 insertions(+), 12 deletions(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 9f027047..0297e4ac 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.30.1 + +- fix: applying translations with `dart run slang apply` should use only modifiers from the base locale (#192) + ## 3.30.0 - feat: add parameter types (e.g. `Hello {name: String}, you are {age: int} years old`); is `Object` by default @Tienisto diff --git a/slang/lib/src/builder/utils/node_utils.dart b/slang/lib/src/builder/utils/node_utils.dart index 854084b0..32575ac3 100644 --- a/slang/lib/src/builder/utils/node_utils.dart +++ b/slang/lib/src/builder/utils/node_utils.dart @@ -80,7 +80,11 @@ class NodePathInfo { extension StringModifierExt on String { /// Returns the key without modifiers. String get withoutModifiers { - return split('(').first; + final index = indexOf('('); + if (index == -1) { + return this; + } + return substring(0, index); } String withModifier(String modifierKey, [String? modifierValue]) { diff --git a/slang/lib/src/runner/apply.dart b/slang/lib/src/runner/apply.dart index f15785c4..5dcfb679 100644 --- a/slang/lib/src/runner/apply.dart +++ b/slang/lib/src/runner/apply.dart @@ -221,6 +221,7 @@ Map applyMapRecursive({ required bool verbose, }) { final resultMap = {}; + final resultKeys = {}; // keys without modifiers // Keys that have been applied. // They do not have modifiers in their path. @@ -234,7 +235,8 @@ Map applyMapRecursive({ // Add keys according to the order in base map. // Prefer new map over old map. for (final key in baseMap.keys) { - final newEntry = newMap[key.withoutModifiers]; + final keyWithoutModifiers = key.withoutModifiers; + final newEntry = newMap[keyWithoutModifiers]; dynamic actualValue = newEntry ?? oldMap[key]; if (actualValue == null) { continue; @@ -261,12 +263,14 @@ Map applyMapRecursive({ } } resultMap[key] = actualValue; + resultKeys.add(keyWithoutModifiers); } // Add keys from old map that are unknown in base locale. // It may contain the OUTDATED modifier. for (final key in oldMap.keys) { - if (resultMap.containsKey(key)) { + final keyWithoutModifiers = key.withoutModifiers; + if (resultKeys.contains(keyWithoutModifiers)) { continue; } @@ -296,12 +300,13 @@ Map applyMapRecursive({ _printAdding(currPath, actualValue); } resultMap[key] = actualValue; + resultKeys.add(keyWithoutModifiers); } // Add remaining new keys that are not in base locale and not in old map. for (final entry in newMap.entries) { final keyWithoutModifiers = entry.key.withoutModifiers; - if (resultMap.containsKey(keyWithoutModifiers)) { + if (resultKeys.contains(keyWithoutModifiers)) { continue; } diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index 5068fa21..cedd602d 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -1,6 +1,6 @@ name: slang description: Localization / Internationalization (i18n) solution. Use JSON, YAML, CSV, or ARB files to create typesafe translations via source generation. -version: 3.30.0 +version: 3.30.1 repository: https://github.com/slang-i18n/slang topics: - i18n diff --git a/slang/test/unit/runner/apply_test.dart b/slang/test/unit/runner/apply_test.dart index f4929807..e5ec38d3 100644 --- a/slang/test/unit/runner/apply_test.dart +++ b/slang/test/unit/runner/apply_test.dart @@ -5,13 +5,13 @@ void main() { group('applyMapRecursive', () { test('ignore strings in baseMap if not applied', () { final result = applyMapRecursive( - baseMap: {'c': 'C'}, - newMap: {'a': 'A'}, - oldMap: {'b': 'B'}, + baseMap: {'base': 'BB'}, + newMap: {'new': 'NN'}, + oldMap: {'old': 'OO'}, verbose: false, ); - expect(result, {'b': 'B', 'a': 'A'}); - expect(result.keys.toList(), ['b', 'a']); + expect(result, {'new': 'NN', 'old': 'OO'}); + expect(result.keys.toList(), ['old', 'new']); }); test('handle empty newMap', () { @@ -145,5 +145,25 @@ void main() { expect(result.keys.toList(), ['c', 'a', 'b']); expect((result['a'] as Map).keys.toList(), ['y', 'x', 'z', '0']); }); + + test('ignore new modifier', () { + final result = applyMapRecursive( + baseMap: {}, + newMap: {'a(param=arg0)': 'A'}, + oldMap: {}, + verbose: false, + ); + expect(result, {'a': 'A'}); + }); + + test('apply modifier of base map', () { + final result = applyMapRecursive( + baseMap: {'a(param=arg0)': 'base'}, + newMap: {'a': 'new'}, + oldMap: {'a': 'old'}, + verbose: false, + ); + expect(result, {'a(param=arg0)': 'new'}); + }); }); } diff --git a/slang_gpt/CHANGELOG.md b/slang_gpt/CHANGELOG.md index cabbb9aa..a784e299 100644 --- a/slang_gpt/CHANGELOG.md +++ b/slang_gpt/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.10.1 + +- deps: bump slang to 3.30.1 +- fix: duplicate keys when there are modifiers #192 + ## 0.10.0 - deps: bump slang to 3.25.0 diff --git a/slang_gpt/lib/util/maps.dart b/slang_gpt/lib/util/maps.dart index 3cd79504..409cf432 100644 --- a/slang_gpt/lib/util/maps.dart +++ b/slang_gpt/lib/util/maps.dart @@ -3,7 +3,7 @@ import 'package:slang/src/builder/utils/node_utils.dart'; const ignoreGpt = 'ignoreGpt'; -/// Remove all entries from [map] that have the "ignoreMissing" modifier. +/// Remove all entries from [map] that have the "ignoreGpt" modifier. /// This method removes the entries in-place. void removeIgnoreGpt({ required Map map, diff --git a/slang_gpt/pubspec.yaml b/slang_gpt/pubspec.yaml index 71a1ed6d..9ae0d3c8 100644 --- a/slang_gpt/pubspec.yaml +++ b/slang_gpt/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: collection: ^1.15.0 http: '>=0.13.0 <2.0.0' - slang: ^3.25.0 + slang: ^3.30.1 dev_dependencies: lints: ^2.0.0 From e5ab224a34aa730ce1f9247e33825a769e6b58cb Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 3 Mar 2024 02:51:25 +0100 Subject: [PATCH 029/118] release(gpt): 0.10.1 --- slang_gpt/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slang_gpt/pubspec.yaml b/slang_gpt/pubspec.yaml index 9ae0d3c8..75c5d9bc 100644 --- a/slang_gpt/pubspec.yaml +++ b/slang_gpt/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_gpt description: Use GPT to automatically translate at compile time. A tool for slang. -version: 0.10.0 +version: 0.10.1 repository: https://github.com/slang-i18n/slang environment: From fe23de46683ee3952500ae8c8477694a47cb0791 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 3 Mar 2024 03:01:23 +0100 Subject: [PATCH 030/118] docs: update changelog --- slang/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 0297e4ac..5e98ee81 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,6 +1,6 @@ ## 3.30.1 -- fix: applying translations with `dart run slang apply` should use only modifiers from the base locale (#192) +- fix: when applying translations with `dart run slang apply`, only modifiers from the base locale should be used (#192) ## 3.30.0 From 306e64258ed56d9d6e9f490e425cc0828f21307a Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 3 Mar 2024 17:25:56 +0100 Subject: [PATCH 031/118] docs: update readme --- slang/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/slang/README.md b/slang/README.md index 2c3c1e70..246e771b 100644 --- a/slang/README.md +++ b/slang/README.md @@ -100,6 +100,7 @@ dart run slang migrate arb src.arb dest.json # migrate arb to json - [slang x riverpod](#-slang-x-riverpod) - [FAQ](#faq) - [Further Reading](#further-reading) +- [Ecosystem](#ecosystem) - [Slang in production](#slang-in-production) ## Getting Started @@ -1887,11 +1888,17 @@ The second one always returns a new instance. Feel free to extend this list :) +## Ecosystem + +- [slang_gpt](https://pub.dev/packages/slang_gpt) - Use GPT to internationalize your app with context-aware translations. +- [Apparencekit](https://apparencekit.dev/docs/other/internationalization/) - Boilerplate solution + ## Slang in production Open source: - [LocalSend (file sharing app)](https://github.com/localsend/localsend) +- [ReVanced](https://github.com/ReVanced/revanced-manager) - [Hiddify](https://github.com/hiddify/hiddify-next) - [Saber (notes app)](https://github.com/adil192/saber) - [Boorusphere (booru viewer)](https://github.com/nullxception/boorusphere) From 5dbdb38a373f95a388b4cd1195c1d59ad418bdbd Mon Sep 17 00:00:00 2001 From: nikaera Date: Sun, 17 Mar 2024 16:21:21 +0900 Subject: [PATCH 032/118] fix: exclude comments from analysis during the execution of 'analyze' --- slang/lib/src/runner/analyze.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/slang/lib/src/runner/analyze.dart b/slang/lib/src/runner/analyze.dart index 1418ed0b..d7e9179c 100644 --- a/slang/lib/src/runner/analyze.dart +++ b/slang/lib/src/runner/analyze.dart @@ -347,9 +347,16 @@ void _getUnusedTranslationsInSourceCodeRecursive({ /// and joins them into a single (huge) string without any spaces. String loadSourceCode(List files) { final buffer = StringBuffer(); - final regex = RegExp(r'\s'); + final spacesRegex = RegExp(r'\s'); + final singleLineCommentsRegex = RegExp(r'\/\/.*'); + final multiLineCommentsRegex = RegExp(r'\/\*.*?\*\/', multiLine: true); + for (final file in files) { - buffer.write(file.readAsStringSync().replaceAll(regex, '')); + buffer.write(file + .readAsStringSync() + .replaceAll(singleLineCommentsRegex, '') + .replaceAll(multiLineCommentsRegex, '') + .replaceAll(spacesRegex, '')); } return buffer.toString(); From d70c7b3d50856110f1abe20b057e36debd8efd50 Mon Sep 17 00:00:00 2001 From: nikaera Date: Sun, 17 Mar 2024 16:36:55 +0900 Subject: [PATCH 033/118] fix: bugfix in processing when encoding CSV --- slang/lib/src/builder/utils/file_utils.dart | 26 ++++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/slang/lib/src/builder/utils/file_utils.dart b/slang/lib/src/builder/utils/file_utils.dart index 01e4452b..079d33f4 100644 --- a/slang/lib/src/builder/utils/file_utils.dart +++ b/slang/lib/src/builder/utils/file_utils.dart @@ -52,22 +52,23 @@ class FileUtils { return escaped; } - Map encodeRow({ + void encodeCsvRows({ String key = '', required dynamic value, + required Map result, }) { if (value is Map) { - final keyPrefix = key.isEmpty ? '' : '$key.'; - return value.map((k, v) { - final map = encodeRow(key: '$keyPrefix$k', value: v); - final mapEntry = map.entries.first; - - return MapEntry(mapEntry.key, mapEntry.value); - }); + for (final k in value.keys) { + final prefix = key.isEmpty ? '' : '$key.'; + encodeCsvRows( + key: '$prefix$k', + value: value[k], + result: result, + ); + } } else if (value is String) { - return {key: escapeRow(value)}; + result[key] = escapeRow(value); } - return {}; } final Map> columns = {}; @@ -76,7 +77,10 @@ class FileUtils { columns[INFO_KEY] = {INFO_KEY: escapeRow(info.join('\\n'))}; } for (final e in content.entries) { - columns[e.key] = encodeRow(value: e.value); + final result = {}; + encodeCsvRows(result: result, value: e.value); + + columns[e.key] = result; } // get all translation keys From 19e71bbf83db2c8b00eae8ca186e9c767c163435 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 17 Mar 2024 14:52:42 +0100 Subject: [PATCH 034/118] fix: use dotAll --- slang/lib/src/runner/analyze.dart | 4 ++-- slang/test/unit/runner/analyze_test.dart | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/slang/lib/src/runner/analyze.dart b/slang/lib/src/runner/analyze.dart index d7e9179c..b18c647b 100644 --- a/slang/lib/src/runner/analyze.dart +++ b/slang/lib/src/runner/analyze.dart @@ -348,8 +348,8 @@ void _getUnusedTranslationsInSourceCodeRecursive({ String loadSourceCode(List files) { final buffer = StringBuffer(); final spacesRegex = RegExp(r'\s'); - final singleLineCommentsRegex = RegExp(r'\/\/.*'); - final multiLineCommentsRegex = RegExp(r'\/\*.*?\*\/', multiLine: true); + final singleLineCommentsRegex = RegExp(r'//.*'); + final multiLineCommentsRegex = RegExp(r'/\*.*?\*/', dotAll: true); for (final file in files) { buffer.write(file diff --git a/slang/test/unit/runner/analyze_test.dart b/slang/test/unit/runner/analyze_test.dart index ef62a6a1..fe4f75af 100644 --- a/slang/test/unit/runner/analyze_test.dart +++ b/slang/test/unit/runner/analyze_test.dart @@ -28,5 +28,29 @@ void main() { expect(result, 'ABCDEFGH;'); }); + + test('should ignore inline comments', () { + final files = [ + FakeFile('A // B\nC'), + FakeFile('D /* E */ F'), + FakeFile('G'), + ]; + + final result = loadSourceCode(files); + + expect(result, 'ACDFG'); + }); + + test('should ignore block comments', () { + final files = [ + FakeFile('A /* B\nC */ D'), + FakeFile('E // F'), + FakeFile('G //'), + ]; + + final result = loadSourceCode(files); + + expect(result, 'ADEG'); + }); }); } From 2ce2a6335c2efb7475a5394fde7f3b96c69652e9 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 17 Mar 2024 15:35:53 +0100 Subject: [PATCH 035/118] deps: loosen csv constraint --- slang/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index cedd602d..29f0e0c6 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -18,7 +18,7 @@ environment: dependencies: collection: ^1.15.0 - csv: ^5.0.1 + csv: ">=5.0.1 <7.0.0" json2yaml: ^3.0.0 watcher: ^1.0.2 yaml: ^3.1.0 From b6422949f59287771ee733122a75c5dcbd90a110 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 17 Mar 2024 15:37:36 +0100 Subject: [PATCH 036/118] ci: bump flutter to 3.19.3 --- .fvm/fvm_config.json | 4 ---- .fvmrc | 4 ++++ .github/workflows/ci.yml | 2 +- .github/workflows/publish_template.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 .fvm/fvm_config.json create mode 100644 .fvmrc diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json deleted file mode 100644 index 7691f753..00000000 --- a/.fvm/fvm_config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "flutterSdkVersion": "3.19.2", - "flavors": {} -} \ No newline at end of file diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..f79f9b46 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.19.3", + "flavors": {} +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 538b0428..a4394be6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ on: env: FLUTTER_VERSION_OLDEST: "3.0.5" - FLUTTER_VERSION_NEWEST: "3.19.2" + FLUTTER_VERSION_NEWEST: "3.19.3" jobs: format: diff --git a/.github/workflows/publish_template.yml b/.github/workflows/publish_template.yml index 4bc3e6ee..5f7e53e4 100644 --- a/.github/workflows/publish_template.yml +++ b/.github/workflows/publish_template.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 with: - flutter-version: '3.7.12' + flutter-version: '3.19.3' channel: 'stable' - name: Dependencies (core) run: flutter pub get From 804f2ae1428a244eebfd0887cc10135af446b9fa Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 25 Mar 2024 15:15:23 +0100 Subject: [PATCH 037/118] ci: bump flutter to 3.19.4 --- .fvmrc | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/publish_template.yml | 2 +- .gitignore | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.fvmrc b/.fvmrc index f79f9b46..b231bd0e 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,4 +1,4 @@ { - "flutter": "3.19.3", + "flutter": "3.19.4", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4394be6..91fd503b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ on: env: FLUTTER_VERSION_OLDEST: "3.0.5" - FLUTTER_VERSION_NEWEST: "3.19.3" + FLUTTER_VERSION_NEWEST: "3.19.4" jobs: format: diff --git a/.github/workflows/publish_template.yml b/.github/workflows/publish_template.yml index 5f7e53e4..13c9ae95 100644 --- a/.github/workflows/publish_template.yml +++ b/.github/workflows/publish_template.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 with: - flutter-version: '3.19.3' + flutter-version: '3.19.4' channel: 'stable' - name: Dependencies (core) run: flutter pub get diff --git a/.gitignore b/.gitignore index 95d05dc0..dd3f8373 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.fvm/flutter_sdk +/.fvm # testing /slang/lib/builder/i18n/ From 4ce72e5b1f501e0138cbc59c87c7a2325a6edd7e Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 25 Mar 2024 15:15:52 +0100 Subject: [PATCH 038/118] fix: only avoid interpolation on linked translations --- slang/lib/builder/model/node.dart | 6 ++++++ .../generator/generate_translation_map.dart | 3 ++- .../generator/generate_translations.dart | 19 ++++++++++++------- slang/lib/src/builder/generator/helper.dart | 6 ++++-- .../resources/main/_expected_de.output | 1 + .../resources/main/_expected_en.output | 1 + .../_expected_fallback_base_locale.output | 6 +++++- .../resources/main/_expected_main.output | 2 +- .../resources/main/_expected_map.output | 2 ++ .../main/_expected_obfuscation.output | 6 +++++- .../resources/main/_expected_single.output | 6 +++++- .../_expected_translation_overrides.output | 6 +++++- .../resources/main/csv_compact.csv | 1 + .../integration/resources/main/csv_de.csv | 1 + .../integration/resources/main/csv_en.csv | 1 + .../integration/resources/main/json_de.json | 1 + .../integration/resources/main/json_en.json | 1 + .../integration/resources/main/yaml_de.yaml | 1 + .../integration/resources/main/yaml_en.yaml | 1 + slang/test/unit/utils/secret_test.dart | 14 +++++++++----- 20 files changed, 65 insertions(+), 20 deletions(-) diff --git a/slang/lib/builder/model/node.dart b/slang/lib/builder/model/node.dart index af29815e..710ceb30 100644 --- a/slang/lib/builder/model/node.dart +++ b/slang/lib/builder/model/node.dart @@ -407,6 +407,7 @@ class RichTextNode extends TextNode { return LiteralSpan( literal: parsedLinksResult.parsedContent, isConstant: parsedLinksResult.links.isEmpty, + links: parsedLinksResult.links, ); }, onMatch: (match) { @@ -424,6 +425,7 @@ class RichTextNode extends TextNode { return FunctionSpan( functionName: parsed.paramName, arg: parsedLinksResult.parsedContent, + links: parsedLinksResult.links, ); } else { return VariableSpan(parsed.paramName); @@ -615,20 +617,24 @@ abstract class BaseSpan {} class LiteralSpan extends BaseSpan { final String literal; final bool isConstant; + final Set links; LiteralSpan({ required this.literal, required this.isConstant, + required this.links, }); } class FunctionSpan extends BaseSpan { final String functionName; final String arg; + final Set links; FunctionSpan({ required this.functionName, required this.arg, + required this.links, }); } diff --git a/slang/lib/src/builder/generator/generate_translation_map.dart b/slang/lib/src/builder/generator/generate_translation_map.dart index 71bcd683..1c28bc2d 100644 --- a/slang/lib/src/builder/generator/generate_translation_map.dart +++ b/slang/lib/src/builder/generator/generate_translation_map.dart @@ -58,7 +58,8 @@ _generateTranslationMapRecursive({ final translationOverrides = config.translationOverrides ? 'TranslationOverrides.string(_root.\$meta, \'${curr.path}\', ${_toParameterMap(curr.params)}) ?? ' : ''; - final stringLiteral = getStringLiteral(curr.content, config.obfuscation); + final stringLiteral = + getStringLiteral(curr.content, curr.links.length, config.obfuscation); if (curr.params.isEmpty) { buffer.writeln( '\t\t\tcase \'${curr.path}\': return $translationOverrides$stringLiteral;'); diff --git a/slang/lib/src/builder/generator/generate_translations.dart b/slang/lib/src/builder/generator/generate_translations.dart index 65b716d5..5af3e44c 100644 --- a/slang/lib/src/builder/generator/generate_translations.dart +++ b/slang/lib/src/builder/generator/generate_translations.dart @@ -297,7 +297,8 @@ void _generateClass( final translationOverrides = config.translationOverrides ? 'TranslationOverrides.string(_root.\$meta, \'${value.path}\', ${_toParameterMap(value.params)}) ?? ' : ''; - final stringLiteral = getStringLiteral(value.content, config.obfuscation); + final stringLiteral = getStringLiteral( + value.content, value.links.length, config.obfuscation); if (value.params.isEmpty) { buffer.writeln( 'String$optional get $key => $translationOverrides$stringLiteral;'); @@ -409,7 +410,8 @@ void _generateMap({ // Maps cannot contain rich texts // because there is no way to add the "rich" modifier. if (value is StringTextNode) { - final stringLiteral = getStringLiteral(value.content, config.obfuscation); + final stringLiteral = getStringLiteral( + value.content, value.links.length, config.obfuscation); if (value.params.isEmpty) { buffer.writeln('\'$key\': $stringLiteral,'); } else { @@ -512,7 +514,8 @@ void _generateList({ // Lists cannot contain rich texts // because there is no way to add the "rich" modifier. if (value is StringTextNode) { - final stringLiteral = getStringLiteral(value.content, config.obfuscation); + final stringLiteral = getStringLiteral( + value.content, value.links.length, config.obfuscation); if (value.params.isEmpty) { buffer.writeln('$stringLiteral,'); } else { @@ -708,8 +711,9 @@ void _addPluralizationCall({ '$translationOverrides(_root.\$meta.${prefix}Resolver ?? PluralResolvers.$prefix(\'$language\'))(${node.paramName},'); for (final quantity in node.quantities.entries) { _addTabs(buffer, depth + 2); + final textNode = quantity.value as StringTextNode; buffer.writeln( - '${quantity.key.paramName()}: ${getStringLiteral((quantity.value as StringTextNode).content, config.obfuscation)},'); + '${quantity.key.paramName()}: ${getStringLiteral(textNode.content, textNode.links.length, config.obfuscation)},'); } } @@ -754,7 +758,7 @@ void _addRichTextCall({ if (span is LiteralSpan) { buffer.write( - "${!config.obfuscation.enabled && span.isConstant ? 'const ' : ''}TextSpan(text: ${getStringLiteral(span.literal, config.obfuscation)})", + "${!config.obfuscation.enabled && span.isConstant ? 'const ' : ''}TextSpan(text: ${getStringLiteral(span.literal, span.links.length, config.obfuscation)})", ); } else if (span is VariableSpan) { if (variableNameResolver != null) { @@ -764,7 +768,7 @@ void _addRichTextCall({ } } else if (span is FunctionSpan) { buffer.write( - '${span.functionName}(${getStringLiteral(span.arg, config.obfuscation)})', + '${span.functionName}(${getStringLiteral(span.arg, span.links.length, config.obfuscation)})', ); } buffer.writeln(','); @@ -852,8 +856,9 @@ void _addContextCall({ forceSemicolon: true, ); } else { + final textNode = entry.value as StringTextNode; buffer.writeln( - '${getStringLiteral((entry.value as StringTextNode).content, config.obfuscation)};', + '${getStringLiteral(textNode.content, textNode.links.length, config.obfuscation)};', ); } } diff --git a/slang/lib/src/builder/generator/helper.dart b/slang/lib/src/builder/generator/helper.dart index 47ea0321..9bb145f2 100644 --- a/slang/lib/src/builder/generator/helper.dart +++ b/slang/lib/src/builder/generator/helper.dart @@ -40,10 +40,12 @@ const _NULL_FLAG = '\u0000'; /// Either returns the plain string or the obfuscated one. /// Whenever translation strings gets rendered, this method must be called. -String getStringLiteral(String value, ObfuscationConfig config) { +String getStringLiteral(String value, int linkCount, ObfuscationConfig config) { if (!config.enabled || value.isEmpty) { // Return the plain version - if (value.startsWith(r'${') && value.indexOf('}') == value.length - 1) { + if (value.startsWith(r'${') && + value.indexOf('}') == value.length - 1 && + linkCount == 1) { // We can just remove the ${ and } since it's already a string return value.substring(2, value.length - 1); } else { diff --git a/slang/test/integration/resources/main/_expected_de.output b/slang/test/integration/resources/main/_expected_de.output index fea4028f..3d8fa787 100644 --- a/slang/test/integration/resources/main/_expected_de.output +++ b/slang/test/integration/resources/main/_expected_de.output @@ -56,6 +56,7 @@ class _TranslationsOnboardingDe implements _TranslationsOnboardingEn { // Translations @override String welcome({required Object fullName}) => 'Willkommen ${fullName}'; @override String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + @override String welcomeOnlyParam({required Object firstName}) => '${firstName}'; /// Bye text @override String bye({required Object firstName}) => 'Tschüss ${firstName}'; diff --git a/slang/test/integration/resources/main/_expected_en.output b/slang/test/integration/resources/main/_expected_en.output index 88cc12a7..4ec7bba3 100644 --- a/slang/test/integration/resources/main/_expected_en.output +++ b/slang/test/integration/resources/main/_expected_en.output @@ -62,6 +62,7 @@ class _TranslationsOnboardingEn { // Translations String welcome({required Object fullName}) => 'Welcome ${fullName}'; String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + String welcomeOnlyParam({required Object firstName}) => '${firstName}'; /// Bye text String bye({required Object firstName}) => 'Bye ${firstName}'; diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale.output b/slang/test/integration/resources/main/_expected_fallback_base_locale.output index a1a96ae0..d0d2837f 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 56 (28 per locale) +/// Strings: 58 (29 per locale) // coverage:ignore-file // ignore_for_file: type=lint @@ -214,6 +214,7 @@ class _TranslationsOnboardingEn { // Translations String welcome({required Object fullName}) => 'Welcome ${fullName}'; String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + String welcomeOnlyParam({required Object firstName}) => '${firstName}'; /// Bye text String bye({required Object firstName}) => 'Bye ${firstName}'; @@ -383,6 +384,7 @@ class _TranslationsOnboardingDe extends _TranslationsOnboardingEn { // Translations @override String welcome({required Object fullName}) => 'Willkommen ${fullName}'; @override String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + @override String welcomeOnlyParam({required Object firstName}) => '${firstName}'; /// Bye text @override String bye({required Object firstName}) => 'Tschüss ${firstName}'; @@ -508,6 +510,7 @@ extension on Translations { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => 'Welcome ${fullName}'; case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; case 'onboarding.bye': return ({required Object firstName}) => 'Bye ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), @@ -572,6 +575,7 @@ extension on _TranslationsDe { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => 'Willkommen ${fullName}'; case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; case 'onboarding.bye': return ({required Object firstName}) => 'Tschüss ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), diff --git a/slang/test/integration/resources/main/_expected_main.output b/slang/test/integration/resources/main/_expected_main.output index 9b289514..b54ef83e 100644 --- a/slang/test/integration/resources/main/_expected_main.output +++ b/slang/test/integration/resources/main/_expected_main.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 56 (28 per locale) +/// Strings: 58 (29 per locale) // coverage:ignore-file // ignore_for_file: type=lint diff --git a/slang/test/integration/resources/main/_expected_map.output b/slang/test/integration/resources/main/_expected_map.output index 4ff278a8..bd3b4e63 100644 --- a/slang/test/integration/resources/main/_expected_map.output +++ b/slang/test/integration/resources/main/_expected_map.output @@ -14,6 +14,7 @@ extension on Translations { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => 'Welcome ${fullName}'; case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; case 'onboarding.bye': return ({required Object firstName}) => 'Bye ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), @@ -78,6 +79,7 @@ extension on _TranslationsDe { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => 'Willkommen ${fullName}'; case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; case 'onboarding.bye': return ({required Object firstName}) => 'Tschüss ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), diff --git a/slang/test/integration/resources/main/_expected_obfuscation.output b/slang/test/integration/resources/main/_expected_obfuscation.output index 239b2952..b54924f2 100644 --- a/slang/test/integration/resources/main/_expected_obfuscation.output +++ b/slang/test/integration/resources/main/_expected_obfuscation.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 56 (28 per locale) +/// Strings: 58 (29 per locale) // coverage:ignore-file // ignore_for_file: type=lint @@ -216,6 +216,7 @@ class _TranslationsOnboardingEn { // Translations String welcome({required Object fullName}) => _root.$meta.d([12, 62, 55, 56, 52, 54, 62, 123]) + fullName.toString(); String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + String welcomeOnlyParam({required Object firstName}) => firstName.toString(); /// Bye text String bye({required Object firstName}) => _root.$meta.d([25, 34, 62, 123]) + firstName.toString(); @@ -384,6 +385,7 @@ class _TranslationsOnboardingDe implements _TranslationsOnboardingEn { // Translations @override String welcome({required Object fullName}) => _root.$meta.d([12, 50, 55, 55, 48, 52, 54, 54, 62, 53, 123]) + fullName.toString(); @override String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + @override String welcomeOnlyParam({required Object firstName}) => firstName.toString(); /// Bye text @override String bye({required Object firstName}) => _root.$meta.d([15, 40, 56, 51, 167, 40, 40, 123]) + firstName.toString(); @@ -509,6 +511,7 @@ extension on Translations { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => _root.$meta.d([12, 62, 55, 56, 52, 54, 62, 123]) + fullName.toString(); case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => firstName.toString(); case 'onboarding.bye': return ({required Object firstName}) => _root.$meta.d([25, 34, 62, 123]) + firstName.toString(); case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ TextSpan(text: _root.$meta.d([19, 50, 123])), @@ -573,6 +576,7 @@ extension on _TranslationsDe { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => _root.$meta.d([12, 50, 55, 55, 48, 52, 54, 54, 62, 53, 123]) + fullName.toString(); case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => firstName.toString(); case 'onboarding.bye': return ({required Object firstName}) => _root.$meta.d([15, 40, 56, 51, 167, 40, 40, 123]) + firstName.toString(); case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ TextSpan(text: _root.$meta.d([19, 50, 123])), diff --git a/slang/test/integration/resources/main/_expected_single.output b/slang/test/integration/resources/main/_expected_single.output index ef3bdfe8..4669c9e8 100644 --- a/slang/test/integration/resources/main/_expected_single.output +++ b/slang/test/integration/resources/main/_expected_single.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 56 (28 per locale) +/// Strings: 58 (29 per locale) // coverage:ignore-file // ignore_for_file: type=lint @@ -214,6 +214,7 @@ class _TranslationsOnboardingEn { // Translations String welcome({required Object fullName}) => 'Welcome ${fullName}'; String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + String welcomeOnlyParam({required Object firstName}) => '${firstName}'; /// Bye text String bye({required Object firstName}) => 'Bye ${firstName}'; @@ -381,6 +382,7 @@ class _TranslationsOnboardingDe implements _TranslationsOnboardingEn { // Translations @override String welcome({required Object fullName}) => 'Willkommen ${fullName}'; @override String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + @override String welcomeOnlyParam({required Object firstName}) => '${firstName}'; /// Bye text @override String bye({required Object firstName}) => 'Tschüss ${firstName}'; @@ -506,6 +508,7 @@ extension on Translations { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => 'Welcome ${fullName}'; case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; case 'onboarding.bye': return ({required Object firstName}) => 'Bye ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), @@ -570,6 +573,7 @@ extension on _TranslationsDe { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => 'Willkommen ${fullName}'; case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; case 'onboarding.bye': return ({required Object firstName}) => 'Tschüss ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), diff --git a/slang/test/integration/resources/main/_expected_translation_overrides.output b/slang/test/integration/resources/main/_expected_translation_overrides.output index 6b0f383a..06cd8b79 100644 --- a/slang/test/integration/resources/main/_expected_translation_overrides.output +++ b/slang/test/integration/resources/main/_expected_translation_overrides.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 56 (28 per locale) +/// Strings: 58 (29 per locale) // coverage:ignore-file // ignore_for_file: type=lint @@ -240,6 +240,7 @@ class _TranslationsOnboardingEn { // Translations String welcome({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcome', {'fullName': fullName}) ?? 'Welcome ${fullName}'; String welcomeAlias({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeAlias', {'fullName': fullName}) ?? _root.onboarding.welcome(fullName: fullName); + String welcomeOnlyParam({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeOnlyParam', {'firstName': firstName}) ?? '${firstName}'; /// Bye text String bye({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Bye ${firstName}'; @@ -415,6 +416,7 @@ class _TranslationsOnboardingDe implements _TranslationsOnboardingEn { // Translations @override String welcome({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcome', {'fullName': fullName}) ?? 'Willkommen ${fullName}'; @override String welcomeAlias({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeAlias', {'fullName': fullName}) ?? _root.onboarding.welcome(fullName: fullName); + @override String welcomeOnlyParam({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeOnlyParam', {'firstName': firstName}) ?? '${firstName}'; /// Bye text @override String bye({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Tschüss ${firstName}'; @@ -548,6 +550,7 @@ extension on Translations { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcome', {'fullName': fullName}) ?? 'Welcome ${fullName}'; case 'onboarding.welcomeAlias': return ({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeAlias', {'fullName': fullName}) ?? _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeOnlyParam', {'firstName': firstName}) ?? '${firstName}'; case 'onboarding.bye': return ({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Bye ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TranslationOverridesFlutter.rich(_root.$meta, 'onboarding.hi', {'name': name, 'lastName': lastName, 'context': context, 'fullName': fullName, 'firstName': firstName}) ?? TextSpan(children: [ const TextSpan(text: 'Hi '), @@ -620,6 +623,7 @@ extension on _TranslationsDe { switch (path) { case 'onboarding.welcome': return ({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcome', {'fullName': fullName}) ?? 'Willkommen ${fullName}'; case 'onboarding.welcomeAlias': return ({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeAlias', {'fullName': fullName}) ?? _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeOnlyParam', {'firstName': firstName}) ?? '${firstName}'; case 'onboarding.bye': return ({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Tschüss ${firstName}'; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TranslationOverridesFlutter.rich(_root.$meta, 'onboarding.hi', {'name': name, 'lastName': lastName, 'context': context, 'fullName': fullName, 'firstName': firstName}) ?? TextSpan(children: [ const TextSpan(text: 'Hi '), diff --git a/slang/test/integration/resources/main/csv_compact.csv b/slang/test/integration/resources/main/csv_compact.csv index 0f740449..476b9e09 100644 --- a/slang/test/integration/resources/main/csv_compact.csv +++ b/slang/test/integration/resources/main/csv_compact.csv @@ -1,6 +1,7 @@ key,(comments),en,de onboarding.welcome,,Welcome {fullName},Willkommen {fullName} onboarding.welcomeAlias,,@:onboarding.welcome,@:onboarding.welcome +onboarding.welcomeOnlyParam,,{firstName},{firstName} onboarding.bye,Bye text,Bye {firstName},Tschüss {firstName} onboarding.hi(rich),,Hi {name} and @:onboarding.greet,Hi {name} und @:onboarding.greet onboarding.pages.0.title,,First Page,Erste Seite diff --git a/slang/test/integration/resources/main/csv_de.csv b/slang/test/integration/resources/main/csv_de.csv index cb15b9d2..af59b9a1 100644 --- a/slang/test/integration/resources/main/csv_de.csv +++ b/slang/test/integration/resources/main/csv_de.csv @@ -1,5 +1,6 @@ onboarding.welcome(!!!this file is in LF which is part of the integration test!!!),Willkommen {fullName} onboarding.welcomeAlias,@:onboarding.welcome +onboarding.welcomeOnlyParam,{firstName} onboarding.bye,Tschüss {firstName} onboarding.@bye,Bye text onboarding.hi(rich),Hi {name} und @:onboarding.greet diff --git a/slang/test/integration/resources/main/csv_en.csv b/slang/test/integration/resources/main/csv_en.csv index 3e3a6329..c0a011ec 100644 --- a/slang/test/integration/resources/main/csv_en.csv +++ b/slang/test/integration/resources/main/csv_en.csv @@ -1,5 +1,6 @@ onboarding.welcome,Welcome {fullName} onboarding.welcomeAlias,@:onboarding.welcome +onboarding.welcomeOnlyParam,{firstName}" onboarding.bye,Bye {firstName} onboarding.@bye,Bye text onboarding.hi(rich),Hi {name} and @:onboarding.greet diff --git a/slang/test/integration/resources/main/json_de.json b/slang/test/integration/resources/main/json_de.json index 84c6b34f..631a0d74 100644 --- a/slang/test/integration/resources/main/json_de.json +++ b/slang/test/integration/resources/main/json_de.json @@ -2,6 +2,7 @@ "onboarding": { "welcome": "Willkommen {fullName}", "welcomeAlias": "@:onboarding.welcome", + "welcomeOnlyParam": "{firstName}", "bye": "Tschüss {firstName}", "@bye": "Bye text", "hi(rich)": "Hi {name} und @:onboarding.greet", diff --git a/slang/test/integration/resources/main/json_en.json b/slang/test/integration/resources/main/json_en.json index e0860db3..3209a0ee 100644 --- a/slang/test/integration/resources/main/json_en.json +++ b/slang/test/integration/resources/main/json_en.json @@ -2,6 +2,7 @@ "onboarding": { "welcome": "Welcome {fullName}", "welcomeAlias": "@:onboarding.welcome", + "welcomeOnlyParam": "{firstName}", "bye": "Bye {firstName}", "@bye": { "this should be ignored": "ignored", diff --git a/slang/test/integration/resources/main/yaml_de.yaml b/slang/test/integration/resources/main/yaml_de.yaml index acbf0134..321090ba 100644 --- a/slang/test/integration/resources/main/yaml_de.yaml +++ b/slang/test/integration/resources/main/yaml_de.yaml @@ -1,6 +1,7 @@ onboarding: welcome: Willkommen {fullName} welcomeAlias: "@:onboarding.welcome" + welcomeOnlyParam: "{firstName}" bye: Tschüss {firstName} "@bye": Bye text hi(rich): Hi {name} und @:onboarding.greet diff --git a/slang/test/integration/resources/main/yaml_en.yaml b/slang/test/integration/resources/main/yaml_en.yaml index 96c95af4..4032f778 100644 --- a/slang/test/integration/resources/main/yaml_en.yaml +++ b/slang/test/integration/resources/main/yaml_en.yaml @@ -1,6 +1,7 @@ onboarding: welcome: Welcome {fullName} welcomeAlias: "@:onboarding.welcome" + welcomeOnlyParam: "{firstName}" bye: Bye {firstName} "@bye": Bye text hi(rich): Hi {name} and @:onboarding.greet diff --git a/slang/test/unit/utils/secret_test.dart b/slang/test/unit/utils/secret_test.dart index dadb0343..065bd586 100644 --- a/slang/test/unit/utils/secret_test.dart +++ b/slang/test/unit/utils/secret_test.dart @@ -6,7 +6,7 @@ void main() { group('getStringLiteral', () { test('Should return text as is', () { final config = ObfuscationConfig.disabled(); - expect(getStringLiteral('Hello World', config), "'Hello World'"); + expect(getStringLiteral('Hello World', 0, config), "'Hello World'"); }); test('Should obfuscate word with zero XOR', () { @@ -14,7 +14,8 @@ void main() { enabled: true, secret: 0, ); - expect(getStringLiteral('abc', config), '_root.\$meta.d([97, 98, 99])'); + expect( + getStringLiteral('abc', 0, config), '_root.\$meta.d([97, 98, 99])'); }); test('Should obfuscate word with positive XOR', () { @@ -22,7 +23,8 @@ void main() { enabled: true, secret: 1, ); - expect(getStringLiteral('abc', config), '_root.\$meta.d([96, 99, 98])'); + expect( + getStringLiteral('abc', 0, config), '_root.\$meta.d([96, 99, 98])'); expect(_d([96, 99, 98], 1), 'abc'); }); @@ -31,7 +33,8 @@ void main() { enabled: true, secret: 1, ); - expect(getStringLiteral('a\\nb', config), '_root.\$meta.d([96, 11, 99])'); + expect( + getStringLiteral('a\\nb', 0, config), '_root.\$meta.d([96, 11, 99])'); expect(_d([96, 11, 99], 1), 'a\nb'); }); @@ -40,7 +43,8 @@ void main() { enabled: true, secret: 1, ); - expect(getStringLiteral("a\\'b", config), '_root.\$meta.d([96, 38, 99])'); + expect( + getStringLiteral("a\\'b", 0, config), '_root.\$meta.d([96, 38, 99])'); expect(_d([96, 38, 99], 1), "a'b"); }); }); From 266523faeac1d3ee4d6b72d759931456593430e6 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 25 Mar 2024 15:33:26 +0100 Subject: [PATCH 039/118] release: 3.30.2 --- slang/CHANGELOG.md | 6 ++++++ slang/pubspec.yaml | 2 +- slang/test/integration/resources/main/csv_en.csv | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 5e98ee81..7aad81d8 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.30.2 + +- fix: commented out translations should be declared as unused during `slang analyze` (#200) @nikaera +- fix: should use interpolation for strings with a single parameter (#207) @Tienisto +- fix: encode csv files correctly (#202) @nikaera + ## 3.30.1 - fix: when applying translations with `dart run slang apply`, only modifiers from the base locale should be used (#192) diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index 29f0e0c6..804c156c 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -1,6 +1,6 @@ name: slang description: Localization / Internationalization (i18n) solution. Use JSON, YAML, CSV, or ARB files to create typesafe translations via source generation. -version: 3.30.1 +version: 3.30.2 repository: https://github.com/slang-i18n/slang topics: - i18n diff --git a/slang/test/integration/resources/main/csv_en.csv b/slang/test/integration/resources/main/csv_en.csv index c0a011ec..4aa35309 100644 --- a/slang/test/integration/resources/main/csv_en.csv +++ b/slang/test/integration/resources/main/csv_en.csv @@ -1,6 +1,6 @@ onboarding.welcome,Welcome {fullName} onboarding.welcomeAlias,@:onboarding.welcome -onboarding.welcomeOnlyParam,{firstName}" +onboarding.welcomeOnlyParam,{firstName} onboarding.bye,Bye {firstName} onboarding.@bye,Bye text onboarding.hi(rich),Hi {name} and @:onboarding.greet From 0b80b3059c8d481676f4733fae4c62f1318f3cee Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 1 Apr 2024 19:01:04 +0200 Subject: [PATCH 040/118] docs: add blog --- slang/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/slang/README.md b/slang/README.md index 246e771b..d6060c5c 100644 --- a/slang/README.md +++ b/slang/README.md @@ -1873,6 +1873,7 @@ The second one always returns a new instance. **Blogs** - [Medium (English)](https://medium.com/swlh/flutter-i18n-made-easy-1fd9ccd82cb3) +- [Medium (English)](https://maruf-hassan.medium.com/handling-flutter-internationalization-like-a-pro-699ac2f6d856) - [Хабр (Russian)](https://habr.com/ru/post/718310/) - [Qiita (Japanese)](https://qiita.com/popy1017/items/3495be9fdc028161bef9) - [okaryo (Japanese)](https://blog.okaryo.io/20230104-split-and-manage-arb-files-for-internationalized-flutter-app-in-yaml-format) From 6345d3eef7ce512ea466e79a6fb0d89690bb9c48 Mon Sep 17 00:00:00 2001 From: poppingmoon <63451158+poppingmoon@users.noreply.github.com> Date: Wed, 3 Apr 2024 21:26:40 +0000 Subject: [PATCH 041/118] feat: sort input files before generating translations (#211) --- .../builder/builder/slang_file_collection_builder.dart | 2 +- .../unit/builder/slang_file_collection_builder_test.dart | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/slang/lib/builder/builder/slang_file_collection_builder.dart b/slang/lib/builder/builder/slang_file_collection_builder.dart index 828e3d02..581b10b5 100644 --- a/slang/lib/builder/builder/slang_file_collection_builder.dart +++ b/slang/lib/builder/builder/slang_file_collection_builder.dart @@ -139,7 +139,7 @@ class SlangFileCollectionBuilder { return null; }) .whereNotNull() - .toList(), + .sortedBy((file) => '${file.locale}-${file.namespace}'), ); } } diff --git a/slang/test/unit/builder/slang_file_collection_builder_test.dart b/slang/test/unit/builder/slang_file_collection_builder_test.dart index 000c5453..29b8f2cc 100644 --- a/slang/test/unit/builder/slang_file_collection_builder_test.dart +++ b/slang/test/unit/builder/slang_file_collection_builder_test.dart @@ -35,8 +35,8 @@ void main() { ); expect(model.files.length, 3); - expect(model.files[0].locale.language, 'en'); - expect(model.files[1].locale.language, 'de'); + expect(model.files[0].locale.language, 'de'); + expect(model.files[1].locale.language, 'en'); expect(model.files[2].locale.language, 'fr'); expect(model.files[2].locale.country, 'FR'); }); @@ -97,9 +97,9 @@ void main() { expect(model.files.length, 2); expect(model.files[0].locale.language, 'fr'); - expect(model.files[0].namespace, 'dialogs'); + expect(model.files[0].namespace, 'ab_cd'); expect(model.files[1].locale.language, 'fr'); - expect(model.files[1].namespace, 'ab_cd'); + expect(model.files[1].namespace, 'dialogs'); }); }); } From b2a4e17d8c8f15c8e4c33e92d79cdb5a4228a93d Mon Sep 17 00:00:00 2001 From: sladomic Date: Thu, 4 Apr 2024 12:29:32 +0200 Subject: [PATCH 042/118] Resolve #208 remove Enum case conversion (#213) --- slang/README.md | 4 ++-- slang/lib/builder/builder/raw_config_builder.dart | 2 +- slang/test/unit/builder/build_config_builder_test.dart | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/slang/README.md b/slang/README.md index d6060c5c..f33019f2 100644 --- a/slang/README.md +++ b/slang/README.md @@ -313,7 +313,7 @@ pluralization: ordinal: - someKey.place contexts: - gender_context: + GenderContext: enum: - male - female @@ -384,7 +384,7 @@ targets: ordinal: - someKey.place contexts: - gender_context: + GenderContext: enum: - male - female diff --git a/slang/lib/builder/builder/raw_config_builder.dart b/slang/lib/builder/builder/raw_config_builder.dart index 3a30243c..4cfefce2 100644 --- a/slang/lib/builder/builder/raw_config_builder.dart +++ b/slang/lib/builder/builder/raw_config_builder.dart @@ -118,7 +118,7 @@ extension on Map { /// Parses the 'contexts' config List toContextTypes() { return entries.map((e) { - final enumName = e.key.toCase(CaseStyle.pascal); + final enumName = e.key; final config = e.value as Map; if (config['auto'] != null) { diff --git a/slang/test/unit/builder/build_config_builder_test.dart b/slang/test/unit/builder/build_config_builder_test.dart index b56cda33..f18da17c 100644 --- a/slang/test/unit/builder/build_config_builder_test.dart +++ b/slang/test/unit/builder/build_config_builder_test.dart @@ -35,7 +35,7 @@ void main() { final result = RawConfigBuilder.fromMap( { 'contexts': { - 'gender_context': { + 'GenderContext': { 'enum': [ 'male', 'female', @@ -57,7 +57,7 @@ void main() { final result = RawConfigBuilder.fromMap( { 'contexts': { - 'gender_context': { + 'GenderContext': { 'enum': [ 'male', 'female', From 87fb30f226e8cfd8c2f2635f7deeb91d1071f807 Mon Sep 17 00:00:00 2001 From: Hakim Bawa <43802542+bawahakim@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:27:07 -0400 Subject: [PATCH 043/118] feat: add support for gp4 turbo (#216) --- slang_gpt/lib/model/gpt_model.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/slang_gpt/lib/model/gpt_model.dart b/slang_gpt/lib/model/gpt_model.dart index 6c958b91..170a5f60 100644 --- a/slang_gpt/lib/model/gpt_model.dart +++ b/slang_gpt/lib/model/gpt_model.dart @@ -15,6 +15,10 @@ enum GptModel { defaultInputLength: 4000, costPerInputToken: 0.00003, costPerOutputToken: 0.00006), + gpt4_turbo('gpt-4-turbo', GptProvider.openai, + defaultInputLength: 64000, + costPerInputToken: 0.00001, + costPerOutputToken: 0.00002), ; const GptModel( From e5c74f033e281fbddc7369a5ea5aa9906c208994 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 15 Apr 2024 21:51:56 +0200 Subject: [PATCH 044/118] chore: add ignore --- slang_gpt/lib/model/gpt_model.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/slang_gpt/lib/model/gpt_model.dart b/slang_gpt/lib/model/gpt_model.dart index 170a5f60..c4f0e649 100644 --- a/slang_gpt/lib/model/gpt_model.dart +++ b/slang_gpt/lib/model/gpt_model.dart @@ -2,6 +2,7 @@ enum GptProvider { openai, } +// ignore_for_file: constant_identifier_names enum GptModel { gpt3_5_4k('gpt-3.5-turbo', GptProvider.openai, defaultInputLength: 2000, From 97f936a0452f6251488209b5e3a4847e07997652 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Thu, 23 May 2024 00:15:27 +0200 Subject: [PATCH 045/118] feat: add normalize tool --- slang/bin/normalize.dart | 5 ++ slang/bin/slang.dart | 15 ++++ slang/lib/src/runner/normalize.dart | 114 ++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 slang/bin/normalize.dart create mode 100644 slang/lib/src/runner/normalize.dart diff --git a/slang/bin/normalize.dart b/slang/bin/normalize.dart new file mode 100644 index 00000000..0b41c5ae --- /dev/null +++ b/slang/bin/normalize.dart @@ -0,0 +1,5 @@ +import 'slang.dart' as main_runner; + +void main(List arguments) async { + main_runner.main(['normalize', ...arguments]); +} diff --git a/slang/bin/slang.dart b/slang/bin/slang.dart index dc20c926..66dfb9a9 100644 --- a/slang/bin/slang.dart +++ b/slang/bin/slang.dart @@ -14,6 +14,7 @@ import 'package:slang/src/runner/apply.dart'; import 'package:slang/src/runner/clean.dart'; import 'package:slang/src/runner/edit.dart'; import 'package:slang/src/runner/migrate.dart'; +import 'package:slang/src/runner/normalize.dart'; import 'package:slang/src/runner/stats.dart'; import 'package:watcher/watcher.dart'; @@ -29,6 +30,7 @@ enum RunnerMode { outdated, // add 'OUTDATED' modifier to secondary locales add, // add a translation clean, // clean unused translations + normalize, // normalize translations according to base locale } /// To run this: @@ -68,9 +70,13 @@ void main(List arguments) async { case 'clean': mode = RunnerMode.clean; break; + case 'normalize': + mode = RunnerMode.normalize; + break; default: mode = RunnerMode.generate; } + verbose = mode == RunnerMode.generate || mode == RunnerMode.watch || (arguments.length == 2 && @@ -104,6 +110,9 @@ void main(List arguments) async { case RunnerMode.clean: print('Removing unused translations...\n'); break; + case RunnerMode.normalize: + print('Normalizing translations...\n'); + break; } final stopwatch = Stopwatch(); @@ -165,6 +174,12 @@ void main(List arguments) async { arguments: arguments, ); break; + case RunnerMode.normalize: + await runNormalize( + fileCollection: fileCollection, + arguments: arguments, + ); + break; } } diff --git a/slang/lib/src/runner/normalize.dart b/slang/lib/src/runner/normalize.dart new file mode 100644 index 00000000..a081962a --- /dev/null +++ b/slang/lib/src/runner/normalize.dart @@ -0,0 +1,114 @@ +import 'package:collection/collection.dart'; +import 'package:slang/builder/builder/translation_map_builder.dart'; +import 'package:slang/builder/model/enums.dart'; +import 'package:slang/builder/model/i18n_locale.dart'; +import 'package:slang/builder/model/slang_file_collection.dart'; +import 'package:slang/src/builder/utils/file_utils.dart'; +import 'package:slang/src/builder/utils/path_utils.dart'; +import 'package:slang/src/runner/apply.dart'; + +const _supportedFiles = [FileType.json, FileType.yaml]; + +/// Normalizes the translation files according to the base locale. +Future runNormalize({ + required SlangFileCollection fileCollection, + required List arguments, +}) async { + I18nLocale? targetLocale; // only this locale will be considered + for (final a in arguments) { + if (a.startsWith('--locale=')) { + targetLocale = I18nLocale.fromString(a.substring(9)); + } + } + + final translationMap = await TranslationMapBuilder.build( + fileCollection: fileCollection, + verbose: false, + ); + + final baseTranslationMap = translationMap[fileCollection.config.baseLocale]!; + + if (targetLocale != null) { + print('Target: <${targetLocale.languageTag}>'); + + await _normalizeLocale( + fileCollection: fileCollection, + locale: targetLocale, + baseTranslations: baseTranslationMap, + ); + } else { + print('Target: all locales'); + + for (final locale in translationMap.getLocales()) { + if (locale == fileCollection.config.baseLocale) { + continue; + } + + await _normalizeLocale( + fileCollection: fileCollection, + locale: locale, + baseTranslations: baseTranslationMap, + ); + } + } + + print('Normalization finished!'); +} + +/// Normalizes all files for the given [locale]. +Future _normalizeLocale({ + required SlangFileCollection fileCollection, + required I18nLocale locale, + required Map> baseTranslations, +}) async { + final fileMap = {}; // namespace -> file + + for (final file in fileCollection.files) { + if (file.locale == locale) { + fileMap[file.namespace] = file; + } + } + + if (fileMap.isEmpty) { + throw 'Could not find a file for locale <${locale.languageTag}>'; + } + + for (final entry in fileMap.entries) { + await _normalizeFile( + baseTranslations: baseTranslations[entry.key] ?? {}, + destinationFile: entry.value, + ); + } +} + +/// Reads the [destinationFile] +/// and normalizes the order according to [baseTranslations]. +/// +/// In namespace mode, this function represents ONE namespace. +/// [baseTranslations] should also only contain the selected namespace. +Future _normalizeFile({ + required Map baseTranslations, + required TranslationFile destinationFile, +}) async { + final existingFile = destinationFile; + final fileType = _supportedFiles.firstWhereOrNull( + (type) => type.name == PathUtils.getFileExtension(existingFile.path)); + if (fileType == null) { + throw FileTypeNotSupportedError(existingFile.path); + } + + final parsedContent = await existingFile.readAndParse(fileType); + + final appliedTranslations = applyMapRecursive( + baseMap: baseTranslations, + newMap: const {}, + oldMap: parsedContent, + verbose: true, + ); + + FileUtils.writeFileOfType( + fileType: fileType, + path: existingFile.path, + content: appliedTranslations, + ); +} From 85eb7e231d6e6a6ab5c444bf0b2b9ee301335700 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Thu, 23 May 2024 00:35:32 +0200 Subject: [PATCH 046/118] feat: support parameter types in ARB format --- .../lib/src/builder/decoder/arb_decoder.dart | 104 +++++++++++++----- slang/test/unit/decoder/arb_decoder_test.dart | 20 +++- 2 files changed, 98 insertions(+), 26 deletions(-) diff --git a/slang/lib/src/builder/decoder/arb_decoder.dart b/slang/lib/src/builder/decoder/arb_decoder.dart index 41f867d2..f2d48e18 100644 --- a/slang/lib/src/builder/decoder/arb_decoder.dart +++ b/slang/lib/src/builder/decoder/arb_decoder.dart @@ -12,31 +12,45 @@ class ArbDecoder extends BaseDecoder { @override Map decode(String raw) { final sourceMap = json.decode(raw) as Map; - final resultMap = {}; - sourceMap.forEach((key, value) { - if (key.startsWith('@@')) { - // add without modifications - resultMap[key] = value.toString(); - return; + final entryMetadata = {}; // key -> metadata + + // Parse metadata first + for (final key in sourceMap.keys) { + final value = sourceMap[key]; + if (key.length > 1 && key.startsWith('@')) { + entryMetadata[key.substring(1)] = _EntryMetadata.parseEntry( + value as Map, + ); } + } + final resultMap = {}; + + for (final key in sourceMap.keys) { if (key.startsWith('@')) { - // take description - final description = value['description'] as String?; - if (description == null) { - return; - } - - resultMap[key] = description; - } else { - _addEntry( - key: key, - value: value, - resultMap: resultMap, - ); + continue; } - }); + + final metadata = entryMetadata[key] ?? + const _EntryMetadata( + description: null, + paramTypeMap: {}, + ); + + final value = sourceMap[key]; + + _addEntry( + key: key, + metadata: metadata, + value: value, + resultMap: resultMap, + ); + + if (metadata.description != null) { + resultMap['@$key'] = metadata.description; + } + } return resultMap; } @@ -44,6 +58,7 @@ class ArbDecoder extends BaseDecoder { void _addEntry({ required final String key, + required final _EntryMetadata metadata, required final String value, required final Map resultMap, }) { @@ -69,7 +84,7 @@ void _addEntry({ map: resultMap, destinationPath: '$key(${isPlural ? 'plural' : 'context=${variable.toCase(CaseStyle.pascal)}'}, param=$variable).$partName', - item: _digestLeafText(partContent), + item: _digestLeafText(partContent, metadata.paramTypeMap), ); } return; @@ -98,6 +113,7 @@ void _addEntry({ // create new key _addEntry( key: '${key}__$parameter', + metadata: metadata, value: match.group(0)!, resultMap: resultMap, ); @@ -109,19 +125,21 @@ void _addEntry({ brackets = BracketsUtils.findTopLevelBrackets(result); } - resultMap[key] = _digestLeafText(result); + resultMap[key] = _digestLeafText(result, metadata.paramTypeMap); } /// Transforms arguments to camel case /// Adds 'arg' to every positional argument -String _digestLeafText(String text) { +String _digestLeafText(String text, Map paramTypeMap) { return text.replaceBracesInterpolation(replacer: (match) { final param = match.substring(1, match.length - 1); + final paramType = + paramTypeMap[param] != null ? ': ${paramTypeMap[param]}' : ''; final number = int.tryParse(param); if (number != null) { - return '{arg$number}'; + return '{arg$number$paramType}'; } else { - return '{${param.toCase(CaseStyle.camel)}}'; + return '{${param.toCase(CaseStyle.camel)}$paramType}'; } }); } @@ -141,6 +159,42 @@ String _digestPluralKey(String key) { } } +class _EntryMetadata { + final String? description; + final Map paramTypeMap; + + const _EntryMetadata({ + required this.description, + required this.paramTypeMap, + }); + + static _EntryMetadata parseEntry(Map map) { + final description = map['description'] as String?; + + final placeholders = map['placeholders'] as Map?; + if (placeholders == null) { + return _EntryMetadata( + description: description, + paramTypeMap: {}, + ); + } + + final paramTypeMap = {}; + for (final key in placeholders.keys) { + final value = placeholders[key] as Map; + final type = value['type'] as String?; + if (type != null) { + paramTypeMap[key] = type; + } + } + + return _EntryMetadata( + description: description, + paramTypeMap: paramTypeMap, + ); + } +} + class _DistinctNameFactory { final existingNames = {}; diff --git a/slang/test/unit/decoder/arb_decoder_test.dart b/slang/test/unit/decoder/arb_decoder_test.dart index 5202a060..10ddc39b 100644 --- a/slang/test/unit/decoder/arb_decoder_test.dart +++ b/slang/test/unit/decoder/arb_decoder_test.dart @@ -26,7 +26,7 @@ void main() { ); }); - test('Should decode with meta', () { + test('Should decode with description', () { expect( _decodeArb({ 'hello': 'world', @@ -39,6 +39,24 @@ void main() { ); }); + test('Should decode with parameter type', () { + expect( + _decodeArb({ + 'age': 'You are {age} years old', + '@age': { + 'placeholders': { + 'age': { + 'type': 'int', + }, + }, + } + }), + { + 'age': 'You are {age: int} years old', + }, + ); + }); + test('Should decode plural string identifiers', () { expect( _decodeArb({ From e597556477037fdab3e601f1854a336b96a6e145 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Thu, 23 May 2024 00:41:04 +0200 Subject: [PATCH 047/118] ci: bump flutter to 3.19.5 --- .fvmrc | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/publish_template.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.fvmrc b/.fvmrc index b231bd0e..db15b841 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,4 +1,4 @@ { - "flutter": "3.19.4", + "flutter": "3.19.5", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91fd503b..441e0ff0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ on: env: FLUTTER_VERSION_OLDEST: "3.0.5" - FLUTTER_VERSION_NEWEST: "3.19.4" + FLUTTER_VERSION_NEWEST: "3.19.5" jobs: format: diff --git a/.github/workflows/publish_template.yml b/.github/workflows/publish_template.yml index 13c9ae95..f5f298f1 100644 --- a/.github/workflows/publish_template.yml +++ b/.github/workflows/publish_template.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 with: - flutter-version: '3.19.4' + flutter-version: '3.19.5' channel: 'stable' - name: Dependencies (core) run: flutter pub get From 7d562c8b82a96de84c02f3f5d1a698194772b468 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Thu, 23 May 2024 00:48:18 +0200 Subject: [PATCH 048/118] docs: update changelog and readme --- slang/CHANGELOG.md | 6 ++++++ slang/README.md | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 7aad81d8..a96abc54 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.31.0 + +- feat: add `dart run slang normalize` to normalize translations based on base locale @Tienisto +- feat: add parameter type support (introduced 3.30.0) to ARB files @Tienisto +- feat: sort translation files for reproducible console output (#210) @poppingmoon + ## 3.30.2 - fix: commented out translations should be declared as unused during `slang analyze` (#200) @nikaera diff --git a/slang/README.md b/slang/README.md index f33019f2..a678cff2 100644 --- a/slang/README.md +++ b/slang/README.md @@ -510,8 +510,11 @@ String interpolation is fixed to `braces` mode. }, "inboxPageCount": "You have {count, plural, one {1 message} other {{count} messages}}", "@inboxPageCount": { + "description": "The number of messages in the user's inbox", "placeholders": { - "count": {} + "count": { + "type": "int" + } } } } From ad37622d711104c45f423e7d983a7e84a6998ce2 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Thu, 23 May 2024 01:26:29 +0200 Subject: [PATCH 049/118] fix: loosen arb ICU regex --- slang/lib/src/builder/decoder/arb_decoder.dart | 6 ++++-- slang/lib/src/builder/utils/regex_utils.dart | 2 +- slang/test/unit/decoder/arb_decoder_test.dart | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/slang/lib/src/builder/decoder/arb_decoder.dart b/slang/lib/src/builder/decoder/arb_decoder.dart index f2d48e18..d691e35d 100644 --- a/slang/lib/src/builder/decoder/arb_decoder.dart +++ b/slang/lib/src/builder/decoder/arb_decoder.dart @@ -18,9 +18,11 @@ class ArbDecoder extends BaseDecoder { // Parse metadata first for (final key in sourceMap.keys) { final value = sourceMap[key]; - if (key.length > 1 && key.startsWith('@')) { + if (key.length > 1 && + key.startsWith('@') && + value is Map) { entryMetadata[key.substring(1)] = _EntryMetadata.parseEntry( - value as Map, + value, ); } } diff --git a/slang/lib/src/builder/utils/regex_utils.dart b/slang/lib/src/builder/utils/regex_utils.dart index e504196d..10b52f39 100644 --- a/slang/lib/src/builder/utils/regex_utils.dart +++ b/slang/lib/src/builder/utils/regex_utils.dart @@ -75,7 +75,7 @@ class RegexUtils { /// 1 - male /// 2 - His birthday static RegExp arbComplexNodeContent = - RegExp(r'((?:=|\w)+){((?:[^}{]+|{[^}]+})+)}'); + RegExp(r'((?:=|\w)+) *{((?:[^}{]+|{[^}]+})+)}'); /// Matches any missing translations file /// _missing_translations.json, _missing_translations_de-DE.json diff --git a/slang/test/unit/decoder/arb_decoder_test.dart b/slang/test/unit/decoder/arb_decoder_test.dart index 10ddc39b..15aea872 100644 --- a/slang/test/unit/decoder/arb_decoder_test.dart +++ b/slang/test/unit/decoder/arb_decoder_test.dart @@ -89,6 +89,20 @@ void main() { ); }); + test('Should decode plural with spaces between identifiers', () { + expect( + _decodeArb({ + 'inboxCount': 'You have {count, plural, one {1 new message} }', + }), + { + 'inboxCount__count(plural, param=count)': { + 'one': '1 new message', + }, + 'inboxCount': 'You have @:inboxCount__count', + }, + ); + }); + test('Should decode custom context', () { expect( _decodeArb({ From d70cc7d030dede7f2b477bc8ec97e01ece95a7a0 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Thu, 23 May 2024 02:43:20 +0200 Subject: [PATCH 050/118] fix: specify plural parameter type --- .../builder/translation_model_builder.dart | 47 +++++++++++-------- slang/lib/builder/model/node.dart | 12 +++-- .../lib/src/builder/builder/text_parser.dart | 4 +- .../generator/generate_translation_map.dart | 2 +- .../generator/generate_translations.dart | 19 ++++---- .../translation_model_builder_test.dart | 24 ++++++++-- 6 files changed, 70 insertions(+), 38 deletions(-) diff --git a/slang/lib/builder/builder/translation_model_builder.dart b/slang/lib/builder/builder/translation_model_builder.dart index 5a3ed99e..f5638c8a 100644 --- a/slang/lib/builder/builder/translation_model_builder.dart +++ b/slang/lib/builder/builder/translation_model_builder.dart @@ -130,39 +130,33 @@ class TranslationModelBuilder { final Iterable textNodes = linkedNode is PluralNode ? linkedNode.quantities.values : (linkedNode as ContextNode).entries.values; - final linkedParamSet = textNodes - .map((e) => e.params) - .expand((params) => params) - .toSet(); + + for (final textNode in textNodes) { + paramSet.addAll(textNode.params); + paramTypeMap.addAll(textNode.paramTypeMap); + } if (linkedNode is PluralNode) { if (linkedNode.rich) { final builderParam = '${linkedNode.paramName}Builder'; - linkedParamSet.add(builderParam); - paramTypeMap[builderParam] = 'InlineSpan Function(num)'; - for (final n in textNodes) { - paramTypeMap.addAll(n.paramTypeMap); - } + paramSet.add(builderParam); + paramTypeMap[builderParam] = + 'InlineSpan Function(${linkedNode.paramType})'; } - linkedParamSet.add(linkedNode.paramName); - paramTypeMap[linkedNode.paramName] = 'num'; + paramSet.add(linkedNode.paramName); + paramTypeMap[linkedNode.paramName] = linkedNode.paramType; } else if (linkedNode is ContextNode) { if (linkedNode.rich) { final builderParam = '${linkedNode.paramName}Builder'; - linkedParamSet.add(builderParam); + paramSet.add(builderParam); paramTypeMap[builderParam] = 'InlineSpan Function(${linkedNode.context.enumName})'; - for (final n in textNodes) { - paramTypeMap.addAll(n.paramTypeMap); - } } - linkedParamSet.add(linkedNode.paramName); + paramSet.add(linkedNode.paramName); paramTypeMap[linkedNode.paramName] = linkedNode.context.enumName; } - paramSet.addAll(linkedParamSet); - // lookup links of children for (final element in textNodes) { for (final child in element.links) { @@ -440,6 +434,19 @@ Map _parseMapNode({ rich: rich, ); } else { + final paramName = + modifiers[NodeModifiers.param] ?? config.pluralParameter; + String paramType = 'num'; + for (final textNode in digestedMap.values) { + final tempType = textNode.paramTypeMap[paramName]; + if (tempType != null && + ((textNode is StringTextNode && tempType != 'Object') || + (textNode is RichTextNode && tempType != 'InlineSpan'))) { + paramType = tempType; + break; + } + } + finalNode = PluralNode( path: currPath, rawPath: currRawPath, @@ -453,8 +460,8 @@ Map _parseMapNode({ // because detection was correct return MapEntry(key.toQuantity()!, value); }), - paramName: - modifiers[NodeModifiers.param] ?? config.pluralParameter, + paramName: paramName, + paramType: paramType, rich: rich, ); } diff --git a/slang/lib/builder/model/node.dart b/slang/lib/builder/model/node.dart index 710ceb30..f465c82e 100644 --- a/slang/lib/builder/model/node.dart +++ b/slang/lib/builder/model/node.dart @@ -130,6 +130,7 @@ class PluralNode extends Node implements LeafNode { final PluralType pluralType; final Map quantities; final String paramName; // name of the plural parameter + final String paramType; // type of the plural parameter defaults to num final bool rich; PluralNode({ @@ -140,6 +141,7 @@ class PluralNode extends Node implements LeafNode { required this.pluralType, required this.quantities, required this.paramName, + required this.paramType, required this.rich, }); @@ -151,15 +153,17 @@ class PluralNode extends Node implements LeafNode { paramTypeMap.addAll(textNode.paramTypeMap); } paramSet.add(paramName); - paramTypeMap[paramName] = 'num'; + paramTypeMap[paramName] = paramType; if (rich) { final builderParam = '${paramName}Builder'; paramSet.add(builderParam); - paramTypeMap[builderParam] = 'InlineSpan Function(num)'; + paramTypeMap[builderParam] = 'InlineSpan Function($paramType)'; } return paramSet.map((param) { return AttributeParameter( - parameterName: param, type: paramTypeMap[param] ?? 'Object'); + parameterName: param, + type: paramTypeMap[param] ?? 'Object', + ); }).toSet(); } @@ -375,7 +379,7 @@ class RichTextNode extends TextNode { final rawParsedResult = _parseInterpolation( raw: shouldEscape ? _escapeContent(raw, interpolation) : raw, interpolation: interpolation, - defaultType: '', // types are ignored + defaultType: 'ignored', // types are ignored paramCase: null, // param case will be applied later ); diff --git a/slang/lib/src/builder/builder/text_parser.dart b/slang/lib/src/builder/builder/text_parser.dart index 933c86f1..9b4c9c6e 100644 --- a/slang/lib/src/builder/builder/text_parser.dart +++ b/slang/lib/src/builder/builder/text_parser.dart @@ -5,7 +5,7 @@ class ParseParamResult { final String paramName; final String paramType; - ParseParamResult(this.paramName, this.paramType); + const ParseParamResult(this.paramName, this.paramType); @override String toString() => @@ -36,7 +36,7 @@ class ParamWithArg { final String paramName; final String? arg; - ParamWithArg(this.paramName, this.arg); + const ParamWithArg(this.paramName, this.arg); @override String toString() => 'ParamWithArg(paramName: $paramName, arg: $arg)'; diff --git a/slang/lib/src/builder/generator/generate_translation_map.dart b/slang/lib/src/builder/generator/generate_translation_map.dart index 1c28bc2d..6a59c8f9 100644 --- a/slang/lib/src/builder/generator/generate_translation_map.dart +++ b/slang/lib/src/builder/generator/generate_translation_map.dart @@ -101,7 +101,7 @@ _generateTranslationMapRecursive({ } } else if (curr is PluralNode) { buffer.write('\t\t\tcase \'${curr.path}\': return '); - _addPluralizationCall( + _addPluralCall( buffer: buffer, config: config, language: language, diff --git a/slang/lib/src/builder/generator/generate_translations.dart b/slang/lib/src/builder/generator/generate_translations.dart index 5af3e44c..60ae8efb 100644 --- a/slang/lib/src/builder/generator/generate_translations.dart +++ b/slang/lib/src/builder/generator/generate_translations.dart @@ -363,7 +363,7 @@ void _generateClass( } else if (value is PluralNode) { final returnType = value.rich ? 'TextSpan' : 'String'; buffer.write('$returnType$optional $key'); - _addPluralizationCall( + _addPluralCall( buffer: buffer, config: config, language: localeData.locale.language, @@ -457,7 +457,7 @@ void _generateMap({ } } else if (value is PluralNode) { buffer.write('\'$key\': '); - _addPluralizationCall( + _addPluralCall( buffer: buffer, config: config, language: locale.language, @@ -567,7 +567,7 @@ void _generateList({ buffer.writeln('$childClassWithLocale._(_root),'); } } else if (value is PluralNode) { - _addPluralizationCall( + _addPluralCall( buffer: buffer, config: config, language: locale.language, @@ -632,7 +632,7 @@ String _toParameterMap(Set params) { return buffer.toString(); } -void _addPluralizationCall({ +void _addPluralCall({ required StringBuffer buffer, required GenerateConfig config, required String language, @@ -653,14 +653,17 @@ void _addPluralizationCall({ paramSet.addAll(textNode.params); paramTypeMap.addAll(textNode.paramTypeMap); } - final params = paramSet.where((p) => p != node.paramName).toList(); + + final builderParam = '${node.paramName}Builder'; + final params = + paramSet.where((p) => p != node.paramName && p != builderParam).toList(); // add plural parameter first - buffer.write('({required num ${node.paramName}'); + buffer.write('({required ${node.paramType} ${node.paramName}'); if (node.rich && paramSet.contains(node.paramName)) { // add builder parameter if it is used - buffer - .write(', required InlineSpan Function(num) ${node.paramName}Builder'); + buffer.write( + ', required InlineSpan Function(${node.paramType}) ${node.paramName}Builder'); } for (int i = 0; i < params.length; i++) { buffer.write(', required ${paramTypeMap[params[i]] ?? 'Object'} '); diff --git a/slang/test/unit/builder/translation_model_builder_test.dart b/slang/test/unit/builder/translation_model_builder_test.dart index b0508881..916556ee 100644 --- a/slang/test/unit/builder/translation_model_builder_test.dart +++ b/slang/test/unit/builder/translation_model_builder_test.dart @@ -103,17 +103,35 @@ void main() { map: { 'a': { 'one': 'ONE', - 'other': r'OTHER $p1', + 'other': r'OTHER ${p1: String}', }, 'b': r'Hello @:a', }, ); final textNode = result.root.entries['b'] as StringTextNode; expect(textNode.params, {'p1', 'n'}); - expect(textNode.paramTypeMap, {'n': 'num'}); + expect(textNode.paramTypeMap, {'n': 'num', 'p1': 'String'}); expect(textNode.content, r'Hello ${_root.a(p1: p1, n: n)}'); }); + test('linked translation with plural and custom number type', () { + final result = TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), + localeDebug: RawConfig.defaultBaseLocale, + map: { + 'a': { + 'one': 'ONE', + 'other': r'OTHER ${n: int} ${p1: String}', + }, + 'b': r'Hello @:a', + }, + ); + final textNode = result.root.entries['b'] as StringTextNode; + expect(textNode.params, {'p1', 'n'}); + expect(textNode.paramTypeMap, {'n': 'int', 'p1': 'String'}); + expect(textNode.content, r'Hello ${_root.a(n: n, p1: p1)}'); + }); + test('linked translation with context', () { final result = TranslationModelBuilder.build( buildConfig: RawConfig.defaultConfig.copyWith(contexts: [ @@ -136,7 +154,7 @@ void main() { ); final textNode = result.root.entries['b'] as StringTextNode; expect(textNode.params, {'p1', 'gender'}); - expect(textNode.paramTypeMap, {'gender': 'GenderCon'}); + expect(textNode.paramTypeMap, {'p1': 'Object', 'gender': 'GenderCon'}); expect(textNode.content, r'Hello ${_root.a(p1: p1, gender: gender)}'); }); From 9a1fb11ae64c048c9e2a4dcbe0429349f0d8504e Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Thu, 23 May 2024 02:48:20 +0200 Subject: [PATCH 051/118] docs: update changelog and readme --- slang/CHANGELOG.md | 2 +- slang/README.md | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index a96abc54..334be7c3 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,7 +1,7 @@ ## 3.31.0 - feat: add `dart run slang normalize` to normalize translations based on base locale @Tienisto -- feat: add parameter type support (introduced 3.30.0) to ARB files @Tienisto +- feat: add parameter type support (introduced 3.30.0) to ARB files (#214) @Tienisto - feat: sort translation files for reproducible console output (#210) @poppingmoon ## 3.30.2 diff --git a/slang/README.md b/slang/README.md index a678cff2..8efd206b 100644 --- a/slang/README.md +++ b/slang/README.md @@ -86,6 +86,7 @@ dart run slang migrate arb src.arb dest.json # migrate arb to json - [Clean Translations](#-clean-translations) - [Apply Translations](#-apply-translations) - [Edit Translations](#-edit-translations) + - [Normalize Translations](#-normalize-translations) - [Outdated Translations](#-outdated-translations) - [Translate with GPT](#-translate-with-gpt) - [Migration](#-migration) @@ -1484,6 +1485,19 @@ dart run slang edit \*\* See [Outdated Translations](#-outdated-translations) +### ➤ Normalize Translations + +To keep the order of the keys consistent, you can normalize the translations. +They will follow the same order as the base locale. + +```sh +dart run slang normalize [--locale=fr-FR] +``` + +| Argument | Usage | +|---------------------|------------------------------------| +| `--locale=` | Normalize only one specific locale | + ### ➤ Outdated Translations You want to update an existing string, but you want to keep the old translations for other locales? From ad2d15493a9fe32ce216a02d42f64a18b3ac3990 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sat, 22 Jun 2024 21:30:08 +0200 Subject: [PATCH 052/118] fix: ignore deprecation warning --- slang/lib/builder/builder/slang_file_collection_builder.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/slang/lib/builder/builder/slang_file_collection_builder.dart b/slang/lib/builder/builder/slang_file_collection_builder.dart index 581b10b5..a011753e 100644 --- a/slang/lib/builder/builder/slang_file_collection_builder.dart +++ b/slang/lib/builder/builder/slang_file_collection_builder.dart @@ -138,6 +138,9 @@ class SlangFileCollectionBuilder { return null; }) + // We cannot use "nonNulls" because this requires Dart 3.0 + // and slang currently supports Dart 2.17 + // ignore: deprecated_member_use .whereNotNull() .sortedBy((file) => '${file.locale}-${file.namespace}'), ); From d4d12f95e4349a6216e98074ccd16694f76aee10 Mon Sep 17 00:00:00 2001 From: Tien Do Nam <38380847+Tienisto@users.noreply.github.com> Date: Sat, 22 Jun 2024 22:35:17 +0200 Subject: [PATCH 053/118] fix: should call overridden translations with linked parameters (#227) --- slang/lib/api/translation_overrides.dart | 7 +++-- .../src/builder/utils/reflection_utils.dart | 15 ++++++++++ .../unit/utils/reflection_utils_test.dart | 28 +++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 slang/lib/src/builder/utils/reflection_utils.dart create mode 100644 slang/test/unit/utils/reflection_utils_test.dart diff --git a/slang/lib/api/translation_overrides.dart b/slang/lib/api/translation_overrides.dart index 3981dc4c..08a24856 100644 --- a/slang/lib/api/translation_overrides.dart +++ b/slang/lib/api/translation_overrides.dart @@ -3,6 +3,7 @@ import 'package:slang/api/pluralization.dart'; import 'package:slang/builder/model/node.dart'; import 'package:slang/builder/model/pluralization.dart'; import 'package:slang/src/builder/generator/helper.dart'; +import 'package:slang/src/builder/utils/reflection_utils.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; import 'package:slang/src/builder/utils/string_interpolation_extensions.dart'; @@ -152,11 +153,13 @@ extension TranslationOverridesStringExt on String { } if (refInFlatMap is Function) { + final parameterList = getFunctionParameters(refInFlatMap); return Function.apply( refInFlatMap, - [], + const [], { - for (final p in param.entries) Symbol(p.key): p.value, + for (final p in param.entries) + if (parameterList.contains(p.key)) Symbol(p.key): p.value, }, ); } diff --git a/slang/lib/src/builder/utils/reflection_utils.dart b/slang/lib/src/builder/utils/reflection_utils.dart new file mode 100644 index 00000000..23fa0723 --- /dev/null +++ b/slang/lib/src/builder/utils/reflection_utils.dart @@ -0,0 +1,15 @@ +/// Returns a list of function parameters of a given [function]. +/// Expects the function to contain either no parameters or +/// only required named parameters. +Set getFunctionParameters(Function function) { + // e.g. ({required num count, required Object name}) => String + final typeString = function.runtimeType.toString(); + + if (typeString.startsWith('() =>')) { + return const {}; + } + + final parameterList = typeString.substring( + typeString.indexOf('({') + 1, typeString.indexOf('})')); + return parameterList.split(',').map((e) => e.split(' ').last).toSet(); +} diff --git a/slang/test/unit/utils/reflection_utils_test.dart b/slang/test/unit/utils/reflection_utils_test.dart new file mode 100644 index 00000000..c9246531 --- /dev/null +++ b/slang/test/unit/utils/reflection_utils_test.dart @@ -0,0 +1,28 @@ +import 'package:slang/src/builder/utils/reflection_utils.dart'; +import 'package:test/test.dart'; + +void main() { + group('getFunctionParameters', () { + test('Should parse empty function', () { + final result = getFunctionParameters(() {}); + expect(result, isEmpty); + }); + + test('Should parse function one parameter', () { + final result = getFunctionParameters(({required int myArg}) {}); + expect(result, {'myArg'}); + }); + + test('Should parse function two parameters', () { + final result = getFunctionParameters( + ({required int myArg, required String myArg2}) {}, + ); + expect(result, {'myArg', 'myArg2'}); + }); + + test('Should parse function with generic type', () { + final result = getFunctionParameters(({required List myArg}) {}); + expect(result, {'myArg'}); + }); + }); +} From c342755736eb1f3715f9de3c5bc46f0341e102b7 Mon Sep 17 00:00:00 2001 From: Tien Do Nam <38380847+Tienisto@users.noreply.github.com> Date: Sat, 22 Jun 2024 23:01:46 +0200 Subject: [PATCH 054/118] fix: analyze should respect linked translations (#228) --- slang/lib/src/runner/analyze.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/slang/lib/src/runner/analyze.dart b/slang/lib/src/runner/analyze.dart index fab27e32..6f702dd8 100644 --- a/slang/lib/src/runner/analyze.dart +++ b/slang/lib/src/runner/analyze.dart @@ -331,7 +331,18 @@ void _getUnusedTranslationsInSourceCodeRecursive({ ); } else { final translationCall = '$translateVar.${child.path}'; - if (!sourceCode.contains(translationCall)) { + const linkedPrefix = r'${_root'; + + // We only need to check if the translateVar is not part of the linked string. + // Since most developers use the default "t" as translateVar, + // we can ignore the linked call because it is already covered by the translateVar. + final linkedCall = linkedPrefix.endsWith(translateVar) + ? null + : '$linkedPrefix.${child.path}'; + + final isUsed = sourceCode.contains(translationCall) || + (linkedCall != null && sourceCode.contains(linkedCall)); + if (!isUsed) { // add whole base node which is expected _addNodeRecursive( node: child, From 50602aeaf767911cb73adea4b431ea90d889b8b2 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sat, 22 Jun 2024 23:21:12 +0200 Subject: [PATCH 055/118] chore: bump fvm flutter to 3.22.2 --- .fvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.fvmrc b/.fvmrc index db15b841..26d017c2 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,4 +1,4 @@ { - "flutter": "3.19.5", + "flutter": "3.22.2", "flavors": {} } \ No newline at end of file From 8eb87b0517f07efb4893828e3b91a71d93105ec8 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sat, 22 Jun 2024 23:28:17 +0200 Subject: [PATCH 056/118] docs: update changelog --- slang/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 334be7c3..87ea6def 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.31.1 + +- fix: "translation overrides" do not work with parameterized linked translations (#226) @Tienisto +- fix: linked translations should not be unused when running `dart run slang analyze` (#222) @Tienisto + ## 3.31.0 - feat: add `dart run slang normalize` to normalize translations based on base locale @Tienisto From cdc609d4800416946302e4c6d8c1de33186da1ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cem=20Avc=C4=B1?= <101732768+cem256@users.noreply.github.com> Date: Thu, 27 Jun 2024 19:23:57 +0300 Subject: [PATCH 057/118] Add support to GPT-4o #229 (#230) --- slang_gpt/README.md | 4 +++- slang_gpt/lib/model/gpt_model.dart | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/slang_gpt/README.md b/slang_gpt/README.md index e7fb0d3e..6c1f46a2 100644 --- a/slang_gpt/README.md +++ b/slang_gpt/README.md @@ -74,8 +74,10 @@ dart run slang_gpt --target=fr --api-key= | Model name | Provider | Context length | Cost per 1k input token | Cost per input word (English) | |---------------------|----------|----------------|-------------------------|-------------------------------| | `gpt-3.5-turbo` | Open AI | 4096 | $0.0015 | $0.000001125 | -| `gpt-3.5-turbo-16k` | Open AI | 16384 | $0.003 | $0.00000225 | +| `gpt-3.5-turbo-16k` | Open AI | 16384 | $0.003 | $0.00000225 | | `gpt-4` | Open AI | 8192 | $0.03 | $0.0000225 | +| `gpt-4-turbo` | Open AI | 64000 | $0.01 | $0.00001 | +| `gpt-4o` | Open AI | 128000 | $0.005 | $0.000005 | ## GPT context length diff --git a/slang_gpt/lib/model/gpt_model.dart b/slang_gpt/lib/model/gpt_model.dart index c4f0e649..f2b08f80 100644 --- a/slang_gpt/lib/model/gpt_model.dart +++ b/slang_gpt/lib/model/gpt_model.dart @@ -20,6 +20,10 @@ enum GptModel { defaultInputLength: 64000, costPerInputToken: 0.00001, costPerOutputToken: 0.00002), + gpt4o('gpt-4o', GptProvider.openai, + defaultInputLength: 128000, + costPerInputToken: 0.000005, + costPerOutputToken: 0.000015), ; const GptModel( From 8633d2b29d92e4e40ce5f342723b83878c13694f Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Thu, 27 Jun 2024 18:48:09 +0200 Subject: [PATCH 058/118] release(gpt) 0.10.2 --- slang_gpt/CHANGELOG.md | 4 ++++ slang_gpt/README.md | 16 ++++++++------- slang_gpt/lib/model/gpt_model.dart | 32 +++++++++++++++++------------- slang_gpt/pubspec.yaml | 2 +- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/slang_gpt/CHANGELOG.md b/slang_gpt/CHANGELOG.md index a784e299..3c167323 100644 --- a/slang_gpt/CHANGELOG.md +++ b/slang_gpt/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.2 + +- feat: add `gpt-4o`, `gpt-4-turbo` models + ## 0.10.1 - deps: bump slang to 3.30.1 diff --git a/slang_gpt/README.md b/slang_gpt/README.md index 6c1f46a2..a3f68bb1 100644 --- a/slang_gpt/README.md +++ b/slang_gpt/README.md @@ -71,13 +71,15 @@ dart run slang_gpt --target=fr --api-key= ## Models -| Model name | Provider | Context length | Cost per 1k input token | Cost per input word (English) | -|---------------------|----------|----------------|-------------------------|-------------------------------| -| `gpt-3.5-turbo` | Open AI | 4096 | $0.0015 | $0.000001125 | -| `gpt-3.5-turbo-16k` | Open AI | 16384 | $0.003 | $0.00000225 | -| `gpt-4` | Open AI | 8192 | $0.03 | $0.0000225 | -| `gpt-4-turbo` | Open AI | 64000 | $0.01 | $0.00001 | -| `gpt-4o` | Open AI | 128000 | $0.005 | $0.000005 | +| Model name | Provider | Context length | Cost per 1k input token | Cost per 1k output token | +|---------------------|----------|----------------|-------------------------|--------------------------| +| `gpt-3.5-turbo` | Open AI | 4096 | $0.0005 | $0.0015 | +| `gpt-3.5-turbo-16k` | Open AI | 16384 | $0.003 | $0.004 | +| `gpt-4` | Open AI | 8192 | $0.03 | $0.06 | +| `gpt-4-turbo` | Open AI | 64000 | $0.01 | $0.03 | +| `gpt-4o` | Open AI | 128000 | $0.005 | $0.015 | + +1k tokens = 750 words (English) ## GPT context length diff --git a/slang_gpt/lib/model/gpt_model.dart b/slang_gpt/lib/model/gpt_model.dart index f2b08f80..5f321fb4 100644 --- a/slang_gpt/lib/model/gpt_model.dart +++ b/slang_gpt/lib/model/gpt_model.dart @@ -6,32 +6,32 @@ enum GptProvider { enum GptModel { gpt3_5_4k('gpt-3.5-turbo', GptProvider.openai, defaultInputLength: 2000, - costPerInputToken: 0.0000015, - costPerOutputToken: 0.000002), + costPer1kInputToken: 0.0005, + costPer1kOutputToken: 0.0015), gpt3_5_16k('gpt-3.5-turbo-16k', GptProvider.openai, defaultInputLength: 8000, - costPerInputToken: 0.000003, - costPerOutputToken: 0.000004), + costPer1kInputToken: 0.003, + costPer1kOutputToken: 0.004), gpt4_8k('gpt-4', GptProvider.openai, defaultInputLength: 4000, - costPerInputToken: 0.00003, - costPerOutputToken: 0.00006), + costPer1kInputToken: 0.03, + costPer1kOutputToken: 0.06), gpt4_turbo('gpt-4-turbo', GptProvider.openai, defaultInputLength: 64000, - costPerInputToken: 0.00001, - costPerOutputToken: 0.00002), + costPer1kInputToken: 0.01, + costPer1kOutputToken: 0.03), gpt4o('gpt-4o', GptProvider.openai, defaultInputLength: 128000, - costPerInputToken: 0.000005, - costPerOutputToken: 0.000015), + costPer1kInputToken: 0.005, + costPer1kOutputToken: 0.015), ; const GptModel( this.id, this.provider, { required this.defaultInputLength, - required this.costPerInputToken, - required this.costPerOutputToken, + required this.costPer1kInputToken, + required this.costPer1kOutputToken, }); /// The id of this model. @@ -50,8 +50,12 @@ enum GptModel { final int defaultInputLength; /// The cost per input token in USD. - final double costPerInputToken; + final double costPer1kInputToken; + + double get costPerInputToken => costPer1kInputToken / 1000; /// The cost per output token in USD. - final double costPerOutputToken; + final double costPer1kOutputToken; + + double get costPerOutputToken => costPer1kOutputToken / 1000; } diff --git a/slang_gpt/pubspec.yaml b/slang_gpt/pubspec.yaml index 75c5d9bc..dec77023 100644 --- a/slang_gpt/pubspec.yaml +++ b/slang_gpt/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_gpt description: Use GPT to automatically translate at compile time. A tool for slang. -version: 0.10.1 +version: 0.10.2 repository: https://github.com/slang-i18n/slang environment: From a2a5669b784cb566dc96db687eb51ac18645cfdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cem=20Avc=C4=B1?= <101732768+cem256@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:11:06 +0300 Subject: [PATCH 059/118] Add suport to GPT-4o mini #235 (#236) --- slang_gpt/README.md | 1 + slang_gpt/lib/model/gpt_model.dart | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/slang_gpt/README.md b/slang_gpt/README.md index a3f68bb1..2233de23 100644 --- a/slang_gpt/README.md +++ b/slang_gpt/README.md @@ -78,6 +78,7 @@ dart run slang_gpt --target=fr --api-key= | `gpt-4` | Open AI | 8192 | $0.03 | $0.06 | | `gpt-4-turbo` | Open AI | 64000 | $0.01 | $0.03 | | `gpt-4o` | Open AI | 128000 | $0.005 | $0.015 | +| `gpt-4o-mini` | Open AI | 128000 | $0.00015 | $0.0006 | 1k tokens = 750 words (English) diff --git a/slang_gpt/lib/model/gpt_model.dart b/slang_gpt/lib/model/gpt_model.dart index 5f321fb4..fea03848 100644 --- a/slang_gpt/lib/model/gpt_model.dart +++ b/slang_gpt/lib/model/gpt_model.dart @@ -24,6 +24,10 @@ enum GptModel { defaultInputLength: 128000, costPer1kInputToken: 0.005, costPer1kOutputToken: 0.015), + gpt4o_mini('gpt-4o-mini', GptProvider.openai, + defaultInputLength: 128000, + costPer1kInputToken: 0.00015, + costPer1kOutputToken: 0.0006), ; const GptModel( From e0a684f57adea5a3e7f7b60e95df7fae1f639667 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Wed, 31 Jul 2024 15:11:05 +0200 Subject: [PATCH 060/118] release(gpt): 0.10.3 --- slang_gpt/CHANGELOG.md | 4 ++++ slang_gpt/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/slang_gpt/CHANGELOG.md b/slang_gpt/CHANGELOG.md index 3c167323..6f3bcf28 100644 --- a/slang_gpt/CHANGELOG.md +++ b/slang_gpt/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.3 + +- feat: add `gpt-4o-mini` model + ## 0.10.2 - feat: add `gpt-4o`, `gpt-4-turbo` models diff --git a/slang_gpt/pubspec.yaml b/slang_gpt/pubspec.yaml index dec77023..97b1fe4f 100644 --- a/slang_gpt/pubspec.yaml +++ b/slang_gpt/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_gpt description: Use GPT to automatically translate at compile time. A tool for slang. -version: 0.10.2 +version: 0.10.3 repository: https://github.com/slang-i18n/slang environment: From 4b035f0e9d29240a06a2cf133e98ef9e07aa0e5e Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sat, 24 Aug 2024 17:35:21 +0200 Subject: [PATCH 061/118] refactor: make string to enum parsers internal --- .../builder/builder/raw_config_builder.dart | 74 ++++++++++++++++++ slang/lib/builder/model/enums.dart | 76 ------------------- 2 files changed, 74 insertions(+), 76 deletions(-) diff --git a/slang/lib/builder/builder/raw_config_builder.dart b/slang/lib/builder/builder/raw_config_builder.dart index 4cfefce2..05b2233f 100644 --- a/slang/lib/builder/builder/raw_config_builder.dart +++ b/slang/lib/builder/builder/raw_config_builder.dart @@ -225,4 +225,78 @@ extension on String { String removeTrailingSlash() { return endsWith('/') ? substring(0, length - 1) : this; } + + FallbackStrategy? toFallbackStrategy() { + switch (this) { + case 'none': + return FallbackStrategy.none; + case 'base_locale': + return FallbackStrategy.baseLocale; + case 'base_locale_empty_string': + return FallbackStrategy.baseLocaleEmptyString; + default: + return null; + } + } + + OutputFormat? toOutputFormat() { + switch (this) { + case 'single_file': + return OutputFormat.singleFile; + case 'multiple_files': + return OutputFormat.multipleFiles; + default: + return null; + } + } + + TranslationClassVisibility? toTranslationClassVisibility() { + switch (this) { + case 'private': + return TranslationClassVisibility.private; + case 'public': + return TranslationClassVisibility.public; + default: + return null; + } + } + + StringInterpolation? toStringInterpolation() { + switch (this) { + case 'dart': + return StringInterpolation.dart; + case 'braces': + return StringInterpolation.braces; + case 'double_braces': + return StringInterpolation.doubleBraces; + default: + return null; + } + } + + CaseStyle? toCaseStyle() { + switch (this) { + case 'camel': + return CaseStyle.camel; + case 'snake': + return CaseStyle.snake; + case 'pascal': + return CaseStyle.pascal; + default: + return null; + } + } + + PluralAuto? toPluralAuto() { + switch (this) { + case 'off': + return PluralAuto.off; + case 'cardinal': + return PluralAuto.cardinal; + case 'ordinal': + return PluralAuto.ordinal; + default: + return null; + } + } } diff --git a/slang/lib/builder/model/enums.dart b/slang/lib/builder/model/enums.dart index bf4e17eb..7417a24f 100644 --- a/slang/lib/builder/model/enums.dart +++ b/slang/lib/builder/model/enums.dart @@ -16,82 +16,6 @@ enum CaseStyle { camel, pascal, snake } enum PluralAuto { off, cardinal, ordinal } -extension Parser on String { - FallbackStrategy? toFallbackStrategy() { - switch (this) { - case 'none': - return FallbackStrategy.none; - case 'base_locale': - return FallbackStrategy.baseLocale; - case 'base_locale_empty_string': - return FallbackStrategy.baseLocaleEmptyString; - default: - return null; - } - } - - OutputFormat? toOutputFormat() { - switch (this) { - case 'single_file': - return OutputFormat.singleFile; - case 'multiple_files': - return OutputFormat.multipleFiles; - default: - return null; - } - } - - TranslationClassVisibility? toTranslationClassVisibility() { - switch (this) { - case 'private': - return TranslationClassVisibility.private; - case 'public': - return TranslationClassVisibility.public; - default: - return null; - } - } - - StringInterpolation? toStringInterpolation() { - switch (this) { - case 'dart': - return StringInterpolation.dart; - case 'braces': - return StringInterpolation.braces; - case 'double_braces': - return StringInterpolation.doubleBraces; - default: - return null; - } - } - - CaseStyle? toCaseStyle() { - switch (this) { - case 'camel': - return CaseStyle.camel; - case 'snake': - return CaseStyle.snake; - case 'pascal': - return CaseStyle.pascal; - default: - return null; - } - } - - PluralAuto? toPluralAuto() { - switch (this) { - case 'off': - return PluralAuto.off; - case 'cardinal': - return PluralAuto.cardinal; - case 'ordinal': - return PluralAuto.ordinal; - default: - return null; - } - } -} - extension FallbackStrategyExt on FallbackStrategy { GenerateFallbackStrategy toGenerateFallbackStrategy() { switch (this) { From c93a54d65ae54b11e079d89d7afb119e1c140c89 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sat, 24 Aug 2024 17:38:49 +0200 Subject: [PATCH 062/118] ci: bump to flutter 3.24.1 --- .fvmrc | 2 +- .github/workflows/ci.yml | 2 +- .gitignore | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.fvmrc b/.fvmrc index 26d017c2..03436d59 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,4 +1,4 @@ { - "flutter": "3.22.2", + "flutter": "3.24.1", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 441e0ff0..47b120db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ on: env: FLUTTER_VERSION_OLDEST: "3.0.5" - FLUTTER_VERSION_NEWEST: "3.19.5" + FLUTTER_VERSION_NEWEST: "3.24.1" jobs: format: diff --git a/.gitignore b/.gitignore index dd3f8373..c4c8e0ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -/.fvm - # testing /slang/lib/builder/i18n/ @@ -79,4 +77,7 @@ build/ !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages -pubspec.lock \ No newline at end of file +pubspec.lock + +# FVM Version Cache +.fvm/ From 8dd41a4bc1219909cfb2ebbc1c0074f41848254d Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sat, 24 Aug 2024 17:44:51 +0200 Subject: [PATCH 063/118] feat: add cargokit_build to ignored directories --- slang/lib/builder/builder/slang_file_collection_builder.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/slang/lib/builder/builder/slang_file_collection_builder.dart b/slang/lib/builder/builder/slang_file_collection_builder.dart index a011753e..8029763c 100644 --- a/slang/lib/builder/builder/slang_file_collection_builder.dart +++ b/slang/lib/builder/builder/slang_file_collection_builder.dart @@ -46,6 +46,7 @@ class SlangFileCollectionBuilder { '.flutter.git', '.dart_tool', '.symlinks', + 'cargokit_build', }, ); } From 338c46b7456ed0fda13f0c1be1f4b4535c8f9712 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Fri, 30 Aug 2024 01:37:42 +0200 Subject: [PATCH 064/118] fix: language matching --- slang/CHANGELOG.md | 4 ++++ slang/lib/api/singleton.dart | 29 ++++++++++++++----------- slang/test/unit/api/singleton_test.dart | 17 ++++++++++++++- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 87ea6def..9c1fa6ae 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.31.2 + +- fix: should match first language code if there are no matches by country code and at least one match by language code (#241) @Tienisto + ## 3.31.1 - fix: "translation overrides" do not work with parameterized linked translations (#226) @Tienisto diff --git a/slang/lib/api/singleton.dart b/slang/lib/api/singleton.dart index 8855f163..3845a403 100644 --- a/slang/lib/api/singleton.dart +++ b/slang/lib/api/singleton.dart @@ -89,25 +89,28 @@ extension AppLocaleUtilsExt, return candidates.first; } - if (countryCode == null) { - // no country code given - return candidates.firstOrNull ?? baseLocale; - } - if (candidates.isEmpty) { - // match country code + // no matching language, try match country code only return locales.firstWhereOrNull((supported) { return supported.countryCode == countryCode; }) ?? baseLocale; - } else { - // there are multiple locales with same language code - // e.g. zh-Hans, zh-Hant-HK, zh-Hant-TW - return candidates.firstWhereOrNull((candidate) { - return candidate.countryCode == countryCode; - }) ?? - baseLocale; } + + // There is at least a locale with matching language code + final fallback = candidates.first; + + if (countryCode == null) { + // no country code given + return fallback; + } + + // there are multiple locales with same language code + // e.g. zh-Hans, zh-Hant-HK, zh-Hant-TW + return candidates.firstWhereOrNull((candidate) { + return candidate.countryCode == countryCode; + }) ?? + fallback; } /// Gets supported locales in string format. diff --git a/slang/test/unit/api/singleton_test.dart b/slang/test/unit/api/singleton_test.dart index d4f87f59..cd1e97bb 100644 --- a/slang/test/unit/api/singleton_test.dart +++ b/slang/test/unit/api/singleton_test.dart @@ -53,7 +53,7 @@ void main() { ); }); - test('should match first language', () { + test('should match first language when there is no country code', () { final utils = AppLocaleUtils( baseLocale: esEs, locales: [ @@ -69,6 +69,21 @@ void main() { ); }); + test('should match first language when there is a country code', () { + final utils = AppLocaleUtils( + baseLocale: esEs, + locales: [ + deDe, + deAu, + ], + ); + + expect( + utils.parseLocaleParts(languageCode: 'de', countryCode: 'CH'), + deDe, + ); + }); + test('should match country', () { final utils = AppLocaleUtils( baseLocale: deDe, From bfa8e751fa71bf3b59d88dc619c99635b1b26ce7 Mon Sep 17 00:00:00 2001 From: Piotr Orzechowski Date: Sat, 21 Sep 2024 21:12:03 +0200 Subject: [PATCH 065/118] Add Polish pluralization resolvers (#245) --- slang/lib/api/plural_resolver_map.dart | 33 +++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/slang/lib/api/plural_resolver_map.dart b/slang/lib/api/plural_resolver_map.dart index 08c87fb7..8a364d93 100644 --- a/slang/lib/api/plural_resolver_map.dart +++ b/slang/lib/api/plural_resolver_map.dart @@ -18,7 +18,7 @@ final _defaultResolver = _Resolvers( ); /// Predefined pluralization resolvers -/// See https://unicode-org.github.io/cldr-staging/charts/latest/supplemental/language_plural_rules.html +/// See https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html /// Sorted by language alphabetically /// /// Contribution would be nice! (Only this file needs to be changed) @@ -138,6 +138,37 @@ final Map _resolverMap = { return other!; }, ), + // Polish + 'pl': _Resolvers( + cardinal: (n, {zero, one, two, few, many, other}) { + final i = n.toInt(); + final v = i == n ? 0 : n.toString().split('.')[1].length; + + if (v == 0) { + if (i == 0) { + return zero ?? other!; + } + + if (i == 1) { + return one ?? other!; + } + + final r10 = i % 10; + final r100 = i % 100; + + if (r10 > 1 && r10 < 5 && (r100 < 12 || r100 > 14)) { + return few ?? other!; + } + + if (r10 < 2 || (r10 > 4 && r10 < 10) || (r100 > 11 && r100 < 15)) { + return many ?? other!; + } + } + + return other!; + }, + ordinal: (n, {zero, one, two, few, many, other}) => other!, + ), // Russian 'ru': _Resolvers( cardinal: (n, {zero, one, two, few, many, other}) { From 645a2554c718c4f81f3c4d377d614f0f8d9d2df4 Mon Sep 17 00:00:00 2001 From: Piotr Orzechowski Date: Sun, 22 Sep 2024 07:15:27 +0200 Subject: [PATCH 066/118] Update Unicode CLDR links (#246) --- slang/README.md | 2 +- slang/lib/api/singleton.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/slang/README.md b/slang/README.md index 8efd206b..a9d87959 100644 --- a/slang/README.md +++ b/slang/README.md @@ -711,7 +711,7 @@ If namespaces are used, then it has to be specified in the path too. ### ➤ Pluralization -This library uses the concept defined [here](https://unicode-org.github.io/cldr-staging/charts/latest/supplemental/language_plural_rules.html). +This library uses the concept defined [here](https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html). Some languages have support out of the box. See [here](https://github.com/slang-i18n/slang/blob/main/slang/lib/api/plural_resolver_map.dart). diff --git a/slang/lib/api/singleton.dart b/slang/lib/api/singleton.dart index 3845a403..7184589a 100644 --- a/slang/lib/api/singleton.dart +++ b/slang/lib/api/singleton.dart @@ -271,7 +271,7 @@ extension LocaleSettingsExt, } /// Sets plural resolvers. - /// See https://unicode-org.github.io/cldr-staging/charts/latest/supplemental/language_plural_rules.html + /// See https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html /// See https://github.com/slang-i18n/slang/blob/main/slang/lib/api/plural_resolver_map.dart /// Either specify [language], or [locale]. [locale] has precedence. void setPluralResolver({ From 3fb77d9bc4633fabc044080933ad5538e3ec8fce Mon Sep 17 00:00:00 2001 From: Sebastian Faust Date: Tue, 1 Oct 2024 20:10:59 +0200 Subject: [PATCH 067/118] Allow escaping linked translations (#248) --- slang/README.md | 12 ++++++ slang/lib/builder/model/node.dart | 2 +- slang/lib/src/builder/utils/regex_utils.dart | 8 +++- .../string_interpolation_extensions.dart | 13 ++++++ slang/test/unit/model/node_test.dart | 42 +++++++++++++++++++ 5 files changed, 74 insertions(+), 3 deletions(-) diff --git a/slang/README.md b/slang/README.md index a9d87959..9eb2edbd 100644 --- a/slang/README.md +++ b/slang/README.md @@ -709,6 +709,18 @@ If namespaces are used, then it has to be specified in the path too. [RichTexts](#-richtext) can also contain links! But only [RichTexts](#-richtext) can link to [RichTexts](#-richtext). +Optionally, you can escape linked translations with this syntax: + + +```json +{ + "fields": { + "nbsp": "\u00a0" + }, + "message": "10@:{fields.nbsp}Days" +} +``` + ### ➤ Pluralization This library uses the concept defined [here](https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html). diff --git a/slang/lib/builder/model/node.dart b/slang/lib/builder/model/node.dart index f465c82e..a2c033b5 100644 --- a/slang/lib/builder/model/node.dart +++ b/slang/lib/builder/model/node.dart @@ -576,7 +576,7 @@ _ParseLinksResult _parseLinks({ }) { final links = {}; final parsedContent = input.replaceAllMapped(RegexUtils.linkedRegex, (match) { - final linkedPath = match.group(1)!; + final linkedPath = (match.group(1) ?? match.group(2))!; links.add(linkedPath); if (linkParamMap == null) { diff --git a/slang/lib/src/builder/utils/regex_utils.dart b/slang/lib/src/builder/utils/regex_utils.dart index 10b52f39..2d602927 100644 --- a/slang/lib/src/builder/utils/regex_utils.dart +++ b/slang/lib/src/builder/utils/regex_utils.dart @@ -4,8 +4,12 @@ class RegexUtils { /// 2 = argument of ${argument} static RegExp argumentsDartRegex = RegExp(r'(?= 2 && + curr[startIndex - 1] == ':' && + curr[startIndex - 2] == '@') { + // ignore because of preceding @: which indicates an escaped, linked translation + buffer.write(curr.substring(0, startIndex + 1)); + if (startIndex + 1 < curr.length) { + curr = curr.substring(startIndex + startCharacterLength); + continue; + } else { + break; + } + } + if (startIndex != 0) { // add prefix buffer.write(curr.substring(0, startIndex)); diff --git a/slang/test/unit/model/node_test.dart b/slang/test/unit/model/node_test.dart index 75c88884..a8959e16 100644 --- a/slang/test/unit/model/node_test.dart +++ b/slang/test/unit/model/node_test.dart @@ -271,6 +271,14 @@ void main() { expect(node.params, {}); }); + test('with escaped links', () { + final test = r'@:.c@:{a}@:{hi}@:wow. @:{nice.cool} @:nice.cool'; + final node = textNode(test, StringInterpolation.dart); + expect(node.content, + r'@:.c${_root.a}${_root.hi}${_root.wow}. ${_root.nice.cool} ${_root.nice.cool}'); + expect(node.params, {}); + }); + test('with links and params', () { final test = r'@:a @:b'; final node = textNode(test, StringInterpolation.dart, linkParamMap: { @@ -351,6 +359,23 @@ void main() { expect(node.content, r'Nice ${coolHi} ${wow} ${yes}a ${noYes}'); expect(node.params, {'coolHi', 'wow', 'yes', 'noYes'}); }); + + test('with links', () { + final test = r'{myArg} @:myLink'; + final node = textNode(test, StringInterpolation.braces); + expect(node.content, r'${myArg} ${_root.myLink}'); + expect(node.params, {'myArg'}); + }); + + test('with escaped links', () { + final test = r'{myArg} @:linkA @:{linkB}hello @:{linkC}'; + final node = textNode(test, StringInterpolation.braces); + expect( + node.content, + r'${myArg} ${_root.linkA} ${_root.linkB}hello ${_root.linkC}', + ); + expect(node.params, {'myArg'}); + }); }); group(StringInterpolation.doubleBraces, () { @@ -406,6 +431,23 @@ void main() { expect(node.content, r'Nice ${coolHi} ${wow} ${yes}a ${noYes}'); expect(node.params, {'coolHi', 'wow', 'yes', 'noYes'}); }); + + test('with links', () { + final test = r'{{myArg}} @:myLink'; + final node = textNode(test, StringInterpolation.doubleBraces); + expect(node.content, r'${myArg} ${_root.myLink}'); + expect(node.params, {'myArg'}); + }); + + test('with escaped links', () { + final test = r'{{myArg}} @:linkA @:{linkB}hello @:{linkC}'; + final node = textNode(test, StringInterpolation.doubleBraces); + expect( + node.content, + r'${myArg} ${_root.linkA} ${_root.linkB}hello ${_root.linkC}', + ); + expect(node.params, {'myArg'}); + }); }); }); From a41a39a4d50066b330c4354435d38938d73f3d8a Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Tue, 1 Oct 2024 20:21:58 +0200 Subject: [PATCH 068/118] test: add replaceBracesInterpolation test --- .../unit/utils/string_interpolation_extensions_test.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/slang/test/unit/utils/string_interpolation_extensions_test.dart b/slang/test/unit/utils/string_interpolation_extensions_test.dart index b7978563..d882c941 100644 --- a/slang/test/unit/utils/string_interpolation_extensions_test.dart +++ b/slang/test/unit/utils/string_interpolation_extensions_test.dart @@ -112,6 +112,11 @@ void main() { final input = 'Hello \\{ World {arg}!'; expect(input.braces(), 'Hello { World X!'); }); + + test('ignore @:', () { + final input = 'Hello @:{arg}!'; + expect(input.braces(), 'Hello @:{arg}!'); + }); }); group('double braces', () { From 0833113053117538ecd1878bf1cf329196167c6c Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Tue, 1 Oct 2024 20:22:06 +0200 Subject: [PATCH 069/118] docs: update readme --- slang/README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/slang/README.md b/slang/README.md index 9eb2edbd..4cf5af9d 100644 --- a/slang/README.md +++ b/slang/README.md @@ -108,6 +108,8 @@ dart run slang migrate arb src.arb dest.json # migrate arb to json Coming from ARB? There is a [tool](#arb) for that. +Are you using Slang without Flutter? Check out the [Dart only](#-dart-only) section. + **Step 1: Add dependencies** You will probably need at least 2 packages: [slang](https://pub.dev/packages/slang) and [slang_flutter](https://pub.dev/packages/slang_flutter). @@ -709,15 +711,14 @@ If namespaces are used, then it has to be specified in the path too. [RichTexts](#-richtext) can also contain links! But only [RichTexts](#-richtext) can link to [RichTexts](#-richtext). -Optionally, you can escape linked translations with this syntax: - +Optionally, you can escape linked translations by surrounding the path with `{}`: ```json { "fields": { - "nbsp": "\u00a0" + "name": "my name is {firstName}" }, - "message": "10@:{fields.nbsp}Days" + "introduce": "Hello, @:{fields.name}inator" } ``` @@ -1397,6 +1398,12 @@ An experienced reverse engineer can still find the strings given enough time. You can use this library without flutter. +```yaml +# pubspec.yaml +dependencies: + slang: +``` + ```yaml # Config flutter_integration: false # set this From 208aa30008c1e926c2a4a83337b87c58711c2b7e Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Tue, 1 Oct 2024 20:26:26 +0200 Subject: [PATCH 070/118] docs: update changelog --- slang/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 9c1fa6ae..f0f0f60e 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.32.0 + +- feat: add syntax to escape linked translations (#248) @Fasust +- i18n: add Polish plural resolver (#245) @0rzech +- docs: broken Unicode CLDR link (#246) @0rzech + ## 3.31.2 - fix: should match first language code if there are no matches by country code and at least one match by language code (#241) @Tienisto From 002b57db00aa5b56e8df926bdfb86ffcf02ea318 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 6 Oct 2024 03:01:51 +0200 Subject: [PATCH 071/118] ci: bump to Flutter 3.24.3 --- .fvmrc | 2 +- .github/workflows/ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.fvmrc b/.fvmrc index 03436d59..c62692b4 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,4 +1,4 @@ { - "flutter": "3.24.1", + "flutter": "3.24.3", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47b120db..13cc5c9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ on: env: FLUTTER_VERSION_OLDEST: "3.0.5" - FLUTTER_VERSION_NEWEST: "3.24.1" + FLUTTER_VERSION_NEWEST: "3.24.3" jobs: format: From 65d5833246652f194e4b2bea01bb48aa0664b34b Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Fri, 11 Oct 2024 23:03:51 +0200 Subject: [PATCH 072/118] feat: lazy loading --- .github/workflows/ci.yml | 2 +- slang/CHANGELOG.md | 12 + slang/README.md | 2 - slang/bin/slang.dart | 90 +-- .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 13 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- slang/example/ios/Runner/AppDelegate.swift | 2 +- slang/example/ios/Runner/Info.plist | 4 + slang/example/lib/i18n/strings.g.dart | 247 +++---- slang/example/lib/i18n/strings.i18n.json | 3 +- slang/example/lib/i18n/strings_de.g.dart | 76 +++ slang/example/lib/i18n/strings_de.i18n.json | 3 +- slang/example/lib/i18n/strings_en.g.dart | 81 +++ slang/example/lib/i18n/strings_fr_FR.g.dart | 76 +++ .../example/lib/i18n/strings_fr_FR.i18n.json | 15 + slang/example/lib/main.dart | 4 +- slang/example/pubspec.yaml | 3 +- slang/lib/builder/model/build_result.dart | 30 - slang/lib/node.dart | 1 + slang/lib/overrides.dart | 4 + slang/lib/secret.dart | 1 + slang/lib/slang.dart | 6 +- slang/lib/{ => src}/api/locale.dart | 75 ++- .../{ => src}/api/plural_resolver_map.dart | 0 slang/lib/{ => src}/api/pluralization.dart | 0 slang/lib/{ => src}/api/secret.dart | 0 slang/lib/{ => src}/api/singleton.dart | 306 +++++++-- slang/lib/{ => src}/api/state.dart | 2 +- .../{ => src}/api/translation_overrides.dart | 8 +- .../builder/build_model_config_builder.dart | 4 +- .../builder/generate_config_builder.dart | 15 +- .../builder/builder/raw_config_builder.dart | 37 +- .../slang_file_collection_builder.dart | 8 +- .../lib/src/builder/builder/text_parser.dart | 2 +- .../builder/translation_map_builder.dart | 8 +- .../builder/translation_model_builder.dart | 51 +- .../translation_model_list_builder.dart | 10 +- .../lib/src/builder/decoder/arb_decoder.dart | 2 +- .../lib/src/builder/decoder/base_decoder.dart | 2 +- .../builder/generator/generate_header.dart | 193 +++--- .../generator/generate_translation_map.dart | 50 +- .../generator/generate_translations.dart | 144 ++-- .../lib/src/builder/generator/generator.dart | 24 +- slang/lib/src/builder/generator/helper.dart | 30 +- slang/lib/src/builder/generator_facade.dart | 16 +- .../builder/model/build_model_config.dart | 8 +- slang/lib/src/builder/model/build_result.dart | 14 + .../{ => src}/builder/model/context_type.dart | 31 +- slang/lib/{ => src}/builder/model/enums.dart | 2 - .../builder/model/generate_config.dart | 16 +- .../{ => src}/builder/model/i18n_data.dart | 8 +- .../{ => src}/builder/model/i18n_locale.dart | 2 +- .../{ => src}/builder/model/interface.dart | 0 slang/lib/{ => src}/builder/model/node.dart | 10 +- .../builder/model/obfuscation_config.dart | 0 .../builder/model/pluralization.dart | 0 .../{ => src}/builder/model/raw_config.dart | 20 +- .../builder/model/slang_file_collection.dart | 6 +- .../builder/model/translation_map.dart | 2 +- slang/lib/src/builder/utils/file_utils.dart | 2 +- slang/lib/src/builder/utils/node_utils.dart | 2 +- slang/lib/src/builder/utils/path_utils.dart | 2 +- .../src/builder/utils/string_extensions.dart | 2 +- slang/lib/src/runner/analyze.dart | 14 +- slang/lib/src/runner/apply.dart | 12 +- slang/lib/src/runner/clean.dart | 4 +- slang/lib/src/runner/edit.dart | 10 +- slang/lib/src/runner/migrate_arb.dart | 2 +- slang/lib/src/runner/normalize.dart | 8 +- slang/lib/src/runner/stats.dart | 10 +- .../src/runner/utils/read_analysis_file.dart | 4 +- slang/pubspec.yaml | 2 +- .../integration/main/compilation_test.dart | 2 + .../integration/main/csv_compact_test.dart | 19 +- slang/test/integration/main/csv_test.dart | 19 +- .../main/fallback_base_locale_test.dart | 52 +- .../main/json_multiple_files_test.dart | 54 -- slang/test/integration/main/json_test.dart | 19 +- .../integration/main/no_flutter_test.dart | 13 +- .../main/no_locale_handling_test.dart | 11 +- .../integration/main/obfuscation_test.dart | 23 +- .../test/integration/main/rich_text_test.dart | 13 +- .../main/translation_overrides_test.dart | 25 +- slang/test/integration/main/yaml_test.dart | 19 +- .../resources/main/_expected_de.output | 108 ++- .../resources/main/_expected_en.output | 115 +++- .../_expected_fallback_base_locale_de.output | 247 +++++++ .../_expected_fallback_base_locale_en.output | 250 +++++++ ..._expected_fallback_base_locale_main.output | 218 ++++++ ...ted_fallback_base_locale_special_de.output | 64 ++ ...ted_fallback_base_locale_special_en.output | 67 ++ ...d_fallback_base_locale_special_main.output | 183 +++++ .../resources/main/_expected_main.output | 93 ++- .../main/_expected_no_flutter.output | 124 ++-- .../main/_expected_no_locale_handling.output | 106 ++- .../main/_expected_obfuscation_de.output | 247 +++++++ .../main/_expected_obfuscation_en.output | 251 +++++++ .../main/_expected_obfuscation_main.output | 219 ++++++ .../resources/main/_expected_rich_text.output | 127 +--- .../resources/main/_expected_single.output | 634 ------------------ .../_expected_translation_overrides_de.output | 262 ++++++++ .../_expected_translation_overrides_en.output | 266 ++++++++ ...expected_translation_overrides_main.output | 246 +++++++ .../resources/main/build_config.yaml | 1 - slang/test/integration/update.dart | 124 ++-- slang/test/unit/api/locale_settings_test.dart | 16 +- slang/test/unit/api/secret_test.dart | 2 +- slang/test/unit/api/singleton_test.dart | 4 +- .../unit/api/translation_overrides_test.dart | 52 +- .../builder/build_config_builder_test.dart | 37 +- .../slang_file_collection_builder_test.dart | 8 +- slang/test/unit/builder/text_parser_test.dart | 2 +- .../translation_model_builder_test.dart | 20 +- slang/test/unit/model/i18n_data_test.dart | 6 +- slang/test/unit/model/i18n_locale_test.dart | 2 +- slang/test/unit/model/interface_test.dart | 2 +- slang/test/unit/model/node_test.dart | 4 +- slang/test/unit/utils/path_utils_test.dart | 2 +- slang/test/unit/utils/secret_test.dart | 2 +- .../unit/utils/string_extensions_test.dart | 2 +- slang/test/util/text_node_builder.dart | 4 +- .../lib/slang_build_runner.dart | 63 +- slang_build_runner/pubspec.yaml | 2 +- slang_flutter/lib/slang_flutter.dart | 33 +- slang_flutter/pubspec.yaml | 6 +- .../multi_package/gen/strings_a.g.dart | 223 +++--- .../multi_package/gen/strings_a_de.g.dart | 55 ++ .../multi_package/gen/strings_a_en.g.dart | 61 ++ .../multi_package/gen/strings_b.g.dart | 223 +++--- .../multi_package/gen/strings_b_de.g.dart | 55 ++ .../multi_package/gen/strings_b_en.g.dart | 61 ++ .../multi_package/pubspec_overrides.yaml | 5 + slang_gpt/lib/model/gpt_config.dart | 3 +- slang_gpt/lib/prompt/prompt.dart | 9 +- slang_gpt/lib/runner.dart | 9 +- slang_gpt/lib/util/locales.dart | 3 +- slang_gpt/lib/util/logger.dart | 4 +- slang_gpt/test/unit/prompt/prompt_test.dart | 6 +- slang_gpt/test/unit/util/locales_test.dart | 3 +- 140 files changed, 4791 insertions(+), 2278 deletions(-) create mode 100644 slang/example/lib/i18n/strings_de.g.dart create mode 100644 slang/example/lib/i18n/strings_en.g.dart create mode 100644 slang/example/lib/i18n/strings_fr_FR.g.dart create mode 100644 slang/example/lib/i18n/strings_fr_FR.i18n.json delete mode 100644 slang/lib/builder/model/build_result.dart create mode 100644 slang/lib/node.dart create mode 100644 slang/lib/overrides.dart create mode 100644 slang/lib/secret.dart rename slang/lib/{ => src}/api/locale.dart (69%) rename slang/lib/{ => src}/api/plural_resolver_map.dart (100%) rename slang/lib/{ => src}/api/pluralization.dart (100%) rename slang/lib/{ => src}/api/secret.dart (100%) rename slang/lib/{ => src}/api/singleton.dart (58%) rename slang/lib/{ => src}/api/state.dart (96%) rename slang/lib/{ => src}/api/translation_overrides.dart (96%) rename slang/lib/{ => src}/builder/builder/build_model_config_builder.dart (80%) rename slang/lib/{ => src}/builder/builder/generate_config_builder.dart (71%) rename slang/lib/{ => src}/builder/builder/raw_config_builder.dart (91%) rename slang/lib/{ => src}/builder/builder/slang_file_collection_builder.dart (96%) rename slang/lib/{ => src}/builder/builder/translation_map_builder.dart (94%) rename slang/lib/{ => src}/builder/builder/translation_model_builder.dart (94%) rename slang/lib/{ => src}/builder/builder/translation_model_list_builder.dart (85%) rename slang/lib/{ => src}/builder/model/build_model_config.dart (80%) create mode 100644 slang/lib/src/builder/model/build_result.dart rename slang/lib/{ => src}/builder/model/context_type.dart (55%) rename slang/lib/{ => src}/builder/model/enums.dart (94%) rename slang/lib/{ => src}/builder/model/generate_config.dart (75%) rename slang/lib/{ => src}/builder/model/i18n_data.dart (81%) rename slang/lib/{ => src}/builder/model/i18n_locale.dart (96%) rename slang/lib/{ => src}/builder/model/interface.dart (100%) rename slang/lib/{ => src}/builder/model/node.dart (98%) rename slang/lib/{ => src}/builder/model/obfuscation_config.dart (100%) rename slang/lib/{ => src}/builder/model/pluralization.dart (100%) rename slang/lib/{ => src}/builder/model/raw_config.dart (94%) rename slang/lib/{ => src}/builder/model/slang_file_collection.dart (94%) rename slang/lib/{ => src}/builder/model/translation_map.dart (94%) delete mode 100644 slang/test/integration/main/json_multiple_files_test.dart create mode 100644 slang/test/integration/resources/main/_expected_fallback_base_locale_de.output create mode 100644 slang/test/integration/resources/main/_expected_fallback_base_locale_en.output create mode 100644 slang/test/integration/resources/main/_expected_fallback_base_locale_main.output create mode 100644 slang/test/integration/resources/main/_expected_fallback_base_locale_special_de.output create mode 100644 slang/test/integration/resources/main/_expected_fallback_base_locale_special_en.output create mode 100644 slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output create mode 100644 slang/test/integration/resources/main/_expected_obfuscation_de.output create mode 100644 slang/test/integration/resources/main/_expected_obfuscation_en.output create mode 100644 slang/test/integration/resources/main/_expected_obfuscation_main.output delete mode 100644 slang/test/integration/resources/main/_expected_single.output create mode 100644 slang/test/integration/resources/main/_expected_translation_overrides_de.output create mode 100644 slang/test/integration/resources/main/_expected_translation_overrides_en.output create mode 100644 slang/test/integration/resources/main/_expected_translation_overrides_main.output create mode 100644 slang_flutter/test/integration/multi_package/gen/strings_a_de.g.dart create mode 100644 slang_flutter/test/integration/multi_package/gen/strings_a_en.g.dart create mode 100644 slang_flutter/test/integration/multi_package/gen/strings_b_de.g.dart create mode 100644 slang_flutter/test/integration/multi_package/gen/strings_b_en.g.dart create mode 100644 slang_flutter/test/integration/multi_package/pubspec_overrides.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13cc5c9d..e373d2ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ on: branches: [ main ] env: - FLUTTER_VERSION_OLDEST: "3.0.5" + FLUTTER_VERSION_OLDEST: "3.19.6" FLUTTER_VERSION_NEWEST: "3.24.3" jobs: diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index f0f0f60e..78b45395 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,15 @@ +## 4.0.0 + +**Number formats and Lazy loading** + +On web, [Deferred loading](https://dart.dev/language/libraries#lazily-loading-a-library) is used to reduce initial load time. + +- **Breaking:** `setLocale`, `setLocaleRaw`, and `useDeviceLocale` returns a Future, use `-Sync` suffix for synchronous calls +- **Breaking:** `output_format` removed, always generates multiple files now +- **Breaking:** deprecated functions in `LocaleSettings` (`supportedLocales`, `supportedLocalesRaw`) removed +- **Breaking:** defining contexts (enums) is no longer allowed in `build.yaml` or `slang.yaml` (deprecated in v3.19.0) +- **Breaking:** enums specified in `context` are no longer transformed into pascal case keeping the original case + ## 3.32.0 - feat: add syntax to escape linked translations (#248) @Fasust diff --git a/slang/README.md b/slang/README.md index 4cf5af9d..6ef4161f 100644 --- a/slang/README.md +++ b/slang/README.md @@ -442,8 +442,6 @@ targets: | `pluralization`/`default_parameter` | `String` | default plural parameter [(i)](#-pluralization) | `n` | | `pluralization`/`cardinal` | `List` | entries which have cardinals | `[]` | | `pluralization`/`ordinal` | `List` | entries which have ordinals | `[]` | -| ``/`enum` | `List` | DEPRECATED: context forms [(i)](#-custom-contexts--enums) | no default | -| ``/`paths` | `List` | DEPRECATED: entries using this context | `[]` | | ``/`default_parameter` | `String` | default parameter name | `context` | | ``/`generate_enum` | `Boolean` | generate enum | `true` | | `children of interfaces` | `Pairs of Alias:Path` | alias interfaces [(i)](#-interfaces) | `null` | diff --git a/slang/bin/slang.dart b/slang/bin/slang.dart index 66dfb9a9..d74b799b 100644 --- a/slang/bin/slang.dart +++ b/slang/bin/slang.dart @@ -1,12 +1,10 @@ import 'dart:io'; -import 'package:collection/collection.dart'; -import 'package:slang/builder/builder/slang_file_collection_builder.dart'; -import 'package:slang/builder/builder/translation_map_builder.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/raw_config.dart'; -import 'package:slang/builder/model/slang_file_collection.dart'; +import 'package:slang/src/builder/builder/slang_file_collection_builder.dart'; +import 'package:slang/src/builder/builder/translation_map_builder.dart'; import 'package:slang/src/builder/generator_facade.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/slang_file_collection.dart'; import 'package:slang/src/builder/utils/file_utils.dart'; import 'package:slang/src/builder/utils/path_utils.dart'; import 'package:slang/src/runner/analyze.dart'; @@ -317,78 +315,45 @@ Future generateTranslations({ // STEP 3: generate .g.dart content final result = GeneratorFacade.generate( rawConfig: fileCollection.config, - baseName: fileCollection.config.outputFileName.getFileNameNoExtension(), translationMap: translationMap, inputDirectoryHint: fileCollection.determineInputPath(), ); // STEP 4: write output to hard drive FileUtils.createMissingFolders(filePath: outputFilePath); - if (fileCollection.config.outputFormat == OutputFormat.singleFile) { - // single file - FileUtils.writeFile( - path: outputFilePath, - content: result.joinAsSingleOutput(), - ); - } else { - // multiple files + + FileUtils.writeFile( + path: BuildResultPaths.mainPath(outputFilePath), + content: result.main, + ); + for (final entry in result.translations.entries) { + final locale = entry.key; + final localeTranslations = entry.value; FileUtils.writeFile( - path: BuildResultPaths.mainPath(outputFilePath), - content: result.header, + path: BuildResultPaths.localePath( + outputPath: outputFilePath, + locale: locale, + ), + content: localeTranslations, ); - for (final entry in result.translations.entries) { - final locale = entry.key; - final localeTranslations = entry.value; - FileUtils.writeFile( - path: BuildResultPaths.localePath( - outputPath: outputFilePath, - locale: locale, - ), - content: localeTranslations, - ); - } - if (result.flatMap != null) { - FileUtils.writeFile( - path: BuildResultPaths.flatMapPath( - outputPath: outputFilePath, - ), - content: result.flatMap!, - ); - } } if (verbose) { print(''); - if (fileCollection.config.outputFormat == OutputFormat.singleFile) { - print('Output: $outputFilePath'); - } else { - print('Output:'); - print(' -> $outputFilePath'); - for (final locale in result.translations.keys) { - print(' -> ${BuildResultPaths.localePath( - outputPath: outputFilePath, - locale: locale, - )}'); - } - if (result.flatMap != null) { - print(' -> ${BuildResultPaths.flatMapPath( - outputPath: outputFilePath, - )}'); - } - print(''); + print('Output:'); + print(' -> $outputFilePath'); + for (final locale in result.translations.keys) { + print(' -> ${BuildResultPaths.localePath( + outputPath: outputFilePath, + locale: locale, + )}'); } + print(''); if (stopwatch != null) { print( '${_GREEN}Translations generated successfully. ${stopwatch.elapsedSeconds}$_RESET'); } - - final deprecatedContext = fileCollection.config.contexts - .firstWhereOrNull((c) => c.enumValues != null || c.paths.isNotEmpty); - if (deprecatedContext != null) { - print( - '$_YELLOW[Deprecated] Use explicit context modifiers instead of populating the config: ${deprecatedContext.enumName} (see: https://github.com/slang-i18n/slang/blob/main/slang/MIGRATION.md#use-context-modifier-since-3190)$_RESET'); - } } } @@ -403,11 +368,6 @@ extension on String { String getFileName() { return PathUtils.getFileName(this); } - - /// converts /some/path/file.json to file - String getFileNameNoExtension() { - return PathUtils.getFileNameNoExtension(this); - } } String? _lastPrint; diff --git a/slang/example/ios/Flutter/AppFrameworkInfo.plist b/slang/example/ios/Flutter/AppFrameworkInfo.plist index 9367d483..7c569640 100644 --- a/slang/example/ios/Flutter/AppFrameworkInfo.plist +++ b/slang/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 12.0 diff --git a/slang/example/ios/Runner.xcodeproj/project.pbxproj b/slang/example/ios/Runner.xcodeproj/project.pbxproj index c6759a6e..1f6541e2 100644 --- a/slang/example/ios/Runner.xcodeproj/project.pbxproj +++ b/slang/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -127,7 +127,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -171,10 +171,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -185,6 +187,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -272,7 +275,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -346,7 +349,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -395,7 +398,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/slang/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/slang/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cf..e67b2808 100644 --- a/slang/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/slang/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ en de + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/slang/example/lib/i18n/strings.g.dart b/slang/example/lib/i18n/strings.g.dart index 93be7c6c..afa1e789 100644 --- a/slang/example/lib/i18n/strings.g.dart +++ b/slang/example/lib/i18n/strings.g.dart @@ -3,20 +3,22 @@ /// Original: lib/i18n /// To regenerate, run: `dart run slang` /// -/// Locales: 2 -/// Strings: 12 (6 per locale) +/// Locales: 3 +/// Strings: 21 (7 per locale) /// -/// Built on 2023-12-04 at 01:28 UTC +/// Built on 2024-10-18 at 00:05 UTC // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; -import 'package:slang/builder/model/node.dart'; +import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; -const AppLocale _baseLocale = AppLocale.en; +import 'strings_de.g.dart' deferred as _$de; +import 'strings_fr_FR.g.dart' deferred as _$fr_FR; +part 'strings_en.g.dart'; /// Supported locales, see extension methods below. /// @@ -25,18 +27,80 @@ const AppLocale _baseLocale = AppLocale.en; /// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum /// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check enum AppLocale with BaseAppLocale { - en(languageCode: 'en', build: Translations.build), - de(languageCode: 'de', build: _StringsDe.build); + en(languageCode: 'en'), + de(languageCode: 'de'), + frFr(languageCode: 'fr', countryCode: 'FR'); - const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element + const AppLocale({ + required this.languageCode, + this.scriptCode, // ignore: unused_element + this.countryCode, // ignore: unused_element + }); @override final String languageCode; @override final String? scriptCode; @override final String? countryCode; - @override final TranslationBuilder build; + + @override + Future build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + await _$de.loadLibrary(); + return _$de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.frFr: + await _$fr_FR.loadLibrary(); + return _$fr_FR.TranslationsFrFr( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + @override + Translations buildSync({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + return _$de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.frFr: + return _$fr_FR.TranslationsFrFr( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } /// Gets current instance managed by [LocaleSettings]. - Translations get translations => LocaleSettings.instance.translationMap[this]!; + Translations get translations => LocaleSettings.instance.getTranslations(this); } /// Method A: Simple @@ -89,12 +153,21 @@ class LocaleSettings extends BaseFlutterLocaleSettings // static aliases (checkout base methods for documentation) static AppLocale get currentLocale => instance.currentLocale; static Stream getLocaleStream() => instance.getLocaleStream(); - static AppLocale setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale useDeviceLocale() => instance.useDeviceLocale(); - @Deprecated('Use [AppLocaleUtils.supportedLocales]') static List get supportedLocales => instance.supportedLocales; - @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') static List get supportedLocalesRaw => instance.supportedLocalesRaw; - static void setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( + static Future setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + static Future setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static Future useDeviceLocale() => instance.useDeviceLocale(); + static Future setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + + // synchronous versions + static AppLocale setLocaleSync(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale useDeviceLocaleSync() => instance.useDeviceLocaleSync(); + static void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync( language: language, locale: locale, cardinalResolver: cardinalResolver, @@ -104,7 +177,10 @@ class LocaleSettings extends BaseFlutterLocaleSettings /// Provides utility functions without any side effects. class AppLocaleUtils extends BaseAppLocaleUtils { - AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values); + AppLocaleUtils._() : super( + baseLocale: AppLocale.en, + locales: AppLocale.values, + ); static final instance = AppLocaleUtils._(); @@ -115,138 +191,3 @@ class AppLocaleUtils extends BaseAppLocaleUtils { static List get supportedLocales => instance.supportedLocales; static List get supportedLocalesRaw => instance.supportedLocalesRaw; } - -// translations - -// Path: -class Translations implements BaseTranslations { - /// Returns the current translations of the given [context]. - /// - /// Usage: - /// final t = Translations.of(context); - static Translations of(BuildContext context) => InheritedLocaleData.of(context).translations; - - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - Translations.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.en, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override final TranslationMetadata $meta; - - /// Access flat map - dynamic operator[](String key) => $meta.getTranslation(key); - - late final Translations _root = this; // ignore: unused_field - - // Translations - late final _StringsMainScreenEn mainScreen = _StringsMainScreenEn._(_root); - Map get locales => { - 'en': 'English', - 'de': 'German', - }; -} - -// Path: mainScreen -class _StringsMainScreenEn { - _StringsMainScreenEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - String get title => 'An English Title'; - String counter({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - one: 'You pressed ${n} time.', - other: 'You pressed ${n} times.', - ); - String get tapMe => 'Tap me'; -} - -// Path: -class _StringsDe implements Translations { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - _StringsDe.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.de, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override final TranslationMetadata $meta; - - /// Access flat map - @override dynamic operator[](String key) => $meta.getTranslation(key); - - @override late final _StringsDe _root = this; // ignore: unused_field - - // Translations - @override late final _StringsMainScreenDe mainScreen = _StringsMainScreenDe._(_root); - @override Map get locales => { - 'en': 'Englisch', - 'de': 'Deutsch', - }; -} - -// Path: mainScreen -class _StringsMainScreenDe implements _StringsMainScreenEn { - _StringsMainScreenDe._(this._root); - - @override final _StringsDe _root; // ignore: unused_field - - // Translations - @override String get title => 'Ein deutscher Titel'; - @override String counter({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, - one: 'Du hast einmal gedrückt.', - other: 'Du hast ${n} mal gedrückt.', - ); - @override String get tapMe => 'Drück mich'; -} - -/// Flat map(s) containing all translations. -/// Only for edge cases! For simple maps, use the map function of this library. - -extension on Translations { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'mainScreen.title': return 'An English Title'; - case 'mainScreen.counter': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - one: 'You pressed ${n} time.', - other: 'You pressed ${n} times.', - ); - case 'mainScreen.tapMe': return 'Tap me'; - case 'locales.en': return 'English'; - case 'locales.de': return 'German'; - default: return null; - } - } -} - -extension on _StringsDe { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'mainScreen.title': return 'Ein deutscher Titel'; - case 'mainScreen.counter': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, - one: 'Du hast einmal gedrückt.', - other: 'Du hast ${n} mal gedrückt.', - ); - case 'mainScreen.tapMe': return 'Drück mich'; - case 'locales.en': return 'Englisch'; - case 'locales.de': return 'Deutsch'; - default: return null; - } - } -} diff --git a/slang/example/lib/i18n/strings.i18n.json b/slang/example/lib/i18n/strings.i18n.json index c754733a..45b233bb 100644 --- a/slang/example/lib/i18n/strings.i18n.json +++ b/slang/example/lib/i18n/strings.i18n.json @@ -9,6 +9,7 @@ }, "locales(map)": { "en": "English", - "de": "German" + "de": "German", + "fr": "French" } } \ No newline at end of file diff --git a/slang/example/lib/i18n/strings_de.g.dart b/slang/example/lib/i18n/strings_de.g.dart new file mode 100644 index 00000000..984c236f --- /dev/null +++ b/slang/example/lib/i18n/strings_de.g.dart @@ -0,0 +1,76 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:slang/node.dart'; +import 'strings.g.dart'; + +// Path: +class TranslationsDe implements Translations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + TranslationsDe({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.de, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + @override dynamic operator[](String key) => $meta.getTranslation(key); + + late final TranslationsDe _root = this; // ignore: unused_field + + // Translations + @override late final _TranslationsMainScreenDe mainScreen = _TranslationsMainScreenDe._(_root); + @override Map get locales => { + 'en': 'Englisch', + 'de': 'Deutsch', + 'fr': 'Französisch', + }; +} + +// Path: mainScreen +class _TranslationsMainScreenDe implements TranslationsMainScreenEn { + _TranslationsMainScreenDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get title => 'Ein deutscher Titel'; + @override String counter({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, + one: 'Du hast einmal gedrückt.', + other: 'Du hast ${n} mal gedrückt.', + ); + @override String get tapMe => 'Drück mich'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on TranslationsDe { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'mainScreen.title': return 'Ein deutscher Titel'; + case 'mainScreen.counter': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, + one: 'Du hast einmal gedrückt.', + other: 'Du hast ${n} mal gedrückt.', + ); + case 'mainScreen.tapMe': return 'Drück mich'; + case 'locales.en': return 'Englisch'; + case 'locales.de': return 'Deutsch'; + case 'locales.fr': return 'Französisch'; + default: return null; + } + } +} + diff --git a/slang/example/lib/i18n/strings_de.i18n.json b/slang/example/lib/i18n/strings_de.i18n.json index 2572eb71..3bb4ea9d 100644 --- a/slang/example/lib/i18n/strings_de.i18n.json +++ b/slang/example/lib/i18n/strings_de.i18n.json @@ -9,6 +9,7 @@ }, "locales(map)": { "en": "Englisch", - "de": "Deutsch" + "de": "Deutsch", + "fr": "Französisch" } } \ No newline at end of file diff --git a/slang/example/lib/i18n/strings_en.g.dart b/slang/example/lib/i18n/strings_en.g.dart new file mode 100644 index 00000000..16e41df4 --- /dev/null +++ b/slang/example/lib/i18n/strings_en.g.dart @@ -0,0 +1,81 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +part of 'strings.g.dart'; + +// Path: +typedef TranslationsEn = Translations; // ignore: unused_element +class Translations implements BaseTranslations { + /// Returns the current translations of the given [context]. + /// + /// Usage: + /// final t = Translations.of(context); + static Translations of(BuildContext context) => InheritedLocaleData.of(context).translations; + + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + Translations({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.en, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + dynamic operator[](String key) => $meta.getTranslation(key); + + late final Translations _root = this; // ignore: unused_field + + // Translations + late final TranslationsMainScreenEn mainScreen = TranslationsMainScreenEn._(_root); + Map get locales => { + 'en': 'English', + 'de': 'German', + 'fr': 'French', + }; +} + +// Path: mainScreen +class TranslationsMainScreenEn { + TranslationsMainScreenEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'An English Title'; + String counter({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + one: 'You pressed ${n} time.', + other: 'You pressed ${n} times.', + ); + String get tapMe => 'Tap me'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on Translations { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'mainScreen.title': return 'An English Title'; + case 'mainScreen.counter': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + one: 'You pressed ${n} time.', + other: 'You pressed ${n} times.', + ); + case 'mainScreen.tapMe': return 'Tap me'; + case 'locales.en': return 'English'; + case 'locales.de': return 'German'; + case 'locales.fr': return 'French'; + default: return null; + } + } +} + diff --git a/slang/example/lib/i18n/strings_fr_FR.g.dart b/slang/example/lib/i18n/strings_fr_FR.g.dart new file mode 100644 index 00000000..c0e2c8d1 --- /dev/null +++ b/slang/example/lib/i18n/strings_fr_FR.g.dart @@ -0,0 +1,76 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:slang/node.dart'; +import 'strings.g.dart'; + +// Path: +class TranslationsFrFr implements Translations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + TranslationsFrFr({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.frFr, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + @override dynamic operator[](String key) => $meta.getTranslation(key); + + late final TranslationsFrFr _root = this; // ignore: unused_field + + // Translations + @override late final _TranslationsMainScreenFrFr mainScreen = _TranslationsMainScreenFrFr._(_root); + @override Map get locales => { + 'en': 'Anglais', + 'de': 'Allemand', + 'fr': 'Français', + }; +} + +// Path: mainScreen +class _TranslationsMainScreenFrFr implements TranslationsMainScreenEn { + _TranslationsMainScreenFrFr._(this._root); + + final TranslationsFrFr _root; // ignore: unused_field + + // Translations + @override String get title => 'Le titre français'; + @override String counter({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('fr'))(n, + one: 'Vous avez appuyé une fois.', + other: 'Vous avez appuyé ${n} fois.', + ); + @override String get tapMe => 'Appuyez-moi'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on TranslationsFrFr { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'mainScreen.title': return 'Le titre français'; + case 'mainScreen.counter': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('fr'))(n, + one: 'Vous avez appuyé une fois.', + other: 'Vous avez appuyé ${n} fois.', + ); + case 'mainScreen.tapMe': return 'Appuyez-moi'; + case 'locales.en': return 'Anglais'; + case 'locales.de': return 'Allemand'; + case 'locales.fr': return 'Français'; + default: return null; + } + } +} + diff --git a/slang/example/lib/i18n/strings_fr_FR.i18n.json b/slang/example/lib/i18n/strings_fr_FR.i18n.json new file mode 100644 index 00000000..2d03a48d --- /dev/null +++ b/slang/example/lib/i18n/strings_fr_FR.i18n.json @@ -0,0 +1,15 @@ +{ + "mainScreen": { + "title": "Le titre français", + "counter": { + "one": "Vous avez appuyé une fois.", + "other": "Vous avez appuyé $n fois." + }, + "tapMe": "Appuyez-moi" + }, + "locales(map)": { + "en": "Anglais", + "de": "Allemand", + "fr": "Français" + } +} \ No newline at end of file diff --git a/slang/example/lib/main.dart b/slang/example/lib/main.dart index e2a07c99..eb5c8832 100644 --- a/slang/example/lib/main.dart +++ b/slang/example/lib/main.dart @@ -18,7 +18,9 @@ class MyApp extends StatelessWidget { title: 'Flutter Demo', locale: TranslationProvider.of(context).flutterLocale, supportedLocales: AppLocaleUtils.supportedLocales, - localizationsDelegates: GlobalMaterialLocalizations.delegates, + localizationsDelegates: [ + ...GlobalMaterialLocalizations.delegates, + ], home: MyHomePage(), ); } diff --git a/slang/example/pubspec.yaml b/slang/example/pubspec.yaml index d002d5bd..ff677fd4 100644 --- a/slang/example/pubspec.yaml +++ b/slang/example/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: slang_flutter: ^3.16.0 # add this flutter_localizations: # add this sdk: flutter + intl: any dev_dependencies: lints: ^2.0.0 @@ -22,4 +23,4 @@ dev_dependencies: # slang_build_runner: any flutter: - uses-material-design: true \ No newline at end of file + uses-material-design: true diff --git a/slang/lib/builder/model/build_result.dart b/slang/lib/builder/model/build_result.dart deleted file mode 100644 index 09b670d4..00000000 --- a/slang/lib/builder/model/build_result.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:slang/builder/model/i18n_locale.dart'; - -/// the resulting output strings -/// It can either be rendered as a single file -/// or as multiple files. -class BuildResult { - final String header; - final Map translations; - final String? flatMap; - - BuildResult({ - required this.header, - required this.translations, - required this.flatMap, - }); - - String joinAsSingleOutput() { - final buffer = StringBuffer(); - buffer.writeln(header); - buffer.writeln('// translations'); - for (final localeTranslations in translations.values) { - buffer.write(localeTranslations); - } - if (flatMap != null) { - buffer.writeln(); - buffer.write(flatMap); - } - return buffer.toString(); - } -} diff --git a/slang/lib/node.dart b/slang/lib/node.dart new file mode 100644 index 00000000..596db32b --- /dev/null +++ b/slang/lib/node.dart @@ -0,0 +1 @@ +export 'package:slang/src/builder/model/node.dart'; diff --git a/slang/lib/overrides.dart b/slang/lib/overrides.dart new file mode 100644 index 00000000..12830b56 --- /dev/null +++ b/slang/lib/overrides.dart @@ -0,0 +1,4 @@ +export 'package:slang/src/api/translation_overrides.dart'; +export 'package:slang/src/builder/model/build_model_config.dart'; +export 'package:slang/src/builder/model/context_type.dart'; +export 'package:slang/src/builder/model/enums.dart'; diff --git a/slang/lib/secret.dart b/slang/lib/secret.dart new file mode 100644 index 00000000..bad59ad7 --- /dev/null +++ b/slang/lib/secret.dart @@ -0,0 +1 @@ +export 'package:slang/src/api/secret.dart'; diff --git a/slang/lib/slang.dart b/slang/lib/slang.dart index b9f4fd9d..df3065b2 100644 --- a/slang/lib/slang.dart +++ b/slang/lib/slang.dart @@ -1,3 +1,3 @@ -export 'api/locale.dart'; -export 'api/pluralization.dart'; -export 'api/singleton.dart'; +export 'src/api/locale.dart'; +export 'src/api/pluralization.dart'; +export 'src/api/singleton.dart'; diff --git a/slang/lib/api/locale.dart b/slang/lib/src/api/locale.dart similarity index 69% rename from slang/lib/api/locale.dart rename to slang/lib/src/api/locale.dart index dcda6d41..7553f1c6 100644 --- a/slang/lib/api/locale.dart +++ b/slang/lib/src/api/locale.dart @@ -1,5 +1,5 @@ -import 'package:slang/api/pluralization.dart'; -import 'package:slang/builder/model/node.dart'; +import 'package:slang/src/api/pluralization.dart'; +import 'package:slang/src/builder/model/node.dart'; /// Root translation class of ONE locale /// Entry point for every translation @@ -50,19 +50,10 @@ class TranslationMetadata, } } -/// Returns a new translation instance. -typedef TranslationBuilder, - T extends BaseTranslations> - = T Function({ - Map? overrides, - PluralResolver? cardinalResolver, - PluralResolver? ordinalResolver, -}); - /// Similar to flutter locale /// but available without any flutter dependencies. /// Subclasses will be enums. -abstract class BaseAppLocale, +abstract mixin class BaseAppLocale, T extends BaseTranslations> { String get languageCode; @@ -75,9 +66,22 @@ abstract class BaseAppLocale, /// Suitable for dependency injection and unit tests. /// /// Usage: - /// final t = AppLocale.en.build(); // build + /// final t = await AppLocale.en.build(); // build /// String a = t.my.path; // access - TranslationBuilder get build; + Future build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }); + + /// Similar to [build] but synchronous. + /// This might throw an error on Web if + /// the library is not loaded yet (Deferred Loading). + T buildSync({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }); static final BaseAppLocale undefinedLocale = FakeAppLocale(languageCode: 'und'); @@ -114,17 +118,38 @@ class FakeAppLocale extends BaseAppLocale { }); @override - TranslationBuilder get build { - return ({overrides, cardinalResolver, ordinalResolver}) => FakeTranslations( - FakeAppLocale( - languageCode: languageCode, - scriptCode: scriptCode, - countryCode: countryCode, - ), - overrides: overrides, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ); + Future build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async => + FakeTranslations( + FakeAppLocale( + languageCode: languageCode, + scriptCode: scriptCode, + countryCode: countryCode, + ), + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + + @override + FakeTranslations buildSync({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) { + return FakeTranslations( + FakeAppLocale( + languageCode: languageCode, + scriptCode: scriptCode, + countryCode: countryCode, + ), + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); } } diff --git a/slang/lib/api/plural_resolver_map.dart b/slang/lib/src/api/plural_resolver_map.dart similarity index 100% rename from slang/lib/api/plural_resolver_map.dart rename to slang/lib/src/api/plural_resolver_map.dart diff --git a/slang/lib/api/pluralization.dart b/slang/lib/src/api/pluralization.dart similarity index 100% rename from slang/lib/api/pluralization.dart rename to slang/lib/src/api/pluralization.dart diff --git a/slang/lib/api/secret.dart b/slang/lib/src/api/secret.dart similarity index 100% rename from slang/lib/api/secret.dart rename to slang/lib/src/api/secret.dart diff --git a/slang/lib/api/singleton.dart b/slang/lib/src/api/singleton.dart similarity index 58% rename from slang/lib/api/singleton.dart rename to slang/lib/src/api/singleton.dart index 7184589a..beb4f23b 100644 --- a/slang/lib/api/singleton.dart +++ b/slang/lib/src/api/singleton.dart @@ -1,11 +1,11 @@ import 'package:collection/collection.dart'; -import 'package:slang/api/locale.dart'; -import 'package:slang/api/pluralization.dart'; -import 'package:slang/api/state.dart'; -import 'package:slang/builder/builder/translation_model_builder.dart'; -import 'package:slang/builder/model/build_model_config.dart'; -import 'package:slang/builder/model/enums.dart'; +import 'package:slang/src/api/locale.dart'; +import 'package:slang/src/api/pluralization.dart'; +import 'package:slang/src/api/state.dart'; +import 'package:slang/src/builder/builder/translation_model_builder.dart'; import 'package:slang/src/builder/decoder/base_decoder.dart'; +import 'package:slang/src/builder/model/build_model_config.dart'; +import 'package:slang/src/builder/model/enums.dart'; import 'package:slang/src/builder/utils/map_utils.dart'; import 'package:slang/src/builder/utils/node_utils.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; @@ -119,14 +119,31 @@ extension AppLocaleUtilsExt, } /// Creates a translation instance with overrides stored in [content]. - T buildWithOverrides({ + Future buildWithOverrides({ + required E locale, + required FileType fileType, + required String content, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + return await buildWithOverridesFromMap( + locale: locale, + isFlatMap: false, + map: BaseDecoder.decodeWithFileType(fileType, content), + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + + /// Sync version of [buildWithOverrides]. + T buildWithOverridesSync({ required E locale, required FileType fileType, required String content, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, }) { - return buildWithOverridesFromMap( + return buildWithOverridesFromMapSync( locale: locale, isFlatMap: false, map: BaseDecoder.decodeWithFileType(fileType, content), @@ -136,7 +153,50 @@ extension AppLocaleUtilsExt, } /// Creates a translation instance using the given [map]. - T buildWithOverridesFromMap({ + Future buildWithOverridesFromMap({ + required E locale, + required bool isFlatMap, + required Map map, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + final buildResult = _buildWithOverridesFromMap( + locale: locale, + isFlatMap: isFlatMap, + map: map, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + return await locale.build( + overrides: buildResult.root.toFlatMap(), + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + + /// Sync version of [buildWithOverridesFromMap]. + T buildWithOverridesFromMapSync({ + required E locale, + required bool isFlatMap, + required Map map, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) { + final buildResult = _buildWithOverridesFromMap( + locale: locale, + isFlatMap: isFlatMap, + map: map, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + return locale.buildSync( + overrides: buildResult.root.toFlatMap(), + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + + BuildModelResult _buildWithOverridesFromMap({ required E locale, required bool isFlatMap, required Map map, @@ -162,28 +222,29 @@ extension AppLocaleUtilsExt, digestedMap = MapUtils.deepCast(map); } - final buildResult = TranslationModelBuilder.build( + return TranslationModelBuilder.build( buildConfig: buildConfig!, map: digestedMap, handleLinks: false, shouldEscapeText: false, localeDebug: locale.languageTag, ); - - return locale.build( - overrides: buildResult.root.toFlatMap(), - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ); } } abstract class BaseLocaleSettings, T extends BaseTranslations> { - /// Internal: Manages all translation instances - /// May be modified when setting a custom plural resolver + /// Internal: Manages all translation instances. + /// The base locale is always included. + /// Additional locales are added when calling [loadLocale]. + /// May be modified when setting a custom plural resolver. final Map translationMap; + /// Internal: + /// Keeps track of loading translations to prevent multiple requests. + /// This lock is sufficient because Dart's async loop is single-threaded. + final Set translationsLoading = {}; + /// Internal: Reference to utils instance final BaseAppLocaleUtils utils; @@ -193,7 +254,9 @@ abstract class BaseLocaleSettings, BaseLocaleSettings({ required this.utils, - }) : translationMap = _buildMap(utils.locales); + }) : translationMap = { + utils.baseLocale: utils.baseLocale.buildSync(), + }; /// Updates the provider state and therefore triggers a rebuild /// on all widgets listening to this provider. @@ -206,12 +269,70 @@ abstract class BaseLocaleSettings, // We use extension methods here to have a workaround for static members of the same name extension LocaleSettingsExt, T extends BaseTranslations> on BaseLocaleSettings { - /// Gets current locale. + /// Returns true if the translations of the given [locale] are loaded. + bool isLocaleLoaded(E locale) { + return translationMap.containsKey(locale); + } + + /// Loads the translations of the given [locale] if not already loaded. + Future loadLocale(E locale) async { + if (translationMap.containsKey(locale)) { + // already loaded + return; + } + + if (translationsLoading.contains(locale)) { + // already loading + return; + } + + translationsLoading.add(locale); + translationMap[locale] = await locale.build(); + translationsLoading.remove(locale); + } + + /// Sync version of [loadLocale]. + void loadLocaleSync(E locale) { + if (translationMap.containsKey(locale)) { + // already loaded + return; + } + + translationMap[locale] = locale.buildSync(); + } + + /// Loads all locales. + Future loadAllLocales() async { + for (final locale in utils.locales) { + await loadLocale(locale); + } + } + + /// Sync version of [loadAllLocales]. + void loadAllLocalesSync() { + for (final locale in utils.locales) { + loadLocaleSync(locale); + } + } + + /// Gets the current locale. E get currentLocale { final locale = GlobalLocaleState.instance.getLocale(); return utils.parseAppLocale(locale); } + /// Gets the current translations. + /// Falls back to the base locale if the current locale is not loaded. + T get currentTranslations { + return translationMap[currentLocale] ?? translationMap[utils.baseLocale]!; + } + + /// Gets the translations of the given [locale]. + /// Falls back to the base locale if the given locale is not loaded. + T getTranslations(E locale) { + return translationMap[locale] ?? translationMap[utils.baseLocale]!; + } + /// Gets the broadcast stream to keep track of every locale change. /// /// It fires every time LocaleSettings.setLocale, LocaleSettings.setLocaleRaw, @@ -233,11 +354,6 @@ extension LocaleSettingsExt, }); } - /// Gets current translations - T get currentTranslations { - return translationMap[currentLocale]!; - } - /// Gets supported locales in string format. List get supportedLocalesRaw { return utils.supportedLocalesRaw; @@ -249,7 +365,18 @@ extension LocaleSettingsExt, /// Locale gets changed automatically if [listenToDeviceLocale] is true /// and [TranslationProvider] is used. If null, then the last state is used. /// By default, calling this method disables the listener. - E setLocale(E locale, {bool? listenToDeviceLocale = false}) { + Future setLocale(E locale, {bool? listenToDeviceLocale = false}) async { + await loadLocale(locale); + return _setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + } + + /// Sync version of [setLocale]. + E setLocaleSync(E locale, {bool? listenToDeviceLocale = false}) { + loadLocaleSync(locale); + return _setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + } + + E _setLocale(locale, {required bool? listenToDeviceLocale}) { GlobalLocaleState.instance.setLocale(locale); updateProviderState(locale); if (listenToDeviceLocale != null) { @@ -265,37 +392,65 @@ extension LocaleSettingsExt, /// Locale gets changed automatically if [listenToDeviceLocale] is true /// and [TranslationProvider] is used. If null, then the last state is used. /// By default, calling this method disables the listener. - E setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) { + Future setLocaleRaw( + String rawLocale, { + bool? listenToDeviceLocale = false, + }) async { final E locale = utils.parse(rawLocale); - return setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + return await setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + } + + /// Sync version of [setLocaleRaw]. + E setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) { + final E locale = utils.parse(rawLocale); + return setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale); } /// Sets plural resolvers. /// See https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html /// See https://github.com/slang-i18n/slang/blob/main/slang/lib/api/plural_resolver_map.dart /// Either specify [language], or [locale]. [locale] has precedence. - void setPluralResolver({ + Future setPluralResolver({ String? language, E? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, - }) { - final List targetLocales; - if (locale != null) { - // take only this locale - targetLocales = [locale]; - } else if (language != null) { - // map to language - targetLocales = - utils.locales.where((l) => l.languageCode == language).toList(); - } else { - throw 'Either language or locale must be specified'; + }) async { + final List targetLocales = _getTargetLocales( + language: language, + locale: locale, + ); + + // update translation instances + for (final curr in targetLocales) { + await loadLocale(curr); + final overrides = translationMap[curr]!.$meta.overrides; + translationMap[curr] = await curr.build( + // keep old overrides + overrides: overrides.isNotEmpty ? overrides : null, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); } + } + + /// Sync version of [setPluralResolver]. + void setPluralResolverSync({ + String? language, + E? locale, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + final List targetLocales = _getTargetLocales( + language: language, + locale: locale, + ); // update translation instances for (final curr in targetLocales) { + loadLocaleSync(curr); final overrides = translationMap[curr]!.$meta.overrides; - translationMap[curr] = curr.build( + translationMap[curr] = curr.buildSync( // keep old overrides overrides: overrides.isNotEmpty ? overrides : null, cardinalResolver: cardinalResolver, @@ -304,6 +459,21 @@ extension LocaleSettingsExt, } } + List _getTargetLocales({ + String? language, + E? locale, + }) { + if (locale != null) { + // take only this locale + return [locale]; + } else if (language != null) { + // map to language + return utils.locales.where((l) => l.languageCode == language).toList(); + } else { + throw 'Either language or locale must be specified'; + } + } + /// Overrides existing translations of [locale] with new ones from [content]. /// The [content] should be formatted and structured exactly the same way /// as the original files. @@ -315,13 +485,32 @@ extension LocaleSettingsExt, /// Calling this method multiple times will delete the old overrides. /// /// Please do a try-catch to prevent app crashes! - void overrideTranslations({ + Future overrideTranslations({ + required E locale, + required FileType fileType, + required String content, + }) async { + final currentMetadata = translationMap[locale]!.$meta; + translationMap[locale] = await utils.buildWithOverrides( + locale: locale, + content: content, + fileType: fileType, + cardinalResolver: currentMetadata.cardinalResolver, + ordinalResolver: currentMetadata.ordinalResolver, + ); + if (locale == currentLocale) { + updateProviderState(locale); + } + } + + /// Sync version of [overrideTranslations]. + void overrideTranslationsSync({ required E locale, required FileType fileType, required String content, }) { final currentMetadata = translationMap[locale]!.$meta; - translationMap[locale] = utils.buildWithOverrides( + translationMap[locale] = utils.buildWithOverridesSync( locale: locale, content: content, fileType: fileType, @@ -342,13 +531,13 @@ extension LocaleSettingsExt, /// Checkout [overrideTranslations] for more documentation. /// /// Please do a try-catch to prevent app crashes! - void overrideTranslationsFromMap({ + Future overrideTranslationsFromMap({ required E locale, required bool isFlatMap, required Map map, - }) { + }) async { final currentMetadata = translationMap[locale]!.$meta; - translationMap[locale] = utils.buildWithOverridesFromMap( + translationMap[locale] = await utils.buildWithOverridesFromMap( locale: locale, isFlatMap: isFlatMap, map: map, @@ -359,12 +548,23 @@ extension LocaleSettingsExt, updateProviderState(locale); } } -} -Map - _buildMap, T extends BaseTranslations>( - List locales) { - return { - for (final key in locales) key: key.build(), - }; + /// Sync version of [overrideTranslationsFromMap]. + void overrideTranslationsFromMapSync({ + required E locale, + required bool isFlatMap, + required Map map, + }) { + final currentMetadata = translationMap[locale]!.$meta; + translationMap[locale] = utils.buildWithOverridesFromMapSync( + locale: locale, + isFlatMap: isFlatMap, + map: map, + cardinalResolver: currentMetadata.cardinalResolver, + ordinalResolver: currentMetadata.ordinalResolver, + ); + if (locale == currentLocale) { + updateProviderState(locale); + } + } } diff --git a/slang/lib/api/state.dart b/slang/lib/src/api/state.dart similarity index 96% rename from slang/lib/api/state.dart rename to slang/lib/src/api/state.dart index 493ddec4..335bd86b 100644 --- a/slang/lib/api/state.dart +++ b/slang/lib/src/api/state.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:slang/api/locale.dart'; +import 'package:slang/src/api/locale.dart'; /// The [GlobalLocaleState] storing the global locale. /// It is *shared* among all packages of an app. diff --git a/slang/lib/api/translation_overrides.dart b/slang/lib/src/api/translation_overrides.dart similarity index 96% rename from slang/lib/api/translation_overrides.dart rename to slang/lib/src/api/translation_overrides.dart index 08a24856..6104d8ac 100644 --- a/slang/lib/api/translation_overrides.dart +++ b/slang/lib/src/api/translation_overrides.dart @@ -1,8 +1,8 @@ -import 'package:slang/api/locale.dart'; -import 'package:slang/api/pluralization.dart'; -import 'package:slang/builder/model/node.dart'; -import 'package:slang/builder/model/pluralization.dart'; +import 'package:slang/src/api/locale.dart'; +import 'package:slang/src/api/pluralization.dart'; import 'package:slang/src/builder/generator/helper.dart'; +import 'package:slang/src/builder/model/node.dart'; +import 'package:slang/src/builder/model/pluralization.dart'; import 'package:slang/src/builder/utils/reflection_utils.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; import 'package:slang/src/builder/utils/string_interpolation_extensions.dart'; diff --git a/slang/lib/builder/builder/build_model_config_builder.dart b/slang/lib/src/builder/builder/build_model_config_builder.dart similarity index 80% rename from slang/lib/builder/builder/build_model_config_builder.dart rename to slang/lib/src/builder/builder/build_model_config_builder.dart index ca8d7e36..7e249cd9 100644 --- a/slang/lib/builder/builder/build_model_config_builder.dart +++ b/slang/lib/src/builder/builder/build_model_config_builder.dart @@ -1,5 +1,5 @@ -import 'package:slang/builder/model/build_model_config.dart'; -import 'package:slang/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/build_model_config.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; extension BuildModelConfigBuilder on RawConfig { BuildModelConfig toBuildModelConfig() { diff --git a/slang/lib/builder/builder/generate_config_builder.dart b/slang/lib/src/builder/builder/generate_config_builder.dart similarity index 71% rename from slang/lib/builder/builder/generate_config_builder.dart rename to slang/lib/src/builder/builder/generate_config_builder.dart index a5f1d435..bb526e23 100644 --- a/slang/lib/builder/builder/generate_config_builder.dart +++ b/slang/lib/src/builder/builder/generate_config_builder.dart @@ -1,13 +1,12 @@ -import 'package:slang/builder/builder/build_model_config_builder.dart'; -import 'package:slang/builder/model/context_type.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/generate_config.dart'; -import 'package:slang/builder/model/interface.dart'; -import 'package:slang/builder/model/raw_config.dart'; +import 'package:slang/src/builder/builder/build_model_config_builder.dart'; +import 'package:slang/src/builder/model/context_type.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/generate_config.dart'; +import 'package:slang/src/builder/model/interface.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; class GenerateConfigBuilder { static GenerateConfig build({ - required String baseName, required RawConfig config, required String inputDirectoryHint, required List contexts, @@ -16,11 +15,9 @@ class GenerateConfigBuilder { return GenerateConfig( buildConfig: config.toBuildModelConfig(), inputDirectoryHint: inputDirectoryHint, - baseName: baseName, baseLocale: config.baseLocale, fallbackStrategy: config.fallbackStrategy.toGenerateFallbackStrategy(), outputFileName: config.outputFileName, - outputFormat: config.outputFormat, localeHandling: config.localeHandling, flutterIntegration: config.flutterIntegration, translateVariable: config.translateVar, diff --git a/slang/lib/builder/builder/raw_config_builder.dart b/slang/lib/src/builder/builder/raw_config_builder.dart similarity index 91% rename from slang/lib/builder/builder/raw_config_builder.dart rename to slang/lib/src/builder/builder/raw_config_builder.dart index 05b2233f..15dc63a5 100644 --- a/slang/lib/builder/builder/raw_config_builder.dart +++ b/slang/lib/src/builder/builder/raw_config_builder.dart @@ -1,9 +1,9 @@ -import 'package:slang/builder/model/context_type.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/interface.dart'; -import 'package:slang/builder/model/obfuscation_config.dart'; -import 'package:slang/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/context_type.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/interface.dart'; +import 'package:slang/src/builder/model/obfuscation_config.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; import 'package:slang/src/builder/utils/map_utils.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; import 'package:slang/src/builder/utils/string_extensions.dart'; @@ -47,6 +47,12 @@ class RawConfigBuilder { /// Parses the config entry static RawConfig fromMap(Map map) { + if (map['output_format'] != null) { + print( + 'The "output_format" key is no longer supported since slang v4. Always generates multiple files now.', + ); + } + return RawConfig( baseLocale: I18nLocale.fromString( map['base_locale'] ?? RawConfig.defaultBaseLocale), @@ -63,8 +69,6 @@ class RawConfigBuilder { RawConfig.defaultOutputDirectory, outputFileName: map['output_file_name'] ?? RawConfig.defaultOutputFileName, - outputFormat: (map['output_format'] as String?)?.toOutputFormat() ?? - RawConfig.defaultOutputFormat, localeHandling: map['locale_handling'] ?? RawConfig.defaultLocaleHandling, flutterIntegration: map['flutter_integration'] ?? RawConfig.defaultFlutterIntegration, @@ -121,14 +125,8 @@ extension on Map { final enumName = e.key; final config = e.value as Map; - if (config['auto'] != null) { - print('context "auto" config is redundant. Remove it.'); - } - return ContextType( enumName: enumName, - enumValues: config['enum']?.cast(), - paths: config['paths']?.cast() ?? ContextType.defaultPaths, defaultParameter: config['default_parameter'] ?? ContextType.DEFAULT_PARAMETER, generateEnum: @@ -239,17 +237,6 @@ extension on String { } } - OutputFormat? toOutputFormat() { - switch (this) { - case 'single_file': - return OutputFormat.singleFile; - case 'multiple_files': - return OutputFormat.multipleFiles; - default: - return null; - } - } - TranslationClassVisibility? toTranslationClassVisibility() { switch (this) { case 'private': diff --git a/slang/lib/builder/builder/slang_file_collection_builder.dart b/slang/lib/src/builder/builder/slang_file_collection_builder.dart similarity index 96% rename from slang/lib/builder/builder/slang_file_collection_builder.dart rename to slang/lib/src/builder/builder/slang_file_collection_builder.dart index 8029763c..e8565357 100644 --- a/slang/lib/builder/builder/slang_file_collection_builder.dart +++ b/slang/lib/src/builder/builder/slang_file_collection_builder.dart @@ -2,10 +2,10 @@ import 'dart:collection'; import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/raw_config.dart'; -import 'package:slang/builder/model/slang_file_collection.dart'; +import 'package:slang/src/builder/builder/raw_config_builder.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/slang_file_collection.dart'; import 'package:slang/src/builder/utils/path_utils.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; diff --git a/slang/lib/src/builder/builder/text_parser.dart b/slang/lib/src/builder/builder/text_parser.dart index 9b4c9c6e..21b17448 100644 --- a/slang/lib/src/builder/builder/text_parser.dart +++ b/slang/lib/src/builder/builder/text_parser.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/model/enums.dart'; +import 'package:slang/src/builder/model/enums.dart'; import 'package:slang/src/builder/utils/string_extensions.dart'; class ParseParamResult { diff --git a/slang/lib/builder/builder/translation_map_builder.dart b/slang/lib/src/builder/builder/translation_map_builder.dart similarity index 94% rename from slang/lib/builder/builder/translation_map_builder.dart rename to slang/lib/src/builder/builder/translation_map_builder.dart index e0f6aee6..6e43e9ad 100644 --- a/slang/lib/builder/builder/translation_map_builder.dart +++ b/slang/lib/src/builder/builder/translation_map_builder.dart @@ -1,9 +1,9 @@ -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/slang_file_collection.dart'; -import 'package:slang/builder/model/translation_map.dart'; import 'package:slang/src/builder/decoder/base_decoder.dart'; import 'package:slang/src/builder/decoder/csv_decoder.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/slang_file_collection.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; class TranslationMapBuilder { /// This method transforms files to an intermediate model [TranslationMap]. diff --git a/slang/lib/builder/builder/translation_model_builder.dart b/slang/lib/src/builder/builder/translation_model_builder.dart similarity index 94% rename from slang/lib/builder/builder/translation_model_builder.dart rename to slang/lib/src/builder/builder/translation_model_builder.dart index f5638c8a..155e397d 100644 --- a/slang/lib/builder/builder/translation_model_builder.dart +++ b/slang/lib/src/builder/builder/translation_model_builder.dart @@ -1,12 +1,12 @@ import 'dart:collection'; import 'package:collection/collection.dart'; -import 'package:slang/builder/model/build_model_config.dart'; -import 'package:slang/builder/model/context_type.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/interface.dart'; -import 'package:slang/builder/model/node.dart'; -import 'package:slang/builder/model/pluralization.dart'; +import 'package:slang/src/builder/model/build_model_config.dart'; +import 'package:slang/src/builder/model/context_type.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/interface.dart'; +import 'package:slang/src/builder/model/node.dart'; +import 'package:slang/src/builder/model/pluralization.dart'; import 'package:slang/src/builder/utils/node_utils.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; import 'package:slang/src/builder/utils/string_extensions.dart'; @@ -66,7 +66,8 @@ class TranslationModelBuilder { }; final contextCollection = { - for (final context in buildConfig.contexts) context.enumName: context, + for (final context in buildConfig.contexts) + context.enumName: context.toPending(), }; // 1st iteration: Build nodes according to given map @@ -226,7 +227,7 @@ Map _parseMapNode({ required BuildModelConfig config, required CaseStyle? keyCase, required Map leavesMap, - required Map contextCollection, + required Map contextCollection, required BuildModelResult? baseData, required Map? baseContexts, required bool shouldEscapeText, @@ -392,20 +393,17 @@ Map _parseMapNode({ } if (detectedType.nodeType == _DetectionType.context) { - ContextType? context = contextCollection[detectedType.contextHint!]; - if (context == null || context.enumValues == null) { - // infer new context type - context = ContextType( - enumName: detectedType.contextHint!, - enumValues: digestedMap.keys.toList(), - paths: context?.paths ?? ContextType.defaultPaths, - defaultParameter: - context?.defaultParameter ?? ContextType.DEFAULT_PARAMETER, - generateEnum: - context?.generateEnum ?? ContextType.defaultGenerateEnum, + final enumName = detectedType.contextHint!; + PendingContextType? context = contextCollection[enumName]; + if (context == null) { + context = PendingContextType( + enumName: enumName, + defaultParameter: ContextType.DEFAULT_PARAMETER, + generateEnum: ContextType.defaultGenerateEnum, ); contextCollection[context.enumName] = context; } + context.enumValues ??= digestedMap.keys.toList(); if (config.fallbackStrategy == FallbackStrategy.baseLocale || config.fallbackStrategy == @@ -558,21 +556,6 @@ _DetectionResult _determineNodeType( } } - for (final contextType in config.contexts) { - if (contextType.paths.contains(nodePath)) { - return _DetectionResult(_DetectionType.context, contextType.enumName); - } else if (contextType.paths.isEmpty) { - // empty paths => auto detection - final isContext = contextType.enumValues != null && - childrenSplitByComma.length == contextType.enumValues!.length && - childrenSplitByComma - .every((key) => contextType.enumValues!.any((e) => e == key)); - if (isContext) { - return _DetectionResult(_DetectionType.context, contextType.enumName); - } - } - } - // fallback: every node is a class by default return _DetectionResult(_DetectionType.classType); } diff --git a/slang/lib/builder/builder/translation_model_list_builder.dart b/slang/lib/src/builder/builder/translation_model_list_builder.dart similarity index 85% rename from slang/lib/builder/builder/translation_model_list_builder.dart rename to slang/lib/src/builder/builder/translation_model_list_builder.dart index 09a765ac..e9197c62 100644 --- a/slang/lib/builder/builder/translation_model_list_builder.dart +++ b/slang/lib/src/builder/builder/translation_model_list_builder.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/builder/build_model_config_builder.dart'; -import 'package:slang/builder/builder/translation_model_builder.dart'; -import 'package:slang/builder/model/i18n_data.dart'; -import 'package:slang/builder/model/raw_config.dart'; -import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/builder/build_model_config_builder.dart'; +import 'package:slang/src/builder/builder/translation_model_builder.dart'; +import 'package:slang/src/builder/model/i18n_data.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; class TranslationModelListBuilder { /// Combine all namespaces and build the internal model diff --git a/slang/lib/src/builder/decoder/arb_decoder.dart b/slang/lib/src/builder/decoder/arb_decoder.dart index d691e35d..21b11915 100644 --- a/slang/lib/src/builder/decoder/arb_decoder.dart +++ b/slang/lib/src/builder/decoder/arb_decoder.dart @@ -1,7 +1,7 @@ import 'dart:convert'; -import 'package:slang/builder/model/enums.dart'; import 'package:slang/src/builder/decoder/base_decoder.dart'; +import 'package:slang/src/builder/model/enums.dart'; import 'package:slang/src/builder/utils/brackets_utils.dart'; import 'package:slang/src/builder/utils/map_utils.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; diff --git a/slang/lib/src/builder/decoder/base_decoder.dart b/slang/lib/src/builder/decoder/base_decoder.dart index 8d83fec9..c7c52cef 100644 --- a/slang/lib/src/builder/decoder/base_decoder.dart +++ b/slang/lib/src/builder/decoder/base_decoder.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/model/enums.dart'; import 'package:slang/src/builder/decoder/arb_decoder.dart'; import 'package:slang/src/builder/decoder/csv_decoder.dart'; import 'package:slang/src/builder/decoder/json_decoder.dart'; import 'package:slang/src/builder/decoder/yaml_decoder.dart'; +import 'package:slang/src/builder/model/enums.dart'; abstract class BaseDecoder { /// Transforms the raw string (json, yaml, csv) diff --git a/slang/lib/src/builder/generator/generate_header.dart b/slang/lib/src/builder/generator/generate_header.dart index c9bfefe3..a878630b 100644 --- a/slang/lib/src/builder/generator/generate_header.dart +++ b/slang/lib/src/builder/generator/generate_header.dart @@ -1,17 +1,15 @@ -import 'package:slang/builder/model/build_model_config.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/generate_config.dart'; -import 'package:slang/builder/model/i18n_data.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/node.dart'; import 'package:slang/src/builder/generator/helper.dart'; +import 'package:slang/src/builder/model/build_model_config.dart'; +import 'package:slang/src/builder/model/generate_config.dart'; +import 'package:slang/src/builder/model/i18n_data.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/node.dart'; import 'package:slang/src/builder/utils/path_utils.dart'; String generateHeader( GenerateConfig config, List allLocales, ) { - const String baseLocaleVar = '_baseLocale'; const String pluralResolverType = 'PluralResolver'; const String pluralResolverMapCardinal = '_pluralResolversCardinal'; const String pluralResolverMapOrdinal = '_pluralResolversOrdinal'; @@ -27,13 +25,11 @@ String generateHeader( _generateImports(config, buffer); - if (config.outputFormat == OutputFormat.multipleFiles) { - _generateParts( - buffer: buffer, - config: config, - locales: allLocales, - ); - } + _generateLocaleImports( + buffer: buffer, + config: config, + locales: allLocales, + ); if (config.translationOverrides) { _generateBuildConfig( @@ -42,12 +38,6 @@ String generateHeader( ); } - _generateBaseLocale( - buffer: buffer, - config: config, - baseLocaleVar: baseLocaleVar, - ); - _generateEnum( buffer: buffer, config: config, @@ -73,7 +63,6 @@ String generateHeader( _generateUtil( buffer: buffer, config: config, - baseLocaleVar: baseLocaleVar, ); _generateContextEnums(buffer: buffer, config: config); @@ -131,22 +120,16 @@ void _generateHeaderComment({ /// To regenerate, run: `dart run slang`$statisticsStr$timestampStr // coverage:ignore-file -// ignore_for_file: type=lint'''); +// ignore_for_file: type=lint, unused_import'''); } void _generateImports(GenerateConfig config, StringBuffer buffer) { buffer.writeln(); final imports = [ ...config.imports, - 'package:slang/builder/model/node.dart', - if (config.obfuscation.enabled) 'package:slang/api/secret.dart', - if (config.translationOverrides) ...[ - 'package:slang/api/translation_overrides.dart', - 'package:slang/builder/model/build_model_config.dart', - 'package:slang/builder/model/enums.dart', - if (config.contexts.isNotEmpty) - 'package:slang/builder/model/context_type.dart', - ], + 'package:slang/node.dart', + if (config.obfuscation.enabled) 'package:slang/secret.dart', + if (config.translationOverrides) 'package:slang/overrides.dart', if (config.flutterIntegration) ...[ 'package:flutter/widgets.dart', 'package:slang_flutter/slang_flutter.dart', @@ -166,20 +149,21 @@ void _generateImports(GenerateConfig config, StringBuffer buffer) { } } -void _generateParts({ +void _generateLocaleImports({ required StringBuffer buffer, required GenerateConfig config, required List locales, }) { buffer.writeln(); - for (final locale in locales) { - buffer.writeln( - 'part \'${BuildResultPaths.localePath(outputPath: config.baseName, locale: locale.locale)}\';'); - } - if (config.renderFlatMap) { + for (final locale in locales.skip(1)) { + final localeImportName = getImportName( + locale: locale.locale, + ); buffer.writeln( - 'part \'${BuildResultPaths.flatMapPath(outputPath: config.baseName)}\';'); + 'import \'${BuildResultPaths.localePath(outputPath: config.outputFileName, locale: locale.locale)}\' deferred as $localeImportName;'); } + buffer.writeln( + 'part \'${BuildResultPaths.localePath(outputPath: config.outputFileName, locale: locales.first.locale)}\';'); } void _generateBuildConfig({ @@ -212,25 +196,13 @@ void _generateBuildConfig({ buffer.write('\tcontexts: ['); for (final context in config.contexts) { buffer.write( - 'ContextType(enumName: \'${context.enumName}\', enumValues: ${context.enumValues != null ? '[${context.enumValues!.map((e) => '\'$e\'').join(', ')}]' : 'null'}, paths: [${context.paths.map((p) => '\'$p\'').join(', ')}], defaultParameter: \'${context.defaultParameter}\', generateEnum: ${context.generateEnum}),'); + 'ContextType(enumName: \'${context.enumName}\', defaultParameter: \'${context.defaultParameter}\', generateEnum: ${context.generateEnum}),'); } buffer.writeln('],'); buffer.writeln('\tinterfaces: [], // currently not supported'); buffer.writeln(');'); } -void _generateBaseLocale({ - required StringBuffer buffer, - required GenerateConfig config, - required String baseLocaleVar, -}) { - final String enumName = config.enumName; - - buffer.writeln(); - buffer.writeln( - 'const $enumName $baseLocaleVar = $enumName.${config.baseLocale.enumConstant};'); -} - void _generateEnum({ required StringBuffer buffer, required GenerateConfig config, @@ -264,15 +236,7 @@ void _generateEnum({ if (locale.country != null) { buffer.write(', countryCode: \'${locale.country}\''); } - - final String className = allLocales[i].base - ? config.className - : getClassNameRoot( - baseName: config.baseName, - visibility: config.translationClassVisibility, - locale: locale, - ); - buffer.write(', build: $className.build)'); + buffer.write(')'); if (i != allLocales.length - 1) { buffer.writeln(','); @@ -282,20 +246,59 @@ void _generateEnum({ } buffer.writeln(); - buffer.writeln( - '\tconst $enumName({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element'); + buffer.writeln('\tconst $enumName({'); + buffer.writeln('\t\trequired this.languageCode,'); + buffer.writeln('\t\tthis.scriptCode, // ignore: unused_element'); + buffer.writeln('\t\tthis.countryCode, // ignore: unused_element'); + buffer.writeln('\t});'); buffer.writeln(); buffer.writeln('\t@override final String languageCode;'); buffer.writeln('\t@override final String? scriptCode;'); buffer.writeln('\t@override final String? countryCode;'); - buffer.writeln( - '\t@override final TranslationBuilder<$enumName, ${config.className}> build;'); + + void generateBuildMethod(StringBuffer buffer, bool sync) { + buffer.writeln(); + buffer.writeln('\t@override'); + buffer.writeln( + '\t${sync ? 'Translations' : 'Future'} build${sync ? 'Sync' : ''}({'); + buffer.writeln('\t\tMap? overrides,'); + buffer.writeln('\t\tPluralResolver? cardinalResolver,'); + buffer.writeln('\t\tPluralResolver? ordinalResolver,'); + buffer.writeln('\t}) ${sync ? '' : 'async '}{'); + buffer.writeln('\t\tswitch (this) {'); + for (final locale in allLocales) { + final localeImportName = getImportName( + locale: locale.locale, + ); + final className = getClassNameRoot( + className: config.className, + locale: locale.locale, + ); + + buffer.writeln('\t\t\tcase $enumName.${locale.locale.enumConstant}:'); + if (!locale.base && !sync) { + buffer.writeln('\t\t\t\tawait $localeImportName.loadLibrary();'); + } + buffer.writeln( + '\t\t\t\treturn ${locale.base ? '' : '$localeImportName.'}$className('); + buffer.writeln('\t\t\t\t\toverrides: overrides,'); + buffer.writeln('\t\t\t\t\tcardinalResolver: cardinalResolver,'); + buffer.writeln('\t\t\t\t\tordinalResolver: ordinalResolver,'); + buffer.writeln('\t\t\t\t);'); + } + buffer.writeln('\t\t}'); + buffer.writeln('\t}'); + } + + generateBuildMethod(buffer, false); + generateBuildMethod(buffer, true); + if (config.localeHandling) { buffer.writeln(); buffer.writeln('\t/// Gets current instance managed by [LocaleSettings].'); buffer.writeln( - '\t${config.className} get translations => LocaleSettings.instance.translationMap[this]!;'); + '\t${config.className} get translations => LocaleSettings.instance.getTranslations(this);'); } buffer.writeln('}'); @@ -409,29 +412,54 @@ void _generateLocaleSettings({ buffer.writeln( '\tstatic Stream<$enumName> getLocaleStream() => instance.getLocaleStream();'); buffer.writeln( - '\tstatic $enumName setLocale($enumName locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale);'); + '\tstatic Future<$enumName> setLocale($enumName locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale);'); buffer.writeln( - '\tstatic $enumName setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale);'); + '\tstatic Future<$enumName> setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale);'); + if (config.flutterIntegration) { buffer.writeln( - '\tstatic $enumName useDeviceLocale() => instance.useDeviceLocale();'); + '\tstatic Future<$enumName> useDeviceLocale() => instance.useDeviceLocale();'); + } + + buffer.writeln( + '\tstatic Future setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver('); + buffer.writeln('\t\tlanguage: language,'); + buffer.writeln('\t\tlocale: locale,'); + buffer.writeln('\t\tcardinalResolver: cardinalResolver,'); + buffer.writeln('\t\tordinalResolver: ordinalResolver,'); + buffer.writeln('\t);'); + if (config.translationOverrides) { + buffer.writeln( + '\tstatic Future overrideTranslations({required AppLocale locale, required FileType fileType, required String content}) => instance.overrideTranslations(locale: locale, fileType: fileType, content: content);'); buffer.writeln( - '\t@Deprecated(\'Use [AppLocaleUtils.supportedLocales]\') static List get supportedLocales => instance.supportedLocales;'); + '\tstatic Future overrideTranslationsFromMap({required AppLocale locale, required bool isFlatMap, required Map map}) => instance.overrideTranslationsFromMap(locale: locale, isFlatMap: isFlatMap, map: map);'); } + + // sync versions + buffer.writeln(); + buffer.writeln('\t// synchronous versions'); + buffer.writeln( + '\tstatic $enumName setLocaleSync($enumName locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale);'); buffer.writeln( - '\t@Deprecated(\'Use [AppLocaleUtils.supportedLocalesRaw]\') static List get supportedLocalesRaw => instance.supportedLocalesRaw;'); + '\tstatic $enumName setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale);'); + if (config.flutterIntegration) { + buffer.writeln( + '\tstatic $enumName useDeviceLocaleSync() => instance.useDeviceLocaleSync();'); + } + buffer.writeln( - '\tstatic void setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver('); + '\tstatic void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync('); buffer.writeln('\t\tlanguage: language,'); buffer.writeln('\t\tlocale: locale,'); buffer.writeln('\t\tcardinalResolver: cardinalResolver,'); buffer.writeln('\t\tordinalResolver: ordinalResolver,'); buffer.writeln('\t);'); + if (config.translationOverrides) { buffer.writeln( - '\tstatic void overrideTranslations({required AppLocale locale, required FileType fileType, required String content}) => instance.overrideTranslations(locale: locale, fileType: fileType, content: content);'); + '\tstatic void overrideTranslationsSync({required AppLocale locale, required FileType fileType, required String content}) => instance.overrideTranslationsSync(locale: locale, fileType: fileType, content: content);'); buffer.writeln( - '\tstatic void overrideTranslationsFromMap({required AppLocale locale, required bool isFlatMap, required Map map}) => instance.overrideTranslationsFromMap(locale: locale, isFlatMap: isFlatMap, map: map);'); + '\tstatic void overrideTranslationsFromMapSync({required AppLocale locale, required bool isFlatMap, required Map map}) => instance.overrideTranslationsFromMapSync(locale: locale, isFlatMap: isFlatMap, map: map);'); } buffer.writeln('}'); @@ -440,7 +468,6 @@ void _generateLocaleSettings({ void _generateUtil({ required StringBuffer buffer, required GenerateConfig config, - required String baseLocaleVar, }) { const String utilClass = 'AppLocaleUtils'; final String enumName = config.enumName; @@ -449,8 +476,14 @@ void _generateUtil({ buffer.writeln('/// Provides utility functions without any side effects.'); buffer.writeln( 'class $utilClass extends BaseAppLocaleUtils<$enumName, ${config.className}> {'); - buffer.writeln( - '\t$utilClass._() : super(baseLocale: $baseLocaleVar, locales: $enumName.values${config.translationOverrides ? ', buildConfig: _buildConfig' : ''});'); + buffer.writeln('\t$utilClass._() : super('); + buffer + .writeln('\t\tbaseLocale: $enumName.${config.baseLocale.enumConstant},'); + buffer.writeln('\t\tlocales: $enumName.values,'); + if (config.translationOverrides) { + buffer.writeln('\t\tbuildConfig: _buildConfig,'); + } + buffer.writeln('\t);'); buffer.writeln(); buffer.writeln('\tstatic final instance = $utilClass._();'); @@ -471,9 +504,13 @@ void _generateUtil({ '\tstatic List get supportedLocalesRaw => instance.supportedLocalesRaw;'); if (config.translationOverrides) { buffer.writeln( - '\tstatic ${config.className} buildWithOverrides({required AppLocale locale, required FileType fileType, required String content, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverrides(locale: locale, fileType: fileType, content: content, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);'); + '\tstatic Future<${config.className}> buildWithOverrides({required AppLocale locale, required FileType fileType, required String content, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverrides(locale: locale, fileType: fileType, content: content, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);'); + buffer.writeln( + '\tstatic Future<${config.className}> buildWithOverridesFromMap({required AppLocale locale, required bool isFlatMap, required Map map, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverridesFromMap(locale: locale, isFlatMap: isFlatMap, map: map, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);'); + buffer.writeln( + '\tstatic ${config.className} buildWithOverridesSync({required AppLocale locale, required FileType fileType, required String content, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverridesSync(locale: locale, fileType: fileType, content: content, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);'); buffer.writeln( - '\tstatic ${config.className} buildWithOverridesFromMap({required AppLocale locale, required bool isFlatMap, required Map map, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverridesFromMap(locale: locale, isFlatMap: isFlatMap, map: map, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);'); + '\tstatic ${config.className} buildWithOverridesFromMapSync({required AppLocale locale, required bool isFlatMap, required Map map, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverridesFromMapSync(locale: locale, isFlatMap: isFlatMap, map: map, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);'); } buffer.writeln('}'); diff --git a/slang/lib/src/builder/generator/generate_translation_map.dart b/slang/lib/src/builder/generator/generate_translation_map.dart index 6a59c8f9..05e44d2f 100644 --- a/slang/lib/src/builder/generator/generate_translation_map.dart +++ b/slang/lib/src/builder/generator/generate_translation_map.dart @@ -1,49 +1,31 @@ part of 'generate_translations.dart'; +/// Generates the flat map(s) containing all translations for one locale. String generateTranslationMap( GenerateConfig config, - List translations, + I18nData localeData, ) { final buffer = StringBuffer(); - if (config.outputFormat == OutputFormat.multipleFiles) { - // this is a part file - - buffer.writeln(''' -/// -/// Generated file. Do not edit. -/// -// coverage:ignore-file -// ignore_for_file: type=lint - -part of '${config.outputFileName}';'''); - buffer.writeln(); - } - buffer.writeln('/// Flat map(s) containing all translations.'); buffer.writeln( '/// Only for edge cases! For simple maps, use the map function of this library.'); - for (I18nData localeData in translations) { - final language = localeData.locale.language; - - buffer.writeln(); - buffer.writeln( - 'extension on ${localeData.base ? config.className : getClassNameRoot(baseName: config.baseName, locale: localeData.locale, visibility: config.translationClassVisibility)} {'); - buffer.writeln('\tdynamic _flatMapFunction(String path) {'); + buffer.writeln( + 'extension on ${localeData.base ? config.className : getClassNameRoot(className: config.className, locale: localeData.locale)} {'); + buffer.writeln('\tdynamic _flatMapFunction(String path) {'); - buffer.writeln('\t\tswitch (path) {'); - _generateTranslationMapRecursive( - buffer: buffer, - curr: localeData.root, - config: config, - language: language, - ); - buffer.writeln('\t\t\tdefault: return null;'); - buffer.writeln('\t\t}'); - buffer.writeln('\t}'); - buffer.writeln('}'); - } + buffer.writeln('\t\tswitch (path) {'); + _generateTranslationMapRecursive( + buffer: buffer, + curr: localeData.root, + config: config, + language: localeData.locale.language, + ); + buffer.writeln('\t\t\tdefault: return null;'); + buffer.writeln('\t\t}'); + buffer.writeln('\t}'); + buffer.writeln('}'); return buffer.toString(); } diff --git a/slang/lib/src/builder/generator/generate_translations.dart b/slang/lib/src/builder/generator/generate_translations.dart index 60ae8efb..d4e506ba 100644 --- a/slang/lib/src/builder/generator/generate_translations.dart +++ b/slang/lib/src/builder/generator/generate_translations.dart @@ -1,12 +1,12 @@ import 'dart:collection'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/generate_config.dart'; -import 'package:slang/builder/model/i18n_data.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/node.dart'; -import 'package:slang/builder/model/pluralization.dart'; import 'package:slang/src/builder/generator/helper.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/generate_config.dart'; +import 'package:slang/src/builder/model/i18n_data.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/node.dart'; +import 'package:slang/src/builder/model/pluralization.dart'; import 'package:slang/src/builder/utils/encryption_utils.dart'; part 'generate_translation_map.dart'; @@ -26,23 +26,34 @@ String generateTranslations(GenerateConfig config, I18nData localeData) { final queue = Queue(); final buffer = StringBuffer(); - if (config.outputFormat == OutputFormat.multipleFiles) { - // this is a part file - - buffer.writeln(''' + buffer.writeln(''' /// /// Generated file. Do not edit. /// // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import +'''); -part of '${config.outputFileName}';'''); + if (localeData.base) { + buffer.writeln("part of '${config.outputFileName}';"); + } else { + final imports = [ + config.outputFileName, + ...config.imports, + 'package:slang/node.dart', + if (config.obfuscation.enabled) 'package:slang/secret.dart', + if (config.translationOverrides) 'package:slang/overrides.dart', + if (config.flutterIntegration) 'package:flutter/widgets.dart', + ]..sort((a, b) => a.compareTo(b)); + + for (final i in imports) { + buffer.writeln('import \'$i\';'); + } } queue.add(ClassTask( getClassNameRoot( - baseName: config.baseName, - visibility: config.translationClassVisibility, + className: config.className, ), localeData.root, )); @@ -54,11 +65,23 @@ part of '${config.outputFileName}';'''); ClassTask task = queue.removeFirst(); _generateClass( - config, localeData, buffer, queue, task.className, task.node, root); + config, + localeData, + buffer, + queue, + task.className, + task.node, + root, + ); root = false; } while (queue.isNotEmpty); + if (config.renderFlatMap) { + buffer.writeln(); + buffer.writeln(generateTranslationMap(config, localeData)); + } + return buffer.toString(); } @@ -85,35 +108,37 @@ void _generateClass( final rootClassName = localeData.base ? config.className : getClassNameRoot( - baseName: config.baseName, - visibility: config.translationClassVisibility, + className: config.className, locale: localeData.locale, ); // The current class name. - final finalClassName = root && localeData.base - ? config.className - : getClassName( - parentName: className, - locale: localeData.locale, - ); + final finalClassName = switch (root) { + true => switch (localeData.base) { + true => config.className, + false => + getClassNameRoot(className: className, locale: localeData.locale), + }, + false => getClassName( + base: localeData.base, + visibility: config.translationClassVisibility, + parentName: className, + locale: localeData.locale, + ), + }; final mixinStr = node.interface != null ? ' with ${node.interface!.name}' : ''; if (localeData.base) { if (root) { - if (config.translationClassVisibility == - TranslationClassVisibility.public) { - // Add typedef for backwards compatibility - final legacyClassName = getClassNameRoot( - baseName: config.baseName, - visibility: config.translationClassVisibility, - locale: localeData.locale, - ); - buffer.writeln( - 'typedef $legacyClassName = ${config.className}; // ignore: unused_element'); - } + // Add typedef for backwards compatibility + final legacyClassName = getClassNameRoot( + className: config.className, + locale: localeData.locale, + ); + buffer.writeln( + 'typedef $legacyClassName = ${config.className}; // ignore: unused_element'); buffer.writeln( 'class $finalClassName$mixinStr implements BaseTranslations<${config.enumName}, ${config.className}> {'); } else { @@ -124,6 +149,8 @@ void _generateClass( final baseClassName = root ? config.className : getClassName( + base: true, + visibility: TranslationClassVisibility.public, parentName: className, locale: config.baseLocale, ); @@ -162,7 +189,7 @@ void _generateClass( } buffer.writeln( - '\t$finalClassName.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver})'); + '\t$finalClassName({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver})'); if (!config.translationOverrides) { buffer.write( '\t\t: assert(overrides == null, \'Set "translation_overrides: true" in order to enable this feature.\'),\n\t\t '); @@ -244,11 +271,7 @@ void _generateClass( // root buffer.writeln(); - if (!localeData.base) { - buffer.write('\t@override '); - } else { - buffer.write('\t'); - } + buffer.write('\t'); if (root) { buffer.write('late final $rootClassName _root = this;'); @@ -336,8 +359,12 @@ void _generateClass( depth: 0, ); } else if (value is ObjectNode) { - String childClassNoLocale = - getClassName(parentName: className, childName: key); + String childClassNoLocale = getClassName( + base: localeData.base, + visibility: config.translationClassVisibility, + parentName: className, + childName: key, + ); if (value.isMap) { // inline map @@ -356,7 +383,12 @@ void _generateClass( // generate a class later on queue.add(ClassTask(childClassNoLocale, value)); String childClassWithLocale = getClassName( - parentName: className, childName: key, locale: localeData.locale); + base: localeData.base, + visibility: config.translationClassVisibility, + parentName: className, + childName: key, + locale: localeData.locale, + ); buffer.writeln( 'late final $childClassWithLocale$optional $key = $childClassWithLocale._(_root);'); } @@ -432,8 +464,12 @@ void _generateMap({ depth: depth + 1, ); } else if (value is ObjectNode) { - String childClassNoLocale = - getClassName(parentName: className, childName: key); + String childClassNoLocale = getClassName( + base: base, + visibility: config.translationClassVisibility, + parentName: className, + childName: key, + ); if (value.isMap) { // inline map @@ -451,8 +487,13 @@ void _generateMap({ } else { // generate a class later on queue.add(ClassTask(childClassNoLocale, value)); - String childClassWithLocale = - getClassName(parentName: className, childName: key, locale: locale); + String childClassWithLocale = getClassName( + base: base, + visibility: config.translationClassVisibility, + parentName: className, + childName: key, + locale: locale, + ); buffer.writeln('\'$key\': $childClassWithLocale._(_root),'); } } else if (value is PluralNode) { @@ -541,8 +582,11 @@ void _generateList({ 'i' + i.toString() + r'$'; - final String childClassNoLocale = - getClassName(parentName: className, childName: key); + final String childClassNoLocale = getClassName( + base: base, + visibility: config.translationClassVisibility, + parentName: className, + childName: key); if (value.isMap) { // inline map @@ -560,6 +604,8 @@ void _generateList({ // generate a class later on queue.add(ClassTask(childClassNoLocale, value)); String childClassWithLocale = getClassName( + base: base, + visibility: config.translationClassVisibility, parentName: className, childName: key, locale: locale, diff --git a/slang/lib/src/builder/generator/generator.dart b/slang/lib/src/builder/generator/generator.dart index c6837ed1..be6bed6d 100644 --- a/slang/lib/src/builder/generator/generator.dart +++ b/slang/lib/src/builder/generator/generator.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/model/build_result.dart'; -import 'package:slang/builder/model/generate_config.dart'; -import 'package:slang/builder/model/i18n_data.dart'; import 'package:slang/src/builder/generator/generate_header.dart'; import 'package:slang/src/builder/generator/generate_translations.dart'; +import 'package:slang/src/builder/model/build_result.dart'; +import 'package:slang/src/builder/model/generate_config.dart'; +import 'package:slang/src/builder/model/i18n_data.dart'; class Generator { /// main generate function @@ -11,21 +11,11 @@ class Generator { required GenerateConfig config, required List translations, }) { - final header = generateHeader(config, translations); - final list = { - for (final t in translations) t.locale: generateTranslations(config, t), - }; - final String? flatMap; - if (config.renderFlatMap) { - flatMap = generateTranslationMap(config, translations); - } else { - flatMap = null; - } - return BuildResult( - header: header, - translations: list, - flatMap: flatMap, + main: generateHeader(config, translations), + translations: { + for (final t in translations) t.locale: generateTranslations(config, t), + }, ); } } diff --git a/slang/lib/src/builder/generator/helper.dart b/slang/lib/src/builder/generator/helper.dart index 9bb145f2..cd16b302 100644 --- a/slang/lib/src/builder/generator/helper.dart +++ b/slang/lib/src/builder/generator/helper.dart @@ -1,6 +1,6 @@ -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/obfuscation_config.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/obfuscation_config.dart'; import 'package:slang/src/builder/utils/encryption_utils.dart'; import 'package:slang/src/builder/utils/string_extensions.dart'; import 'package:slang/src/builder/utils/string_interpolation_extensions.dart'; @@ -8,21 +8,27 @@ import 'package:slang/src/builder/utils/string_interpolation_extensions.dart'; /// Pragmatic way to detect links within interpolations. const String characteristicLinkPrefix = '_root.'; +String getImportName({ + required I18nLocale locale, +}) { + return '_\$${locale.languageTag.replaceAll('-', '_')}'; +} + /// Returns the class name of the root translation class. String getClassNameRoot({ - required String baseName, + required String className, I18nLocale? locale, - required TranslationClassVisibility visibility, }) { - String result = baseName.toCase(CaseStyle.pascal) + + String result = className + (locale != null ? locale.languageTag.toCaseOfLocale(CaseStyle.pascal) : ''); - if (visibility == TranslationClassVisibility.private) result = '_$result'; return result; } String getClassName({ + required bool base, + required TranslationClassVisibility visibility, required String parentName, String childName = '', I18nLocale? locale, @@ -33,6 +39,16 @@ String getClassName({ } else { languageTag = ''; } + if (base) { + visibility = TranslationClassVisibility.public; + } + if (!parentName.startsWith('_') && + visibility == TranslationClassVisibility.private) { + parentName = '_$parentName'; + } else if (parentName.startsWith('_') && + visibility == TranslationClassVisibility.public) { + parentName = parentName.substring(1); + } return parentName + childName.toCase(CaseStyle.pascal) + languageTag; } diff --git a/slang/lib/src/builder/generator_facade.dart b/slang/lib/src/builder/generator_facade.dart index e615a95a..f7163269 100644 --- a/slang/lib/src/builder/generator_facade.dart +++ b/slang/lib/src/builder/generator_facade.dart @@ -1,17 +1,16 @@ -import 'package:slang/builder/builder/generate_config_builder.dart'; -import 'package:slang/builder/builder/translation_model_list_builder.dart'; -import 'package:slang/builder/model/build_result.dart'; -import 'package:slang/builder/model/context_type.dart'; -import 'package:slang/builder/model/interface.dart'; -import 'package:slang/builder/model/raw_config.dart'; -import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/builder/generate_config_builder.dart'; +import 'package:slang/src/builder/builder/translation_model_list_builder.dart'; import 'package:slang/src/builder/generator/generator.dart'; +import 'package:slang/src/builder/model/build_result.dart'; +import 'package:slang/src/builder/model/context_type.dart'; +import 'package:slang/src/builder/model/interface.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; class GeneratorFacade { /// Common step used by custom runner and builder to get the .g.dart content static BuildResult generate({ required RawConfig rawConfig, - required String baseName, required TranslationMap translationMap, required String inputDirectoryHint, }) { @@ -49,7 +48,6 @@ class GeneratorFacade { // generate config final config = GenerateConfigBuilder.build( - baseName: baseName, config: rawConfig, inputDirectoryHint: inputDirectoryHint, contexts: contextMap.values.toList(), diff --git a/slang/lib/builder/model/build_model_config.dart b/slang/lib/src/builder/model/build_model_config.dart similarity index 80% rename from slang/lib/builder/model/build_model_config.dart rename to slang/lib/src/builder/model/build_model_config.dart index af04fadb..5a16ef69 100644 --- a/slang/lib/builder/model/build_model_config.dart +++ b/slang/lib/src/builder/model/build_model_config.dart @@ -1,7 +1,7 @@ -import 'package:slang/builder/model/context_type.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/interface.dart'; -import 'package:slang/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/context_type.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/interface.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; /// Config to generate the model. /// A subset of [RawConfig]. diff --git a/slang/lib/src/builder/model/build_result.dart b/slang/lib/src/builder/model/build_result.dart new file mode 100644 index 00000000..8f857133 --- /dev/null +++ b/slang/lib/src/builder/model/build_result.dart @@ -0,0 +1,14 @@ +import 'package:slang/src/builder/model/i18n_locale.dart'; + +/// the resulting output strings +/// It can either be rendered as a single file +/// or as multiple files. +class BuildResult { + final String main; + final Map translations; + + BuildResult({ + required this.main, + required this.translations, + }); +} diff --git a/slang/lib/builder/model/context_type.dart b/slang/lib/src/builder/model/context_type.dart similarity index 55% rename from slang/lib/builder/model/context_type.dart rename to slang/lib/src/builder/model/context_type.dart index 3c11951a..2f30699b 100644 --- a/slang/lib/builder/model/context_type.dart +++ b/slang/lib/src/builder/model/context_type.dart @@ -2,19 +2,40 @@ /// Enum values may be null. In this case, they will be inferred during model build step. class ContextType { static const String DEFAULT_PARAMETER = 'context'; - static const List defaultPaths = []; static const bool defaultGenerateEnum = true; final String enumName; - final List? enumValues; - final List paths; final String defaultParameter; final bool generateEnum; ContextType({ required this.enumName, - required this.enumValues, - required this.paths, + required this.defaultParameter, + required this.generateEnum, + }); + + PendingContextType toPending() { + return PendingContextType( + enumName: enumName, + defaultParameter: defaultParameter, + generateEnum: generateEnum, + ); + } +} + +/// Used during model build step. +class PendingContextType { + final String enumName; + + /// Always null in the beginning. + /// Will be inferred during the model build step. + List? enumValues; + + final String defaultParameter; + final bool generateEnum; + + PendingContextType({ + required this.enumName, required this.defaultParameter, required this.generateEnum, }); diff --git a/slang/lib/builder/model/enums.dart b/slang/lib/src/builder/model/enums.dart similarity index 94% rename from slang/lib/builder/model/enums.dart rename to slang/lib/src/builder/model/enums.dart index 7417a24f..ceef2623 100644 --- a/slang/lib/builder/model/enums.dart +++ b/slang/lib/src/builder/model/enums.dart @@ -6,8 +6,6 @@ enum FallbackStrategy { none, baseLocale, baseLocaleEmptyString } /// has been already handled in the previous step. enum GenerateFallbackStrategy { none, baseLocale } -enum OutputFormat { singleFile, multipleFiles } - enum StringInterpolation { dart, braces, doubleBraces } enum TranslationClassVisibility { private, public } diff --git a/slang/lib/builder/model/generate_config.dart b/slang/lib/src/builder/model/generate_config.dart similarity index 75% rename from slang/lib/builder/model/generate_config.dart rename to slang/lib/src/builder/model/generate_config.dart index 1d603d21..eeeed80f 100644 --- a/slang/lib/builder/model/generate_config.dart +++ b/slang/lib/src/builder/model/generate_config.dart @@ -1,20 +1,18 @@ -import 'package:slang/builder/model/build_model_config.dart'; -import 'package:slang/builder/model/context_type.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/interface.dart'; -import 'package:slang/builder/model/obfuscation_config.dart'; +import 'package:slang/src/builder/model/build_model_config.dart'; +import 'package:slang/src/builder/model/context_type.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/interface.dart'; +import 'package:slang/src/builder/model/obfuscation_config.dart'; /// Config for the generation step (generate dart-content from model) /// Applies to all locales class GenerateConfig { final BuildModelConfig buildConfig; // for translation overrides final String inputDirectoryHint; // for comment - final String baseName; // name of all i18n files, like strings or messages final I18nLocale baseLocale; // defaults to 'en' final GenerateFallbackStrategy fallbackStrategy; final String outputFileName; - final OutputFormat outputFormat; final bool localeHandling; final bool flutterIntegration; final String translateVariable; @@ -33,11 +31,9 @@ class GenerateConfig { GenerateConfig({ required this.buildConfig, required this.inputDirectoryHint, - required this.baseName, required this.baseLocale, required this.fallbackStrategy, required this.outputFileName, - required this.outputFormat, required this.localeHandling, required this.flutterIntegration, required this.translateVariable, diff --git a/slang/lib/builder/model/i18n_data.dart b/slang/lib/src/builder/model/i18n_data.dart similarity index 81% rename from slang/lib/builder/model/i18n_data.dart rename to slang/lib/src/builder/model/i18n_data.dart index 05978fef..c43e05bb 100644 --- a/slang/lib/builder/model/i18n_data.dart +++ b/slang/lib/src/builder/model/i18n_data.dart @@ -1,7 +1,7 @@ -import 'package:slang/builder/model/context_type.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/interface.dart'; -import 'package:slang/builder/model/node.dart'; +import 'package:slang/src/builder/model/context_type.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/interface.dart'; +import 'package:slang/src/builder/model/node.dart'; typedef I18nDataComparator = int Function(I18nData a, I18nData b); diff --git a/slang/lib/builder/model/i18n_locale.dart b/slang/lib/src/builder/model/i18n_locale.dart similarity index 96% rename from slang/lib/builder/model/i18n_locale.dart rename to slang/lib/src/builder/model/i18n_locale.dart index 9100ea41..3ffcf088 100644 --- a/slang/lib/builder/model/i18n_locale.dart +++ b/slang/lib/src/builder/model/i18n_locale.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/model/enums.dart'; +import 'package:slang/src/builder/model/enums.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; import 'package:slang/src/builder/utils/string_extensions.dart'; diff --git a/slang/lib/builder/model/interface.dart b/slang/lib/src/builder/model/interface.dart similarity index 100% rename from slang/lib/builder/model/interface.dart rename to slang/lib/src/builder/model/interface.dart diff --git a/slang/lib/builder/model/node.dart b/slang/lib/src/builder/model/node.dart similarity index 98% rename from slang/lib/builder/model/node.dart rename to slang/lib/src/builder/model/node.dart index a2c033b5..c0a2413f 100644 --- a/slang/lib/builder/model/node.dart +++ b/slang/lib/src/builder/model/node.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/model/context_type.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/interface.dart'; -import 'package:slang/builder/model/pluralization.dart'; import 'package:slang/src/builder/builder/text_parser.dart'; +import 'package:slang/src/builder/model/context_type.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/interface.dart'; +import 'package:slang/src/builder/model/pluralization.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; import 'package:slang/src/builder/utils/string_interpolation_extensions.dart'; @@ -172,7 +172,7 @@ class PluralNode extends Node implements LeafNode { } class ContextNode extends Node implements LeafNode { - final ContextType context; + final PendingContextType context; final Map entries; final String paramName; // name of the context parameter final bool rich; diff --git a/slang/lib/builder/model/obfuscation_config.dart b/slang/lib/src/builder/model/obfuscation_config.dart similarity index 100% rename from slang/lib/builder/model/obfuscation_config.dart rename to slang/lib/src/builder/model/obfuscation_config.dart diff --git a/slang/lib/builder/model/pluralization.dart b/slang/lib/src/builder/model/pluralization.dart similarity index 100% rename from slang/lib/builder/model/pluralization.dart rename to slang/lib/src/builder/model/pluralization.dart diff --git a/slang/lib/builder/model/raw_config.dart b/slang/lib/src/builder/model/raw_config.dart similarity index 94% rename from slang/lib/builder/model/raw_config.dart rename to slang/lib/src/builder/model/raw_config.dart index 617df817..df898e8b 100644 --- a/slang/lib/builder/model/raw_config.dart +++ b/slang/lib/src/builder/model/raw_config.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/model/context_type.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/interface.dart'; -import 'package:slang/builder/model/obfuscation_config.dart'; +import 'package:slang/src/builder/model/context_type.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/interface.dart'; +import 'package:slang/src/builder/model/obfuscation_config.dart'; /// represents a build.yaml or a slang.yaml file class RawConfig { @@ -12,7 +12,6 @@ class RawConfig { static const String defaultInputFilePattern = '.i18n.json'; static const String? defaultOutputDirectory = null; static const String defaultOutputFileName = 'strings.g.dart'; - static const OutputFormat defaultOutputFormat = OutputFormat.singleFile; static const bool defaultLocaleHandling = true; static const bool defaultFlutterIntegration = true; static const bool defaultNamespaces = false; @@ -48,7 +47,6 @@ class RawConfig { final String inputFilePattern; final String? outputDirectory; final String outputFileName; - final OutputFormat outputFormat; final bool localeHandling; final bool flutterIntegration; final bool namespaces; @@ -84,7 +82,6 @@ class RawConfig { required this.inputFilePattern, required this.outputDirectory, required this.outputFileName, - required this.outputFormat, required this.localeHandling, required this.flutterIntegration, required this.namespaces, @@ -121,7 +118,6 @@ class RawConfig { FallbackStrategy? fallbackStrategy, String? inputFilePattern, String? outputFileName, - OutputFormat? outputFormat, bool? localeHandling, bool? flutterIntegration, bool? namespaces, @@ -147,7 +143,6 @@ class RawConfig { inputFilePattern: inputFilePattern ?? this.inputFilePattern, outputDirectory: outputDirectory, outputFileName: outputFileName ?? this.outputFileName, - outputFormat: outputFormat ?? this.outputFormat, localeHandling: localeHandling ?? this.localeHandling, flutterIntegration: flutterIntegration ?? this.flutterIntegration, namespaces: namespaces ?? this.namespaces, @@ -206,7 +201,6 @@ class RawConfig { print( ' -> outputDirectory: ${outputDirectory ?? 'null (directory of input)'}'); print(' -> outputFileName: $outputFileName'); - print(' -> outputFileFormat: ${outputFormat.name}'); print(' -> localeHandling: $localeHandling'); print(' -> flutterIntegration: $flutterIntegration'); print(' -> namespaces: $namespaces'); @@ -231,8 +225,7 @@ class RawConfig { print(' -> pluralization/ordinal: $pluralOrdinal'); print(' -> contexts: ${contexts.isEmpty ? 'no custom contexts' : ''}'); for (final contextType in contexts) { - print( - ' - ${contextType.enumName} { ${contextType.enumValues?.join(', ') ?? '(inferred)'} }'); + print(' - ${contextType.enumName}'); } print(' -> interfaces: ${interfaces.isEmpty ? 'no interfaces' : ''}'); for (final interface in interfaces) { @@ -263,7 +256,6 @@ class RawConfig { inputFilePattern: RawConfig.defaultInputFilePattern, outputDirectory: RawConfig.defaultOutputDirectory, outputFileName: RawConfig.defaultOutputFileName, - outputFormat: RawConfig.defaultOutputFormat, localeHandling: RawConfig.defaultLocaleHandling, flutterIntegration: RawConfig.defaultFlutterIntegration, namespaces: RawConfig.defaultNamespaces, diff --git a/slang/lib/builder/model/slang_file_collection.dart b/slang/lib/src/builder/model/slang_file_collection.dart similarity index 94% rename from slang/lib/builder/model/slang_file_collection.dart rename to slang/lib/src/builder/model/slang_file_collection.dart index eb5a1600..6e0cefcc 100644 --- a/slang/lib/builder/model/slang_file_collection.dart +++ b/slang/lib/src/builder/model/slang_file_collection.dart @@ -1,7 +1,7 @@ -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/raw_config.dart'; import 'package:slang/src/builder/decoder/base_decoder.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; import 'package:slang/src/builder/utils/path_utils.dart'; /// A collection of translation files that can be read in a later step. diff --git a/slang/lib/builder/model/translation_map.dart b/slang/lib/src/builder/model/translation_map.dart similarity index 94% rename from slang/lib/builder/model/translation_map.dart rename to slang/lib/src/builder/model/translation_map.dart index 97faf94c..22a6170a 100644 --- a/slang/lib/builder/model/translation_map.dart +++ b/slang/lib/src/builder/model/translation_map.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; /// Contains ALL translations of ALL locales /// Represented as pure maps without modifications diff --git a/slang/lib/src/builder/utils/file_utils.dart b/slang/lib/src/builder/utils/file_utils.dart index 079d33f4..fe15a965 100644 --- a/slang/lib/src/builder/utils/file_utils.dart +++ b/slang/lib/src/builder/utils/file_utils.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:json2yaml/json2yaml.dart'; -import 'package:slang/builder/model/enums.dart'; +import 'package:slang/src/builder/model/enums.dart'; const String INFO_KEY = '@@info'; diff --git a/slang/lib/src/builder/utils/node_utils.dart b/slang/lib/src/builder/utils/node_utils.dart index 32575ac3..b71b8944 100644 --- a/slang/lib/src/builder/utils/node_utils.dart +++ b/slang/lib/src/builder/utils/node_utils.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/model/node.dart'; +import 'package:slang/src/builder/model/node.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; class NodeUtils { diff --git a/slang/lib/src/builder/utils/path_utils.dart b/slang/lib/src/builder/utils/path_utils.dart index a7d7ab40..147df96a 100644 --- a/slang/lib/src/builder/utils/path_utils.dart +++ b/slang/lib/src/builder/utils/path_utils.dart @@ -1,5 +1,5 @@ import 'package:collection/collection.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; /// Operations on paths diff --git a/slang/lib/src/builder/utils/string_extensions.dart b/slang/lib/src/builder/utils/string_extensions.dart index d197a477..741118b4 100644 --- a/slang/lib/src/builder/utils/string_extensions.dart +++ b/slang/lib/src/builder/utils/string_extensions.dart @@ -1,5 +1,5 @@ import 'package:collection/collection.dart'; -import 'package:slang/builder/model/enums.dart'; +import 'package:slang/src/builder/model/enums.dart'; extension StringExtensions on String { /// capitalizes a given string diff --git a/slang/lib/src/runner/analyze.dart b/slang/lib/src/runner/analyze.dart index 6f702dd8..860e9cd6 100644 --- a/slang/lib/src/runner/analyze.dart +++ b/slang/lib/src/runner/analyze.dart @@ -1,13 +1,13 @@ import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:slang/builder/builder/translation_model_list_builder.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_data.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/node.dart'; -import 'package:slang/builder/model/raw_config.dart'; -import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/builder/translation_model_list_builder.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_data.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/node.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; import 'package:slang/src/builder/utils/file_utils.dart'; import 'package:slang/src/builder/utils/map_utils.dart'; import 'package:slang/src/builder/utils/node_utils.dart'; diff --git a/slang/lib/src/runner/apply.dart b/slang/lib/src/runner/apply.dart index 5dcfb679..b84fcd5d 100644 --- a/slang/lib/src/runner/apply.dart +++ b/slang/lib/src/runner/apply.dart @@ -1,12 +1,12 @@ import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:slang/builder/builder/translation_map_builder.dart'; -import 'package:slang/builder/builder/translation_model_list_builder.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/node.dart'; -import 'package:slang/builder/model/slang_file_collection.dart'; +import 'package:slang/src/builder/builder/translation_map_builder.dart'; +import 'package:slang/src/builder/builder/translation_model_list_builder.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/node.dart'; +import 'package:slang/src/builder/model/slang_file_collection.dart'; import 'package:slang/src/builder/utils/file_utils.dart'; import 'package:slang/src/builder/utils/node_utils.dart'; import 'package:slang/src/builder/utils/path_utils.dart'; diff --git a/slang/lib/src/runner/clean.dart b/slang/lib/src/runner/clean.dart index 08057e19..48c3dbad 100644 --- a/slang/lib/src/runner/clean.dart +++ b/slang/lib/src/runner/clean.dart @@ -1,7 +1,7 @@ import 'dart:io'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/slang_file_collection.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/slang_file_collection.dart'; import 'package:slang/src/builder/utils/file_utils.dart'; import 'package:slang/src/builder/utils/map_utils.dart'; import 'package:slang/src/runner/utils/read_analysis_file.dart'; diff --git a/slang/lib/src/runner/edit.dart b/slang/lib/src/runner/edit.dart index 4d9cb997..095fa1ed 100644 --- a/slang/lib/src/runner/edit.dart +++ b/slang/lib/src/runner/edit.dart @@ -1,11 +1,11 @@ import 'dart:math'; import 'package:collection/collection.dart'; -import 'package:slang/builder/builder/translation_map_builder.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/node.dart'; -import 'package:slang/builder/model/slang_file_collection.dart'; +import 'package:slang/src/builder/builder/translation_map_builder.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/node.dart'; +import 'package:slang/src/builder/model/slang_file_collection.dart'; import 'package:slang/src/builder/utils/file_utils.dart'; import 'package:slang/src/builder/utils/map_utils.dart'; import 'package:slang/src/builder/utils/node_utils.dart'; diff --git a/slang/lib/src/runner/migrate_arb.dart b/slang/lib/src/runner/migrate_arb.dart index 8ce553f9..b1911297 100644 --- a/slang/lib/src/runner/migrate_arb.dart +++ b/slang/lib/src/runner/migrate_arb.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:slang/builder/model/enums.dart'; +import 'package:slang/src/builder/model/enums.dart'; import 'package:slang/src/builder/utils/brackets_utils.dart'; import 'package:slang/src/builder/utils/file_utils.dart'; import 'package:slang/src/builder/utils/map_utils.dart'; diff --git a/slang/lib/src/runner/normalize.dart b/slang/lib/src/runner/normalize.dart index a081962a..c822988c 100644 --- a/slang/lib/src/runner/normalize.dart +++ b/slang/lib/src/runner/normalize.dart @@ -1,8 +1,8 @@ import 'package:collection/collection.dart'; -import 'package:slang/builder/builder/translation_map_builder.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/slang_file_collection.dart'; +import 'package:slang/src/builder/builder/translation_map_builder.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/slang_file_collection.dart'; import 'package:slang/src/builder/utils/file_utils.dart'; import 'package:slang/src/builder/utils/path_utils.dart'; import 'package:slang/src/runner/apply.dart'; diff --git a/slang/lib/src/runner/stats.dart b/slang/lib/src/runner/stats.dart index bba65617..6c66f9f3 100644 --- a/slang/lib/src/runner/stats.dart +++ b/slang/lib/src/runner/stats.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/builder/translation_model_list_builder.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/node.dart'; -import 'package:slang/builder/model/raw_config.dart'; -import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/builder/translation_model_list_builder.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/node.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; StatsResult getStats({ diff --git a/slang/lib/src/runner/utils/read_analysis_file.dart b/slang/lib/src/runner/utils/read_analysis_file.dart index 2d6da14c..be10e485 100644 --- a/slang/lib/src/runner/utils/read_analysis_file.dart +++ b/slang/lib/src/runner/utils/read_analysis_file.dart @@ -1,9 +1,9 @@ import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/decoder/base_decoder.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/utils/file_utils.dart'; import 'package:slang/src/builder/utils/path_utils.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index 804c156c..9c24278d 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -14,7 +14,7 @@ funding: - https://github.com/sponsors/Tienisto/ environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=3.3.0 <4.0.0" dependencies: collection: ^1.15.0 diff --git a/slang/test/integration/main/compilation_test.dart b/slang/test/integration/main/compilation_test.dart index 6ffb0134..9debf8af 100644 --- a/slang/test/integration/main/compilation_test.dart +++ b/slang/test/integration/main/compilation_test.dart @@ -1,3 +1,5 @@ +@Skip('not updated for multiple files') + import 'package:expect_error/expect_error.dart'; import 'package:test/test.dart'; diff --git a/slang/test/integration/main/csv_compact_test.dart b/slang/test/integration/main/csv_compact_test.dart index 42a03e3f..84c9dd84 100644 --- a/slang/test/integration/main/csv_compact_test.dart +++ b/slang/test/integration/main/csv_compact_test.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/builder/raw_config_builder.dart'; import 'package:slang/src/builder/decoder/csv_decoder.dart'; import 'package:slang/src/builder/generator_facade.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; @@ -10,12 +10,16 @@ import '../../util/resources_utils.dart'; void main() { late String compactInput; late String buildYaml; - late String expectedOutput; + late String expectedMainOutput; + late String expectedEnOutput; + late String expectedDeOutput; setUp(() { compactInput = loadResource('main/csv_compact.csv'); buildYaml = loadResource('main/build_config.yaml'); - expectedOutput = loadResource('main/_expected_single.output'); + expectedMainOutput = loadResource('main/_expected_main.output'); + expectedEnOutput = loadResource('main/_expected_en.output'); + expectedDeOutput = loadResource('main/_expected_de.output'); }); test('compact csv', () { @@ -23,7 +27,6 @@ void main() { final result = GeneratorFacade.generate( rawConfig: RawConfigBuilder.fromYaml(buildYaml)!, - baseName: 'translations', translationMap: TranslationMap() ..addTranslations( locale: I18nLocale.fromString('en'), @@ -36,6 +39,8 @@ void main() { inputDirectoryHint: 'fake/path/integration', ); - expect(result.joinAsSingleOutput(), expectedOutput); + expect(result.main, expectedMainOutput); + expect(result.translations[I18nLocale.fromString('en')], expectedEnOutput); + expect(result.translations[I18nLocale.fromString('de')], expectedDeOutput); }); } diff --git a/slang/test/integration/main/csv_test.dart b/slang/test/integration/main/csv_test.dart index b9fc0e37..4184e610 100644 --- a/slang/test/integration/main/csv_test.dart +++ b/slang/test/integration/main/csv_test.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/builder/raw_config_builder.dart'; import 'package:slang/src/builder/decoder/csv_decoder.dart'; import 'package:slang/src/builder/generator_facade.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; @@ -11,19 +11,22 @@ void main() { late String enInput; late String deInput; late String buildYaml; - late String expectedOutput; + late String expectedMainOutput; + late String expectedEnOutput; + late String expectedDeOutput; setUp(() { enInput = loadResource('main/csv_en.csv'); deInput = loadResource('main/csv_de.csv'); buildYaml = loadResource('main/build_config.yaml'); - expectedOutput = loadResource('main/_expected_single.output'); + expectedMainOutput = loadResource('main/_expected_main.output'); + expectedEnOutput = loadResource('main/_expected_en.output'); + expectedDeOutput = loadResource('main/_expected_de.output'); }); test('separated csv', () { final result = GeneratorFacade.generate( rawConfig: RawConfigBuilder.fromYaml(buildYaml)!, - baseName: 'translations', translationMap: TranslationMap() ..addTranslations( locale: I18nLocale.fromString('en'), @@ -36,6 +39,8 @@ void main() { inputDirectoryHint: 'fake/path/integration', ); - expect(result.joinAsSingleOutput(), expectedOutput); + expect(result.main, expectedMainOutput); + expect(result.translations[I18nLocale.fromString('en')], expectedEnOutput); + expect(result.translations[I18nLocale.fromString('de')], expectedDeOutput); }); } diff --git a/slang/test/integration/main/fallback_base_locale_test.dart b/slang/test/integration/main/fallback_base_locale_test.dart index f01446d3..17d9b07e 100644 --- a/slang/test/integration/main/fallback_base_locale_test.dart +++ b/slang/test/integration/main/fallback_base_locale_test.dart @@ -1,10 +1,10 @@ -import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/builder/raw_config_builder.dart'; import 'package:slang/src/builder/decoder/csv_decoder.dart'; import 'package:slang/src/builder/decoder/json_decoder.dart'; import 'package:slang/src/builder/generator_facade.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; @@ -12,23 +12,39 @@ import '../../util/resources_utils.dart'; void main() { late String compactInput; late String buildYaml; - late String expectedOutput; + late String expectedMainOutput; + late String expectedEnOutput; + late String expectedDeOutput; late String specialEnInput; late String specialDeInput; - late String specialExpectedOutput; + late String specialExpectedMainOutput; + late String specialExpectedEnOutput; + late String specialExpectedDeOutput; setUp(() { compactInput = loadResource('main/csv_compact.csv'); buildYaml = loadResource('main/build_config.yaml'); - expectedOutput = loadResource( - 'main/_expected_fallback_base_locale.output', + expectedMainOutput = loadResource( + 'main/_expected_fallback_base_locale_main.output', + ); + expectedEnOutput = loadResource( + 'main/_expected_fallback_base_locale_en.output', + ); + expectedDeOutput = loadResource( + 'main/_expected_fallback_base_locale_de.output', ); specialEnInput = loadResource('main/fallback_en.json'); specialDeInput = loadResource('main/fallback_de.json'); - specialExpectedOutput = loadResource( - 'main/_expected_fallback_base_locale_special.output', + specialExpectedMainOutput = loadResource( + 'main/_expected_fallback_base_locale_special_main.output', + ); + specialExpectedEnOutput = loadResource( + 'main/_expected_fallback_base_locale_special_en.output', + ); + specialExpectedDeOutput = loadResource( + 'main/_expected_fallback_base_locale_special_de.output', ); }); @@ -39,7 +55,6 @@ void main() { rawConfig: RawConfigBuilder.fromYaml(buildYaml)!.copyWith( fallbackStrategy: FallbackStrategy.baseLocale, ), - baseName: 'translations', translationMap: TranslationMap() ..addTranslations( locale: I18nLocale.fromString('en'), @@ -52,7 +67,9 @@ void main() { inputDirectoryHint: 'fake/path/integration', ); - expect(result.joinAsSingleOutput(), expectedOutput); + expect(result.main, expectedMainOutput); + expect(result.translations[I18nLocale.fromString('en')], expectedEnOutput); + expect(result.translations[I18nLocale.fromString('de')], expectedDeOutput); }); test('fallback with special integration data', () { @@ -60,7 +77,6 @@ void main() { rawConfig: RawConfigBuilder.fromYaml(buildYaml)!.copyWith( fallbackStrategy: FallbackStrategy.baseLocale, ), - baseName: 'translations', translationMap: TranslationMap() ..addTranslations( locale: I18nLocale.fromString('en'), @@ -73,6 +89,14 @@ void main() { inputDirectoryHint: 'fake/path/integration', ); - expect(result.joinAsSingleOutput(), specialExpectedOutput); + expect(result.main, specialExpectedMainOutput); + expect( + result.translations[I18nLocale.fromString('en')], + specialExpectedEnOutput, + ); + expect( + result.translations[I18nLocale.fromString('de')], + specialExpectedDeOutput, + ); }); } diff --git a/slang/test/integration/main/json_multiple_files_test.dart b/slang/test/integration/main/json_multiple_files_test.dart deleted file mode 100644 index 3ff9b011..00000000 --- a/slang/test/integration/main/json_multiple_files_test.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/translation_map.dart'; -import 'package:slang/src/builder/decoder/json_decoder.dart'; -import 'package:slang/src/builder/generator_facade.dart'; -import 'package:test/test.dart'; - -import '../../util/resources_utils.dart'; - -void main() { - late String enInput; - late String deInput; - late String buildYaml; - late String expectedMainOutput; - late String expectedEnOutput; - late String expectedDeOutput; - late String expectedFlatMapOutput; - - setUp(() { - enInput = loadResource('main/json_en.json'); - deInput = loadResource('main/json_de.json'); - buildYaml = loadResource('main/build_config.yaml'); - expectedMainOutput = loadResource('main/_expected_main.output'); - expectedEnOutput = loadResource('main/_expected_en.output'); - expectedDeOutput = loadResource('main/_expected_de.output'); - expectedFlatMapOutput = loadResource('main/_expected_map.output'); - }); - - test('json', () { - final result = GeneratorFacade.generate( - rawConfig: RawConfigBuilder.fromYaml(buildYaml)!.copyWith( - outputFormat: OutputFormat.multipleFiles, - outputFileName: 'translations.cgm.dart', - ), - baseName: 'translations', - translationMap: TranslationMap() - ..addTranslations( - locale: I18nLocale.fromString('en'), - translations: JsonDecoder().decode(enInput), - ) - ..addTranslations( - locale: I18nLocale.fromString('de'), - translations: JsonDecoder().decode(deInput), - ), - inputDirectoryHint: 'fake/path/integration', - ); - - expect(result.header, expectedMainOutput); - expect(result.translations[I18nLocale.fromString('en')], expectedEnOutput); - expect(result.translations[I18nLocale.fromString('de')], expectedDeOutput); - expect(result.flatMap, expectedFlatMapOutput); - }); -} diff --git a/slang/test/integration/main/json_test.dart b/slang/test/integration/main/json_test.dart index 8e6b9580..4f4efb1d 100644 --- a/slang/test/integration/main/json_test.dart +++ b/slang/test/integration/main/json_test.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/builder/raw_config_builder.dart'; import 'package:slang/src/builder/decoder/json_decoder.dart'; import 'package:slang/src/builder/generator_facade.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; @@ -11,19 +11,22 @@ void main() { late String enInput; late String deInput; late String buildYaml; - late String expectedOutput; + late String expectedMainOutput; + late String expectedEnOutput; + late String expectedDeOutput; setUp(() { enInput = loadResource('main/json_en.json'); deInput = loadResource('main/json_de.json'); buildYaml = loadResource('main/build_config.yaml'); - expectedOutput = loadResource('main/_expected_single.output'); + expectedMainOutput = loadResource('main/_expected_main.output'); + expectedEnOutput = loadResource('main/_expected_en.output'); + expectedDeOutput = loadResource('main/_expected_de.output'); }); test('json', () { final result = GeneratorFacade.generate( rawConfig: RawConfigBuilder.fromYaml(buildYaml)!, - baseName: 'translations', translationMap: TranslationMap() ..addTranslations( locale: I18nLocale.fromString('en'), @@ -36,6 +39,8 @@ void main() { inputDirectoryHint: 'fake/path/integration', ); - expect(result.joinAsSingleOutput(), expectedOutput); + expect(result.main, expectedMainOutput); + expect(result.translations[I18nLocale.fromString('en')], expectedEnOutput); + expect(result.translations[I18nLocale.fromString('de')], expectedDeOutput); }); } diff --git a/slang/test/integration/main/no_flutter_test.dart b/slang/test/integration/main/no_flutter_test.dart index b4ea8fa4..ba85d431 100644 --- a/slang/test/integration/main/no_flutter_test.dart +++ b/slang/test/integration/main/no_flutter_test.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/builder/raw_config_builder.dart'; import 'package:slang/src/builder/decoder/json_decoder.dart'; import 'package:slang/src/builder/generator_facade.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; @@ -10,12 +10,12 @@ import '../../util/resources_utils.dart'; void main() { late String input; late String buildYaml; - late String expectedOutput; + late String expectedMainOutput; setUp(() { input = loadResource('main/json_simple.json'); buildYaml = loadResource('main/build_config.yaml'); - expectedOutput = loadResource('main/_expected_no_flutter.output'); + expectedMainOutput = loadResource('main/_expected_no_flutter.output'); }); test('no flutter', () { @@ -23,7 +23,6 @@ void main() { rawConfig: RawConfigBuilder.fromYaml(buildYaml)!.copyWith( flutterIntegration: false, ), - baseName: 'translations', translationMap: TranslationMap() ..addTranslations( locale: I18nLocale.fromString('en'), @@ -32,6 +31,6 @@ void main() { inputDirectoryHint: 'fake/path/integration', ); - expect(result.joinAsSingleOutput(), expectedOutput); + expect(result.main, expectedMainOutput); }); } diff --git a/slang/test/integration/main/no_locale_handling_test.dart b/slang/test/integration/main/no_locale_handling_test.dart index b6be7cb4..2eeac31e 100644 --- a/slang/test/integration/main/no_locale_handling_test.dart +++ b/slang/test/integration/main/no_locale_handling_test.dart @@ -1,9 +1,9 @@ -import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/builder/raw_config_builder.dart'; import 'package:slang/src/builder/decoder/json_decoder.dart'; import 'package:slang/src/builder/generator_facade.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; @@ -25,7 +25,6 @@ void main() { localeHandling: false, translationClassVisibility: TranslationClassVisibility.public, ), - baseName: 'translations', translationMap: TranslationMap() ..addTranslations( locale: I18nLocale.fromString('en'), @@ -34,6 +33,6 @@ void main() { inputDirectoryHint: 'fake/path/integration', ); - expect(result.joinAsSingleOutput(), expectedOutput); + expect(result.main, expectedOutput); }); } diff --git a/slang/test/integration/main/obfuscation_test.dart b/slang/test/integration/main/obfuscation_test.dart index ba1387d2..0ef35edc 100644 --- a/slang/test/integration/main/obfuscation_test.dart +++ b/slang/test/integration/main/obfuscation_test.dart @@ -1,9 +1,9 @@ -import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/obfuscation_config.dart'; -import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/builder/raw_config_builder.dart'; import 'package:slang/src/builder/decoder/csv_decoder.dart'; import 'package:slang/src/builder/generator_facade.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/obfuscation_config.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; @@ -11,14 +11,16 @@ import '../../util/resources_utils.dart'; void main() { late String compactInput; late String buildYaml; - late String expectedOutput; + late String expectedMainOutput; + late String expectedEnOutput; + late String expectedDeOutput; setUp(() { compactInput = loadResource('main/csv_compact.csv'); buildYaml = loadResource('main/build_config.yaml'); - expectedOutput = loadResource( - 'main/_expected_obfuscation.output', - ); + expectedMainOutput = loadResource('main/_expected_obfuscation_main.output'); + expectedEnOutput = loadResource('main/_expected_obfuscation_en.output'); + expectedDeOutput = loadResource('main/_expected_obfuscation_de.output'); }); test('obfuscation', () { @@ -31,7 +33,6 @@ void main() { secret: 'abc', ), ), - baseName: 'translations', translationMap: TranslationMap() ..addTranslations( locale: I18nLocale.fromString('en'), @@ -44,6 +45,8 @@ void main() { inputDirectoryHint: 'fake/path/integration', ); - expect(result.joinAsSingleOutput(), expectedOutput); + expect(result.main, expectedMainOutput); + expect(result.translations[I18nLocale.fromString('en')], expectedEnOutput); + expect(result.translations[I18nLocale.fromString('de')], expectedDeOutput); }); } diff --git a/slang/test/integration/main/rich_text_test.dart b/slang/test/integration/main/rich_text_test.dart index 20fa3ca9..22f544bb 100644 --- a/slang/test/integration/main/rich_text_test.dart +++ b/slang/test/integration/main/rich_text_test.dart @@ -1,19 +1,19 @@ -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/raw_config.dart'; -import 'package:slang/builder/model/translation_map.dart'; import 'package:slang/src/builder/decoder/json_decoder.dart'; import 'package:slang/src/builder/generator_facade.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; void main() { late String input; - late String expectedOutput; + late String expectedEnOutput; setUp(() { input = loadResource('main/json_rich_text.json'); - expectedOutput = loadResource( + expectedEnOutput = loadResource( 'main/_expected_rich_text.output', ); }); @@ -23,7 +23,6 @@ void main() { rawConfig: RawConfig.defaultConfig.copyWith( renderTimestamp: false, ), - baseName: 'translations', translationMap: TranslationMap() ..addTranslations( locale: I18nLocale.fromString('en'), @@ -32,6 +31,6 @@ void main() { inputDirectoryHint: 'fake/path/integration', ); - expect(result.joinAsSingleOutput(), expectedOutput); + expect(result.translations[I18nLocale(language: 'en')], expectedEnOutput); }); } diff --git a/slang/test/integration/main/translation_overrides_test.dart b/slang/test/integration/main/translation_overrides_test.dart index 572a29b1..1210dffc 100644 --- a/slang/test/integration/main/translation_overrides_test.dart +++ b/slang/test/integration/main/translation_overrides_test.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/builder/raw_config_builder.dart'; import 'package:slang/src/builder/decoder/csv_decoder.dart'; import 'package:slang/src/builder/generator_facade.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; @@ -10,13 +10,21 @@ import '../../util/resources_utils.dart'; void main() { late String compactInput; late String buildYaml; - late String expectedOutput; + late String expectedMainOutput; + late String expectedEnOutput; + late String expectedDeOutput; setUp(() { compactInput = loadResource('main/csv_compact.csv'); buildYaml = loadResource('main/build_config.yaml'); - expectedOutput = loadResource( - 'main/_expected_translation_overrides.output', + expectedMainOutput = loadResource( + 'main/_expected_translation_overrides_main.output', + ); + expectedEnOutput = loadResource( + 'main/_expected_translation_overrides_en.output', + ); + expectedDeOutput = loadResource( + 'main/_expected_translation_overrides_de.output', ); }); @@ -27,7 +35,6 @@ void main() { rawConfig: RawConfigBuilder.fromYaml(buildYaml)!.copyWith( translationOverrides: true, ), - baseName: 'translations', translationMap: TranslationMap() ..addTranslations( locale: I18nLocale.fromString('en'), @@ -40,6 +47,8 @@ void main() { inputDirectoryHint: 'fake/path/integration', ); - expect(result.joinAsSingleOutput(), expectedOutput); + expect(result.main, expectedMainOutput); + expect(result.translations[I18nLocale.fromString('en')], expectedEnOutput); + expect(result.translations[I18nLocale.fromString('de')], expectedDeOutput); }); } diff --git a/slang/test/integration/main/yaml_test.dart b/slang/test/integration/main/yaml_test.dart index 4d419bb5..f23ddad8 100644 --- a/slang/test/integration/main/yaml_test.dart +++ b/slang/test/integration/main/yaml_test.dart @@ -1,8 +1,8 @@ -import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/builder/raw_config_builder.dart'; import 'package:slang/src/builder/decoder/yaml_decoder.dart'; import 'package:slang/src/builder/generator_facade.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; @@ -11,19 +11,22 @@ void main() { late String enInput; late String deInput; late String buildYaml; - late String expectedOutput; + late String expectedMainOutput; + late String expectedEnOutput; + late String expectedDeOutput; setUp(() { enInput = loadResource('main/yaml_en.yaml'); deInput = loadResource('main/yaml_de.yaml'); buildYaml = loadResource('main/build_config.yaml'); - expectedOutput = loadResource('main/_expected_single.output'); + expectedMainOutput = loadResource('main/_expected_main.output'); + expectedEnOutput = loadResource('main/_expected_en.output'); + expectedDeOutput = loadResource('main/_expected_de.output'); }); test('yaml', () { final result = GeneratorFacade.generate( rawConfig: RawConfigBuilder.fromYaml(buildYaml)!, - baseName: 'translations', translationMap: TranslationMap() ..addTranslations( locale: I18nLocale.fromString('en'), @@ -36,6 +39,8 @@ void main() { inputDirectoryHint: 'fake/path/integration', ); - expect(result.joinAsSingleOutput(), expectedOutput); + expect(result.main, expectedMainOutput); + expect(result.translations[I18nLocale.fromString('en')], expectedEnOutput); + expect(result.translations[I18nLocale.fromString('de')], expectedDeOutput); }); } diff --git a/slang/test/integration/resources/main/_expected_de.output b/slang/test/integration/resources/main/_expected_de.output index 3d8fa787..ca32cdc1 100644 --- a/slang/test/integration/resources/main/_expected_de.output +++ b/slang/test/integration/resources/main/_expected_de.output @@ -2,15 +2,17 @@ /// Generated file. Do not edit. /// // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import -part of 'translations.cgm.dart'; +import 'package:flutter/widgets.dart'; +import 'package:slang/node.dart'; +import 'translations.cgm.dart'; // Path: -class _TranslationsDe implements Translations { +class TranslationsDe implements Translations { /// You can call this constructor and build your own translation instance of this locale. /// Constructing via the enum [AppLocale.build] is preferred. - _TranslationsDe.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + TranslationsDe({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), $meta = TranslationMetadata( locale: AppLocale.de, @@ -27,7 +29,7 @@ class _TranslationsDe implements Translations { /// Access flat map @override dynamic operator[](String key) => $meta.getTranslation(key); - @override late final _TranslationsDe _root = this; // ignore: unused_field + late final TranslationsDe _root = this; // ignore: unused_field // Translations @override late final _TranslationsOnboardingDe onboarding = _TranslationsOnboardingDe._(_root); @@ -48,10 +50,10 @@ class _TranslationsDe implements Translations { } // Path: onboarding -class _TranslationsOnboardingDe implements _TranslationsOnboardingEn { +class _TranslationsOnboardingDe implements TranslationsOnboardingEn { _TranslationsOnboardingDe._(this._root); - @override final _TranslationsDe _root; // ignore: unused_field + final TranslationsDe _root; // ignore: unused_field // Translations @override String welcome({required Object fullName}) => 'Willkommen ${fullName}'; @@ -97,10 +99,10 @@ class _TranslationsOnboardingDe implements _TranslationsOnboardingEn { } // Path: group -class _TranslationsGroupDe implements _TranslationsGroupEn { +class _TranslationsGroupDe implements TranslationsGroupEn { _TranslationsGroupDe._(this._root); - @override final _TranslationsDe _root; // ignore: unused_field + final TranslationsDe _root; // ignore: unused_field // Translations @override String users({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, @@ -111,10 +113,10 @@ class _TranslationsGroupDe implements _TranslationsGroupEn { } // Path: end -class _TranslationsEndDe with EndData implements _TranslationsEndEn { +class _TranslationsEndDe with EndData implements TranslationsEndEn { _TranslationsEndDe._(this._root); - @override final _TranslationsDe _root; // ignore: unused_field + final TranslationsDe _root; // ignore: unused_field // Translations @override List get stringPages => [ @@ -133,10 +135,10 @@ class _TranslationsEndDe with EndData implements _TranslationsEndEn { } // Path: onboarding.pages.0 -class _TranslationsOnboarding$pages$0i0$De with PageData implements _TranslationsOnboarding$pages$0i0$En { +class _TranslationsOnboarding$pages$0i0$De with PageData implements TranslationsOnboarding$pages$0i0$En { _TranslationsOnboarding$pages$0i0$De._(this._root); - @override final _TranslationsDe _root; // ignore: unused_field + final TranslationsDe _root; // ignore: unused_field // Translations @override String get title => 'Erste Seite'; @@ -144,20 +146,20 @@ class _TranslationsOnboarding$pages$0i0$De with PageData implements _Translation } // Path: onboarding.pages.1 -class _TranslationsOnboarding$pages$0i1$De with PageData implements _TranslationsOnboarding$pages$0i1$En { +class _TranslationsOnboarding$pages$0i1$De with PageData implements TranslationsOnboarding$pages$0i1$En { _TranslationsOnboarding$pages$0i1$De._(this._root); - @override final _TranslationsDe _root; // ignore: unused_field + final TranslationsDe _root; // ignore: unused_field // Translations @override String get title => 'Zweite Seite'; } // Path: onboarding.modifierPages.0 -class _TranslationsOnboarding$modifierPages$0i0$De with MPage implements _TranslationsOnboarding$modifierPages$0i0$En { +class _TranslationsOnboarding$modifierPages$0i0$De with MPage implements TranslationsOnboarding$modifierPages$0i0$En { _TranslationsOnboarding$modifierPages$0i0$De._(this._root); - @override final _TranslationsDe _root; // ignore: unused_field + final TranslationsDe _root; // ignore: unused_field // Translations @override String get title => 'Erste Modifier Seite'; @@ -165,11 +167,79 @@ class _TranslationsOnboarding$modifierPages$0i0$De with MPage implements _Transl } // Path: onboarding.modifierPages.1 -class _TranslationsOnboarding$modifierPages$0i1$De with MPage implements _TranslationsOnboarding$modifierPages$0i1$En { +class _TranslationsOnboarding$modifierPages$0i1$De with MPage implements TranslationsOnboarding$modifierPages$0i1$En { _TranslationsOnboarding$modifierPages$0i1$De._(this._root); - @override final _TranslationsDe _root; // ignore: unused_field + final TranslationsDe _root; // ignore: unused_field // Translations @override String get title => 'Zweite Modifier Seite'; } + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on TranslationsDe { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'onboarding.welcome': return ({required Object fullName}) => 'Willkommen ${fullName}'; + case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; + case 'onboarding.bye': return ({required Object firstName}) => 'Tschüss ${firstName}'; + case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ + const TextSpan(text: 'Hi '), + name, + TextSpan(text: ' und ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), + ]); + case 'onboarding.pages.0.title': return 'Erste Seite'; + case 'onboarding.pages.0.content': return 'Erster Seiteninhalt'; + case 'onboarding.pages.1.title': return 'Zweite Seite'; + case 'onboarding.modifierPages.0.title': return 'Erste Modifier Seite'; + case 'onboarding.modifierPages.0.content': return 'Erster Seiteninhalt'; + case 'onboarding.modifierPages.1.title': return 'Zweite Modifier Seite'; + case 'onboarding.greet': return ({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { + switch (context) { + case GenderContext.male: + return 'Hallo Herr ${lastName} und ${_root.onboarding.welcome(fullName: fullName)}'; + case GenderContext.female: + return 'Hallo Frau ${lastName} und ${_root.onboarding.bye(firstName: firstName)}'; + } + }; + case 'onboarding.greet2': return ({required GenderContext gender}) { + switch (gender) { + case GenderContext.male: + return 'Hallo Herr'; + case GenderContext.female: + return 'Hallo Frau'; + } + }; + case 'onboarding.greetCombination': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => '${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}, ${_root.onboarding.greet2(gender: gender)}'; + case 'onboarding.welcomeLinkedPlural': return ({required num n, required Object fullName, required Object firstName}) => 'Hallo ${_root.group.users(n: n, fullName: fullName, firstName: firstName)}'; + case 'onboarding.welcomeLinkedContext': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => 'Hallo ${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + case 'onboarding.welcomeFullLink': return ({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => 'Ultimative ${_root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName)} and ${_root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + case 'group.users': return ({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, + zero: 'Keine Nutzer und ${_root.onboarding.welcome(fullName: fullName)}', + one: 'Ein Nutzer', + other: '${n} Nutzer und ${_root.onboarding.bye(firstName: firstName)}', + ); + case 'end.stringPages.0': return '1. Seite'; + case 'end.stringPages.1': return '2. Seite'; + case 'end.pages.0.unknown': return 'Unbekannter\nFehler'; + case 'end.pages.1.with space': return 'Ein Fehler'; + case 'end.pages.1.with second space': return 'Ein 2. Fehler'; + case 'advancedPlural': return ({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( + n: count, + resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'), + one: () => TextSpan(children: [ + const TextSpan(text: 'Eins'), + ]), + other: () => TextSpan(children: [ + const TextSpan(text: 'Andere '), + countBuilder(count), + TextSpan(text: ', ${_root.onboarding.greet2(gender: gender)}'), + ]), + ); + default: return null; + } + } +} + diff --git a/slang/test/integration/resources/main/_expected_en.output b/slang/test/integration/resources/main/_expected_en.output index 4ec7bba3..865ccdcc 100644 --- a/slang/test/integration/resources/main/_expected_en.output +++ b/slang/test/integration/resources/main/_expected_en.output @@ -2,11 +2,12 @@ /// Generated file. Do not edit. /// // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import part of 'translations.cgm.dart'; // Path: +typedef TranslationsEn = Translations; // ignore: unused_element class Translations implements BaseTranslations { /// Returns the current translations of the given [context]. /// @@ -16,7 +17,7 @@ class Translations implements BaseTranslations { /// You can call this constructor and build your own translation instance of this locale. /// Constructing via the enum [AppLocale.build] is preferred. - Translations.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + Translations({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), $meta = TranslationMetadata( locale: AppLocale.en, @@ -36,9 +37,9 @@ class Translations implements BaseTranslations { late final Translations _root = this; // ignore: unused_field // Translations - late final _TranslationsOnboardingEn onboarding = _TranslationsOnboardingEn._(_root); - late final _TranslationsGroupEn group = _TranslationsGroupEn._(_root); - late final _TranslationsEndEn end = _TranslationsEndEn._(_root); + late final TranslationsOnboardingEn onboarding = TranslationsOnboardingEn._(_root); + late final TranslationsGroupEn group = TranslationsGroupEn._(_root); + late final TranslationsEndEn end = TranslationsEndEn._(_root); TextSpan advancedPlural({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( n: count, resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'), @@ -54,8 +55,8 @@ class Translations implements BaseTranslations { } // Path: onboarding -class _TranslationsOnboardingEn { - _TranslationsOnboardingEn._(this._root); +class TranslationsOnboardingEn { + TranslationsOnboardingEn._(this._root); final Translations _root; // ignore: unused_field @@ -73,12 +74,12 @@ class _TranslationsOnboardingEn { TextSpan(text: ' and ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), ]); List get pages => [ - _TranslationsOnboarding$pages$0i0$En._(_root), - _TranslationsOnboarding$pages$0i1$En._(_root), + TranslationsOnboarding$pages$0i0$En._(_root), + TranslationsOnboarding$pages$0i1$En._(_root), ]; List get modifierPages => [ - _TranslationsOnboarding$modifierPages$0i0$En._(_root), - _TranslationsOnboarding$modifierPages$0i1$En._(_root), + TranslationsOnboarding$modifierPages$0i0$En._(_root), + TranslationsOnboarding$modifierPages$0i1$En._(_root), ]; String greet({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { switch (context) { @@ -103,8 +104,8 @@ class _TranslationsOnboardingEn { } // Path: group -class _TranslationsGroupEn { - _TranslationsGroupEn._(this._root); +class TranslationsGroupEn { + TranslationsGroupEn._(this._root); final Translations _root; // ignore: unused_field @@ -117,8 +118,8 @@ class _TranslationsGroupEn { } // Path: end -class _TranslationsEndEn with EndData { - _TranslationsEndEn._(this._root); +class TranslationsEndEn with EndData { + TranslationsEndEn._(this._root); final Translations _root; // ignore: unused_field @@ -139,8 +140,8 @@ class _TranslationsEndEn with EndData { } // Path: onboarding.pages.0 -class _TranslationsOnboarding$pages$0i0$En with PageData { - _TranslationsOnboarding$pages$0i0$En._(this._root); +class TranslationsOnboarding$pages$0i0$En with PageData { + TranslationsOnboarding$pages$0i0$En._(this._root); final Translations _root; // ignore: unused_field @@ -150,8 +151,8 @@ class _TranslationsOnboarding$pages$0i0$En with PageData { } // Path: onboarding.pages.1 -class _TranslationsOnboarding$pages$0i1$En with PageData { - _TranslationsOnboarding$pages$0i1$En._(this._root); +class TranslationsOnboarding$pages$0i1$En with PageData { + TranslationsOnboarding$pages$0i1$En._(this._root); final Translations _root; // ignore: unused_field @@ -160,8 +161,8 @@ class _TranslationsOnboarding$pages$0i1$En with PageData { } // Path: onboarding.modifierPages.0 -class _TranslationsOnboarding$modifierPages$0i0$En with MPage { - _TranslationsOnboarding$modifierPages$0i0$En._(this._root); +class TranslationsOnboarding$modifierPages$0i0$En with MPage { + TranslationsOnboarding$modifierPages$0i0$En._(this._root); final Translations _root; // ignore: unused_field @@ -171,11 +172,79 @@ class _TranslationsOnboarding$modifierPages$0i0$En with MPage { } // Path: onboarding.modifierPages.1 -class _TranslationsOnboarding$modifierPages$0i1$En with MPage { - _TranslationsOnboarding$modifierPages$0i1$En._(this._root); +class TranslationsOnboarding$modifierPages$0i1$En with MPage { + TranslationsOnboarding$modifierPages$0i1$En._(this._root); final Translations _root; // ignore: unused_field // Translations @override String get title => 'Second Modifier Page'; } + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on Translations { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'onboarding.welcome': return ({required Object fullName}) => 'Welcome ${fullName}'; + case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; + case 'onboarding.bye': return ({required Object firstName}) => 'Bye ${firstName}'; + case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ + const TextSpan(text: 'Hi '), + name, + TextSpan(text: ' and ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), + ]); + case 'onboarding.pages.0.title': return 'First Page'; + case 'onboarding.pages.0.content': return 'First Page Content'; + case 'onboarding.pages.1.title': return 'Second Page'; + case 'onboarding.modifierPages.0.title': return 'First Modifier Page'; + case 'onboarding.modifierPages.0.content': return 'First Page Content'; + case 'onboarding.modifierPages.1.title': return 'Second Modifier Page'; + case 'onboarding.greet': return ({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { + switch (context) { + case GenderContext.male: + return 'Hello Mr ${lastName} and ${_root.onboarding.welcome(fullName: fullName)}'; + case GenderContext.female: + return 'Hello Ms ${lastName} and ${_root.onboarding.bye(firstName: firstName)}'; + } + }; + case 'onboarding.greet2': return ({required GenderContext gender}) { + switch (gender) { + case GenderContext.male: + return 'Hello Mr'; + case GenderContext.female: + return 'Hello Ms'; + } + }; + case 'onboarding.greetCombination': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => '${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}, ${_root.onboarding.greet2(gender: gender)}'; + case 'onboarding.welcomeLinkedPlural': return ({required num n, required Object fullName, required Object firstName}) => 'Hello ${_root.group.users(n: n, fullName: fullName, firstName: firstName)}'; + case 'onboarding.welcomeLinkedContext': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => 'Hello ${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + case 'onboarding.welcomeFullLink': return ({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => 'Ultimate ${_root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName)} and ${_root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + case 'group.users': return ({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: 'No Users and ${_root.onboarding.welcome(fullName: fullName)}', + one: 'One User', + other: '${n} Users and ${_root.onboarding.bye(firstName: firstName)}', + ); + case 'end.stringPages.0': return '1st Page'; + case 'end.stringPages.1': return '2nd Page'; + case 'end.pages.0.unknown': return 'Unknown\nError'; + case 'end.pages.1.with space': return 'An Error'; + case 'end.pages.1.with second space': return 'An 2nd Error'; + case 'advancedPlural': return ({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( + n: count, + resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'), + one: () => TextSpan(children: [ + const TextSpan(text: 'One'), + ]), + other: () => TextSpan(children: [ + const TextSpan(text: 'Other '), + countBuilder(count), + TextSpan(text: ', ${_root.onboarding.greet2(gender: gender)}'), + ]), + ); + default: return null; + } + } +} + diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output new file mode 100644 index 00000000..c4856b2f --- /dev/null +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output @@ -0,0 +1,247 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:slang/node.dart'; +import 'translations.cgm.dart'; + +// Path: +class TranslationsDe extends Translations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + TranslationsDe({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.de, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ), + super.build(cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver) { + super.$meta.setFlatMapFunction($meta.getTranslation); // copy base translations to super.$meta + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + @override dynamic operator[](String key) => $meta.getTranslation(key) ?? super.$meta.getTranslation(key); + + late final TranslationsDe _root = this; // ignore: unused_field + + // Translations + @override late final _TranslationsOnboardingDe onboarding = _TranslationsOnboardingDe._(_root); + @override late final _TranslationsGroupDe group = _TranslationsGroupDe._(_root); + @override late final _TranslationsEndDe end = _TranslationsEndDe._(_root); + @override TextSpan advancedPlural({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( + n: count, + resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'), + one: () => TextSpan(children: [ + const TextSpan(text: 'Eins'), + ]), + other: () => TextSpan(children: [ + const TextSpan(text: 'Andere '), + countBuilder(count), + TextSpan(text: ', ${_root.onboarding.greet2(gender: gender)}'), + ]), + ); +} + +// Path: onboarding +class _TranslationsOnboardingDe extends TranslationsOnboardingEn { + _TranslationsOnboardingDe._(TranslationsDe root) : this._root = root, super._(root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String welcome({required Object fullName}) => 'Willkommen ${fullName}'; + @override String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + @override String welcomeOnlyParam({required Object firstName}) => '${firstName}'; + + /// Bye text + @override String bye({required Object firstName}) => 'Tschüss ${firstName}'; + + @override TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ + const TextSpan(text: 'Hi '), + name, + TextSpan(text: ' und ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), + ]); + @override List get pages => [ + _TranslationsOnboarding$pages$0i0$De._(_root), + _TranslationsOnboarding$pages$0i1$De._(_root), + ]; + @override List get modifierPages => [ + _TranslationsOnboarding$modifierPages$0i0$De._(_root), + _TranslationsOnboarding$modifierPages$0i1$De._(_root), + ]; + @override String greet({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { + switch (context) { + case GenderContext.male: + return 'Hallo Herr ${lastName} und ${_root.onboarding.welcome(fullName: fullName)}'; + case GenderContext.female: + return 'Hallo Frau ${lastName} und ${_root.onboarding.bye(firstName: firstName)}'; + } + } + @override String greet2({required GenderContext gender}) { + switch (gender) { + case GenderContext.male: + return 'Hallo Herr'; + case GenderContext.female: + return 'Hallo Frau'; + } + } + @override String greetCombination({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => '${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}, ${_root.onboarding.greet2(gender: gender)}'; + @override String welcomeLinkedPlural({required num n, required Object fullName, required Object firstName}) => 'Hallo ${_root.group.users(n: n, fullName: fullName, firstName: firstName)}'; + @override String welcomeLinkedContext({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => 'Hallo ${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + @override String welcomeFullLink({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => 'Ultimative ${_root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName)} and ${_root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; +} + +// Path: group +class _TranslationsGroupDe extends TranslationsGroupEn { + _TranslationsGroupDe._(TranslationsDe root) : this._root = root, super._(root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String users({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, + zero: 'Keine Nutzer und ${_root.onboarding.welcome(fullName: fullName)}', + one: 'Ein Nutzer', + other: '${n} Nutzer und ${_root.onboarding.bye(firstName: firstName)}', + ); +} + +// Path: end +class _TranslationsEndDe extends TranslationsEndEn with EndData { + _TranslationsEndDe._(TranslationsDe root) : this._root = root, super._(root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override List get stringPages => [ + '1. Seite', + '2. Seite', + ]; + @override List> get pages => [ + { + 'unknown': 'Unbekannter\nFehler', + }, + { + 'with space': 'Ein Fehler', + 'with second space': 'Ein 2. Fehler', + }, + ]; +} + +// Path: onboarding.pages.0 +class _TranslationsOnboarding$pages$0i0$De extends TranslationsOnboarding$pages$0i0$En with PageData { + _TranslationsOnboarding$pages$0i0$De._(TranslationsDe root) : this._root = root, super._(root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get title => 'Erste Seite'; + @override String? get content => 'Erster Seiteninhalt'; +} + +// Path: onboarding.pages.1 +class _TranslationsOnboarding$pages$0i1$De extends TranslationsOnboarding$pages$0i1$En with PageData { + _TranslationsOnboarding$pages$0i1$De._(TranslationsDe root) : this._root = root, super._(root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get title => 'Zweite Seite'; +} + +// Path: onboarding.modifierPages.0 +class _TranslationsOnboarding$modifierPages$0i0$De extends TranslationsOnboarding$modifierPages$0i0$En with MPage { + _TranslationsOnboarding$modifierPages$0i0$De._(TranslationsDe root) : this._root = root, super._(root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get title => 'Erste Modifier Seite'; + @override String? get content => 'Erster Seiteninhalt'; +} + +// Path: onboarding.modifierPages.1 +class _TranslationsOnboarding$modifierPages$0i1$De extends TranslationsOnboarding$modifierPages$0i1$En with MPage { + _TranslationsOnboarding$modifierPages$0i1$De._(TranslationsDe root) : this._root = root, super._(root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get title => 'Zweite Modifier Seite'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on TranslationsDe { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'onboarding.welcome': return ({required Object fullName}) => 'Willkommen ${fullName}'; + case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; + case 'onboarding.bye': return ({required Object firstName}) => 'Tschüss ${firstName}'; + case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ + const TextSpan(text: 'Hi '), + name, + TextSpan(text: ' und ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), + ]); + case 'onboarding.pages.0.title': return 'Erste Seite'; + case 'onboarding.pages.0.content': return 'Erster Seiteninhalt'; + case 'onboarding.pages.1.title': return 'Zweite Seite'; + case 'onboarding.modifierPages.0.title': return 'Erste Modifier Seite'; + case 'onboarding.modifierPages.0.content': return 'Erster Seiteninhalt'; + case 'onboarding.modifierPages.1.title': return 'Zweite Modifier Seite'; + case 'onboarding.greet': return ({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { + switch (context) { + case GenderContext.male: + return 'Hallo Herr ${lastName} und ${_root.onboarding.welcome(fullName: fullName)}'; + case GenderContext.female: + return 'Hallo Frau ${lastName} und ${_root.onboarding.bye(firstName: firstName)}'; + } + }; + case 'onboarding.greet2': return ({required GenderContext gender}) { + switch (gender) { + case GenderContext.male: + return 'Hallo Herr'; + case GenderContext.female: + return 'Hallo Frau'; + } + }; + case 'onboarding.greetCombination': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => '${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}, ${_root.onboarding.greet2(gender: gender)}'; + case 'onboarding.welcomeLinkedPlural': return ({required num n, required Object fullName, required Object firstName}) => 'Hallo ${_root.group.users(n: n, fullName: fullName, firstName: firstName)}'; + case 'onboarding.welcomeLinkedContext': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => 'Hallo ${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + case 'onboarding.welcomeFullLink': return ({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => 'Ultimative ${_root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName)} and ${_root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + case 'group.users': return ({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, + zero: 'Keine Nutzer und ${_root.onboarding.welcome(fullName: fullName)}', + one: 'Ein Nutzer', + other: '${n} Nutzer und ${_root.onboarding.bye(firstName: firstName)}', + ); + case 'end.stringPages.0': return '1. Seite'; + case 'end.stringPages.1': return '2. Seite'; + case 'end.pages.0.unknown': return 'Unbekannter\nFehler'; + case 'end.pages.1.with space': return 'Ein Fehler'; + case 'end.pages.1.with second space': return 'Ein 2. Fehler'; + case 'advancedPlural': return ({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( + n: count, + resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'), + one: () => TextSpan(children: [ + const TextSpan(text: 'Eins'), + ]), + other: () => TextSpan(children: [ + const TextSpan(text: 'Andere '), + countBuilder(count), + TextSpan(text: ', ${_root.onboarding.greet2(gender: gender)}'), + ]), + ); + default: return null; + } + } +} + diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output new file mode 100644 index 00000000..b9c4a8b1 --- /dev/null +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output @@ -0,0 +1,250 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +part of 'translations.cgm.dart'; + +// Path: +typedef TranslationsEn = Translations; // ignore: unused_element +class Translations implements BaseTranslations { + /// Returns the current translations of the given [context]. + /// + /// Usage: + /// final t = Translations.of(context); + static Translations of(BuildContext context) => InheritedLocaleData.of(context).translations; + + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + Translations({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.en, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + dynamic operator[](String key) => $meta.getTranslation(key); + + late final Translations _root = this; // ignore: unused_field + + // Translations + late final TranslationsOnboardingEn onboarding = TranslationsOnboardingEn._(_root); + late final TranslationsGroupEn group = TranslationsGroupEn._(_root); + late final TranslationsEndEn end = TranslationsEndEn._(_root); + TextSpan advancedPlural({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( + n: count, + resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'), + one: () => TextSpan(children: [ + const TextSpan(text: 'One'), + ]), + other: () => TextSpan(children: [ + const TextSpan(text: 'Other '), + countBuilder(count), + TextSpan(text: ', ${_root.onboarding.greet2(gender: gender)}'), + ]), + ); +} + +// Path: onboarding +class TranslationsOnboardingEn { + TranslationsOnboardingEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String welcome({required Object fullName}) => 'Welcome ${fullName}'; + String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + String welcomeOnlyParam({required Object firstName}) => '${firstName}'; + + /// Bye text + String bye({required Object firstName}) => 'Bye ${firstName}'; + + TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ + const TextSpan(text: 'Hi '), + name, + TextSpan(text: ' and ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), + ]); + List get pages => [ + TranslationsOnboarding$pages$0i0$En._(_root), + TranslationsOnboarding$pages$0i1$En._(_root), + ]; + List get modifierPages => [ + TranslationsOnboarding$modifierPages$0i0$En._(_root), + TranslationsOnboarding$modifierPages$0i1$En._(_root), + ]; + String greet({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { + switch (context) { + case GenderContext.male: + return 'Hello Mr ${lastName} and ${_root.onboarding.welcome(fullName: fullName)}'; + case GenderContext.female: + return 'Hello Ms ${lastName} and ${_root.onboarding.bye(firstName: firstName)}'; + } + } + String greet2({required GenderContext gender}) { + switch (gender) { + case GenderContext.male: + return 'Hello Mr'; + case GenderContext.female: + return 'Hello Ms'; + } + } + String greetCombination({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => '${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}, ${_root.onboarding.greet2(gender: gender)}'; + String welcomeLinkedPlural({required num n, required Object fullName, required Object firstName}) => 'Hello ${_root.group.users(n: n, fullName: fullName, firstName: firstName)}'; + String welcomeLinkedContext({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => 'Hello ${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + String welcomeFullLink({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => 'Ultimate ${_root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName)} and ${_root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; +} + +// Path: group +class TranslationsGroupEn { + TranslationsGroupEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String users({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: 'No Users and ${_root.onboarding.welcome(fullName: fullName)}', + one: 'One User', + other: '${n} Users and ${_root.onboarding.bye(firstName: firstName)}', + ); +} + +// Path: end +class TranslationsEndEn with EndData { + TranslationsEndEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override List get stringPages => [ + '1st Page', + '2nd Page', + ]; + @override List> get pages => [ + { + 'unknown': 'Unknown\nError', + }, + { + 'with space': 'An Error', + 'with second space': 'An 2nd Error', + }, + ]; +} + +// Path: onboarding.pages.0 +class TranslationsOnboarding$pages$0i0$En with PageData { + TranslationsOnboarding$pages$0i0$En._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override String get title => 'First Page'; + @override String? get content => 'First Page Content'; +} + +// Path: onboarding.pages.1 +class TranslationsOnboarding$pages$0i1$En with PageData { + TranslationsOnboarding$pages$0i1$En._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override String get title => 'Second Page'; +} + +// Path: onboarding.modifierPages.0 +class TranslationsOnboarding$modifierPages$0i0$En with MPage { + TranslationsOnboarding$modifierPages$0i0$En._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override String get title => 'First Modifier Page'; + @override String? get content => 'First Page Content'; +} + +// Path: onboarding.modifierPages.1 +class TranslationsOnboarding$modifierPages$0i1$En with MPage { + TranslationsOnboarding$modifierPages$0i1$En._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override String get title => 'Second Modifier Page'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on Translations { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'onboarding.welcome': return ({required Object fullName}) => 'Welcome ${fullName}'; + case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; + case 'onboarding.bye': return ({required Object firstName}) => 'Bye ${firstName}'; + case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ + const TextSpan(text: 'Hi '), + name, + TextSpan(text: ' and ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), + ]); + case 'onboarding.pages.0.title': return 'First Page'; + case 'onboarding.pages.0.content': return 'First Page Content'; + case 'onboarding.pages.1.title': return 'Second Page'; + case 'onboarding.modifierPages.0.title': return 'First Modifier Page'; + case 'onboarding.modifierPages.0.content': return 'First Page Content'; + case 'onboarding.modifierPages.1.title': return 'Second Modifier Page'; + case 'onboarding.greet': return ({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { + switch (context) { + case GenderContext.male: + return 'Hello Mr ${lastName} and ${_root.onboarding.welcome(fullName: fullName)}'; + case GenderContext.female: + return 'Hello Ms ${lastName} and ${_root.onboarding.bye(firstName: firstName)}'; + } + }; + case 'onboarding.greet2': return ({required GenderContext gender}) { + switch (gender) { + case GenderContext.male: + return 'Hello Mr'; + case GenderContext.female: + return 'Hello Ms'; + } + }; + case 'onboarding.greetCombination': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => '${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}, ${_root.onboarding.greet2(gender: gender)}'; + case 'onboarding.welcomeLinkedPlural': return ({required num n, required Object fullName, required Object firstName}) => 'Hello ${_root.group.users(n: n, fullName: fullName, firstName: firstName)}'; + case 'onboarding.welcomeLinkedContext': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => 'Hello ${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + case 'onboarding.welcomeFullLink': return ({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => 'Ultimate ${_root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName)} and ${_root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + case 'group.users': return ({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: 'No Users and ${_root.onboarding.welcome(fullName: fullName)}', + one: 'One User', + other: '${n} Users and ${_root.onboarding.bye(firstName: firstName)}', + ); + case 'end.stringPages.0': return '1st Page'; + case 'end.stringPages.1': return '2nd Page'; + case 'end.pages.0.unknown': return 'Unknown\nError'; + case 'end.pages.1.with space': return 'An Error'; + case 'end.pages.1.with second space': return 'An 2nd Error'; + case 'advancedPlural': return ({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( + n: count, + resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'), + one: () => TextSpan(children: [ + const TextSpan(text: 'One'), + ]), + other: () => TextSpan(children: [ + const TextSpan(text: 'Other '), + countBuilder(count), + TextSpan(text: ', ${_root.onboarding.greet2(gender: gender)}'), + ]), + ); + default: return null; + } + } +} + diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output new file mode 100644 index 00000000..549b8bdb --- /dev/null +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output @@ -0,0 +1,218 @@ +/// Generated file. Do not edit. +/// +/// Original: fake/path/integration +/// To regenerate, run: `dart run slang` +/// +/// Locales: 2 +/// Strings: 58 (29 per locale) + +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:slang/node.dart'; +import 'package:slang_flutter/slang_flutter.dart'; +export 'package:slang_flutter/slang_flutter.dart'; + +import 'translations_de.g.dart' deferred as _$de; +part 'translations_en.g.dart'; + +/// Supported locales, see extension methods below. +/// +/// Usage: +/// - LocaleSettings.setLocale(AppLocale.en) // set locale +/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum +/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check +enum AppLocale with BaseAppLocale { + en(languageCode: 'en'), + de(languageCode: 'de'); + + const AppLocale({ + required this.languageCode, + this.scriptCode, // ignore: unused_element + this.countryCode, // ignore: unused_element + }); + + @override final String languageCode; + @override final String? scriptCode; + @override final String? countryCode; + + @override + Future build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + await _$de.loadLibrary(); + return _$de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + @override + Translations buildSync({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + return _$de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + /// Gets current instance managed by [LocaleSettings]. + Translations get translations => LocaleSettings.instance.getTranslations(this); +} + +/// Method A: Simple +/// +/// No rebuild after locale change. +/// Translation happens during initialization of the widget (call of t). +/// Configurable via 'translate_var'. +/// +/// Usage: +/// String a = t.someKey.anotherKey; +/// String b = t['someKey.anotherKey']; // Only for edge cases! +Translations get t => LocaleSettings.instance.currentTranslations; + +/// Method B: Advanced +/// +/// All widgets using this method will trigger a rebuild when locale changes. +/// Use this if you have e.g. a settings page where the user can select the locale during runtime. +/// +/// Step 1: +/// wrap your App with +/// TranslationProvider( +/// child: MyApp() +/// ); +/// +/// Step 2: +/// final t = Translations.of(context); // Get t variable. +/// String a = t.someKey.anotherKey; // Use t variable. +/// String b = t['someKey.anotherKey']; // Only for edge cases! +class TranslationProvider extends BaseTranslationProvider { + TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance); + + static InheritedLocaleData of(BuildContext context) => InheritedLocaleData.of(context); +} + +/// Method B shorthand via [BuildContext] extension method. +/// Configurable via 'translate_var'. +/// +/// Usage (e.g. in a widget's build method): +/// context.t.someKey.anotherKey +extension BuildContextTranslationsExtension on BuildContext { + Translations get t => TranslationProvider.of(this).translations; +} + +/// Manages all translation instances and the current locale +class LocaleSettings extends BaseFlutterLocaleSettings { + LocaleSettings._() : super(utils: AppLocaleUtils.instance); + + static final instance = LocaleSettings._(); + + // static aliases (checkout base methods for documentation) + static AppLocale get currentLocale => instance.currentLocale; + static Stream getLocaleStream() => instance.getLocaleStream(); + static Future setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + static Future setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static Future useDeviceLocale() => instance.useDeviceLocale(); + static Future setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + + // synchronous versions + static AppLocale setLocaleSync(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale useDeviceLocaleSync() => instance.useDeviceLocaleSync(); + static void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); +} + +/// Provides utility functions without any side effects. +class AppLocaleUtils extends BaseAppLocaleUtils { + AppLocaleUtils._() : super( + baseLocale: AppLocale.en, + locales: AppLocale.values, + ); + + static final instance = AppLocaleUtils._(); + + // static aliases (checkout base methods for documentation) + static AppLocale parse(String rawLocale) => instance.parse(rawLocale); + static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode); + static AppLocale findDeviceLocale() => instance.findDeviceLocale(); + static List get supportedLocales => instance.supportedLocales; + static List get supportedLocalesRaw => instance.supportedLocalesRaw; +} + +// context enums + +enum GenderContext { + male, + female, +} + +// interfaces generated as mixins + +mixin PageData { + String get title; + String? get content => null; + + @override + bool operator ==(Object other) => other is PageData && title == other.title && content == other.content; + + @override + int get hashCode => title.hashCode * content.hashCode; +} + +mixin MPage { + String get title; + String? get content => null; + + @override + bool operator ==(Object other) => other is MPage && title == other.title && content == other.content; + + @override + int get hashCode => title.hashCode * content.hashCode; +} + +mixin EndData { + List get stringPages; + List> get pages; + + @override + bool operator ==(Object other) => other is EndData && stringPages == other.stringPages && pages == other.pages; + + @override + int get hashCode => stringPages.hashCode * pages.hashCode; +} diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_de.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_de.output new file mode 100644 index 00000000..9ce3d000 --- /dev/null +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_de.output @@ -0,0 +1,64 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:slang/node.dart'; +import 'translations.cgm.dart'; + +// Path: +class TranslationsDe extends Translations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + TranslationsDe({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.de, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ), + super.build(cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver) { + super.$meta.setFlatMapFunction($meta.getTranslation); // copy base translations to super.$meta + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + @override dynamic operator[](String key) => $meta.getTranslation(key) ?? super.$meta.getTranslation(key); + + late final TranslationsDe _root = this; // ignore: unused_field + + // Translations + @override String greet({required Gender context}) { + switch (context) { + case Gender.male: + return 'Hallo Herr'; + case Gender.female: + return 'Hello Mrs'; + } + } +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on TranslationsDe { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'greet': return ({required Gender context}) { + switch (context) { + case Gender.male: + return 'Hallo Herr'; + case Gender.female: + return 'Hello Mrs'; + } + }; + default: return null; + } + } +} + diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_en.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_en.output new file mode 100644 index 00000000..e2cb9a95 --- /dev/null +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_en.output @@ -0,0 +1,67 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +part of 'translations.cgm.dart'; + +// Path: +typedef TranslationsEn = Translations; // ignore: unused_element +class Translations implements BaseTranslations { + /// Returns the current translations of the given [context]. + /// + /// Usage: + /// final t = Translations.of(context); + static Translations of(BuildContext context) => InheritedLocaleData.of(context).translations; + + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + Translations({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.en, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + dynamic operator[](String key) => $meta.getTranslation(key); + + late final Translations _root = this; // ignore: unused_field + + // Translations + String greet({required Gender context}) { + switch (context) { + case Gender.male: + return 'Hello Mr'; + case Gender.female: + return 'Hello Mrs'; + } + } +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on Translations { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'greet': return ({required Gender context}) { + switch (context) { + case Gender.male: + return 'Hello Mr'; + case Gender.female: + return 'Hello Mrs'; + } + }; + default: return null; + } + } +} + diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output new file mode 100644 index 00000000..eaccf446 --- /dev/null +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output @@ -0,0 +1,183 @@ +/// Generated file. Do not edit. +/// +/// Original: fake/path/integration +/// To regenerate, run: `dart run slang` +/// +/// Locales: 2 +/// Strings: 4 (2 per locale) + +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:slang/node.dart'; +import 'package:slang_flutter/slang_flutter.dart'; +export 'package:slang_flutter/slang_flutter.dart'; + +import 'translations_de.g.dart' deferred as _$de; +part 'translations_en.g.dart'; + +/// Supported locales, see extension methods below. +/// +/// Usage: +/// - LocaleSettings.setLocale(AppLocale.en) // set locale +/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum +/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check +enum AppLocale with BaseAppLocale { + en(languageCode: 'en'), + de(languageCode: 'de'); + + const AppLocale({ + required this.languageCode, + this.scriptCode, // ignore: unused_element + this.countryCode, // ignore: unused_element + }); + + @override final String languageCode; + @override final String? scriptCode; + @override final String? countryCode; + + @override + Future build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + await _$de.loadLibrary(); + return _$de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + @override + Translations buildSync({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + return _$de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + /// Gets current instance managed by [LocaleSettings]. + Translations get translations => LocaleSettings.instance.getTranslations(this); +} + +/// Method A: Simple +/// +/// No rebuild after locale change. +/// Translation happens during initialization of the widget (call of t). +/// Configurable via 'translate_var'. +/// +/// Usage: +/// String a = t.someKey.anotherKey; +/// String b = t['someKey.anotherKey']; // Only for edge cases! +Translations get t => LocaleSettings.instance.currentTranslations; + +/// Method B: Advanced +/// +/// All widgets using this method will trigger a rebuild when locale changes. +/// Use this if you have e.g. a settings page where the user can select the locale during runtime. +/// +/// Step 1: +/// wrap your App with +/// TranslationProvider( +/// child: MyApp() +/// ); +/// +/// Step 2: +/// final t = Translations.of(context); // Get t variable. +/// String a = t.someKey.anotherKey; // Use t variable. +/// String b = t['someKey.anotherKey']; // Only for edge cases! +class TranslationProvider extends BaseTranslationProvider { + TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance); + + static InheritedLocaleData of(BuildContext context) => InheritedLocaleData.of(context); +} + +/// Method B shorthand via [BuildContext] extension method. +/// Configurable via 'translate_var'. +/// +/// Usage (e.g. in a widget's build method): +/// context.t.someKey.anotherKey +extension BuildContextTranslationsExtension on BuildContext { + Translations get t => TranslationProvider.of(this).translations; +} + +/// Manages all translation instances and the current locale +class LocaleSettings extends BaseFlutterLocaleSettings { + LocaleSettings._() : super(utils: AppLocaleUtils.instance); + + static final instance = LocaleSettings._(); + + // static aliases (checkout base methods for documentation) + static AppLocale get currentLocale => instance.currentLocale; + static Stream getLocaleStream() => instance.getLocaleStream(); + static Future setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + static Future setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static Future useDeviceLocale() => instance.useDeviceLocale(); + static Future setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + + // synchronous versions + static AppLocale setLocaleSync(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale useDeviceLocaleSync() => instance.useDeviceLocaleSync(); + static void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); +} + +/// Provides utility functions without any side effects. +class AppLocaleUtils extends BaseAppLocaleUtils { + AppLocaleUtils._() : super( + baseLocale: AppLocale.en, + locales: AppLocale.values, + ); + + static final instance = AppLocaleUtils._(); + + // static aliases (checkout base methods for documentation) + static AppLocale parse(String rawLocale) => instance.parse(rawLocale); + static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode); + static AppLocale findDeviceLocale() => instance.findDeviceLocale(); + static List get supportedLocales => instance.supportedLocales; + static List get supportedLocalesRaw => instance.supportedLocalesRaw; +} + +// context enums + +enum Gender { + male, + female, +} diff --git a/slang/test/integration/resources/main/_expected_main.output b/slang/test/integration/resources/main/_expected_main.output index b54ef83e..549b8bdb 100644 --- a/slang/test/integration/resources/main/_expected_main.output +++ b/slang/test/integration/resources/main/_expected_main.output @@ -7,18 +7,15 @@ /// Strings: 58 (29 per locale) // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; -import 'package:slang/builder/model/node.dart'; +import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; +import 'translations_de.g.dart' deferred as _$de; part 'translations_en.g.dart'; -part 'translations_de.g.dart'; -part 'translations_map.g.dart'; - -const AppLocale _baseLocale = AppLocale.en; /// Supported locales, see extension methods below. /// @@ -27,18 +24,66 @@ const AppLocale _baseLocale = AppLocale.en; /// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum /// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check enum AppLocale with BaseAppLocale { - en(languageCode: 'en', build: Translations.build), - de(languageCode: 'de', build: _TranslationsDe.build); + en(languageCode: 'en'), + de(languageCode: 'de'); - const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element + const AppLocale({ + required this.languageCode, + this.scriptCode, // ignore: unused_element + this.countryCode, // ignore: unused_element + }); @override final String languageCode; @override final String? scriptCode; @override final String? countryCode; - @override final TranslationBuilder build; + + @override + Future build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + await _$de.loadLibrary(); + return _$de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + @override + Translations buildSync({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + return _$de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } /// Gets current instance managed by [LocaleSettings]. - Translations get translations => LocaleSettings.instance.translationMap[this]!; + Translations get translations => LocaleSettings.instance.getTranslations(this); } /// Method A: Simple @@ -91,12 +136,21 @@ class LocaleSettings extends BaseFlutterLocaleSettings // static aliases (checkout base methods for documentation) static AppLocale get currentLocale => instance.currentLocale; static Stream getLocaleStream() => instance.getLocaleStream(); - static AppLocale setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale useDeviceLocale() => instance.useDeviceLocale(); - @Deprecated('Use [AppLocaleUtils.supportedLocales]') static List get supportedLocales => instance.supportedLocales; - @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') static List get supportedLocalesRaw => instance.supportedLocalesRaw; - static void setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( + static Future setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + static Future setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static Future useDeviceLocale() => instance.useDeviceLocale(); + static Future setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + + // synchronous versions + static AppLocale setLocaleSync(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale useDeviceLocaleSync() => instance.useDeviceLocaleSync(); + static void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync( language: language, locale: locale, cardinalResolver: cardinalResolver, @@ -106,7 +160,10 @@ class LocaleSettings extends BaseFlutterLocaleSettings /// Provides utility functions without any side effects. class AppLocaleUtils extends BaseAppLocaleUtils { - AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values); + AppLocaleUtils._() : super( + baseLocale: AppLocale.en, + locales: AppLocale.values, + ); static final instance = AppLocaleUtils._(); diff --git a/slang/test/integration/resources/main/_expected_no_flutter.output b/slang/test/integration/resources/main/_expected_no_flutter.output index 0cb40850..aa9c77ba 100644 --- a/slang/test/integration/resources/main/_expected_no_flutter.output +++ b/slang/test/integration/resources/main/_expected_no_flutter.output @@ -7,13 +7,13 @@ /// Strings: 2 // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import -import 'package:slang/builder/model/node.dart'; +import 'package:slang/node.dart'; import 'package:slang/slang.dart'; export 'package:slang/slang.dart'; -const AppLocale _baseLocale = AppLocale.en; +part 'translations_en.g.dart'; /// Supported locales, see extension methods below. /// @@ -22,17 +22,52 @@ const AppLocale _baseLocale = AppLocale.en; /// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum /// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check enum AppLocale with BaseAppLocale { - en(languageCode: 'en', build: Translations.build); + en(languageCode: 'en'); - const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element + const AppLocale({ + required this.languageCode, + this.scriptCode, // ignore: unused_element + this.countryCode, // ignore: unused_element + }); @override final String languageCode; @override final String? scriptCode; @override final String? countryCode; - @override final TranslationBuilder build; + + @override + Future build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + @override + Translations buildSync({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } /// Gets current instance managed by [LocaleSettings]. - Translations get translations => LocaleSettings.instance.translationMap[this]!; + Translations get translations => LocaleSettings.instance.getTranslations(this); } /// Method A: Simple @@ -55,10 +90,19 @@ class LocaleSettings extends BaseLocaleSettings { // static aliases (checkout base methods for documentation) static AppLocale get currentLocale => instance.currentLocale; static Stream getLocaleStream() => instance.getLocaleStream(); - static AppLocale setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); - @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') static List get supportedLocalesRaw => instance.supportedLocalesRaw; - static void setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( + static Future setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + static Future setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static Future setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + + // synchronous versions + static AppLocale setLocaleSync(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync( language: language, locale: locale, cardinalResolver: cardinalResolver, @@ -68,7 +112,10 @@ class LocaleSettings extends BaseLocaleSettings { /// Provides utility functions without any side effects. class AppLocaleUtils extends BaseAppLocaleUtils { - AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values); + AppLocaleUtils._() : super( + baseLocale: AppLocale.en, + locales: AppLocale.values, + ); static final instance = AppLocaleUtils._(); @@ -77,56 +124,3 @@ class AppLocaleUtils extends BaseAppLocaleUtils { static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode); static List get supportedLocalesRaw => instance.supportedLocalesRaw; } - -// translations - -// Path: -class Translations implements BaseTranslations { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - Translations.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.en, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override final TranslationMetadata $meta; - - /// Access flat map - dynamic operator[](String key) => $meta.getTranslation(key); - - late final Translations _root = this; // ignore: unused_field - - // Translations - String get a => 'a'; - late final _TranslationsBEn b = _TranslationsBEn._(_root); -} - -// Path: b -class _TranslationsBEn { - _TranslationsBEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - String get bb => 'bb'; -} - -/// Flat map(s) containing all translations. -/// Only for edge cases! For simple maps, use the map function of this library. - -extension on Translations { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'a': return 'a'; - case 'b.bb': return 'bb'; - default: return null; - } - } -} diff --git a/slang/test/integration/resources/main/_expected_no_locale_handling.output b/slang/test/integration/resources/main/_expected_no_locale_handling.output index 519501b9..81e828a2 100644 --- a/slang/test/integration/resources/main/_expected_no_locale_handling.output +++ b/slang/test/integration/resources/main/_expected_no_locale_handling.output @@ -7,14 +7,14 @@ /// Strings: 2 // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; -import 'package:slang/builder/model/node.dart'; +import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; -const AppLocale _baseLocale = AppLocale.en; +part 'translations_en.g.dart'; /// Supported locales, see extension methods below. /// @@ -23,19 +23,57 @@ const AppLocale _baseLocale = AppLocale.en; /// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum /// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check enum AppLocale with BaseAppLocale { - en(languageCode: 'en', build: Translations.build); + en(languageCode: 'en'); - const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element + const AppLocale({ + required this.languageCode, + this.scriptCode, // ignore: unused_element + this.countryCode, // ignore: unused_element + }); @override final String languageCode; @override final String? scriptCode; @override final String? countryCode; - @override final TranslationBuilder build; + + @override + Future build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + @override + Translations buildSync({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } } /// Provides utility functions without any side effects. class AppLocaleUtils extends BaseAppLocaleUtils { - AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values); + AppLocaleUtils._() : super( + baseLocale: AppLocale.en, + locales: AppLocale.values, + ); static final instance = AppLocaleUtils._(); @@ -46,57 +84,3 @@ class AppLocaleUtils extends BaseAppLocaleUtils { static List get supportedLocales => instance.supportedLocales; static List get supportedLocalesRaw => instance.supportedLocalesRaw; } - -// translations - -// Path: -typedef TranslationsEn = Translations; // ignore: unused_element -class Translations implements BaseTranslations { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - Translations.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.en, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override final TranslationMetadata $meta; - - /// Access flat map - dynamic operator[](String key) => $meta.getTranslation(key); - - late final Translations _root = this; // ignore: unused_field - - // Translations - String get a => 'a'; - late final TranslationsBEn b = TranslationsBEn._(_root); -} - -// Path: b -class TranslationsBEn { - TranslationsBEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - String get bb => 'bb'; -} - -/// Flat map(s) containing all translations. -/// Only for edge cases! For simple maps, use the map function of this library. - -extension on Translations { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'a': return 'a'; - case 'b.bb': return 'bb'; - default: return null; - } - } -} diff --git a/slang/test/integration/resources/main/_expected_obfuscation_de.output b/slang/test/integration/resources/main/_expected_obfuscation_de.output new file mode 100644 index 00000000..b5b80033 --- /dev/null +++ b/slang/test/integration/resources/main/_expected_obfuscation_de.output @@ -0,0 +1,247 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:slang/node.dart'; +import 'package:slang/secret.dart'; +import 'translations.cgm.dart'; + +// Path: +class TranslationsDe implements Translations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + TranslationsDe({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.de, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + s: $calc1(7, 0, 106), + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + @override dynamic operator[](String key) => $meta.getTranslation(key); + + late final TranslationsDe _root = this; // ignore: unused_field + + // Translations + @override late final _TranslationsOnboardingDe onboarding = _TranslationsOnboardingDe._(_root); + @override late final _TranslationsGroupDe group = _TranslationsGroupDe._(_root); + @override late final _TranslationsEndDe end = _TranslationsEndDe._(_root); + @override TextSpan advancedPlural({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( + n: count, + resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'), + one: () => TextSpan(children: [ + TextSpan(text: _root.$meta.d([30, 50, 53, 40])), + ]), + other: () => TextSpan(children: [ + TextSpan(text: _root.$meta.d([26, 53, 63, 62, 41, 62, 123])), + countBuilder(count), + TextSpan(text: _root.$meta.d([119, 123]) + _root.onboarding.greet2(gender: gender)), + ]), + ); +} + +// Path: onboarding +class _TranslationsOnboardingDe implements TranslationsOnboardingEn { + _TranslationsOnboardingDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String welcome({required Object fullName}) => _root.$meta.d([12, 50, 55, 55, 48, 52, 54, 54, 62, 53, 123]) + fullName.toString(); + @override String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + @override String welcomeOnlyParam({required Object firstName}) => firstName.toString(); + + /// Bye text + @override String bye({required Object firstName}) => _root.$meta.d([15, 40, 56, 51, 167, 40, 40, 123]) + firstName.toString(); + + @override TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ + TextSpan(text: _root.$meta.d([19, 50, 123])), + name, + TextSpan(text: _root.$meta.d([123, 46, 53, 63, 123]) + _root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)), + ]); + @override List get pages => [ + _TranslationsOnboarding$pages$0i0$De._(_root), + _TranslationsOnboarding$pages$0i1$De._(_root), + ]; + @override List get modifierPages => [ + _TranslationsOnboarding$modifierPages$0i0$De._(_root), + _TranslationsOnboarding$modifierPages$0i1$De._(_root), + ]; + @override String greet({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { + switch (context) { + case GenderContext.male: + return _root.$meta.d([19, 58, 55, 55, 52, 123, 19, 62, 41, 41, 123]) + lastName.toString() + _root.$meta.d([123, 46, 53, 63, 123]) + _root.onboarding.welcome(fullName: fullName); + case GenderContext.female: + return _root.$meta.d([19, 58, 55, 55, 52, 123, 29, 41, 58, 46, 123]) + lastName.toString() + _root.$meta.d([123, 46, 53, 63, 123]) + _root.onboarding.bye(firstName: firstName); + } + } + @override String greet2({required GenderContext gender}) { + switch (gender) { + case GenderContext.male: + return _root.$meta.d([19, 58, 55, 55, 52, 123, 19, 62, 41, 41]); + case GenderContext.female: + return _root.$meta.d([19, 58, 55, 55, 52, 123, 29, 41, 58, 46]); + } + } + @override String greetCombination({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => _root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context) + _root.$meta.d([119, 123]) + _root.onboarding.greet2(gender: gender); + @override String welcomeLinkedPlural({required num n, required Object fullName, required Object firstName}) => _root.$meta.d([19, 58, 55, 55, 52, 123]) + _root.group.users(n: n, fullName: fullName, firstName: firstName); + @override String welcomeLinkedContext({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => _root.$meta.d([19, 58, 55, 55, 52, 123]) + _root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context); + @override String welcomeFullLink({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => _root.$meta.d([14, 55, 47, 50, 54, 58, 47, 50, 45, 62, 123]) + _root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName) + _root.$meta.d([123, 58, 53, 63, 123]) + _root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context); +} + +// Path: group +class _TranslationsGroupDe implements TranslationsGroupEn { + _TranslationsGroupDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String users({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, + zero: _root.$meta.d([16, 62, 50, 53, 62, 123, 21, 46, 47, 33, 62, 41, 123, 46, 53, 63, 123]) + _root.onboarding.welcome(fullName: fullName), + one: _root.$meta.d([30, 50, 53, 123, 21, 46, 47, 33, 62, 41]), + other: n.toString() + _root.$meta.d([123, 21, 46, 47, 33, 62, 41, 123, 46, 53, 63, 123]) + _root.onboarding.bye(firstName: firstName), + ); +} + +// Path: end +class _TranslationsEndDe with EndData implements TranslationsEndEn { + _TranslationsEndDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override List get stringPages => [ + _root.$meta.d([106, 117, 123, 8, 62, 50, 47, 62]), + _root.$meta.d([105, 117, 123, 8, 62, 50, 47, 62]), + ]; + @override List> get pages => [ + { + 'unknown': _root.$meta.d([14, 53, 57, 62, 48, 58, 53, 53, 47, 62, 41, 81, 29, 62, 51, 55, 62, 41]), + }, + { + 'with space': _root.$meta.d([30, 50, 53, 123, 29, 62, 51, 55, 62, 41]), + 'with second space': _root.$meta.d([30, 50, 53, 123, 105, 117, 123, 29, 62, 51, 55, 62, 41]), + }, + ]; +} + +// Path: onboarding.pages.0 +class _TranslationsOnboarding$pages$0i0$De with PageData implements TranslationsOnboarding$pages$0i0$En { + _TranslationsOnboarding$pages$0i0$De._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get title => _root.$meta.d([30, 41, 40, 47, 62, 123, 8, 62, 50, 47, 62]); + @override String get content => _root.$meta.d([30, 41, 40, 47, 62, 41, 123, 8, 62, 50, 47, 62, 53, 50, 53, 51, 58, 55, 47]); +} + +// Path: onboarding.pages.1 +class _TranslationsOnboarding$pages$0i1$De with PageData implements TranslationsOnboarding$pages$0i1$En { + _TranslationsOnboarding$pages$0i1$De._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get title => _root.$meta.d([1, 44, 62, 50, 47, 62, 123, 8, 62, 50, 47, 62]); +} + +// Path: onboarding.modifierPages.0 +class _TranslationsOnboarding$modifierPages$0i0$De with MPage implements TranslationsOnboarding$modifierPages$0i0$En { + _TranslationsOnboarding$modifierPages$0i0$De._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get title => _root.$meta.d([30, 41, 40, 47, 62, 123, 22, 52, 63, 50, 61, 50, 62, 41, 123, 8, 62, 50, 47, 62]); + @override String get content => _root.$meta.d([30, 41, 40, 47, 62, 41, 123, 8, 62, 50, 47, 62, 53, 50, 53, 51, 58, 55, 47]); +} + +// Path: onboarding.modifierPages.1 +class _TranslationsOnboarding$modifierPages$0i1$De with MPage implements TranslationsOnboarding$modifierPages$0i1$En { + _TranslationsOnboarding$modifierPages$0i1$De._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get title => _root.$meta.d([1, 44, 62, 50, 47, 62, 123, 22, 52, 63, 50, 61, 50, 62, 41, 123, 8, 62, 50, 47, 62]); +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on TranslationsDe { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'onboarding.welcome': return ({required Object fullName}) => _root.$meta.d([12, 50, 55, 55, 48, 52, 54, 54, 62, 53, 123]) + fullName.toString(); + case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => firstName.toString(); + case 'onboarding.bye': return ({required Object firstName}) => _root.$meta.d([15, 40, 56, 51, 167, 40, 40, 123]) + firstName.toString(); + case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ + TextSpan(text: _root.$meta.d([19, 50, 123])), + name, + TextSpan(text: _root.$meta.d([123, 46, 53, 63, 123]) + _root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)), + ]); + case 'onboarding.pages.0.title': return _root.$meta.d([30, 41, 40, 47, 62, 123, 8, 62, 50, 47, 62]); + case 'onboarding.pages.0.content': return _root.$meta.d([30, 41, 40, 47, 62, 41, 123, 8, 62, 50, 47, 62, 53, 50, 53, 51, 58, 55, 47]); + case 'onboarding.pages.1.title': return _root.$meta.d([1, 44, 62, 50, 47, 62, 123, 8, 62, 50, 47, 62]); + case 'onboarding.modifierPages.0.title': return _root.$meta.d([30, 41, 40, 47, 62, 123, 22, 52, 63, 50, 61, 50, 62, 41, 123, 8, 62, 50, 47, 62]); + case 'onboarding.modifierPages.0.content': return _root.$meta.d([30, 41, 40, 47, 62, 41, 123, 8, 62, 50, 47, 62, 53, 50, 53, 51, 58, 55, 47]); + case 'onboarding.modifierPages.1.title': return _root.$meta.d([1, 44, 62, 50, 47, 62, 123, 22, 52, 63, 50, 61, 50, 62, 41, 123, 8, 62, 50, 47, 62]); + case 'onboarding.greet': return ({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { + switch (context) { + case GenderContext.male: + return _root.$meta.d([19, 58, 55, 55, 52, 123, 19, 62, 41, 41, 123]) + lastName.toString() + _root.$meta.d([123, 46, 53, 63, 123]) + _root.onboarding.welcome(fullName: fullName); + case GenderContext.female: + return _root.$meta.d([19, 58, 55, 55, 52, 123, 29, 41, 58, 46, 123]) + lastName.toString() + _root.$meta.d([123, 46, 53, 63, 123]) + _root.onboarding.bye(firstName: firstName); + } + }; + case 'onboarding.greet2': return ({required GenderContext gender}) { + switch (gender) { + case GenderContext.male: + return _root.$meta.d([19, 58, 55, 55, 52, 123, 19, 62, 41, 41]); + case GenderContext.female: + return _root.$meta.d([19, 58, 55, 55, 52, 123, 29, 41, 58, 46]); + } + }; + case 'onboarding.greetCombination': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => _root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context) + _root.$meta.d([119, 123]) + _root.onboarding.greet2(gender: gender); + case 'onboarding.welcomeLinkedPlural': return ({required num n, required Object fullName, required Object firstName}) => _root.$meta.d([19, 58, 55, 55, 52, 123]) + _root.group.users(n: n, fullName: fullName, firstName: firstName); + case 'onboarding.welcomeLinkedContext': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => _root.$meta.d([19, 58, 55, 55, 52, 123]) + _root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context); + case 'onboarding.welcomeFullLink': return ({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => _root.$meta.d([14, 55, 47, 50, 54, 58, 47, 50, 45, 62, 123]) + _root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName) + _root.$meta.d([123, 58, 53, 63, 123]) + _root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context); + case 'group.users': return ({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, + zero: _root.$meta.d([16, 62, 50, 53, 62, 123, 21, 46, 47, 33, 62, 41, 123, 46, 53, 63, 123]) + _root.onboarding.welcome(fullName: fullName), + one: _root.$meta.d([30, 50, 53, 123, 21, 46, 47, 33, 62, 41]), + other: n.toString() + _root.$meta.d([123, 21, 46, 47, 33, 62, 41, 123, 46, 53, 63, 123]) + _root.onboarding.bye(firstName: firstName), + ); + case 'end.stringPages.0': return _root.$meta.d([106, 117, 123, 8, 62, 50, 47, 62]); + case 'end.stringPages.1': return _root.$meta.d([105, 117, 123, 8, 62, 50, 47, 62]); + case 'end.pages.0.unknown': return _root.$meta.d([14, 53, 57, 62, 48, 58, 53, 53, 47, 62, 41, 81, 29, 62, 51, 55, 62, 41]); + case 'end.pages.1.with space': return _root.$meta.d([30, 50, 53, 123, 29, 62, 51, 55, 62, 41]); + case 'end.pages.1.with second space': return _root.$meta.d([30, 50, 53, 123, 105, 117, 123, 29, 62, 51, 55, 62, 41]); + case 'advancedPlural': return ({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( + n: count, + resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'), + one: () => TextSpan(children: [ + TextSpan(text: _root.$meta.d([30, 50, 53, 40])), + ]), + other: () => TextSpan(children: [ + TextSpan(text: _root.$meta.d([26, 53, 63, 62, 41, 62, 123])), + countBuilder(count), + TextSpan(text: _root.$meta.d([119, 123]) + _root.onboarding.greet2(gender: gender)), + ]), + ); + default: return null; + } + } +} + diff --git a/slang/test/integration/resources/main/_expected_obfuscation_en.output b/slang/test/integration/resources/main/_expected_obfuscation_en.output new file mode 100644 index 00000000..6de9aeef --- /dev/null +++ b/slang/test/integration/resources/main/_expected_obfuscation_en.output @@ -0,0 +1,251 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +part of 'translations.cgm.dart'; + +// Path: +typedef TranslationsEn = Translations; // ignore: unused_element +class Translations implements BaseTranslations { + /// Returns the current translations of the given [context]. + /// + /// Usage: + /// final t = Translations.of(context); + static Translations of(BuildContext context) => InheritedLocaleData.of(context).translations; + + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + Translations({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.en, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + s: $calc1(7, 0, 106), + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + dynamic operator[](String key) => $meta.getTranslation(key); + + late final Translations _root = this; // ignore: unused_field + + // Translations + late final TranslationsOnboardingEn onboarding = TranslationsOnboardingEn._(_root); + late final TranslationsGroupEn group = TranslationsGroupEn._(_root); + late final TranslationsEndEn end = TranslationsEndEn._(_root); + TextSpan advancedPlural({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( + n: count, + resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'), + one: () => TextSpan(children: [ + TextSpan(text: _root.$meta.d([20, 53, 62])), + ]), + other: () => TextSpan(children: [ + TextSpan(text: _root.$meta.d([20, 47, 51, 62, 41, 123])), + countBuilder(count), + TextSpan(text: _root.$meta.d([119, 123]) + _root.onboarding.greet2(gender: gender)), + ]), + ); +} + +// Path: onboarding +class TranslationsOnboardingEn { + TranslationsOnboardingEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String welcome({required Object fullName}) => _root.$meta.d([12, 62, 55, 56, 52, 54, 62, 123]) + fullName.toString(); + String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + String welcomeOnlyParam({required Object firstName}) => firstName.toString(); + + /// Bye text + String bye({required Object firstName}) => _root.$meta.d([25, 34, 62, 123]) + firstName.toString(); + + TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ + TextSpan(text: _root.$meta.d([19, 50, 123])), + name, + TextSpan(text: _root.$meta.d([123, 58, 53, 63, 123]) + _root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)), + ]); + List get pages => [ + TranslationsOnboarding$pages$0i0$En._(_root), + TranslationsOnboarding$pages$0i1$En._(_root), + ]; + List get modifierPages => [ + TranslationsOnboarding$modifierPages$0i0$En._(_root), + TranslationsOnboarding$modifierPages$0i1$En._(_root), + ]; + String greet({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { + switch (context) { + case GenderContext.male: + return _root.$meta.d([19, 62, 55, 55, 52, 123, 22, 41, 123]) + lastName.toString() + _root.$meta.d([123, 58, 53, 63, 123]) + _root.onboarding.welcome(fullName: fullName); + case GenderContext.female: + return _root.$meta.d([19, 62, 55, 55, 52, 123, 22, 40, 123]) + lastName.toString() + _root.$meta.d([123, 58, 53, 63, 123]) + _root.onboarding.bye(firstName: firstName); + } + } + String greet2({required GenderContext gender}) { + switch (gender) { + case GenderContext.male: + return _root.$meta.d([19, 62, 55, 55, 52, 123, 22, 41]); + case GenderContext.female: + return _root.$meta.d([19, 62, 55, 55, 52, 123, 22, 40]); + } + } + String greetCombination({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => _root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context) + _root.$meta.d([119, 123]) + _root.onboarding.greet2(gender: gender); + String welcomeLinkedPlural({required num n, required Object fullName, required Object firstName}) => _root.$meta.d([19, 62, 55, 55, 52, 123]) + _root.group.users(n: n, fullName: fullName, firstName: firstName); + String welcomeLinkedContext({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => _root.$meta.d([19, 62, 55, 55, 52, 123]) + _root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context); + String welcomeFullLink({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => _root.$meta.d([14, 55, 47, 50, 54, 58, 47, 62, 123]) + _root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName) + _root.$meta.d([123, 58, 53, 63, 123]) + _root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context); +} + +// Path: group +class TranslationsGroupEn { + TranslationsGroupEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String users({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: _root.$meta.d([21, 52, 123, 14, 40, 62, 41, 40, 123, 58, 53, 63, 123]) + _root.onboarding.welcome(fullName: fullName), + one: _root.$meta.d([20, 53, 62, 123, 14, 40, 62, 41]), + other: n.toString() + _root.$meta.d([123, 14, 40, 62, 41, 40, 123, 58, 53, 63, 123]) + _root.onboarding.bye(firstName: firstName), + ); +} + +// Path: end +class TranslationsEndEn with EndData { + TranslationsEndEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override List get stringPages => [ + _root.$meta.d([106, 40, 47, 123, 11, 58, 60, 62]), + _root.$meta.d([105, 53, 63, 123, 11, 58, 60, 62]), + ]; + @override List> get pages => [ + { + 'unknown': _root.$meta.d([14, 53, 48, 53, 52, 44, 53, 81, 30, 41, 41, 52, 41]), + }, + { + 'with space': _root.$meta.d([26, 53, 123, 30, 41, 41, 52, 41]), + 'with second space': _root.$meta.d([26, 53, 123, 105, 53, 63, 123, 30, 41, 41, 52, 41]), + }, + ]; +} + +// Path: onboarding.pages.0 +class TranslationsOnboarding$pages$0i0$En with PageData { + TranslationsOnboarding$pages$0i0$En._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override String get title => _root.$meta.d([29, 50, 41, 40, 47, 123, 11, 58, 60, 62]); + @override String get content => _root.$meta.d([29, 50, 41, 40, 47, 123, 11, 58, 60, 62, 123, 24, 52, 53, 47, 62, 53, 47]); +} + +// Path: onboarding.pages.1 +class TranslationsOnboarding$pages$0i1$En with PageData { + TranslationsOnboarding$pages$0i1$En._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override String get title => _root.$meta.d([8, 62, 56, 52, 53, 63, 123, 11, 58, 60, 62]); +} + +// Path: onboarding.modifierPages.0 +class TranslationsOnboarding$modifierPages$0i0$En with MPage { + TranslationsOnboarding$modifierPages$0i0$En._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override String get title => _root.$meta.d([29, 50, 41, 40, 47, 123, 22, 52, 63, 50, 61, 50, 62, 41, 123, 11, 58, 60, 62]); + @override String get content => _root.$meta.d([29, 50, 41, 40, 47, 123, 11, 58, 60, 62, 123, 24, 52, 53, 47, 62, 53, 47]); +} + +// Path: onboarding.modifierPages.1 +class TranslationsOnboarding$modifierPages$0i1$En with MPage { + TranslationsOnboarding$modifierPages$0i1$En._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override String get title => _root.$meta.d([8, 62, 56, 52, 53, 63, 123, 22, 52, 63, 50, 61, 50, 62, 41, 123, 11, 58, 60, 62]); +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on Translations { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'onboarding.welcome': return ({required Object fullName}) => _root.$meta.d([12, 62, 55, 56, 52, 54, 62, 123]) + fullName.toString(); + case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => firstName.toString(); + case 'onboarding.bye': return ({required Object firstName}) => _root.$meta.d([25, 34, 62, 123]) + firstName.toString(); + case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ + TextSpan(text: _root.$meta.d([19, 50, 123])), + name, + TextSpan(text: _root.$meta.d([123, 58, 53, 63, 123]) + _root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)), + ]); + case 'onboarding.pages.0.title': return _root.$meta.d([29, 50, 41, 40, 47, 123, 11, 58, 60, 62]); + case 'onboarding.pages.0.content': return _root.$meta.d([29, 50, 41, 40, 47, 123, 11, 58, 60, 62, 123, 24, 52, 53, 47, 62, 53, 47]); + case 'onboarding.pages.1.title': return _root.$meta.d([8, 62, 56, 52, 53, 63, 123, 11, 58, 60, 62]); + case 'onboarding.modifierPages.0.title': return _root.$meta.d([29, 50, 41, 40, 47, 123, 22, 52, 63, 50, 61, 50, 62, 41, 123, 11, 58, 60, 62]); + case 'onboarding.modifierPages.0.content': return _root.$meta.d([29, 50, 41, 40, 47, 123, 11, 58, 60, 62, 123, 24, 52, 53, 47, 62, 53, 47]); + case 'onboarding.modifierPages.1.title': return _root.$meta.d([8, 62, 56, 52, 53, 63, 123, 22, 52, 63, 50, 61, 50, 62, 41, 123, 11, 58, 60, 62]); + case 'onboarding.greet': return ({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { + switch (context) { + case GenderContext.male: + return _root.$meta.d([19, 62, 55, 55, 52, 123, 22, 41, 123]) + lastName.toString() + _root.$meta.d([123, 58, 53, 63, 123]) + _root.onboarding.welcome(fullName: fullName); + case GenderContext.female: + return _root.$meta.d([19, 62, 55, 55, 52, 123, 22, 40, 123]) + lastName.toString() + _root.$meta.d([123, 58, 53, 63, 123]) + _root.onboarding.bye(firstName: firstName); + } + }; + case 'onboarding.greet2': return ({required GenderContext gender}) { + switch (gender) { + case GenderContext.male: + return _root.$meta.d([19, 62, 55, 55, 52, 123, 22, 41]); + case GenderContext.female: + return _root.$meta.d([19, 62, 55, 55, 52, 123, 22, 40]); + } + }; + case 'onboarding.greetCombination': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => _root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context) + _root.$meta.d([119, 123]) + _root.onboarding.greet2(gender: gender); + case 'onboarding.welcomeLinkedPlural': return ({required num n, required Object fullName, required Object firstName}) => _root.$meta.d([19, 62, 55, 55, 52, 123]) + _root.group.users(n: n, fullName: fullName, firstName: firstName); + case 'onboarding.welcomeLinkedContext': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => _root.$meta.d([19, 62, 55, 55, 52, 123]) + _root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context); + case 'onboarding.welcomeFullLink': return ({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => _root.$meta.d([14, 55, 47, 50, 54, 58, 47, 62, 123]) + _root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName) + _root.$meta.d([123, 58, 53, 63, 123]) + _root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context); + case 'group.users': return ({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: _root.$meta.d([21, 52, 123, 14, 40, 62, 41, 40, 123, 58, 53, 63, 123]) + _root.onboarding.welcome(fullName: fullName), + one: _root.$meta.d([20, 53, 62, 123, 14, 40, 62, 41]), + other: n.toString() + _root.$meta.d([123, 14, 40, 62, 41, 40, 123, 58, 53, 63, 123]) + _root.onboarding.bye(firstName: firstName), + ); + case 'end.stringPages.0': return _root.$meta.d([106, 40, 47, 123, 11, 58, 60, 62]); + case 'end.stringPages.1': return _root.$meta.d([105, 53, 63, 123, 11, 58, 60, 62]); + case 'end.pages.0.unknown': return _root.$meta.d([14, 53, 48, 53, 52, 44, 53, 81, 30, 41, 41, 52, 41]); + case 'end.pages.1.with space': return _root.$meta.d([26, 53, 123, 30, 41, 41, 52, 41]); + case 'end.pages.1.with second space': return _root.$meta.d([26, 53, 123, 105, 53, 63, 123, 30, 41, 41, 52, 41]); + case 'advancedPlural': return ({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( + n: count, + resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'), + one: () => TextSpan(children: [ + TextSpan(text: _root.$meta.d([20, 53, 62])), + ]), + other: () => TextSpan(children: [ + TextSpan(text: _root.$meta.d([20, 47, 51, 62, 41, 123])), + countBuilder(count), + TextSpan(text: _root.$meta.d([119, 123]) + _root.onboarding.greet2(gender: gender)), + ]), + ); + default: return null; + } + } +} + diff --git a/slang/test/integration/resources/main/_expected_obfuscation_main.output b/slang/test/integration/resources/main/_expected_obfuscation_main.output new file mode 100644 index 00000000..caee762a --- /dev/null +++ b/slang/test/integration/resources/main/_expected_obfuscation_main.output @@ -0,0 +1,219 @@ +/// Generated file. Do not edit. +/// +/// Original: fake/path/integration +/// To regenerate, run: `dart run slang` +/// +/// Locales: 2 +/// Strings: 58 (29 per locale) + +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:slang/node.dart'; +import 'package:slang/secret.dart'; +import 'package:slang_flutter/slang_flutter.dart'; +export 'package:slang_flutter/slang_flutter.dart'; + +import 'translations_de.g.dart' deferred as _$de; +part 'translations_en.g.dart'; + +/// Supported locales, see extension methods below. +/// +/// Usage: +/// - LocaleSettings.setLocale(AppLocale.en) // set locale +/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum +/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check +enum AppLocale with BaseAppLocale { + en(languageCode: 'en'), + de(languageCode: 'de'); + + const AppLocale({ + required this.languageCode, + this.scriptCode, // ignore: unused_element + this.countryCode, // ignore: unused_element + }); + + @override final String languageCode; + @override final String? scriptCode; + @override final String? countryCode; + + @override + Future build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + await _$de.loadLibrary(); + return _$de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + @override + Translations buildSync({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + return _$de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + /// Gets current instance managed by [LocaleSettings]. + Translations get translations => LocaleSettings.instance.getTranslations(this); +} + +/// Method A: Simple +/// +/// No rebuild after locale change. +/// Translation happens during initialization of the widget (call of t). +/// Configurable via 'translate_var'. +/// +/// Usage: +/// String a = t.someKey.anotherKey; +/// String b = t['someKey.anotherKey']; // Only for edge cases! +Translations get t => LocaleSettings.instance.currentTranslations; + +/// Method B: Advanced +/// +/// All widgets using this method will trigger a rebuild when locale changes. +/// Use this if you have e.g. a settings page where the user can select the locale during runtime. +/// +/// Step 1: +/// wrap your App with +/// TranslationProvider( +/// child: MyApp() +/// ); +/// +/// Step 2: +/// final t = Translations.of(context); // Get t variable. +/// String a = t.someKey.anotherKey; // Use t variable. +/// String b = t['someKey.anotherKey']; // Only for edge cases! +class TranslationProvider extends BaseTranslationProvider { + TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance); + + static InheritedLocaleData of(BuildContext context) => InheritedLocaleData.of(context); +} + +/// Method B shorthand via [BuildContext] extension method. +/// Configurable via 'translate_var'. +/// +/// Usage (e.g. in a widget's build method): +/// context.t.someKey.anotherKey +extension BuildContextTranslationsExtension on BuildContext { + Translations get t => TranslationProvider.of(this).translations; +} + +/// Manages all translation instances and the current locale +class LocaleSettings extends BaseFlutterLocaleSettings { + LocaleSettings._() : super(utils: AppLocaleUtils.instance); + + static final instance = LocaleSettings._(); + + // static aliases (checkout base methods for documentation) + static AppLocale get currentLocale => instance.currentLocale; + static Stream getLocaleStream() => instance.getLocaleStream(); + static Future setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + static Future setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static Future useDeviceLocale() => instance.useDeviceLocale(); + static Future setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + + // synchronous versions + static AppLocale setLocaleSync(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale useDeviceLocaleSync() => instance.useDeviceLocaleSync(); + static void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); +} + +/// Provides utility functions without any side effects. +class AppLocaleUtils extends BaseAppLocaleUtils { + AppLocaleUtils._() : super( + baseLocale: AppLocale.en, + locales: AppLocale.values, + ); + + static final instance = AppLocaleUtils._(); + + // static aliases (checkout base methods for documentation) + static AppLocale parse(String rawLocale) => instance.parse(rawLocale); + static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode); + static AppLocale findDeviceLocale() => instance.findDeviceLocale(); + static List get supportedLocales => instance.supportedLocales; + static List get supportedLocalesRaw => instance.supportedLocalesRaw; +} + +// context enums + +enum GenderContext { + male, + female, +} + +// interfaces generated as mixins + +mixin PageData { + String get title; + String? get content => null; + + @override + bool operator ==(Object other) => other is PageData && title == other.title && content == other.content; + + @override + int get hashCode => title.hashCode * content.hashCode; +} + +mixin MPage { + String get title; + String? get content => null; + + @override + bool operator ==(Object other) => other is MPage && title == other.title && content == other.content; + + @override + int get hashCode => title.hashCode * content.hashCode; +} + +mixin EndData { + List get stringPages; + List> get pages; + + @override + bool operator ==(Object other) => other is EndData && stringPages == other.stringPages && pages == other.pages; + + @override + int get hashCode => stringPages.hashCode * pages.hashCode; +} diff --git a/slang/test/integration/resources/main/_expected_rich_text.output b/slang/test/integration/resources/main/_expected_rich_text.output index 9a9ddaa8..23b1addb 100644 --- a/slang/test/integration/resources/main/_expected_rich_text.output +++ b/slang/test/integration/resources/main/_expected_rich_text.output @@ -1,128 +1,13 @@ -/// Generated file. Do not edit. /// -/// Original: fake/path/integration -/// To regenerate, run: `dart run slang` +/// Generated file. Do not edit. /// -/// Locales: 1 -/// Strings: 23 - // coverage:ignore-file -// ignore_for_file: type=lint - -import 'package:flutter/widgets.dart'; -import 'package:slang/builder/model/node.dart'; -import 'package:slang_flutter/slang_flutter.dart'; -export 'package:slang_flutter/slang_flutter.dart'; - -const AppLocale _baseLocale = AppLocale.en; +// ignore_for_file: type=lint, unused_import -/// Supported locales, see extension methods below. -/// -/// Usage: -/// - LocaleSettings.setLocale(AppLocale.en) // set locale -/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum -/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check -enum AppLocale with BaseAppLocale { - en(languageCode: 'en', build: Translations.build); - - const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element - - @override final String languageCode; - @override final String? scriptCode; - @override final String? countryCode; - @override final TranslationBuilder build; - - /// Gets current instance managed by [LocaleSettings]. - Translations get translations => LocaleSettings.instance.translationMap[this]!; -} - -/// Method A: Simple -/// -/// No rebuild after locale change. -/// Translation happens during initialization of the widget (call of t). -/// Configurable via 'translate_var'. -/// -/// Usage: -/// String a = t.someKey.anotherKey; -/// String b = t['someKey.anotherKey']; // Only for edge cases! -Translations get t => LocaleSettings.instance.currentTranslations; - -/// Method B: Advanced -/// -/// All widgets using this method will trigger a rebuild when locale changes. -/// Use this if you have e.g. a settings page where the user can select the locale during runtime. -/// -/// Step 1: -/// wrap your App with -/// TranslationProvider( -/// child: MyApp() -/// ); -/// -/// Step 2: -/// final t = Translations.of(context); // Get t variable. -/// String a = t.someKey.anotherKey; // Use t variable. -/// String b = t['someKey.anotherKey']; // Only for edge cases! -class TranslationProvider extends BaseTranslationProvider { - TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance); - - static InheritedLocaleData of(BuildContext context) => InheritedLocaleData.of(context); -} - -/// Method B shorthand via [BuildContext] extension method. -/// Configurable via 'translate_var'. -/// -/// Usage (e.g. in a widget's build method): -/// context.t.someKey.anotherKey -extension BuildContextTranslationsExtension on BuildContext { - Translations get t => TranslationProvider.of(this).translations; -} - -/// Manages all translation instances and the current locale -class LocaleSettings extends BaseFlutterLocaleSettings { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); - - static final instance = LocaleSettings._(); - - // static aliases (checkout base methods for documentation) - static AppLocale get currentLocale => instance.currentLocale; - static Stream getLocaleStream() => instance.getLocaleStream(); - static AppLocale setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale useDeviceLocale() => instance.useDeviceLocale(); - @Deprecated('Use [AppLocaleUtils.supportedLocales]') static List get supportedLocales => instance.supportedLocales; - @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') static List get supportedLocalesRaw => instance.supportedLocalesRaw; - static void setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( - language: language, - locale: locale, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ); -} - -/// Provides utility functions without any side effects. -class AppLocaleUtils extends BaseAppLocaleUtils { - AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values); - - static final instance = AppLocaleUtils._(); - - // static aliases (checkout base methods for documentation) - static AppLocale parse(String rawLocale) => instance.parse(rawLocale); - static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode); - static AppLocale findDeviceLocale() => instance.findDeviceLocale(); - static List get supportedLocales => instance.supportedLocales; - static List get supportedLocalesRaw => instance.supportedLocalesRaw; -} - -// context enums - -enum Animal { - cat, - dog, -} - -// translations +part of 'strings.g.dart'; // Path: +typedef TranslationsEn = Translations; // ignore: unused_element class Translations implements BaseTranslations { /// Returns the current translations of the given [context]. /// @@ -132,7 +17,7 @@ class Translations implements BaseTranslations { /// You can call this constructor and build your own translation instance of this locale. /// Constructing via the enum [AppLocale.build] is preferred. - Translations.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + Translations({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), $meta = TranslationMetadata( locale: AppLocale.en, @@ -247,7 +132,6 @@ class Translations implements BaseTranslations { /// Flat map(s) containing all translations. /// Only for edge cases! For simple maps, use the map function of this library. - extension on Translations { dynamic _flatMapFunction(String path) { switch (path) { @@ -346,3 +230,4 @@ extension on Translations { } } } + diff --git a/slang/test/integration/resources/main/_expected_single.output b/slang/test/integration/resources/main/_expected_single.output deleted file mode 100644 index 4669c9e8..00000000 --- a/slang/test/integration/resources/main/_expected_single.output +++ /dev/null @@ -1,634 +0,0 @@ -/// Generated file. Do not edit. -/// -/// Original: fake/path/integration -/// To regenerate, run: `dart run slang` -/// -/// Locales: 2 -/// Strings: 58 (29 per locale) - -// coverage:ignore-file -// ignore_for_file: type=lint - -import 'package:flutter/widgets.dart'; -import 'package:slang/builder/model/node.dart'; -import 'package:slang_flutter/slang_flutter.dart'; -export 'package:slang_flutter/slang_flutter.dart'; - -const AppLocale _baseLocale = AppLocale.en; - -/// Supported locales, see extension methods below. -/// -/// Usage: -/// - LocaleSettings.setLocale(AppLocale.en) // set locale -/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum -/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check -enum AppLocale with BaseAppLocale { - en(languageCode: 'en', build: Translations.build), - de(languageCode: 'de', build: _TranslationsDe.build); - - const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element - - @override final String languageCode; - @override final String? scriptCode; - @override final String? countryCode; - @override final TranslationBuilder build; - - /// Gets current instance managed by [LocaleSettings]. - Translations get translations => LocaleSettings.instance.translationMap[this]!; -} - -/// Method A: Simple -/// -/// No rebuild after locale change. -/// Translation happens during initialization of the widget (call of t). -/// Configurable via 'translate_var'. -/// -/// Usage: -/// String a = t.someKey.anotherKey; -/// String b = t['someKey.anotherKey']; // Only for edge cases! -Translations get t => LocaleSettings.instance.currentTranslations; - -/// Method B: Advanced -/// -/// All widgets using this method will trigger a rebuild when locale changes. -/// Use this if you have e.g. a settings page where the user can select the locale during runtime. -/// -/// Step 1: -/// wrap your App with -/// TranslationProvider( -/// child: MyApp() -/// ); -/// -/// Step 2: -/// final t = Translations.of(context); // Get t variable. -/// String a = t.someKey.anotherKey; // Use t variable. -/// String b = t['someKey.anotherKey']; // Only for edge cases! -class TranslationProvider extends BaseTranslationProvider { - TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance); - - static InheritedLocaleData of(BuildContext context) => InheritedLocaleData.of(context); -} - -/// Method B shorthand via [BuildContext] extension method. -/// Configurable via 'translate_var'. -/// -/// Usage (e.g. in a widget's build method): -/// context.t.someKey.anotherKey -extension BuildContextTranslationsExtension on BuildContext { - Translations get t => TranslationProvider.of(this).translations; -} - -/// Manages all translation instances and the current locale -class LocaleSettings extends BaseFlutterLocaleSettings { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); - - static final instance = LocaleSettings._(); - - // static aliases (checkout base methods for documentation) - static AppLocale get currentLocale => instance.currentLocale; - static Stream getLocaleStream() => instance.getLocaleStream(); - static AppLocale setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale useDeviceLocale() => instance.useDeviceLocale(); - @Deprecated('Use [AppLocaleUtils.supportedLocales]') static List get supportedLocales => instance.supportedLocales; - @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') static List get supportedLocalesRaw => instance.supportedLocalesRaw; - static void setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( - language: language, - locale: locale, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ); -} - -/// Provides utility functions without any side effects. -class AppLocaleUtils extends BaseAppLocaleUtils { - AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values); - - static final instance = AppLocaleUtils._(); - - // static aliases (checkout base methods for documentation) - static AppLocale parse(String rawLocale) => instance.parse(rawLocale); - static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode); - static AppLocale findDeviceLocale() => instance.findDeviceLocale(); - static List get supportedLocales => instance.supportedLocales; - static List get supportedLocalesRaw => instance.supportedLocalesRaw; -} - -// context enums - -enum GenderContext { - male, - female, -} - -// interfaces generated as mixins - -mixin PageData { - String get title; - String? get content => null; - - @override - bool operator ==(Object other) => other is PageData && title == other.title && content == other.content; - - @override - int get hashCode => title.hashCode * content.hashCode; -} - -mixin MPage { - String get title; - String? get content => null; - - @override - bool operator ==(Object other) => other is MPage && title == other.title && content == other.content; - - @override - int get hashCode => title.hashCode * content.hashCode; -} - -mixin EndData { - List get stringPages; - List> get pages; - - @override - bool operator ==(Object other) => other is EndData && stringPages == other.stringPages && pages == other.pages; - - @override - int get hashCode => stringPages.hashCode * pages.hashCode; -} - -// translations - -// Path: -class Translations implements BaseTranslations { - /// Returns the current translations of the given [context]. - /// - /// Usage: - /// final t = Translations.of(context); - static Translations of(BuildContext context) => InheritedLocaleData.of(context).translations; - - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - Translations.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.en, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override final TranslationMetadata $meta; - - /// Access flat map - dynamic operator[](String key) => $meta.getTranslation(key); - - late final Translations _root = this; // ignore: unused_field - - // Translations - late final _TranslationsOnboardingEn onboarding = _TranslationsOnboardingEn._(_root); - late final _TranslationsGroupEn group = _TranslationsGroupEn._(_root); - late final _TranslationsEndEn end = _TranslationsEndEn._(_root); - TextSpan advancedPlural({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( - n: count, - resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'), - one: () => TextSpan(children: [ - const TextSpan(text: 'One'), - ]), - other: () => TextSpan(children: [ - const TextSpan(text: 'Other '), - countBuilder(count), - TextSpan(text: ', ${_root.onboarding.greet2(gender: gender)}'), - ]), - ); -} - -// Path: onboarding -class _TranslationsOnboardingEn { - _TranslationsOnboardingEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - String welcome({required Object fullName}) => 'Welcome ${fullName}'; - String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); - String welcomeOnlyParam({required Object firstName}) => '${firstName}'; - - /// Bye text - String bye({required Object firstName}) => 'Bye ${firstName}'; - - TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ - const TextSpan(text: 'Hi '), - name, - TextSpan(text: ' and ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), - ]); - List get pages => [ - _TranslationsOnboarding$pages$0i0$En._(_root), - _TranslationsOnboarding$pages$0i1$En._(_root), - ]; - List get modifierPages => [ - _TranslationsOnboarding$modifierPages$0i0$En._(_root), - _TranslationsOnboarding$modifierPages$0i1$En._(_root), - ]; - String greet({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { - switch (context) { - case GenderContext.male: - return 'Hello Mr ${lastName} and ${_root.onboarding.welcome(fullName: fullName)}'; - case GenderContext.female: - return 'Hello Ms ${lastName} and ${_root.onboarding.bye(firstName: firstName)}'; - } - } - String greet2({required GenderContext gender}) { - switch (gender) { - case GenderContext.male: - return 'Hello Mr'; - case GenderContext.female: - return 'Hello Ms'; - } - } - String greetCombination({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => '${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}, ${_root.onboarding.greet2(gender: gender)}'; - String welcomeLinkedPlural({required num n, required Object fullName, required Object firstName}) => 'Hello ${_root.group.users(n: n, fullName: fullName, firstName: firstName)}'; - String welcomeLinkedContext({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => 'Hello ${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; - String welcomeFullLink({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => 'Ultimate ${_root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName)} and ${_root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; -} - -// Path: group -class _TranslationsGroupEn { - _TranslationsGroupEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - String users({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: 'No Users and ${_root.onboarding.welcome(fullName: fullName)}', - one: 'One User', - other: '${n} Users and ${_root.onboarding.bye(firstName: firstName)}', - ); -} - -// Path: end -class _TranslationsEndEn with EndData { - _TranslationsEndEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - @override List get stringPages => [ - '1st Page', - '2nd Page', - ]; - @override List> get pages => [ - { - 'unknown': 'Unknown\nError', - }, - { - 'with space': 'An Error', - 'with second space': 'An 2nd Error', - }, - ]; -} - -// Path: onboarding.pages.0 -class _TranslationsOnboarding$pages$0i0$En with PageData { - _TranslationsOnboarding$pages$0i0$En._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - @override String get title => 'First Page'; - @override String get content => 'First Page Content'; -} - -// Path: onboarding.pages.1 -class _TranslationsOnboarding$pages$0i1$En with PageData { - _TranslationsOnboarding$pages$0i1$En._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - @override String get title => 'Second Page'; -} - -// Path: onboarding.modifierPages.0 -class _TranslationsOnboarding$modifierPages$0i0$En with MPage { - _TranslationsOnboarding$modifierPages$0i0$En._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - @override String get title => 'First Modifier Page'; - @override String get content => 'First Page Content'; -} - -// Path: onboarding.modifierPages.1 -class _TranslationsOnboarding$modifierPages$0i1$En with MPage { - _TranslationsOnboarding$modifierPages$0i1$En._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - @override String get title => 'Second Modifier Page'; -} - -// Path: -class _TranslationsDe implements Translations { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - _TranslationsDe.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.de, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override final TranslationMetadata $meta; - - /// Access flat map - @override dynamic operator[](String key) => $meta.getTranslation(key); - - @override late final _TranslationsDe _root = this; // ignore: unused_field - - // Translations - @override late final _TranslationsOnboardingDe onboarding = _TranslationsOnboardingDe._(_root); - @override late final _TranslationsGroupDe group = _TranslationsGroupDe._(_root); - @override late final _TranslationsEndDe end = _TranslationsEndDe._(_root); - @override TextSpan advancedPlural({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( - n: count, - resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'), - one: () => TextSpan(children: [ - const TextSpan(text: 'Eins'), - ]), - other: () => TextSpan(children: [ - const TextSpan(text: 'Andere '), - countBuilder(count), - TextSpan(text: ', ${_root.onboarding.greet2(gender: gender)}'), - ]), - ); -} - -// Path: onboarding -class _TranslationsOnboardingDe implements _TranslationsOnboardingEn { - _TranslationsOnboardingDe._(this._root); - - @override final _TranslationsDe _root; // ignore: unused_field - - // Translations - @override String welcome({required Object fullName}) => 'Willkommen ${fullName}'; - @override String welcomeAlias({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); - @override String welcomeOnlyParam({required Object firstName}) => '${firstName}'; - - /// Bye text - @override String bye({required Object firstName}) => 'Tschüss ${firstName}'; - - @override TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ - const TextSpan(text: 'Hi '), - name, - TextSpan(text: ' und ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), - ]); - @override List get pages => [ - _TranslationsOnboarding$pages$0i0$De._(_root), - _TranslationsOnboarding$pages$0i1$De._(_root), - ]; - @override List get modifierPages => [ - _TranslationsOnboarding$modifierPages$0i0$De._(_root), - _TranslationsOnboarding$modifierPages$0i1$De._(_root), - ]; - @override String greet({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { - switch (context) { - case GenderContext.male: - return 'Hallo Herr ${lastName} und ${_root.onboarding.welcome(fullName: fullName)}'; - case GenderContext.female: - return 'Hallo Frau ${lastName} und ${_root.onboarding.bye(firstName: firstName)}'; - } - } - @override String greet2({required GenderContext gender}) { - switch (gender) { - case GenderContext.male: - return 'Hallo Herr'; - case GenderContext.female: - return 'Hallo Frau'; - } - } - @override String greetCombination({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => '${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}, ${_root.onboarding.greet2(gender: gender)}'; - @override String welcomeLinkedPlural({required num n, required Object fullName, required Object firstName}) => 'Hallo ${_root.group.users(n: n, fullName: fullName, firstName: firstName)}'; - @override String welcomeLinkedContext({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => 'Hallo ${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; - @override String welcomeFullLink({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => 'Ultimative ${_root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName)} and ${_root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; -} - -// Path: group -class _TranslationsGroupDe implements _TranslationsGroupEn { - _TranslationsGroupDe._(this._root); - - @override final _TranslationsDe _root; // ignore: unused_field - - // Translations - @override String users({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, - zero: 'Keine Nutzer und ${_root.onboarding.welcome(fullName: fullName)}', - one: 'Ein Nutzer', - other: '${n} Nutzer und ${_root.onboarding.bye(firstName: firstName)}', - ); -} - -// Path: end -class _TranslationsEndDe with EndData implements _TranslationsEndEn { - _TranslationsEndDe._(this._root); - - @override final _TranslationsDe _root; // ignore: unused_field - - // Translations - @override List get stringPages => [ - '1. Seite', - '2. Seite', - ]; - @override List> get pages => [ - { - 'unknown': 'Unbekannter\nFehler', - }, - { - 'with space': 'Ein Fehler', - 'with second space': 'Ein 2. Fehler', - }, - ]; -} - -// Path: onboarding.pages.0 -class _TranslationsOnboarding$pages$0i0$De with PageData implements _TranslationsOnboarding$pages$0i0$En { - _TranslationsOnboarding$pages$0i0$De._(this._root); - - @override final _TranslationsDe _root; // ignore: unused_field - - // Translations - @override String get title => 'Erste Seite'; - @override String get content => 'Erster Seiteninhalt'; -} - -// Path: onboarding.pages.1 -class _TranslationsOnboarding$pages$0i1$De with PageData implements _TranslationsOnboarding$pages$0i1$En { - _TranslationsOnboarding$pages$0i1$De._(this._root); - - @override final _TranslationsDe _root; // ignore: unused_field - - // Translations - @override String get title => 'Zweite Seite'; -} - -// Path: onboarding.modifierPages.0 -class _TranslationsOnboarding$modifierPages$0i0$De with MPage implements _TranslationsOnboarding$modifierPages$0i0$En { - _TranslationsOnboarding$modifierPages$0i0$De._(this._root); - - @override final _TranslationsDe _root; // ignore: unused_field - - // Translations - @override String get title => 'Erste Modifier Seite'; - @override String get content => 'Erster Seiteninhalt'; -} - -// Path: onboarding.modifierPages.1 -class _TranslationsOnboarding$modifierPages$0i1$De with MPage implements _TranslationsOnboarding$modifierPages$0i1$En { - _TranslationsOnboarding$modifierPages$0i1$De._(this._root); - - @override final _TranslationsDe _root; // ignore: unused_field - - // Translations - @override String get title => 'Zweite Modifier Seite'; -} - -/// Flat map(s) containing all translations. -/// Only for edge cases! For simple maps, use the map function of this library. - -extension on Translations { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'onboarding.welcome': return ({required Object fullName}) => 'Welcome ${fullName}'; - case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); - case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; - case 'onboarding.bye': return ({required Object firstName}) => 'Bye ${firstName}'; - case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ - const TextSpan(text: 'Hi '), - name, - TextSpan(text: ' and ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), - ]); - case 'onboarding.pages.0.title': return 'First Page'; - case 'onboarding.pages.0.content': return 'First Page Content'; - case 'onboarding.pages.1.title': return 'Second Page'; - case 'onboarding.modifierPages.0.title': return 'First Modifier Page'; - case 'onboarding.modifierPages.0.content': return 'First Page Content'; - case 'onboarding.modifierPages.1.title': return 'Second Modifier Page'; - case 'onboarding.greet': return ({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { - switch (context) { - case GenderContext.male: - return 'Hello Mr ${lastName} and ${_root.onboarding.welcome(fullName: fullName)}'; - case GenderContext.female: - return 'Hello Ms ${lastName} and ${_root.onboarding.bye(firstName: firstName)}'; - } - }; - case 'onboarding.greet2': return ({required GenderContext gender}) { - switch (gender) { - case GenderContext.male: - return 'Hello Mr'; - case GenderContext.female: - return 'Hello Ms'; - } - }; - case 'onboarding.greetCombination': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => '${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}, ${_root.onboarding.greet2(gender: gender)}'; - case 'onboarding.welcomeLinkedPlural': return ({required num n, required Object fullName, required Object firstName}) => 'Hello ${_root.group.users(n: n, fullName: fullName, firstName: firstName)}'; - case 'onboarding.welcomeLinkedContext': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => 'Hello ${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; - case 'onboarding.welcomeFullLink': return ({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => 'Ultimate ${_root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName)} and ${_root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; - case 'group.users': return ({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, - zero: 'No Users and ${_root.onboarding.welcome(fullName: fullName)}', - one: 'One User', - other: '${n} Users and ${_root.onboarding.bye(firstName: firstName)}', - ); - case 'end.stringPages.0': return '1st Page'; - case 'end.stringPages.1': return '2nd Page'; - case 'end.pages.0.unknown': return 'Unknown\nError'; - case 'end.pages.1.with space': return 'An Error'; - case 'end.pages.1.with second space': return 'An 2nd Error'; - case 'advancedPlural': return ({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( - n: count, - resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'), - one: () => TextSpan(children: [ - const TextSpan(text: 'One'), - ]), - other: () => TextSpan(children: [ - const TextSpan(text: 'Other '), - countBuilder(count), - TextSpan(text: ', ${_root.onboarding.greet2(gender: gender)}'), - ]), - ); - default: return null; - } - } -} - -extension on _TranslationsDe { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'onboarding.welcome': return ({required Object fullName}) => 'Willkommen ${fullName}'; - case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); - case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; - case 'onboarding.bye': return ({required Object firstName}) => 'Tschüss ${firstName}'; - case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ - const TextSpan(text: 'Hi '), - name, - TextSpan(text: ' und ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), - ]); - case 'onboarding.pages.0.title': return 'Erste Seite'; - case 'onboarding.pages.0.content': return 'Erster Seiteninhalt'; - case 'onboarding.pages.1.title': return 'Zweite Seite'; - case 'onboarding.modifierPages.0.title': return 'Erste Modifier Seite'; - case 'onboarding.modifierPages.0.content': return 'Erster Seiteninhalt'; - case 'onboarding.modifierPages.1.title': return 'Zweite Modifier Seite'; - case 'onboarding.greet': return ({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { - switch (context) { - case GenderContext.male: - return 'Hallo Herr ${lastName} und ${_root.onboarding.welcome(fullName: fullName)}'; - case GenderContext.female: - return 'Hallo Frau ${lastName} und ${_root.onboarding.bye(firstName: firstName)}'; - } - }; - case 'onboarding.greet2': return ({required GenderContext gender}) { - switch (gender) { - case GenderContext.male: - return 'Hallo Herr'; - case GenderContext.female: - return 'Hallo Frau'; - } - }; - case 'onboarding.greetCombination': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => '${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}, ${_root.onboarding.greet2(gender: gender)}'; - case 'onboarding.welcomeLinkedPlural': return ({required num n, required Object fullName, required Object firstName}) => 'Hallo ${_root.group.users(n: n, fullName: fullName, firstName: firstName)}'; - case 'onboarding.welcomeLinkedContext': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => 'Hallo ${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; - case 'onboarding.welcomeFullLink': return ({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => 'Ultimative ${_root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName)} and ${_root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; - case 'group.users': return ({required num n, required Object fullName, required Object firstName}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, - zero: 'Keine Nutzer und ${_root.onboarding.welcome(fullName: fullName)}', - one: 'Ein Nutzer', - other: '${n} Nutzer und ${_root.onboarding.bye(firstName: firstName)}', - ); - case 'end.stringPages.0': return '1. Seite'; - case 'end.stringPages.1': return '2. Seite'; - case 'end.pages.0.unknown': return 'Unbekannter\nFehler'; - case 'end.pages.1.with space': return 'Ein Fehler'; - case 'end.pages.1.with second space': return 'Ein 2. Fehler'; - case 'advancedPlural': return ({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( - n: count, - resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'), - one: () => TextSpan(children: [ - const TextSpan(text: 'Eins'), - ]), - other: () => TextSpan(children: [ - const TextSpan(text: 'Andere '), - countBuilder(count), - TextSpan(text: ', ${_root.onboarding.greet2(gender: gender)}'), - ]), - ); - default: return null; - } - } -} diff --git a/slang/test/integration/resources/main/_expected_translation_overrides_de.output b/slang/test/integration/resources/main/_expected_translation_overrides_de.output new file mode 100644 index 00000000..95f4ded8 --- /dev/null +++ b/slang/test/integration/resources/main/_expected_translation_overrides_de.output @@ -0,0 +1,262 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:slang/node.dart'; +import 'package:slang/overrides.dart'; +import 'translations.cgm.dart'; + +// Path: +class TranslationsDe implements Translations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + /// [AppLocaleUtils.buildWithOverrides] is recommended for overriding. + TranslationsDe({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : $meta = TranslationMetadata( + locale: AppLocale.de, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + @override dynamic operator[](String key) => $meta.getTranslation(key); + + late final TranslationsDe _root = this; // ignore: unused_field + + // Translations + @override late final _TranslationsOnboardingDe onboarding = _TranslationsOnboardingDe._(_root); + @override late final _TranslationsGroupDe group = _TranslationsGroupDe._(_root); + @override late final _TranslationsEndDe end = _TranslationsEndDe._(_root); + @override TextSpan advancedPlural({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => TranslationOverridesFlutter.richPlural(_root.$meta, 'advancedPlural', {'gender': gender, 'count': count, 'countBuilder': countBuilder}) ?? RichPluralResolvers.bridge( + n: count, + resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'), + one: () => TextSpan(children: [ + const TextSpan(text: 'Eins'), + ]), + other: () => TextSpan(children: [ + const TextSpan(text: 'Andere '), + countBuilder(count), + TextSpan(text: ', ${_root.onboarding.greet2(gender: gender)}'), + ]), + ); +} + +// Path: onboarding +class _TranslationsOnboardingDe implements TranslationsOnboardingEn { + _TranslationsOnboardingDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String welcome({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcome', {'fullName': fullName}) ?? 'Willkommen ${fullName}'; + @override String welcomeAlias({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeAlias', {'fullName': fullName}) ?? _root.onboarding.welcome(fullName: fullName); + @override String welcomeOnlyParam({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeOnlyParam', {'firstName': firstName}) ?? '${firstName}'; + + /// Bye text + @override String bye({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Tschüss ${firstName}'; + + @override TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TranslationOverridesFlutter.rich(_root.$meta, 'onboarding.hi', {'name': name, 'lastName': lastName, 'context': context, 'fullName': fullName, 'firstName': firstName}) ?? TextSpan(children: [ + const TextSpan(text: 'Hi '), + name, + TextSpan(text: ' und ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), + ]); + @override List get pages => [ + _TranslationsOnboarding$pages$0i0$De._(_root), + _TranslationsOnboarding$pages$0i1$De._(_root), + ]; + @override List get modifierPages => [ + _TranslationsOnboarding$modifierPages$0i0$De._(_root), + _TranslationsOnboarding$modifierPages$0i1$De._(_root), + ]; + @override String greet({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { + final override = TranslationOverrides.context(_root.$meta, 'onboarding.greet', {'lastName': lastName, 'fullName': fullName, 'firstName': firstName, 'context': context}); + if (override != null) { + return override; + } + switch (context) { + case GenderContext.male: + return 'Hallo Herr ${lastName} und ${_root.onboarding.welcome(fullName: fullName)}'; + case GenderContext.female: + return 'Hallo Frau ${lastName} und ${_root.onboarding.bye(firstName: firstName)}'; + } + } + @override String greet2({required GenderContext gender}) { + final override = TranslationOverrides.context(_root.$meta, 'onboarding.greet2', {'gender': gender}); + if (override != null) { + return override; + } + switch (gender) { + case GenderContext.male: + return 'Hallo Herr'; + case GenderContext.female: + return 'Hallo Frau'; + } + } + @override String greetCombination({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => TranslationOverrides.string(_root.$meta, 'onboarding.greetCombination', {'lastName': lastName, 'fullName': fullName, 'firstName': firstName, 'context': context, 'gender': gender}) ?? '${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}, ${_root.onboarding.greet2(gender: gender)}'; + @override String welcomeLinkedPlural({required num n, required Object fullName, required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeLinkedPlural', {'n': n, 'fullName': fullName, 'firstName': firstName}) ?? 'Hallo ${_root.group.users(n: n, fullName: fullName, firstName: firstName)}'; + @override String welcomeLinkedContext({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeLinkedContext', {'lastName': lastName, 'fullName': fullName, 'firstName': firstName, 'context': context}) ?? 'Hallo ${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + @override String welcomeFullLink({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeFullLink', {'n': n, 'fullName': fullName, 'firstName': firstName, 'lastName': lastName, 'context': context}) ?? 'Ultimative ${_root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName)} and ${_root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; +} + +// Path: group +class _TranslationsGroupDe implements TranslationsGroupEn { + _TranslationsGroupDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String users({required num n, required Object fullName, required Object firstName}) => TranslationOverrides.plural(_root.$meta, 'group.users', {'fullName': fullName, 'firstName': firstName, 'n': n}) ?? (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, + zero: 'Keine Nutzer und ${_root.onboarding.welcome(fullName: fullName)}', + one: 'Ein Nutzer', + other: '${n} Nutzer und ${_root.onboarding.bye(firstName: firstName)}', + ); +} + +// Path: end +class _TranslationsEndDe with EndData implements TranslationsEndEn { + _TranslationsEndDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override List get stringPages => TranslationOverrides.list(_root.$meta, 'end.stringPages') ?? [ + '1. Seite', + '2. Seite', + ]; + @override List> get pages => [ + TranslationOverrides.map(_root.$meta, 'end.pages.0') ?? { + 'unknown': 'Unbekannter\nFehler', + }, + TranslationOverrides.map(_root.$meta, 'end.pages.1') ?? { + 'with space': 'Ein Fehler', + 'with second space': 'Ein 2. Fehler', + }, + ]; +} + +// Path: onboarding.pages.0 +class _TranslationsOnboarding$pages$0i0$De with PageData implements TranslationsOnboarding$pages$0i0$En { + _TranslationsOnboarding$pages$0i0$De._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get title => TranslationOverrides.string(_root.$meta, 'onboarding.pages.0.title', {}) ?? 'Erste Seite'; + @override String get content => TranslationOverrides.string(_root.$meta, 'onboarding.pages.0.content', {}) ?? 'Erster Seiteninhalt'; +} + +// Path: onboarding.pages.1 +class _TranslationsOnboarding$pages$0i1$De with PageData implements TranslationsOnboarding$pages$0i1$En { + _TranslationsOnboarding$pages$0i1$De._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get title => TranslationOverrides.string(_root.$meta, 'onboarding.pages.1.title', {}) ?? 'Zweite Seite'; +} + +// Path: onboarding.modifierPages.0 +class _TranslationsOnboarding$modifierPages$0i0$De with MPage implements TranslationsOnboarding$modifierPages$0i0$En { + _TranslationsOnboarding$modifierPages$0i0$De._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get title => TranslationOverrides.string(_root.$meta, 'onboarding.modifierPages.0.title', {}) ?? 'Erste Modifier Seite'; + @override String get content => TranslationOverrides.string(_root.$meta, 'onboarding.modifierPages.0.content', {}) ?? 'Erster Seiteninhalt'; +} + +// Path: onboarding.modifierPages.1 +class _TranslationsOnboarding$modifierPages$0i1$De with MPage implements TranslationsOnboarding$modifierPages$0i1$En { + _TranslationsOnboarding$modifierPages$0i1$De._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get title => TranslationOverrides.string(_root.$meta, 'onboarding.modifierPages.1.title', {}) ?? 'Zweite Modifier Seite'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on TranslationsDe { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'onboarding.welcome': return ({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcome', {'fullName': fullName}) ?? 'Willkommen ${fullName}'; + case 'onboarding.welcomeAlias': return ({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeAlias', {'fullName': fullName}) ?? _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeOnlyParam', {'firstName': firstName}) ?? '${firstName}'; + case 'onboarding.bye': return ({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Tschüss ${firstName}'; + case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TranslationOverridesFlutter.rich(_root.$meta, 'onboarding.hi', {'name': name, 'lastName': lastName, 'context': context, 'fullName': fullName, 'firstName': firstName}) ?? TextSpan(children: [ + const TextSpan(text: 'Hi '), + name, + TextSpan(text: ' und ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), + ]); + case 'onboarding.pages.0.title': return TranslationOverrides.string(_root.$meta, 'onboarding.pages.0.title', {}) ?? 'Erste Seite'; + case 'onboarding.pages.0.content': return TranslationOverrides.string(_root.$meta, 'onboarding.pages.0.content', {}) ?? 'Erster Seiteninhalt'; + case 'onboarding.pages.1.title': return TranslationOverrides.string(_root.$meta, 'onboarding.pages.1.title', {}) ?? 'Zweite Seite'; + case 'onboarding.modifierPages.0.title': return TranslationOverrides.string(_root.$meta, 'onboarding.modifierPages.0.title', {}) ?? 'Erste Modifier Seite'; + case 'onboarding.modifierPages.0.content': return TranslationOverrides.string(_root.$meta, 'onboarding.modifierPages.0.content', {}) ?? 'Erster Seiteninhalt'; + case 'onboarding.modifierPages.1.title': return TranslationOverrides.string(_root.$meta, 'onboarding.modifierPages.1.title', {}) ?? 'Zweite Modifier Seite'; + case 'onboarding.greet': return ({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { + final override = TranslationOverrides.context(_root.$meta, 'onboarding.greet', {'lastName': lastName, 'fullName': fullName, 'firstName': firstName, 'context': context}); + if (override != null) { + return override; + } + switch (context) { + case GenderContext.male: + return 'Hallo Herr ${lastName} und ${_root.onboarding.welcome(fullName: fullName)}'; + case GenderContext.female: + return 'Hallo Frau ${lastName} und ${_root.onboarding.bye(firstName: firstName)}'; + } + }; + case 'onboarding.greet2': return ({required GenderContext gender}) { + final override = TranslationOverrides.context(_root.$meta, 'onboarding.greet2', {'gender': gender}); + if (override != null) { + return override; + } + switch (gender) { + case GenderContext.male: + return 'Hallo Herr'; + case GenderContext.female: + return 'Hallo Frau'; + } + }; + case 'onboarding.greetCombination': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => TranslationOverrides.string(_root.$meta, 'onboarding.greetCombination', {'lastName': lastName, 'fullName': fullName, 'firstName': firstName, 'context': context, 'gender': gender}) ?? '${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}, ${_root.onboarding.greet2(gender: gender)}'; + case 'onboarding.welcomeLinkedPlural': return ({required num n, required Object fullName, required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeLinkedPlural', {'n': n, 'fullName': fullName, 'firstName': firstName}) ?? 'Hallo ${_root.group.users(n: n, fullName: fullName, firstName: firstName)}'; + case 'onboarding.welcomeLinkedContext': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeLinkedContext', {'lastName': lastName, 'fullName': fullName, 'firstName': firstName, 'context': context}) ?? 'Hallo ${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + case 'onboarding.welcomeFullLink': return ({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeFullLink', {'n': n, 'fullName': fullName, 'firstName': firstName, 'lastName': lastName, 'context': context}) ?? 'Ultimative ${_root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName)} and ${_root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + case 'group.users': return ({required num n, required Object fullName, required Object firstName}) => TranslationOverrides.plural(_root.$meta, 'group.users', {'fullName': fullName, 'firstName': firstName, 'n': n}) ?? (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'))(n, + zero: 'Keine Nutzer und ${_root.onboarding.welcome(fullName: fullName)}', + one: 'Ein Nutzer', + other: '${n} Nutzer und ${_root.onboarding.bye(firstName: firstName)}', + ); + case 'end.stringPages.0': return TranslationOverrides.string(_root.$meta, 'end.stringPages.0', {}) ?? '1. Seite'; + case 'end.stringPages.1': return TranslationOverrides.string(_root.$meta, 'end.stringPages.1', {}) ?? '2. Seite'; + case 'end.pages.0.unknown': return TranslationOverrides.string(_root.$meta, 'end.pages.0.unknown', {}) ?? 'Unbekannter\nFehler'; + case 'end.pages.1.with space': return TranslationOverrides.string(_root.$meta, 'end.pages.1.with space', {}) ?? 'Ein Fehler'; + case 'end.pages.1.with second space': return TranslationOverrides.string(_root.$meta, 'end.pages.1.with second space', {}) ?? 'Ein 2. Fehler'; + case 'advancedPlural': return ({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => TranslationOverridesFlutter.richPlural(_root.$meta, 'advancedPlural', {'gender': gender, 'count': count, 'countBuilder': countBuilder}) ?? RichPluralResolvers.bridge( + n: count, + resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('de'), + one: () => TextSpan(children: [ + const TextSpan(text: 'Eins'), + ]), + other: () => TextSpan(children: [ + const TextSpan(text: 'Andere '), + countBuilder(count), + TextSpan(text: ', ${_root.onboarding.greet2(gender: gender)}'), + ]), + ); + default: return null; + } + } +} + diff --git a/slang/test/integration/resources/main/_expected_translation_overrides_en.output b/slang/test/integration/resources/main/_expected_translation_overrides_en.output new file mode 100644 index 00000000..5d5b1d7f --- /dev/null +++ b/slang/test/integration/resources/main/_expected_translation_overrides_en.output @@ -0,0 +1,266 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +part of 'translations.cgm.dart'; + +// Path: +typedef TranslationsEn = Translations; // ignore: unused_element +class Translations implements BaseTranslations { + /// Returns the current translations of the given [context]. + /// + /// Usage: + /// final t = Translations.of(context); + static Translations of(BuildContext context) => InheritedLocaleData.of(context).translations; + + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + /// [AppLocaleUtils.buildWithOverrides] is recommended for overriding. + Translations({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : $meta = TranslationMetadata( + locale: AppLocale.en, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + dynamic operator[](String key) => $meta.getTranslation(key); + + late final Translations _root = this; // ignore: unused_field + + // Translations + late final TranslationsOnboardingEn onboarding = TranslationsOnboardingEn._(_root); + late final TranslationsGroupEn group = TranslationsGroupEn._(_root); + late final TranslationsEndEn end = TranslationsEndEn._(_root); + TextSpan advancedPlural({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => TranslationOverridesFlutter.richPlural(_root.$meta, 'advancedPlural', {'gender': gender, 'count': count, 'countBuilder': countBuilder}) ?? RichPluralResolvers.bridge( + n: count, + resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'), + one: () => TextSpan(children: [ + const TextSpan(text: 'One'), + ]), + other: () => TextSpan(children: [ + const TextSpan(text: 'Other '), + countBuilder(count), + TextSpan(text: ', ${_root.onboarding.greet2(gender: gender)}'), + ]), + ); +} + +// Path: onboarding +class TranslationsOnboardingEn { + TranslationsOnboardingEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String welcome({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcome', {'fullName': fullName}) ?? 'Welcome ${fullName}'; + String welcomeAlias({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeAlias', {'fullName': fullName}) ?? _root.onboarding.welcome(fullName: fullName); + String welcomeOnlyParam({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeOnlyParam', {'firstName': firstName}) ?? '${firstName}'; + + /// Bye text + String bye({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Bye ${firstName}'; + + TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TranslationOverridesFlutter.rich(_root.$meta, 'onboarding.hi', {'name': name, 'lastName': lastName, 'context': context, 'fullName': fullName, 'firstName': firstName}) ?? TextSpan(children: [ + const TextSpan(text: 'Hi '), + name, + TextSpan(text: ' and ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), + ]); + List get pages => [ + TranslationsOnboarding$pages$0i0$En._(_root), + TranslationsOnboarding$pages$0i1$En._(_root), + ]; + List get modifierPages => [ + TranslationsOnboarding$modifierPages$0i0$En._(_root), + TranslationsOnboarding$modifierPages$0i1$En._(_root), + ]; + String greet({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { + final override = TranslationOverrides.context(_root.$meta, 'onboarding.greet', {'lastName': lastName, 'fullName': fullName, 'firstName': firstName, 'context': context}); + if (override != null) { + return override; + } + switch (context) { + case GenderContext.male: + return 'Hello Mr ${lastName} and ${_root.onboarding.welcome(fullName: fullName)}'; + case GenderContext.female: + return 'Hello Ms ${lastName} and ${_root.onboarding.bye(firstName: firstName)}'; + } + } + String greet2({required GenderContext gender}) { + final override = TranslationOverrides.context(_root.$meta, 'onboarding.greet2', {'gender': gender}); + if (override != null) { + return override; + } + switch (gender) { + case GenderContext.male: + return 'Hello Mr'; + case GenderContext.female: + return 'Hello Ms'; + } + } + String greetCombination({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => TranslationOverrides.string(_root.$meta, 'onboarding.greetCombination', {'lastName': lastName, 'fullName': fullName, 'firstName': firstName, 'context': context, 'gender': gender}) ?? '${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}, ${_root.onboarding.greet2(gender: gender)}'; + String welcomeLinkedPlural({required num n, required Object fullName, required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeLinkedPlural', {'n': n, 'fullName': fullName, 'firstName': firstName}) ?? 'Hello ${_root.group.users(n: n, fullName: fullName, firstName: firstName)}'; + String welcomeLinkedContext({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeLinkedContext', {'lastName': lastName, 'fullName': fullName, 'firstName': firstName, 'context': context}) ?? 'Hello ${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + String welcomeFullLink({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeFullLink', {'n': n, 'fullName': fullName, 'firstName': firstName, 'lastName': lastName, 'context': context}) ?? 'Ultimate ${_root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName)} and ${_root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; +} + +// Path: group +class TranslationsGroupEn { + TranslationsGroupEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String users({required num n, required Object fullName, required Object firstName}) => TranslationOverrides.plural(_root.$meta, 'group.users', {'fullName': fullName, 'firstName': firstName, 'n': n}) ?? (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: 'No Users and ${_root.onboarding.welcome(fullName: fullName)}', + one: 'One User', + other: '${n} Users and ${_root.onboarding.bye(firstName: firstName)}', + ); +} + +// Path: end +class TranslationsEndEn with EndData { + TranslationsEndEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override List get stringPages => TranslationOverrides.list(_root.$meta, 'end.stringPages') ?? [ + '1st Page', + '2nd Page', + ]; + @override List> get pages => [ + TranslationOverrides.map(_root.$meta, 'end.pages.0') ?? { + 'unknown': 'Unknown\nError', + }, + TranslationOverrides.map(_root.$meta, 'end.pages.1') ?? { + 'with space': 'An Error', + 'with second space': 'An 2nd Error', + }, + ]; +} + +// Path: onboarding.pages.0 +class TranslationsOnboarding$pages$0i0$En with PageData { + TranslationsOnboarding$pages$0i0$En._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override String get title => TranslationOverrides.string(_root.$meta, 'onboarding.pages.0.title', {}) ?? 'First Page'; + @override String get content => TranslationOverrides.string(_root.$meta, 'onboarding.pages.0.content', {}) ?? 'First Page Content'; +} + +// Path: onboarding.pages.1 +class TranslationsOnboarding$pages$0i1$En with PageData { + TranslationsOnboarding$pages$0i1$En._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override String get title => TranslationOverrides.string(_root.$meta, 'onboarding.pages.1.title', {}) ?? 'Second Page'; +} + +// Path: onboarding.modifierPages.0 +class TranslationsOnboarding$modifierPages$0i0$En with MPage { + TranslationsOnboarding$modifierPages$0i0$En._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override String get title => TranslationOverrides.string(_root.$meta, 'onboarding.modifierPages.0.title', {}) ?? 'First Modifier Page'; + @override String get content => TranslationOverrides.string(_root.$meta, 'onboarding.modifierPages.0.content', {}) ?? 'First Page Content'; +} + +// Path: onboarding.modifierPages.1 +class TranslationsOnboarding$modifierPages$0i1$En with MPage { + TranslationsOnboarding$modifierPages$0i1$En._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + @override String get title => TranslationOverrides.string(_root.$meta, 'onboarding.modifierPages.1.title', {}) ?? 'Second Modifier Page'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on Translations { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'onboarding.welcome': return ({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcome', {'fullName': fullName}) ?? 'Welcome ${fullName}'; + case 'onboarding.welcomeAlias': return ({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeAlias', {'fullName': fullName}) ?? _root.onboarding.welcome(fullName: fullName); + case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeOnlyParam', {'firstName': firstName}) ?? '${firstName}'; + case 'onboarding.bye': return ({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Bye ${firstName}'; + case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TranslationOverridesFlutter.rich(_root.$meta, 'onboarding.hi', {'name': name, 'lastName': lastName, 'context': context, 'fullName': fullName, 'firstName': firstName}) ?? TextSpan(children: [ + const TextSpan(text: 'Hi '), + name, + TextSpan(text: ' and ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), + ]); + case 'onboarding.pages.0.title': return TranslationOverrides.string(_root.$meta, 'onboarding.pages.0.title', {}) ?? 'First Page'; + case 'onboarding.pages.0.content': return TranslationOverrides.string(_root.$meta, 'onboarding.pages.0.content', {}) ?? 'First Page Content'; + case 'onboarding.pages.1.title': return TranslationOverrides.string(_root.$meta, 'onboarding.pages.1.title', {}) ?? 'Second Page'; + case 'onboarding.modifierPages.0.title': return TranslationOverrides.string(_root.$meta, 'onboarding.modifierPages.0.title', {}) ?? 'First Modifier Page'; + case 'onboarding.modifierPages.0.content': return TranslationOverrides.string(_root.$meta, 'onboarding.modifierPages.0.content', {}) ?? 'First Page Content'; + case 'onboarding.modifierPages.1.title': return TranslationOverrides.string(_root.$meta, 'onboarding.modifierPages.1.title', {}) ?? 'Second Modifier Page'; + case 'onboarding.greet': return ({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { + final override = TranslationOverrides.context(_root.$meta, 'onboarding.greet', {'lastName': lastName, 'fullName': fullName, 'firstName': firstName, 'context': context}); + if (override != null) { + return override; + } + switch (context) { + case GenderContext.male: + return 'Hello Mr ${lastName} and ${_root.onboarding.welcome(fullName: fullName)}'; + case GenderContext.female: + return 'Hello Ms ${lastName} and ${_root.onboarding.bye(firstName: firstName)}'; + } + }; + case 'onboarding.greet2': return ({required GenderContext gender}) { + final override = TranslationOverrides.context(_root.$meta, 'onboarding.greet2', {'gender': gender}); + if (override != null) { + return override; + } + switch (gender) { + case GenderContext.male: + return 'Hello Mr'; + case GenderContext.female: + return 'Hello Ms'; + } + }; + case 'onboarding.greetCombination': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context, required GenderContext gender}) => TranslationOverrides.string(_root.$meta, 'onboarding.greetCombination', {'lastName': lastName, 'fullName': fullName, 'firstName': firstName, 'context': context, 'gender': gender}) ?? '${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}, ${_root.onboarding.greet2(gender: gender)}'; + case 'onboarding.welcomeLinkedPlural': return ({required num n, required Object fullName, required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeLinkedPlural', {'n': n, 'fullName': fullName, 'firstName': firstName}) ?? 'Hello ${_root.group.users(n: n, fullName: fullName, firstName: firstName)}'; + case 'onboarding.welcomeLinkedContext': return ({required Object lastName, required Object fullName, required Object firstName, required GenderContext context}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeLinkedContext', {'lastName': lastName, 'fullName': fullName, 'firstName': firstName, 'context': context}) ?? 'Hello ${_root.onboarding.greet(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + case 'onboarding.welcomeFullLink': return ({required num n, required Object fullName, required Object firstName, required Object lastName, required GenderContext context}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeFullLink', {'n': n, 'fullName': fullName, 'firstName': firstName, 'lastName': lastName, 'context': context}) ?? 'Ultimate ${_root.onboarding.welcomeLinkedPlural(n: n, fullName: fullName, firstName: firstName)} and ${_root.onboarding.welcomeLinkedContext(lastName: lastName, fullName: fullName, firstName: firstName, context: context)}'; + case 'group.users': return ({required num n, required Object fullName, required Object firstName}) => TranslationOverrides.plural(_root.$meta, 'group.users', {'fullName': fullName, 'firstName': firstName, 'n': n}) ?? (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, + zero: 'No Users and ${_root.onboarding.welcome(fullName: fullName)}', + one: 'One User', + other: '${n} Users and ${_root.onboarding.bye(firstName: firstName)}', + ); + case 'end.stringPages.0': return TranslationOverrides.string(_root.$meta, 'end.stringPages.0', {}) ?? '1st Page'; + case 'end.stringPages.1': return TranslationOverrides.string(_root.$meta, 'end.stringPages.1', {}) ?? '2nd Page'; + case 'end.pages.0.unknown': return TranslationOverrides.string(_root.$meta, 'end.pages.0.unknown', {}) ?? 'Unknown\nError'; + case 'end.pages.1.with space': return TranslationOverrides.string(_root.$meta, 'end.pages.1.with space', {}) ?? 'An Error'; + case 'end.pages.1.with second space': return TranslationOverrides.string(_root.$meta, 'end.pages.1.with second space', {}) ?? 'An 2nd Error'; + case 'advancedPlural': return ({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => TranslationOverridesFlutter.richPlural(_root.$meta, 'advancedPlural', {'gender': gender, 'count': count, 'countBuilder': countBuilder}) ?? RichPluralResolvers.bridge( + n: count, + resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'), + one: () => TextSpan(children: [ + const TextSpan(text: 'One'), + ]), + other: () => TextSpan(children: [ + const TextSpan(text: 'Other '), + countBuilder(count), + TextSpan(text: ', ${_root.onboarding.greet2(gender: gender)}'), + ]), + ); + default: return null; + } + } +} + diff --git a/slang/test/integration/resources/main/_expected_translation_overrides_main.output b/slang/test/integration/resources/main/_expected_translation_overrides_main.output new file mode 100644 index 00000000..91f45d1f --- /dev/null +++ b/slang/test/integration/resources/main/_expected_translation_overrides_main.output @@ -0,0 +1,246 @@ +/// Generated file. Do not edit. +/// +/// Original: fake/path/integration +/// To regenerate, run: `dart run slang` +/// +/// Locales: 2 +/// Strings: 58 (29 per locale) + +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:slang/node.dart'; +import 'package:slang/overrides.dart'; +import 'package:slang_flutter/slang_flutter.dart'; +export 'package:slang_flutter/slang_flutter.dart'; + +import 'translations_de.g.dart' deferred as _$de; +part 'translations_en.g.dart'; + +/// Generated by the "Translation Overrides" feature. +/// This config is needed to recreate the translation model exactly +/// the same way as this file was created. +final _buildConfig = BuildModelConfig( + fallbackStrategy: FallbackStrategy.none, + keyCase: null, + keyMapCase: null, + paramCase: null, + stringInterpolation: StringInterpolation.braces, + maps: ['end.pages.0', 'end.pages.1'], + pluralAuto: PluralAuto.cardinal, + pluralParameter: 'n', + pluralCardinal: [], + pluralOrdinal: [], + contexts: [], + interfaces: [], // currently not supported +); + +/// Supported locales, see extension methods below. +/// +/// Usage: +/// - LocaleSettings.setLocale(AppLocale.en) // set locale +/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum +/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check +enum AppLocale with BaseAppLocale { + en(languageCode: 'en'), + de(languageCode: 'de'); + + const AppLocale({ + required this.languageCode, + this.scriptCode, // ignore: unused_element + this.countryCode, // ignore: unused_element + }); + + @override final String languageCode; + @override final String? scriptCode; + @override final String? countryCode; + + @override + Future build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + await _$de.loadLibrary(); + return _$de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + @override + Translations buildSync({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + return _$de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + /// Gets current instance managed by [LocaleSettings]. + Translations get translations => LocaleSettings.instance.getTranslations(this); +} + +/// Method A: Simple +/// +/// No rebuild after locale change. +/// Translation happens during initialization of the widget (call of t). +/// Configurable via 'translate_var'. +/// +/// Usage: +/// String a = t.someKey.anotherKey; +/// String b = t['someKey.anotherKey']; // Only for edge cases! +Translations get t => LocaleSettings.instance.currentTranslations; + +/// Method B: Advanced +/// +/// All widgets using this method will trigger a rebuild when locale changes. +/// Use this if you have e.g. a settings page where the user can select the locale during runtime. +/// +/// Step 1: +/// wrap your App with +/// TranslationProvider( +/// child: MyApp() +/// ); +/// +/// Step 2: +/// final t = Translations.of(context); // Get t variable. +/// String a = t.someKey.anotherKey; // Use t variable. +/// String b = t['someKey.anotherKey']; // Only for edge cases! +class TranslationProvider extends BaseTranslationProvider { + TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance); + + static InheritedLocaleData of(BuildContext context) => InheritedLocaleData.of(context); +} + +/// Method B shorthand via [BuildContext] extension method. +/// Configurable via 'translate_var'. +/// +/// Usage (e.g. in a widget's build method): +/// context.t.someKey.anotherKey +extension BuildContextTranslationsExtension on BuildContext { + Translations get t => TranslationProvider.of(this).translations; +} + +/// Manages all translation instances and the current locale +class LocaleSettings extends BaseFlutterLocaleSettings { + LocaleSettings._() : super(utils: AppLocaleUtils.instance); + + static final instance = LocaleSettings._(); + + // static aliases (checkout base methods for documentation) + static AppLocale get currentLocale => instance.currentLocale; + static Stream getLocaleStream() => instance.getLocaleStream(); + static Future setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + static Future setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static Future useDeviceLocale() => instance.useDeviceLocale(); + static Future setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + static Future overrideTranslations({required AppLocale locale, required FileType fileType, required String content}) => instance.overrideTranslations(locale: locale, fileType: fileType, content: content); + static Future overrideTranslationsFromMap({required AppLocale locale, required bool isFlatMap, required Map map}) => instance.overrideTranslationsFromMap(locale: locale, isFlatMap: isFlatMap, map: map); + + // synchronous versions + static AppLocale setLocaleSync(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale useDeviceLocaleSync() => instance.useDeviceLocaleSync(); + static void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + static void overrideTranslationsSync({required AppLocale locale, required FileType fileType, required String content}) => instance.overrideTranslationsSync(locale: locale, fileType: fileType, content: content); + static void overrideTranslationsFromMapSync({required AppLocale locale, required bool isFlatMap, required Map map}) => instance.overrideTranslationsFromMapSync(locale: locale, isFlatMap: isFlatMap, map: map); +} + +/// Provides utility functions without any side effects. +class AppLocaleUtils extends BaseAppLocaleUtils { + AppLocaleUtils._() : super( + baseLocale: AppLocale.en, + locales: AppLocale.values, + buildConfig: _buildConfig, + ); + + static final instance = AppLocaleUtils._(); + + // static aliases (checkout base methods for documentation) + static AppLocale parse(String rawLocale) => instance.parse(rawLocale); + static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode); + static AppLocale findDeviceLocale() => instance.findDeviceLocale(); + static List get supportedLocales => instance.supportedLocales; + static List get supportedLocalesRaw => instance.supportedLocalesRaw; + static Future buildWithOverrides({required AppLocale locale, required FileType fileType, required String content, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverrides(locale: locale, fileType: fileType, content: content, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver); + static Future buildWithOverridesFromMap({required AppLocale locale, required bool isFlatMap, required Map map, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverridesFromMap(locale: locale, isFlatMap: isFlatMap, map: map, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver); + static Translations buildWithOverridesSync({required AppLocale locale, required FileType fileType, required String content, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverridesSync(locale: locale, fileType: fileType, content: content, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver); + static Translations buildWithOverridesFromMapSync({required AppLocale locale, required bool isFlatMap, required Map map, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverridesFromMapSync(locale: locale, isFlatMap: isFlatMap, map: map, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver); +} + +// context enums + +enum GenderContext { + male, + female, +} + +// interfaces generated as mixins + +mixin PageData { + String get title; + String? get content => null; + + @override + bool operator ==(Object other) => other is PageData && title == other.title && content == other.content; + + @override + int get hashCode => title.hashCode * content.hashCode; +} + +mixin MPage { + String get title; + String? get content => null; + + @override + bool operator ==(Object other) => other is MPage && title == other.title && content == other.content; + + @override + int get hashCode => title.hashCode * content.hashCode; +} + +mixin EndData { + List get stringPages; + List> get pages; + + @override + bool operator ==(Object other) => other is EndData && stringPages == other.stringPages && pages == other.pages; + + @override + int get hashCode => stringPages.hashCode * pages.hashCode; +} diff --git a/slang/test/integration/resources/main/build_config.yaml b/slang/test/integration/resources/main/build_config.yaml index b28471b6..c179266d 100644 --- a/slang/test/integration/resources/main/build_config.yaml +++ b/slang/test/integration/resources/main/build_config.yaml @@ -7,7 +7,6 @@ targets: base_locale: en input_file_pattern: .i18n.json # will be ignored anyways because we put in manually output_file_name: translations.cgm.dart # currently set manually for each test - output_format: single_file # may get changed programmatically locale_handling: true # may get changed programmatically string_interpolation: braces timestamp: false # make every test deterministic diff --git a/slang/test/integration/update.dart b/slang/test/integration/update.dart index 120cfa40..0a55ac04 100644 --- a/slang/test/integration/update.dart +++ b/slang/test/integration/update.dart @@ -1,12 +1,12 @@ -import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/model/build_result.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/obfuscation_config.dart'; -import 'package:slang/builder/model/raw_config.dart'; -import 'package:slang/builder/model/translation_map.dart'; +import 'package:slang/src/builder/builder/raw_config_builder.dart'; import 'package:slang/src/builder/decoder/json_decoder.dart'; import 'package:slang/src/builder/generator_facade.dart'; +import 'package:slang/src/builder/model/build_result.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/obfuscation_config.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; import 'package:slang/src/builder/utils/file_utils.dart'; import '../util/resources_utils.dart'; @@ -28,7 +28,6 @@ void main() { final buildConfig = RawConfigBuilder.fromYaml(loadResource('main/build_config.yaml'))!; generateMainIntegration(buildConfig, en, de); - generateMainSplitIntegration(buildConfig, en, de); generateNoFlutter(buildConfig, simple); generateNoLocaleHandling(buildConfig, simple); generateTranslationOverrides(buildConfig, en, de); @@ -47,40 +46,18 @@ BuildResult _generate({ }) { return GeneratorFacade.generate( rawConfig: rawConfig, - baseName: 'translations', translationMap: translationMap, inputDirectoryHint: 'fake/path/integration', ); } -void generateMainIntegration(RawConfig buildConfig, String en, String de) { - final result = _generate( - rawConfig: buildConfig, - baseName: 'translations', - translationMap: TranslationMap() - ..addTranslations( - locale: I18nLocale.fromString('en'), - translations: JsonDecoder().decode(en), - ) - ..addTranslations( - locale: I18nLocale.fromString('de'), - translations: JsonDecoder().decode(de), - ), - ).joinAsSingleOutput(); - - _write( - path: 'main/_expected_single', - content: result, - ); -} - -void generateMainSplitIntegration( +void generateMainIntegration( RawConfig buildConfig, String en, String de, ) { final result = _generate( - rawConfig: buildConfig.copyWith(outputFormat: OutputFormat.multipleFiles), + rawConfig: buildConfig, baseName: 'translations', translationMap: TranslationMap() ..addTranslations( @@ -95,7 +72,7 @@ void generateMainSplitIntegration( _write( path: 'main/_expected_main', - content: result.header, + content: result.main, ); _write( @@ -107,11 +84,6 @@ void generateMainSplitIntegration( path: 'main/_expected_de', content: result.translations[I18nLocale.fromString('de')]!, ); - - _write( - path: 'main/_expected_map', - content: result.flatMap!, - ); } void generateNoFlutter(RawConfig buildConfig, String simple) { @@ -123,11 +95,11 @@ void generateNoFlutter(RawConfig buildConfig, String simple) { locale: I18nLocale.fromString('en'), translations: JsonDecoder().decode(simple), ), - ).joinAsSingleOutput(); + ); _write( path: 'main/_expected_no_flutter', - content: result, + content: result.main, ); } @@ -143,11 +115,11 @@ void generateNoLocaleHandling(RawConfig buildConfig, String simple) { locale: I18nLocale.fromString('en'), translations: JsonDecoder().decode(simple), ), - ).joinAsSingleOutput(); + ); _write( path: 'main/_expected_no_locale_handling', - content: result, + content: result.main, ); } @@ -164,11 +136,21 @@ void generateTranslationOverrides(RawConfig buildConfig, String en, String de) { locale: I18nLocale.fromString('de'), translations: JsonDecoder().decode(de), ), - ).joinAsSingleOutput(); + ); _write( - path: 'main/_expected_translation_overrides', - content: result, + path: 'main/_expected_translation_overrides_main', + content: result.main, + ); + + _write( + path: 'main/_expected_translation_overrides_en', + content: result.translations[I18nLocale.fromString('en')]!, + ); + + _write( + path: 'main/_expected_translation_overrides_de', + content: result.translations[I18nLocale.fromString('de')]!, ); } @@ -187,11 +169,21 @@ void generateFallbackBaseLocale(RawConfig buildConfig, String en, String de) { locale: I18nLocale.fromString('de'), translations: JsonDecoder().decode(de), ), - ).joinAsSingleOutput(); + ); + + _write( + path: 'main/_expected_fallback_base_locale_main', + content: result.main, + ); _write( - path: 'main/_expected_fallback_base_locale', - content: result, + path: 'main/_expected_fallback_base_locale_en', + content: result.translations[I18nLocale.fromString('en')]!, + ); + + _write( + path: 'main/_expected_fallback_base_locale_de', + content: result.translations[I18nLocale.fromString('de')]!, ); } @@ -214,11 +206,21 @@ void generateFallbackBaseLocaleSpecial( locale: I18nLocale.fromString('de'), translations: JsonDecoder().decode(de), ), - ).joinAsSingleOutput(); + ); + + _write( + path: 'main/_expected_fallback_base_locale_special_main', + content: result.main, + ); _write( - path: 'main/_expected_fallback_base_locale_special', - content: result, + path: 'main/_expected_fallback_base_locale_special_en', + content: result.translations[I18nLocale.fromString('en')]!, + ); + + _write( + path: 'main/_expected_fallback_base_locale_special_de', + content: result.translations[I18nLocale.fromString('de')]!, ); } @@ -240,11 +242,21 @@ void generateObfuscation(RawConfig buildConfig, String en, String de) { locale: I18nLocale.fromString('de'), translations: JsonDecoder().decode(de), ), - ).joinAsSingleOutput(); + ); _write( - path: 'main/_expected_obfuscation', - content: result, + path: 'main/_expected_obfuscation_main', + content: result.main, + ); + + _write( + path: 'main/_expected_obfuscation_en', + content: result.translations[I18nLocale.fromString('en')]!, + ); + + _write( + path: 'main/_expected_obfuscation_de', + content: result.translations[I18nLocale.fromString('de')]!, ); } @@ -260,11 +272,11 @@ void generateRichText() { locale: I18nLocale.fromString('en'), translations: JsonDecoder().decode(en), ), - ).joinAsSingleOutput(); + ); _write( path: 'main/_expected_rich_text', - content: result, + content: result.translations.values.first, ); } diff --git a/slang/test/unit/api/locale_settings_test.dart b/slang/test/unit/api/locale_settings_test.dart index 34fdb19d..a12b4769 100644 --- a/slang/test/unit/api/locale_settings_test.dart +++ b/slang/test/unit/api/locale_settings_test.dart @@ -1,8 +1,8 @@ -import 'package:slang/api/locale.dart'; -import 'package:slang/api/singleton.dart'; -import 'package:slang/api/state.dart'; -import 'package:slang/builder/model/build_model_config.dart'; -import 'package:slang/builder/model/enums.dart'; +import 'package:slang/src/api/locale.dart'; +import 'package:slang/src/api/singleton.dart'; +import 'package:slang/src/api/state.dart'; +import 'package:slang/src/builder/model/build_model_config.dart'; +import 'package:slang/src/builder/model/enums.dart'; import 'package:test/test.dart'; void main() { @@ -24,16 +24,16 @@ void main() { expect(localeSettings.currentTranslations.providedNullOverrides, true); }); - test('should keep overrides when it is previously not empty', () { + test('should keep overrides when it is previously not empty', () async { final localeSettings = _LocaleSettings(); - localeSettings.overrideTranslationsFromMap( + await localeSettings.overrideTranslationsFromMap( locale: _baseLocale, isFlatMap: false, map: {'hello': 'hi'}, ); - localeSettings.setPluralResolver( + await localeSettings.setPluralResolver( language: 'und', cardinalResolver: (n, {zero, one, two, few, many, other}) { return other!; diff --git a/slang/test/unit/api/secret_test.dart b/slang/test/unit/api/secret_test.dart index b5483a71..69bf93c3 100644 --- a/slang/test/unit/api/secret_test.dart +++ b/slang/test/unit/api/secret_test.dart @@ -1,4 +1,4 @@ -import 'package:slang/api/secret.dart'; +import 'package:slang/src/api/secret.dart'; import 'package:slang/src/builder/utils/encryption_utils.dart'; import 'package:test/test.dart'; diff --git a/slang/test/unit/api/singleton_test.dart b/slang/test/unit/api/singleton_test.dart index cd1e97bb..e737cede 100644 --- a/slang/test/unit/api/singleton_test.dart +++ b/slang/test/unit/api/singleton_test.dart @@ -1,5 +1,5 @@ -import 'package:slang/api/locale.dart'; -import 'package:slang/api/singleton.dart'; +import 'package:slang/src/api/locale.dart'; +import 'package:slang/src/api/singleton.dart'; import 'package:test/test.dart'; class AppLocaleUtils diff --git a/slang/test/unit/api/translation_overrides_test.dart b/slang/test/unit/api/translation_overrides_test.dart index fac1f379..7190300d 100644 --- a/slang/test/unit/api/translation_overrides_test.dart +++ b/slang/test/unit/api/translation_overrides_test.dart @@ -1,38 +1,38 @@ -import 'package:slang/api/locale.dart'; -import 'package:slang/api/singleton.dart'; -import 'package:slang/api/translation_overrides.dart'; -import 'package:slang/builder/builder/build_model_config_builder.dart'; -import 'package:slang/builder/model/raw_config.dart'; +import 'package:slang/src/api/locale.dart'; +import 'package:slang/src/api/singleton.dart'; +import 'package:slang/src/api/translation_overrides.dart'; +import 'package:slang/src/builder/builder/build_model_config_builder.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; import 'package:test/test.dart'; void main() { group('string', () { - test('Should return a plain string', () { - final meta = _buildMetaWithOverrides({ + test('Should return a plain string', () async { + final meta = await _buildMetaWithOverrides({ 'aboutPage.title': 'About', }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', {}); expect(parsed, 'About'); }); - test('Should not escape new line', () { - final meta = _buildMetaWithOverrides({ + test('Should not escape new line', () async { + final meta = await _buildMetaWithOverrides({ 'aboutPage.title': 'About\nPage', }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', {}); expect(parsed, 'About\nPage'); }); - test('Should return a plain string without escaping', () { - final meta = _buildMetaWithOverrides({ + test('Should return a plain string without escaping', () async { + final meta = await _buildMetaWithOverrides({ 'aboutPage.title': 'About \' \$ {arg}', }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', {}); expect(parsed, 'About \' \$ {arg}'); }); - test('Should return an interpolated string', () { - final meta = _buildMetaWithOverrides({ + test('Should return an interpolated string', () async { + final meta = await _buildMetaWithOverrides({ 'aboutPage.title': r'About ${arg}', }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { @@ -41,8 +41,8 @@ void main() { expect(parsed, 'About Page'); }); - test('Should ignore type in interpolated string', () { - final meta = _buildMetaWithOverrides({ + test('Should ignore type in interpolated string', () async { + final meta = await _buildMetaWithOverrides({ 'aboutPage.title': r'About ${arg: int}', }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { @@ -51,8 +51,8 @@ void main() { expect(parsed, 'About Page'); }); - test('Should return an interpolated string with dollar only', () { - final meta = _buildMetaWithOverrides({ + test('Should return an interpolated string with dollar only', () async { + final meta = await _buildMetaWithOverrides({ 'aboutPage.title': r'About $arg', }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { @@ -63,17 +63,17 @@ void main() { }); } -TranslationMetadata _buildMetaWithOverrides( +Future> + _buildMetaWithOverrides( Map overrides, -) { +) async { final utils = _Utils(); - return utils - .buildWithOverridesFromMap( - locale: FakeAppLocale(languageCode: 'und'), - isFlatMap: false, - map: overrides, - ) - .$meta; + final translations = await utils.buildWithOverridesFromMap( + locale: FakeAppLocale(languageCode: 'und'), + isFlatMap: false, + map: overrides, + ); + return translations.$meta; } class _Utils extends BaseAppLocaleUtils { diff --git a/slang/test/unit/builder/build_config_builder_test.dart b/slang/test/unit/builder/build_config_builder_test.dart index f18da17c..24daa216 100644 --- a/slang/test/unit/builder/build_config_builder_test.dart +++ b/slang/test/unit/builder/build_config_builder_test.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/builder/raw_config_builder.dart'; +import 'package:slang/src/builder/builder/raw_config_builder.dart'; import 'package:test/test.dart'; void main() { @@ -14,9 +14,6 @@ void main() { fallback_strategy: base_locale contexts: GenderContext: - enum: - - male - - female default_parameter: gender render_enum: false '''); @@ -24,9 +21,7 @@ void main() { expect(result, isNotNull); expect(result!.contexts.length, 1); expect(result.contexts.first.enumName, 'GenderContext'); - expect(result.contexts.first.enumValues, ['male', 'female']); expect(result.contexts.first.defaultParameter, 'gender'); - expect(result.contexts.first.paths, []); }); }); @@ -48,37 +43,7 @@ void main() { expect(result.contexts.length, 1); expect(result.contexts.first.enumName, 'GenderContext'); - expect(result.contexts.first.enumValues, ['male', 'female', 'neutral']); expect(result.contexts.first.defaultParameter, 'context'); - expect(result.contexts.first.paths, []); - }); - - test('context gender with path', () { - final result = RawConfigBuilder.fromMap( - { - 'contexts': { - 'GenderContext': { - 'enum': [ - 'male', - 'female', - ], - 'paths': [ - 'myPath', - 'mySecondPath.subPath', - ], - }, - }, - }, - ); - - expect(result.contexts.length, 1); - expect(result.contexts.first.enumName, 'GenderContext'); - expect(result.contexts.first.enumValues, ['male', 'female']); - expect(result.contexts.first.defaultParameter, 'context'); - expect(result.contexts.first.paths, [ - 'myPath', - 'mySecondPath.subPath', - ]); }); test('Should remove trailing slash', () { diff --git a/slang/test/unit/builder/slang_file_collection_builder_test.dart b/slang/test/unit/builder/slang_file_collection_builder_test.dart index 29b8f2cc..aa4763c5 100644 --- a/slang/test/unit/builder/slang_file_collection_builder_test.dart +++ b/slang/test/unit/builder/slang_file_collection_builder_test.dart @@ -1,7 +1,7 @@ -import 'package:slang/builder/builder/slang_file_collection_builder.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/raw_config.dart'; -import 'package:slang/builder/model/slang_file_collection.dart'; +import 'package:slang/src/builder/builder/slang_file_collection_builder.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/slang_file_collection.dart'; import 'package:test/test.dart'; PlainTranslationFile _file(String path) { diff --git a/slang/test/unit/builder/text_parser_test.dart b/slang/test/unit/builder/text_parser_test.dart index 912f1418..f7a5da51 100644 --- a/slang/test/unit/builder/text_parser_test.dart +++ b/slang/test/unit/builder/text_parser_test.dart @@ -1,5 +1,5 @@ -import 'package:slang/builder/model/enums.dart'; import 'package:slang/src/builder/builder/text_parser.dart'; +import 'package:slang/src/builder/model/enums.dart'; import 'package:test/test.dart'; void main() { diff --git a/slang/test/unit/builder/translation_model_builder_test.dart b/slang/test/unit/builder/translation_model_builder_test.dart index 916556ee..1c88354c 100644 --- a/slang/test/unit/builder/translation_model_builder_test.dart +++ b/slang/test/unit/builder/translation_model_builder_test.dart @@ -1,10 +1,10 @@ -import 'package:slang/builder/builder/build_model_config_builder.dart'; -import 'package:slang/builder/builder/translation_model_builder.dart'; -import 'package:slang/builder/model/context_type.dart'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/interface.dart'; -import 'package:slang/builder/model/node.dart'; -import 'package:slang/builder/model/raw_config.dart'; +import 'package:slang/src/builder/builder/build_model_config_builder.dart'; +import 'package:slang/src/builder/builder/translation_model_builder.dart'; +import 'package:slang/src/builder/model/context_type.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/interface.dart'; +import 'package:slang/src/builder/model/node.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; import 'package:test/test.dart'; void main() { @@ -137,15 +137,13 @@ void main() { buildConfig: RawConfig.defaultConfig.copyWith(contexts: [ ContextType( enumName: 'GenderCon', - enumValues: ['male', 'female'], - paths: [], defaultParameter: 'gender', generateEnum: true, ), ]).toBuildModelConfig(), localeDebug: RawConfig.defaultBaseLocale, map: { - 'a': { + 'a(context=GenderCon)': { 'male': 'MALE', 'female': r'FEMALE $p1', }, @@ -213,8 +211,6 @@ void main() { buildConfig: RawConfig.defaultConfig.copyWith(contexts: [ ContextType( enumName: 'GenderCon', - enumValues: null, - paths: [], defaultParameter: 'gender', generateEnum: true, ), diff --git a/slang/test/unit/model/i18n_data_test.dart b/slang/test/unit/model/i18n_data_test.dart index ed7b758b..29e5de8b 100644 --- a/slang/test/unit/model/i18n_data_test.dart +++ b/slang/test/unit/model/i18n_data_test.dart @@ -1,6 +1,6 @@ -import 'package:slang/builder/model/i18n_data.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/node.dart'; +import 'package:slang/src/builder/model/i18n_data.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/node.dart'; import 'package:test/test.dart'; I18nData _i18n(String locale, [bool base = false]) { diff --git a/slang/test/unit/model/i18n_locale_test.dart b/slang/test/unit/model/i18n_locale_test.dart index 36637c51..5bad3adc 100644 --- a/slang/test/unit/model/i18n_locale_test.dart +++ b/slang/test/unit/model/i18n_locale_test.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:test/test.dart'; void main() { diff --git a/slang/test/unit/model/interface_test.dart b/slang/test/unit/model/interface_test.dart index 0c79b21a..a1db03dc 100644 --- a/slang/test/unit/model/interface_test.dart +++ b/slang/test/unit/model/interface_test.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/model/interface.dart'; +import 'package:slang/src/builder/model/interface.dart'; import 'package:test/test.dart'; Interface _i(Map attributes, [String name = '']) { diff --git a/slang/test/unit/model/node_test.dart b/slang/test/unit/model/node_test.dart index a8959e16..f1cd6855 100644 --- a/slang/test/unit/model/node_test.dart +++ b/slang/test/unit/model/node_test.dart @@ -1,5 +1,5 @@ -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/node.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/node.dart'; import 'package:test/test.dart'; import '../../util/text_node_builder.dart'; diff --git a/slang/test/unit/utils/path_utils_test.dart b/slang/test/unit/utils/path_utils_test.dart index d2bcde6e..74e59ba7 100644 --- a/slang/test/unit/utils/path_utils_test.dart +++ b/slang/test/unit/utils/path_utils_test.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/utils/path_utils.dart'; import 'package:test/test.dart'; diff --git a/slang/test/unit/utils/secret_test.dart b/slang/test/unit/utils/secret_test.dart index 065bd586..60792167 100644 --- a/slang/test/unit/utils/secret_test.dart +++ b/slang/test/unit/utils/secret_test.dart @@ -1,5 +1,5 @@ -import 'package:slang/builder/model/obfuscation_config.dart'; import 'package:slang/src/builder/generator/helper.dart'; +import 'package:slang/src/builder/model/obfuscation_config.dart'; import 'package:test/test.dart'; void main() { diff --git a/slang/test/unit/utils/string_extensions_test.dart b/slang/test/unit/utils/string_extensions_test.dart index b2ff784f..e72534c1 100644 --- a/slang/test/unit/utils/string_extensions_test.dart +++ b/slang/test/unit/utils/string_extensions_test.dart @@ -1,4 +1,4 @@ -import 'package:slang/builder/model/enums.dart'; +import 'package:slang/src/builder/model/enums.dart'; import 'package:slang/src/builder/utils/string_extensions.dart'; import 'package:test/test.dart'; diff --git a/slang/test/util/text_node_builder.dart b/slang/test/util/text_node_builder.dart index 93c62e11..c4b7e185 100644 --- a/slang/test/util/text_node_builder.dart +++ b/slang/test/util/text_node_builder.dart @@ -1,5 +1,5 @@ -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/node.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/node.dart'; StringTextNode textNode( String raw, diff --git a/slang_build_runner/lib/slang_build_runner.dart b/slang_build_runner/lib/slang_build_runner.dart index 4b99b80e..5cf962a6 100644 --- a/slang_build_runner/lib/slang_build_runner.dart +++ b/slang_build_runner/lib/slang_build_runner.dart @@ -2,16 +2,19 @@ import 'dart:async'; import 'package:build/build.dart'; import 'package:glob/glob.dart'; -import 'package:slang/builder/builder/raw_config_builder.dart'; -import 'package:slang/builder/builder/slang_file_collection_builder.dart'; -import 'package:slang/builder/builder/translation_map_builder.dart'; // ignore: implementation_imports -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/raw_config.dart'; -import 'package:slang/builder/model/slang_file_collection.dart'; +import 'package:slang/src/builder/builder/raw_config_builder.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/builder/slang_file_collection_builder.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/builder/translation_map_builder.dart'; // ignore: implementation_imports import 'package:slang/src/builder/generator_facade.dart'; // ignore: implementation_imports +import 'package:slang/src/builder/model/raw_config.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/model/slang_file_collection.dart'; +// ignore: implementation_imports import 'package:slang/src/builder/utils/file_utils.dart'; // ignore: implementation_imports import 'package:slang/src/builder/utils/map_utils.dart'; @@ -71,44 +74,27 @@ class I18nBuilder implements Builder { // STEP 3: generate .g.dart content final result = GeneratorFacade.generate( rawConfig: config, - baseName: config.outputFileName.getFileNameNoExtension(), translationMap: translationMap, inputDirectoryHint: fileCollection.determineInputPath(), ); // STEP 4: write output to hard drive FileUtils.createMissingFolders(filePath: outputFilePath); - if (config.outputFormat == OutputFormat.singleFile) { - // single file - FileUtils.writeFile( - path: outputFilePath, - content: result.joinAsSingleOutput(), - ); - } else { - // multiple files + + FileUtils.writeFile( + path: BuildResultPaths.mainPath(outputFilePath), + content: result.main, + ); + for (final entry in result.translations.entries) { + final locale = entry.key; + final localeTranslations = entry.value; FileUtils.writeFile( - path: BuildResultPaths.mainPath(outputFilePath), - content: result.header, + path: BuildResultPaths.localePath( + outputPath: outputFilePath, + locale: locale, + ), + content: localeTranslations, ); - for (final entry in result.translations.entries) { - final locale = entry.key; - final localeTranslations = entry.value; - FileUtils.writeFile( - path: BuildResultPaths.localePath( - outputPath: outputFilePath, - locale: locale, - ), - content: localeTranslations, - ); - } - if (result.flatMap != null) { - FileUtils.writeFile( - path: BuildResultPaths.flatMapPath( - outputPath: outputFilePath, - ), - content: result.flatMap!, - ); - } } } @@ -119,11 +105,6 @@ class I18nBuilder implements Builder { } extension on String { - /// converts /some/path/file.json to file - String getFileNameNoExtension() { - return PathUtils.getFileNameNoExtension(this); - } - /// converts /some/path/file.i18n.json to i18n.json String getFileExtension() { return PathUtils.getFileExtension(this); diff --git a/slang_build_runner/pubspec.yaml b/slang_build_runner/pubspec.yaml index 52d14f8f..9bbd118c 100644 --- a/slang_build_runner/pubspec.yaml +++ b/slang_build_runner/pubspec.yaml @@ -4,7 +4,7 @@ version: 3.30.0 repository: https://github.com/slang-i18n/slang environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=3.3.0 <4.0.0" dependencies: build: ^2.2.1 diff --git a/slang_flutter/lib/slang_flutter.dart b/slang_flutter/lib/slang_flutter.dart index d557eb02..b0cf025e 100644 --- a/slang_flutter/lib/slang_flutter.dart +++ b/slang_flutter/lib/slang_flutter.dart @@ -1,8 +1,9 @@ import 'package:flutter/widgets.dart'; -import 'package:slang/api/translation_overrides.dart'; -import 'package:slang/builder/model/node.dart'; -import 'package:slang/builder/model/pluralization.dart'; import 'package:slang/slang.dart'; +import 'package:slang/node.dart'; +import 'package:slang/overrides.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/model/pluralization.dart'; export 'package:slang/slang.dart'; @@ -61,9 +62,15 @@ extension ExtBaseLocaleSettings, T extends BaseTranslations> on BaseFlutterLocaleSettings { /// Uses locale of the device, fallbacks to base locale. /// Returns the locale which has been set. - E useDeviceLocale() { + Future useDeviceLocale() async { final E locale = utils.findDeviceLocale(); - return setLocale(locale, listenToDeviceLocale: true); + return await setLocale(locale, listenToDeviceLocale: true); + } + + /// Sync version of [useDeviceLocale]. + E useDeviceLocaleSync() { + final E locale = utils.findDeviceLocale(); + return setLocaleSync(locale, listenToDeviceLocale: true); } /// Gets supported locales (as Locale objects) with base locale sorted first. @@ -74,14 +81,12 @@ extension ExtBaseLocaleSettings, abstract class BaseTranslationProvider, T extends BaseTranslations> extends StatefulWidget { - final T initTranslations; final BaseFlutterLocaleSettings settings; BaseTranslationProvider({ required this.settings, required this.child, - }) : initTranslations = settings.currentTranslations, - super( + }) : super( key: _GlobalKeyHandler.instance.register( baseLocale: settings.utils.baseLocale, ), @@ -91,15 +96,13 @@ abstract class BaseTranslationProvider, @override _TranslationProviderState createState() => - _TranslationProviderState(initTranslations); + _TranslationProviderState(); } class _TranslationProviderState, T extends BaseTranslations> extends State> with WidgetsBindingObserver { - T translations; - - _TranslationProviderState(this.translations); + T? translations; @override void initState() { @@ -137,8 +140,9 @@ class _TranslationProviderState, /// Updates the provider state. /// Widgets listening to this provider will rebuild if [translations] differ. - void updateState(BaseAppLocale locale) { + void updateState(BaseAppLocale locale) async { final E localeTyped = widget.settings.utils.parseAppLocale(locale); + await widget.settings.loadLocale(localeTyped); setState(() { this.translations = widget.settings.translationMap[localeTyped]!; }); @@ -146,8 +150,9 @@ class _TranslationProviderState, @override Widget build(BuildContext context) { + translations ??= widget.settings.currentTranslations; return InheritedLocaleData( - translations: translations, + translations: translations!, child: widget.child, ); } diff --git a/slang_flutter/pubspec.yaml b/slang_flutter/pubspec.yaml index 8f87c115..2b431610 100644 --- a/slang_flutter/pubspec.yaml +++ b/slang_flutter/pubspec.yaml @@ -4,12 +4,14 @@ version: 3.30.0 repository: https://github.com/slang-i18n/slang environment: - flutter: '>=3.0.0' - sdk: ">=2.17.0 <4.0.0" + flutter: '>=3.19.0' + sdk: ">=3.3.0 <4.0.0" dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter # Use a tight version to ensure that all features are available slang: '>=3.30.0 <3.31.0' diff --git a/slang_flutter/test/integration/multi_package/gen/strings_a.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_a.g.dart index 449ede8e..6e6fad35 100644 --- a/slang_flutter/test/integration/multi_package/gen/strings_a.g.dart +++ b/slang_flutter/test/integration/multi_package/gen/strings_a.g.dart @@ -7,13 +7,16 @@ /// Strings: 2 (1 per locale) // coverage:ignore-file -// ignore_for_file: type=lint, unused_element +// ignore_for_file: type=lint import 'package:flutter/widgets.dart'; -import 'package:slang/builder/model/node.dart'; +import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; +import 'strings_a_de.g.dart' deferred as strings_a_de; +part 'strings_a_en.g.dart'; + const AppLocale _baseLocale = AppLocale.en; /// Supported locales, see extension methods below. @@ -22,15 +25,15 @@ const AppLocale _baseLocale = AppLocale.en; /// - LocaleSettings.setLocale(AppLocale.en) // set locale /// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum /// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check -enum AppLocale with BaseAppLocale { - en(languageCode: 'en', build: _StringsAEn.build), - de(languageCode: 'de', build: _StringsADe.build); +enum AppLocale with BaseAppLocale { + en(languageCode: 'en'), + de(languageCode: 'de'); - const AppLocale( - {required this.languageCode, - this.scriptCode, - this.countryCode, - required this.build}); + const AppLocale({ + required this.languageCode, + this.scriptCode, // ignore: unused_element + this.countryCode, // ignore: unused_element + }); @override final String languageCode; @@ -38,11 +41,55 @@ enum AppLocale with BaseAppLocale { final String? scriptCode; @override final String? countryCode; + @override - final TranslationBuilder build; + Future build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + switch (this) { + case AppLocale.en: + return StringsAEn.build( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + await strings_a_de.loadLibrary(); + return strings_a_de.StringsADe.build( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + @override + Translations buildSync({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) { + switch (this) { + case AppLocale.en: + return StringsAEn.build( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + return strings_a_de.StringsADe.build( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } /// Gets current instance managed by [LocaleSettings]. - _StringsAEn get translations => LocaleSettings.instance.translationMap[this]!; + Translations get translations => + LocaleSettings.instance.getTranslations(this); } /// Method A: Simple @@ -54,7 +101,7 @@ enum AppLocale with BaseAppLocale { /// Usage: /// String a = t.someKey.anotherKey; /// String b = t['someKey.anotherKey']; // Only for edge cases! -_StringsAEn get t => LocaleSettings.instance.currentTranslations; +Translations get t => LocaleSettings.instance.currentTranslations; /// Method B: Advanced /// @@ -71,21 +118,14 @@ _StringsAEn get t => LocaleSettings.instance.currentTranslations; /// final t = Translations.of(context); // Get t variable. /// String a = t.someKey.anotherKey; // Use t variable. /// String b = t['someKey.anotherKey']; // Only for edge cases! -class Translations { - Translations._(); // no constructor - - static _StringsAEn of(BuildContext context) => - InheritedLocaleData.of(context).translations; -} - -/// The provider for method B class TranslationProvider - extends BaseTranslationProvider { + extends BaseTranslationProvider { TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance); - static InheritedLocaleData of(BuildContext context) => - InheritedLocaleData.of(context); + static InheritedLocaleData of( + BuildContext context) => + InheritedLocaleData.of(context); } /// Method B shorthand via [BuildContext] extension method. @@ -94,11 +134,12 @@ class TranslationProvider /// Usage (e.g. in a widget's build method): /// context.t.someKey.anotherKey extension BuildContextTranslationsExtension on BuildContext { - _StringsAEn get t => TranslationProvider.of(this).translations; + Translations get t => TranslationProvider.of(this).translations; } /// Manages all translation instances and the current locale -class LocaleSettings extends BaseFlutterLocaleSettings { +class LocaleSettings + extends BaseFlutterLocaleSettings { LocaleSettings._() : super(utils: AppLocaleUtils.instance); static final instance = LocaleSettings._(); @@ -106,19 +147,15 @@ class LocaleSettings extends BaseFlutterLocaleSettings { // static aliases (checkout base methods for documentation) static AppLocale get currentLocale => instance.currentLocale; static Stream getLocaleStream() => instance.getLocaleStream(); - static AppLocale setLocale(AppLocale locale, + static Future setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale setLocaleRaw(String rawLocale, + static Future setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale useDeviceLocale() => instance.useDeviceLocale(); - @Deprecated('Use [AppLocaleUtils.supportedLocales]') - static List get supportedLocales => instance.supportedLocales; - @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') - static List get supportedLocalesRaw => instance.supportedLocalesRaw; - static void setPluralResolver( + static Future useDeviceLocale() => instance.useDeviceLocale(); + static Future setPluralResolver( {String? language, AppLocale? locale, PluralResolver? cardinalResolver, @@ -129,10 +166,32 @@ class LocaleSettings extends BaseFlutterLocaleSettings { cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, ); + + // synchronous versions + static AppLocale setLocaleSync(AppLocale locale, + {bool? listenToDeviceLocale = false}) => + instance.setLocaleSync(locale, + listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRawSync(String rawLocale, + {bool? listenToDeviceLocale = false}) => + instance.setLocaleRawSync(rawLocale, + listenToDeviceLocale: listenToDeviceLocale); + static AppLocale useDeviceLocaleSync() => instance.useDeviceLocaleSync(); + static void setPluralResolverSync( + {String? language, + AppLocale? locale, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver}) => + instance.setPluralResolverSync( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); } /// Provides utility functions without any side effects. -class AppLocaleUtils extends BaseAppLocaleUtils { +class AppLocaleUtils extends BaseAppLocaleUtils { AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values); @@ -152,97 +211,3 @@ class AppLocaleUtils extends BaseAppLocaleUtils { static List get supportedLocales => instance.supportedLocales; static List get supportedLocalesRaw => instance.supportedLocalesRaw; } - -// translations - -// Path: -class _StringsAEn implements BaseTranslations { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - _StringsAEn.build( - {Map? overrides, - PluralResolver? cardinalResolver, - PluralResolver? ordinalResolver}) - : assert(overrides == null, - 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.en, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override - final TranslationMetadata $meta; - - /// Access flat map - dynamic operator [](String key) => $meta.getTranslation(key); - - late final _StringsAEn _root = this; // ignore: unused_field - - // Translations - String get title => 'Package A (en)'; -} - -// Path: -class _StringsADe implements _StringsAEn { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - _StringsADe.build( - {Map? overrides, - PluralResolver? cardinalResolver, - PluralResolver? ordinalResolver}) - : assert(overrides == null, - 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.de, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override - final TranslationMetadata $meta; - - /// Access flat map - @override - dynamic operator [](String key) => $meta.getTranslation(key); - - @override - late final _StringsADe _root = this; // ignore: unused_field - - // Translations - @override - String get title => 'Package A (de)'; -} - -/// Flat map(s) containing all translations. -/// Only for edge cases! For simple maps, use the map function of this library. - -extension on _StringsAEn { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'title': - return 'Package A (en)'; - default: - return null; - } - } -} - -extension on _StringsADe { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'title': - return 'Package A (de)'; - default: - return null; - } - } -} diff --git a/slang_flutter/test/integration/multi_package/gen/strings_a_de.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_a_de.g.dart new file mode 100644 index 00000000..3309a561 --- /dev/null +++ b/slang_flutter/test/integration/multi_package/gen/strings_a_de.g.dart @@ -0,0 +1,55 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint + +import 'strings_a.g.dart'; +import 'package:slang/node.dart'; + +// Path: +class StringsADe implements Translations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + StringsADe.build( + {Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver}) + : assert(overrides == null, + 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.de, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override + final TranslationMetadata $meta; + + /// Access flat map + @override + dynamic operator [](String key) => $meta.getTranslation(key); + + late final StringsADe _root = this; // ignore: unused_field + + // Translations + @override + String get title => 'Package A (de)'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on StringsADe { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'title': + return 'Package A (de)'; + default: + return null; + } + } +} diff --git a/slang_flutter/test/integration/multi_package/gen/strings_a_en.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_a_en.g.dart new file mode 100644 index 00000000..7150a85b --- /dev/null +++ b/slang_flutter/test/integration/multi_package/gen/strings_a_en.g.dart @@ -0,0 +1,61 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint + +part of 'strings_a.g.dart'; + +// Path: +typedef StringsAEn = Translations; // ignore: unused_element + +class Translations implements BaseTranslations { + /// Returns the current translations of the given [context]. + /// + /// Usage: + /// final t = Translations.of(context); + static Translations of(BuildContext context) => + InheritedLocaleData.of(context).translations; + + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + Translations.build( + {Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver}) + : assert(overrides == null, + 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.en, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override + final TranslationMetadata $meta; + + /// Access flat map + dynamic operator [](String key) => $meta.getTranslation(key); + + late final Translations _root = this; // ignore: unused_field + + // Translations + String get title => 'Package A (en)'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on Translations { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'title': + return 'Package A (en)'; + default: + return null; + } + } +} diff --git a/slang_flutter/test/integration/multi_package/gen/strings_b.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_b.g.dart index 778f5298..539b2ac0 100644 --- a/slang_flutter/test/integration/multi_package/gen/strings_b.g.dart +++ b/slang_flutter/test/integration/multi_package/gen/strings_b.g.dart @@ -7,13 +7,16 @@ /// Strings: 2 (1 per locale) // coverage:ignore-file -// ignore_for_file: type=lint, unused_element +// ignore_for_file: type=lint import 'package:flutter/widgets.dart'; -import 'package:slang/builder/model/node.dart'; +import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; +import 'strings_b_de.g.dart' deferred as strings_b_de; +part 'strings_b_en.g.dart'; + const AppLocale _baseLocale = AppLocale.en; /// Supported locales, see extension methods below. @@ -22,15 +25,15 @@ const AppLocale _baseLocale = AppLocale.en; /// - LocaleSettings.setLocale(AppLocale.en) // set locale /// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum /// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check -enum AppLocale with BaseAppLocale { - en(languageCode: 'en', build: _StringsBEn.build), - de(languageCode: 'de', build: _StringsBDe.build); +enum AppLocale with BaseAppLocale { + en(languageCode: 'en'), + de(languageCode: 'de'); - const AppLocale( - {required this.languageCode, - this.scriptCode, - this.countryCode, - required this.build}); + const AppLocale({ + required this.languageCode, + this.scriptCode, // ignore: unused_element + this.countryCode, // ignore: unused_element + }); @override final String languageCode; @@ -38,11 +41,55 @@ enum AppLocale with BaseAppLocale { final String? scriptCode; @override final String? countryCode; + @override - final TranslationBuilder build; + Future build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + switch (this) { + case AppLocale.en: + return StringsBEn.build( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + await strings_b_de.loadLibrary(); + return strings_b_de.StringsBDe.build( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + @override + Translations buildSync({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) { + switch (this) { + case AppLocale.en: + return StringsBEn.build( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.de: + return strings_b_de.StringsBDe.build( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } /// Gets current instance managed by [LocaleSettings]. - _StringsBEn get translations => LocaleSettings.instance.translationMap[this]!; + Translations get translations => + LocaleSettings.instance.getTranslations(this); } /// Method A: Simple @@ -54,7 +101,7 @@ enum AppLocale with BaseAppLocale { /// Usage: /// String a = t.someKey.anotherKey; /// String b = t['someKey.anotherKey']; // Only for edge cases! -_StringsBEn get t => LocaleSettings.instance.currentTranslations; +Translations get t => LocaleSettings.instance.currentTranslations; /// Method B: Advanced /// @@ -71,21 +118,14 @@ _StringsBEn get t => LocaleSettings.instance.currentTranslations; /// final t = Translations.of(context); // Get t variable. /// String a = t.someKey.anotherKey; // Use t variable. /// String b = t['someKey.anotherKey']; // Only for edge cases! -class Translations { - Translations._(); // no constructor - - static _StringsBEn of(BuildContext context) => - InheritedLocaleData.of(context).translations; -} - -/// The provider for method B class TranslationProvider - extends BaseTranslationProvider { + extends BaseTranslationProvider { TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance); - static InheritedLocaleData of(BuildContext context) => - InheritedLocaleData.of(context); + static InheritedLocaleData of( + BuildContext context) => + InheritedLocaleData.of(context); } /// Method B shorthand via [BuildContext] extension method. @@ -94,11 +134,12 @@ class TranslationProvider /// Usage (e.g. in a widget's build method): /// context.t.someKey.anotherKey extension BuildContextTranslationsExtension on BuildContext { - _StringsBEn get t => TranslationProvider.of(this).translations; + Translations get t => TranslationProvider.of(this).translations; } /// Manages all translation instances and the current locale -class LocaleSettings extends BaseFlutterLocaleSettings { +class LocaleSettings + extends BaseFlutterLocaleSettings { LocaleSettings._() : super(utils: AppLocaleUtils.instance); static final instance = LocaleSettings._(); @@ -106,19 +147,15 @@ class LocaleSettings extends BaseFlutterLocaleSettings { // static aliases (checkout base methods for documentation) static AppLocale get currentLocale => instance.currentLocale; static Stream getLocaleStream() => instance.getLocaleStream(); - static AppLocale setLocale(AppLocale locale, + static Future setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale setLocaleRaw(String rawLocale, + static Future setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale useDeviceLocale() => instance.useDeviceLocale(); - @Deprecated('Use [AppLocaleUtils.supportedLocales]') - static List get supportedLocales => instance.supportedLocales; - @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') - static List get supportedLocalesRaw => instance.supportedLocalesRaw; - static void setPluralResolver( + static Future useDeviceLocale() => instance.useDeviceLocale(); + static Future setPluralResolver( {String? language, AppLocale? locale, PluralResolver? cardinalResolver, @@ -129,10 +166,32 @@ class LocaleSettings extends BaseFlutterLocaleSettings { cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, ); + + // synchronous versions + static AppLocale setLocaleSync(AppLocale locale, + {bool? listenToDeviceLocale = false}) => + instance.setLocaleSync(locale, + listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRawSync(String rawLocale, + {bool? listenToDeviceLocale = false}) => + instance.setLocaleRawSync(rawLocale, + listenToDeviceLocale: listenToDeviceLocale); + static AppLocale useDeviceLocaleSync() => instance.useDeviceLocaleSync(); + static void setPluralResolverSync( + {String? language, + AppLocale? locale, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver}) => + instance.setPluralResolverSync( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); } /// Provides utility functions without any side effects. -class AppLocaleUtils extends BaseAppLocaleUtils { +class AppLocaleUtils extends BaseAppLocaleUtils { AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values); @@ -152,97 +211,3 @@ class AppLocaleUtils extends BaseAppLocaleUtils { static List get supportedLocales => instance.supportedLocales; static List get supportedLocalesRaw => instance.supportedLocalesRaw; } - -// translations - -// Path: -class _StringsBEn implements BaseTranslations { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - _StringsBEn.build( - {Map? overrides, - PluralResolver? cardinalResolver, - PluralResolver? ordinalResolver}) - : assert(overrides == null, - 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.en, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override - final TranslationMetadata $meta; - - /// Access flat map - dynamic operator [](String key) => $meta.getTranslation(key); - - late final _StringsBEn _root = this; // ignore: unused_field - - // Translations - String get title => 'Package B (en)'; -} - -// Path: -class _StringsBDe implements _StringsBEn { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - _StringsBDe.build( - {Map? overrides, - PluralResolver? cardinalResolver, - PluralResolver? ordinalResolver}) - : assert(overrides == null, - 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.de, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override - final TranslationMetadata $meta; - - /// Access flat map - @override - dynamic operator [](String key) => $meta.getTranslation(key); - - @override - late final _StringsBDe _root = this; // ignore: unused_field - - // Translations - @override - String get title => 'Package B (de)'; -} - -/// Flat map(s) containing all translations. -/// Only for edge cases! For simple maps, use the map function of this library. - -extension on _StringsBEn { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'title': - return 'Package B (en)'; - default: - return null; - } - } -} - -extension on _StringsBDe { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'title': - return 'Package B (de)'; - default: - return null; - } - } -} diff --git a/slang_flutter/test/integration/multi_package/gen/strings_b_de.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_b_de.g.dart new file mode 100644 index 00000000..04a80096 --- /dev/null +++ b/slang_flutter/test/integration/multi_package/gen/strings_b_de.g.dart @@ -0,0 +1,55 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint + +import 'strings_b.g.dart'; +import 'package:slang/node.dart'; + +// Path: +class StringsBDe implements Translations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + StringsBDe.build( + {Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver}) + : assert(overrides == null, + 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.de, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override + final TranslationMetadata $meta; + + /// Access flat map + @override + dynamic operator [](String key) => $meta.getTranslation(key); + + late final StringsBDe _root = this; // ignore: unused_field + + // Translations + @override + String get title => 'Package B (de)'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on StringsBDe { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'title': + return 'Package B (de)'; + default: + return null; + } + } +} diff --git a/slang_flutter/test/integration/multi_package/gen/strings_b_en.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_b_en.g.dart new file mode 100644 index 00000000..0926b7af --- /dev/null +++ b/slang_flutter/test/integration/multi_package/gen/strings_b_en.g.dart @@ -0,0 +1,61 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint + +part of 'strings_b.g.dart'; + +// Path: +typedef StringsBEn = Translations; // ignore: unused_element + +class Translations implements BaseTranslations { + /// Returns the current translations of the given [context]. + /// + /// Usage: + /// final t = Translations.of(context); + static Translations of(BuildContext context) => + InheritedLocaleData.of(context).translations; + + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + Translations.build( + {Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver}) + : assert(overrides == null, + 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.en, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override + final TranslationMetadata $meta; + + /// Access flat map + dynamic operator [](String key) => $meta.getTranslation(key); + + late final Translations _root = this; // ignore: unused_field + + // Translations + String get title => 'Package B (en)'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on Translations { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'title': + return 'Package B (en)'; + default: + return null; + } + } +} diff --git a/slang_flutter/test/integration/multi_package/pubspec_overrides.yaml b/slang_flutter/test/integration/multi_package/pubspec_overrides.yaml new file mode 100644 index 00000000..063bc9d4 --- /dev/null +++ b/slang_flutter/test/integration/multi_package/pubspec_overrides.yaml @@ -0,0 +1,5 @@ +dependency_overrides: + slang: + path: ../../../../slang + slang_flutter: + path: ../../.. diff --git a/slang_gpt/lib/model/gpt_config.dart b/slang_gpt/lib/model/gpt_config.dart index 2b6755f0..55bd90bc 100644 --- a/slang_gpt/lib/model/gpt_config.dart +++ b/slang_gpt/lib/model/gpt_config.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang_gpt/model/gpt_model.dart'; /// Represents the gpt node in build.yaml diff --git a/slang_gpt/lib/prompt/prompt.dart b/slang_gpt/lib/prompt/prompt.dart index 99222b83..86360ad6 100644 --- a/slang_gpt/lib/prompt/prompt.dart +++ b/slang_gpt/lib/prompt/prompt.dart @@ -1,8 +1,11 @@ import 'dart:convert'; -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/raw_config.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/model/enums.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/model/i18n_locale.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/model/raw_config.dart'; import 'package:slang_gpt/model/gpt_config.dart'; import 'package:slang_gpt/model/gpt_prompt.dart'; import 'package:slang_gpt/util/locales.dart'; diff --git a/slang_gpt/lib/runner.dart b/slang_gpt/lib/runner.dart index 3c651273..8bea8560 100644 --- a/slang_gpt/lib/runner.dart +++ b/slang_gpt/lib/runner.dart @@ -2,12 +2,15 @@ import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; -import 'package:slang/builder/builder/slang_file_collection_builder.dart'; -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/slang_file_collection.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/builder/slang_file_collection_builder.dart'; // ignore: implementation_imports import 'package:slang/src/builder/decoder/base_decoder.dart'; // ignore: implementation_imports +import 'package:slang/src/builder/model/i18n_locale.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/model/slang_file_collection.dart'; +// ignore: implementation_imports import 'package:slang/src/builder/utils/file_utils.dart'; // ignore: implementation_imports import 'package:slang/src/builder/utils/map_utils.dart'; diff --git a/slang_gpt/lib/util/locales.dart b/slang_gpt/lib/util/locales.dart index 11ee1ad4..573cede4 100644 --- a/slang_gpt/lib/util/locales.dart +++ b/slang_gpt/lib/util/locales.dart @@ -1,4 +1,5 @@ -import 'package:slang/builder/model/i18n_locale.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/model/i18n_locale.dart'; const _englishLocales = { 'af': 'Afrikaans', diff --git a/slang_gpt/lib/util/logger.dart b/slang_gpt/lib/util/logger.dart index e723293e..ed4a5389 100644 --- a/slang_gpt/lib/util/logger.dart +++ b/slang_gpt/lib/util/logger.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:io'; - -import 'package:slang/builder/model/i18n_locale.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/model/i18n_locale.dart'; // ignore: implementation_imports import 'package:slang/src/builder/utils/file_utils.dart'; // ignore: implementation_imports diff --git a/slang_gpt/test/unit/prompt/prompt_test.dart b/slang_gpt/test/unit/prompt/prompt_test.dart index 2cfac29a..3810ddbc 100644 --- a/slang_gpt/test/unit/prompt/prompt_test.dart +++ b/slang_gpt/test/unit/prompt/prompt_test.dart @@ -1,5 +1,7 @@ -import 'package:slang/builder/model/i18n_locale.dart'; -import 'package:slang/builder/model/raw_config.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/model/i18n_locale.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/model/raw_config.dart'; import 'package:slang_gpt/model/gpt_config.dart'; import 'package:slang_gpt/model/gpt_model.dart'; import 'package:slang_gpt/prompt/prompt.dart'; diff --git a/slang_gpt/test/unit/util/locales_test.dart b/slang_gpt/test/unit/util/locales_test.dart index cfb96cb2..5b5265ee 100644 --- a/slang_gpt/test/unit/util/locales_test.dart +++ b/slang_gpt/test/unit/util/locales_test.dart @@ -1,4 +1,5 @@ -import 'package:slang/builder/model/i18n_locale.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang_gpt/util/locales.dart'; import 'package:test/test.dart'; From daa8ac23a054405a67d30ed3063857000b260fad Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Fri, 18 Oct 2024 03:08:15 +0200 Subject: [PATCH 073/118] feat: add lazy config --- slang/README.md | 57 +++++---------- slang/example/lib/i18n/strings.g.dart | 11 +-- slang/lib/src/api/singleton.dart | 11 ++- .../builder/generate_config_builder.dart | 1 + .../builder/builder/raw_config_builder.dart | 1 + .../builder/generator/generate_header.dart | 70 +++++++++++-------- .../src/builder/model/generate_config.dart | 2 + slang/lib/src/builder/model/raw_config.dart | 7 ++ ..._expected_fallback_base_locale_main.output | 9 ++- ...d_fallback_base_locale_special_main.output | 9 ++- .../resources/main/_expected_main.output | 9 ++- .../main/_expected_no_flutter.output | 9 ++- .../main/_expected_no_locale_handling.output | 4 +- .../main/_expected_obfuscation_main.output | 9 ++- ...expected_translation_overrides_main.output | 9 ++- slang/test/unit/api/locale_settings_test.dart | 2 +- slang_flutter/lib/slang_flutter.dart | 1 + .../multi_package/gen/strings_a.g.dart | 31 ++++---- .../multi_package/gen/strings_a_de.g.dart | 13 ++-- .../multi_package/gen/strings_a_en.g.dart | 6 +- .../multi_package/gen/strings_b.g.dart | 31 ++++---- .../multi_package/gen/strings_b_de.g.dart | 13 ++-- .../multi_package/gen/strings_b_en.g.dart | 6 +- 23 files changed, 181 insertions(+), 140 deletions(-) diff --git a/slang/README.md b/slang/README.md index 6ef4161f..56c1bc0d 100644 --- a/slang/README.md +++ b/slang/README.md @@ -72,10 +72,10 @@ dart run slang migrate arb src.arb dest.json # migrate arb to json - [Dependency Injection](#-dependency-injection) - [Structuring Features](#structuring-features) - [Namespaces](#-namespaces) - - [Output Format](#-output-format) - [Compact CSV](#-compact-csv) - [Other Features](#other-features) - [Fallback](#-fallback) + - [Lazy Loading](#-lazy-loading) - [Comments](#-comments) - [Recasing](#-recasing) - [Obfuscation](#-obfuscation) @@ -288,7 +288,7 @@ input_directory: lib/i18n input_file_pattern: .i18n.json output_directory: lib/i18n output_file_name: translations.g.dart -output_format: single_file +lazy: true locale_handling: true flutter_integration: true namespaces: false @@ -317,11 +317,6 @@ pluralization: - someKey.place contexts: GenderContext: - enum: - - male - - female - paths: - - my.path.to.greet default_parameter: gender generate_enum: true interfaces: @@ -359,7 +354,7 @@ targets: input_file_pattern: .i18n.json output_directory: lib/i18n output_file_name: translations.g.dart - output_format: single_file + lazy: true locale_handling: true flutter_integration: true namespaces: false @@ -388,11 +383,6 @@ targets: - someKey.place contexts: GenderContext: - enum: - - male - - female - paths: - - my.path.to.greet default_parameter: gender generate_enum: true interfaces: @@ -421,7 +411,7 @@ targets: | `input_file_pattern` | `String` | input file pattern, must end with .json, .yaml, .csv, .arb | `.i18n.json` | | `output_directory` | `String` | path to output directory | `null` | | `output_file_name` | `String` | output file name | `null` | -| `output_format` | `single_file`, `multiple_files` | split output files [(i)](#-output-format) | `single_file` | +| `lazy` | `Boolean` | load translations lazily [(i)](#-lazy-loading) | `true` | | `locale_handling` | `Boolean` | generate locale handling logic [(i)](#-dependency-injection) | `true` | | `flutter_integration` | `Boolean` | generate flutter features [(i)](#-dart-only) | `true` | | `namespaces` | `Boolean` | split input files [(i)](#-namespaces) | `false` | @@ -1174,32 +1164,6 @@ String a = t.widgets.welcomeCard.title; String b = t.errorDialogs.login.wrongPassword; ``` -### ➤ Output Format - -By default, a single `.g.dart` file will be generated. - -You can split this file into multiple ones to improve readability and IDE performance. - -```yaml -# Config -output_file_name: translations.g.dart -output_format: multiple_files # set this -``` - -This will generate the following files: - -```text -lib/ - └── i18n/ - └── translations.g.dart <-- main file - └── translations_en.g.dart <-- translation classes - └── translations_de.g.dart <-- translation classes - └── ... - └── translations_map.g.dart <-- translations stored in flat maps -``` - -You only need to import the main file! - ### ➤ Compact CSV Normally, you would create a new csv file for each locale: @@ -1263,6 +1227,19 @@ fallback_strategy: base_locale # add this To also treat empty strings as missing translations, set `fallback_strategy: base_locale_empty_string`. +### ➤ Lazy Loading + +By default, translations for secondary locales are loaded lazily if [Deferred loading](https://dart.dev/language/libraries#lazily-loading-a-library) is supported (Web). + +This reduces the initial startup time. + +Disable this feature by setting `lazy: false`. In this case, all locales are available immediately. + +```yaml +# Config +lazy: false +``` + ### ➤ Comments You can add comments in your translation files. diff --git a/slang/example/lib/i18n/strings.g.dart b/slang/example/lib/i18n/strings.g.dart index afa1e789..fa491fc7 100644 --- a/slang/example/lib/i18n/strings.g.dart +++ b/slang/example/lib/i18n/strings.g.dart @@ -1,12 +1,12 @@ /// Generated file. Do not edit. /// -/// Original: lib/i18n +/// Source: lib/i18n /// To regenerate, run: `dart run slang` /// /// Locales: 3 /// Strings: 21 (7 per locale) /// -/// Built on 2024-10-18 at 00:05 UTC +/// Built on 2024-10-18 at 01:08 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import @@ -20,7 +20,7 @@ import 'strings_de.g.dart' deferred as _$de; import 'strings_fr_FR.g.dart' deferred as _$fr_FR; part 'strings_en.g.dart'; -/// Supported locales, see extension methods below. +/// Supported locales. /// /// Usage: /// - LocaleSettings.setLocale(AppLocale.en) // set locale @@ -146,7 +146,10 @@ extension BuildContextTranslationsExtension on BuildContext { /// Manages all translation instances and the current locale class LocaleSettings extends BaseFlutterLocaleSettings { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); + LocaleSettings._() : super( + utils: AppLocaleUtils.instance, + lazy: true, + ); static final instance = LocaleSettings._(); diff --git a/slang/lib/src/api/singleton.dart b/slang/lib/src/api/singleton.dart index beb4f23b..a8823b59 100644 --- a/slang/lib/src/api/singleton.dart +++ b/slang/lib/src/api/singleton.dart @@ -254,9 +254,14 @@ abstract class BaseLocaleSettings, BaseLocaleSettings({ required this.utils, - }) : translationMap = { - utils.baseLocale: utils.baseLocale.buildSync(), - }; + required bool lazy, + }) : translationMap = lazy + ? { + utils.baseLocale: utils.baseLocale.buildSync(), + } + : { + for (final locale in utils.locales) locale: locale.buildSync(), + }; /// Updates the provider state and therefore triggers a rebuild /// on all widgets listening to this provider. diff --git a/slang/lib/src/builder/builder/generate_config_builder.dart b/slang/lib/src/builder/builder/generate_config_builder.dart index bb526e23..071ca667 100644 --- a/slang/lib/src/builder/builder/generate_config_builder.dart +++ b/slang/lib/src/builder/builder/generate_config_builder.dart @@ -18,6 +18,7 @@ class GenerateConfigBuilder { baseLocale: config.baseLocale, fallbackStrategy: config.fallbackStrategy.toGenerateFallbackStrategy(), outputFileName: config.outputFileName, + lazy: config.lazy, localeHandling: config.localeHandling, flutterIntegration: config.flutterIntegration, translateVariable: config.translateVar, diff --git a/slang/lib/src/builder/builder/raw_config_builder.dart b/slang/lib/src/builder/builder/raw_config_builder.dart index 15dc63a5..f1623f54 100644 --- a/slang/lib/src/builder/builder/raw_config_builder.dart +++ b/slang/lib/src/builder/builder/raw_config_builder.dart @@ -69,6 +69,7 @@ class RawConfigBuilder { RawConfig.defaultOutputDirectory, outputFileName: map['output_file_name'] ?? RawConfig.defaultOutputFileName, + lazy: map['lazy'] ?? RawConfig.defaultLazy, localeHandling: map['locale_handling'] ?? RawConfig.defaultLocaleHandling, flutterIntegration: map['flutter_integration'] ?? RawConfig.defaultFlutterIntegration, diff --git a/slang/lib/src/builder/generator/generate_header.dart b/slang/lib/src/builder/generator/generate_header.dart index a878630b..35876acc 100644 --- a/slang/lib/src/builder/generator/generate_header.dart +++ b/slang/lib/src/builder/generator/generate_header.dart @@ -116,7 +116,7 @@ void _generateHeaderComment({ buffer.writeln(''' /// Generated file. Do not edit. /// -/// Original: ${config.inputDirectoryHint} +/// Source: ${config.inputDirectoryHint} /// To regenerate, run: `dart run slang`$statisticsStr$timestampStr // coverage:ignore-file @@ -159,8 +159,9 @@ void _generateLocaleImports({ final localeImportName = getImportName( locale: locale.locale, ); + final deferred = config.lazy ? ' deferred' : ''; buffer.writeln( - 'import \'${BuildResultPaths.localePath(outputPath: config.outputFileName, locale: locale.locale)}\' deferred as $localeImportName;'); + 'import \'${BuildResultPaths.localePath(outputPath: config.outputFileName, locale: locale.locale)}\'$deferred as $localeImportName;'); } buffer.writeln( 'part \'${BuildResultPaths.localePath(outputPath: config.outputFileName, locale: locales.first.locale)}\';'); @@ -213,7 +214,7 @@ void _generateEnum({ '$enumName.${config.baseLocale.enumConstant}'; buffer.writeln(); - buffer.writeln('/// Supported locales, see extension methods below.'); + buffer.writeln('/// Supported locales.'); buffer.writeln('///'); buffer.writeln('/// Usage:'); buffer.writeln( @@ -257,7 +258,8 @@ void _generateEnum({ buffer.writeln('\t@override final String? scriptCode;'); buffer.writeln('\t@override final String? countryCode;'); - void generateBuildMethod(StringBuffer buffer, bool sync) { + void generateBuildMethod(StringBuffer buffer, + {required bool sync, required bool proxySync}) { buffer.writeln(); buffer.writeln('\t@override'); buffer.writeln( @@ -266,33 +268,43 @@ void _generateEnum({ buffer.writeln('\t\tPluralResolver? cardinalResolver,'); buffer.writeln('\t\tPluralResolver? ordinalResolver,'); buffer.writeln('\t}) ${sync ? '' : 'async '}{'); - buffer.writeln('\t\tswitch (this) {'); - for (final locale in allLocales) { - final localeImportName = getImportName( - locale: locale.locale, - ); - final className = getClassNameRoot( - className: config.className, - locale: locale.locale, - ); - - buffer.writeln('\t\t\tcase $enumName.${locale.locale.enumConstant}:'); - if (!locale.base && !sync) { - buffer.writeln('\t\t\t\tawait $localeImportName.loadLibrary();'); + + if (proxySync) { + buffer.writeln('\t\treturn buildSync('); + buffer.writeln('\t\t\toverrides: overrides,'); + buffer.writeln('\t\t\tcardinalResolver: cardinalResolver,'); + buffer.writeln('\t\t\tordinalResolver: ordinalResolver,'); + buffer.writeln('\t\t);'); + } else { + buffer.writeln('\t\tswitch (this) {'); + for (final locale in allLocales) { + final localeImportName = getImportName( + locale: locale.locale, + ); + final className = getClassNameRoot( + className: config.className, + locale: locale.locale, + ); + + buffer.writeln('\t\t\tcase $enumName.${locale.locale.enumConstant}:'); + if (!locale.base && !sync) { + buffer.writeln('\t\t\t\tawait $localeImportName.loadLibrary();'); + } + buffer.writeln( + '\t\t\t\treturn ${locale.base ? '' : '$localeImportName.'}$className('); + buffer.writeln('\t\t\t\t\toverrides: overrides,'); + buffer.writeln('\t\t\t\t\tcardinalResolver: cardinalResolver,'); + buffer.writeln('\t\t\t\t\tordinalResolver: ordinalResolver,'); + buffer.writeln('\t\t\t\t);'); } - buffer.writeln( - '\t\t\t\treturn ${locale.base ? '' : '$localeImportName.'}$className('); - buffer.writeln('\t\t\t\t\toverrides: overrides,'); - buffer.writeln('\t\t\t\t\tcardinalResolver: cardinalResolver,'); - buffer.writeln('\t\t\t\t\tordinalResolver: ordinalResolver,'); - buffer.writeln('\t\t\t\t);'); + buffer.writeln('\t\t}'); } - buffer.writeln('\t\t}'); + buffer.writeln('\t}'); } - generateBuildMethod(buffer, false); - generateBuildMethod(buffer, true); + generateBuildMethod(buffer, sync: false, proxySync: !config.lazy); + generateBuildMethod(buffer, sync: true, proxySync: false); if (config.localeHandling) { buffer.writeln(); @@ -399,8 +411,10 @@ void _generateLocaleSettings({ .writeln('/// Manages all translation instances and the current locale'); buffer.writeln( 'class $settingsClass extends $baseClass<$enumName, ${config.className}> {'); - buffer - .writeln('\t$settingsClass._() : super(utils: AppLocaleUtils.instance);'); + buffer.writeln('\t$settingsClass._() : super('); + buffer.writeln('\t\tutils: AppLocaleUtils.instance,'); + buffer.writeln('\t\tlazy: ${config.lazy},'); + buffer.writeln('\t);'); buffer.writeln(); buffer.writeln('\tstatic final instance = $settingsClass._();'); diff --git a/slang/lib/src/builder/model/generate_config.dart b/slang/lib/src/builder/model/generate_config.dart index eeeed80f..97ff7977 100644 --- a/slang/lib/src/builder/model/generate_config.dart +++ b/slang/lib/src/builder/model/generate_config.dart @@ -13,6 +13,7 @@ class GenerateConfig { final I18nLocale baseLocale; // defaults to 'en' final GenerateFallbackStrategy fallbackStrategy; final String outputFileName; + final bool lazy; final bool localeHandling; final bool flutterIntegration; final String translateVariable; @@ -34,6 +35,7 @@ class GenerateConfig { required this.baseLocale, required this.fallbackStrategy, required this.outputFileName, + required this.lazy, required this.localeHandling, required this.flutterIntegration, required this.translateVariable, diff --git a/slang/lib/src/builder/model/raw_config.dart b/slang/lib/src/builder/model/raw_config.dart index df898e8b..d866ecdb 100644 --- a/slang/lib/src/builder/model/raw_config.dart +++ b/slang/lib/src/builder/model/raw_config.dart @@ -12,6 +12,7 @@ class RawConfig { static const String defaultInputFilePattern = '.i18n.json'; static const String? defaultOutputDirectory = null; static const String defaultOutputFileName = 'strings.g.dart'; + static const bool defaultLazy = true; static const bool defaultLocaleHandling = true; static const bool defaultFlutterIntegration = true; static const bool defaultNamespaces = false; @@ -47,6 +48,7 @@ class RawConfig { final String inputFilePattern; final String? outputDirectory; final String outputFileName; + final bool lazy; final bool localeHandling; final bool flutterIntegration; final bool namespaces; @@ -82,6 +84,7 @@ class RawConfig { required this.inputFilePattern, required this.outputDirectory, required this.outputFileName, + required this.lazy, required this.localeHandling, required this.flutterIntegration, required this.namespaces, @@ -118,6 +121,7 @@ class RawConfig { FallbackStrategy? fallbackStrategy, String? inputFilePattern, String? outputFileName, + bool? lazy, bool? localeHandling, bool? flutterIntegration, bool? namespaces, @@ -143,6 +147,7 @@ class RawConfig { inputFilePattern: inputFilePattern ?? this.inputFilePattern, outputDirectory: outputDirectory, outputFileName: outputFileName ?? this.outputFileName, + lazy: lazy ?? this.lazy, localeHandling: localeHandling ?? this.localeHandling, flutterIntegration: flutterIntegration ?? this.flutterIntegration, namespaces: namespaces ?? this.namespaces, @@ -201,6 +206,7 @@ class RawConfig { print( ' -> outputDirectory: ${outputDirectory ?? 'null (directory of input)'}'); print(' -> outputFileName: $outputFileName'); + print(' -> lazy: $lazy'); print(' -> localeHandling: $localeHandling'); print(' -> flutterIntegration: $flutterIntegration'); print(' -> namespaces: $namespaces'); @@ -256,6 +262,7 @@ class RawConfig { inputFilePattern: RawConfig.defaultInputFilePattern, outputDirectory: RawConfig.defaultOutputDirectory, outputFileName: RawConfig.defaultOutputFileName, + lazy: RawConfig.defaultLazy, localeHandling: RawConfig.defaultLocaleHandling, flutterIntegration: RawConfig.defaultFlutterIntegration, namespaces: RawConfig.defaultNamespaces, diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output index 549b8bdb..cf4e1c5e 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output @@ -1,6 +1,6 @@ /// Generated file. Do not edit. /// -/// Original: fake/path/integration +/// Source: fake/path/integration /// To regenerate, run: `dart run slang` /// /// Locales: 2 @@ -17,7 +17,7 @@ export 'package:slang_flutter/slang_flutter.dart'; import 'translations_de.g.dart' deferred as _$de; part 'translations_en.g.dart'; -/// Supported locales, see extension methods below. +/// Supported locales. /// /// Usage: /// - LocaleSettings.setLocale(AppLocale.en) // set locale @@ -129,7 +129,10 @@ extension BuildContextTranslationsExtension on BuildContext { /// Manages all translation instances and the current locale class LocaleSettings extends BaseFlutterLocaleSettings { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); + LocaleSettings._() : super( + utils: AppLocaleUtils.instance, + lazy: true, + ); static final instance = LocaleSettings._(); diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output index eaccf446..4b356912 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output @@ -1,6 +1,6 @@ /// Generated file. Do not edit. /// -/// Original: fake/path/integration +/// Source: fake/path/integration /// To regenerate, run: `dart run slang` /// /// Locales: 2 @@ -17,7 +17,7 @@ export 'package:slang_flutter/slang_flutter.dart'; import 'translations_de.g.dart' deferred as _$de; part 'translations_en.g.dart'; -/// Supported locales, see extension methods below. +/// Supported locales. /// /// Usage: /// - LocaleSettings.setLocale(AppLocale.en) // set locale @@ -129,7 +129,10 @@ extension BuildContextTranslationsExtension on BuildContext { /// Manages all translation instances and the current locale class LocaleSettings extends BaseFlutterLocaleSettings { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); + LocaleSettings._() : super( + utils: AppLocaleUtils.instance, + lazy: true, + ); static final instance = LocaleSettings._(); diff --git a/slang/test/integration/resources/main/_expected_main.output b/slang/test/integration/resources/main/_expected_main.output index 549b8bdb..cf4e1c5e 100644 --- a/slang/test/integration/resources/main/_expected_main.output +++ b/slang/test/integration/resources/main/_expected_main.output @@ -1,6 +1,6 @@ /// Generated file. Do not edit. /// -/// Original: fake/path/integration +/// Source: fake/path/integration /// To regenerate, run: `dart run slang` /// /// Locales: 2 @@ -17,7 +17,7 @@ export 'package:slang_flutter/slang_flutter.dart'; import 'translations_de.g.dart' deferred as _$de; part 'translations_en.g.dart'; -/// Supported locales, see extension methods below. +/// Supported locales. /// /// Usage: /// - LocaleSettings.setLocale(AppLocale.en) // set locale @@ -129,7 +129,10 @@ extension BuildContextTranslationsExtension on BuildContext { /// Manages all translation instances and the current locale class LocaleSettings extends BaseFlutterLocaleSettings { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); + LocaleSettings._() : super( + utils: AppLocaleUtils.instance, + lazy: true, + ); static final instance = LocaleSettings._(); diff --git a/slang/test/integration/resources/main/_expected_no_flutter.output b/slang/test/integration/resources/main/_expected_no_flutter.output index aa9c77ba..2e0523f8 100644 --- a/slang/test/integration/resources/main/_expected_no_flutter.output +++ b/slang/test/integration/resources/main/_expected_no_flutter.output @@ -1,6 +1,6 @@ /// Generated file. Do not edit. /// -/// Original: fake/path/integration +/// Source: fake/path/integration /// To regenerate, run: `dart run slang` /// /// Locales: 1 @@ -15,7 +15,7 @@ export 'package:slang/slang.dart'; part 'translations_en.g.dart'; -/// Supported locales, see extension methods below. +/// Supported locales. /// /// Usage: /// - LocaleSettings.setLocale(AppLocale.en) // set locale @@ -83,7 +83,10 @@ Translations get t => LocaleSettings.instance.currentTranslations; /// Manages all translation instances and the current locale class LocaleSettings extends BaseLocaleSettings { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); + LocaleSettings._() : super( + utils: AppLocaleUtils.instance, + lazy: true, + ); static final instance = LocaleSettings._(); diff --git a/slang/test/integration/resources/main/_expected_no_locale_handling.output b/slang/test/integration/resources/main/_expected_no_locale_handling.output index 81e828a2..676d99aa 100644 --- a/slang/test/integration/resources/main/_expected_no_locale_handling.output +++ b/slang/test/integration/resources/main/_expected_no_locale_handling.output @@ -1,6 +1,6 @@ /// Generated file. Do not edit. /// -/// Original: fake/path/integration +/// Source: fake/path/integration /// To regenerate, run: `dart run slang` /// /// Locales: 1 @@ -16,7 +16,7 @@ export 'package:slang_flutter/slang_flutter.dart'; part 'translations_en.g.dart'; -/// Supported locales, see extension methods below. +/// Supported locales. /// /// Usage: /// - LocaleSettings.setLocale(AppLocale.en) // set locale diff --git a/slang/test/integration/resources/main/_expected_obfuscation_main.output b/slang/test/integration/resources/main/_expected_obfuscation_main.output index caee762a..03c02257 100644 --- a/slang/test/integration/resources/main/_expected_obfuscation_main.output +++ b/slang/test/integration/resources/main/_expected_obfuscation_main.output @@ -1,6 +1,6 @@ /// Generated file. Do not edit. /// -/// Original: fake/path/integration +/// Source: fake/path/integration /// To regenerate, run: `dart run slang` /// /// Locales: 2 @@ -18,7 +18,7 @@ export 'package:slang_flutter/slang_flutter.dart'; import 'translations_de.g.dart' deferred as _$de; part 'translations_en.g.dart'; -/// Supported locales, see extension methods below. +/// Supported locales. /// /// Usage: /// - LocaleSettings.setLocale(AppLocale.en) // set locale @@ -130,7 +130,10 @@ extension BuildContextTranslationsExtension on BuildContext { /// Manages all translation instances and the current locale class LocaleSettings extends BaseFlutterLocaleSettings { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); + LocaleSettings._() : super( + utils: AppLocaleUtils.instance, + lazy: true, + ); static final instance = LocaleSettings._(); diff --git a/slang/test/integration/resources/main/_expected_translation_overrides_main.output b/slang/test/integration/resources/main/_expected_translation_overrides_main.output index 91f45d1f..93ca22c2 100644 --- a/slang/test/integration/resources/main/_expected_translation_overrides_main.output +++ b/slang/test/integration/resources/main/_expected_translation_overrides_main.output @@ -1,6 +1,6 @@ /// Generated file. Do not edit. /// -/// Original: fake/path/integration +/// Source: fake/path/integration /// To regenerate, run: `dart run slang` /// /// Locales: 2 @@ -36,7 +36,7 @@ final _buildConfig = BuildModelConfig( interfaces: [], // currently not supported ); -/// Supported locales, see extension methods below. +/// Supported locales. /// /// Usage: /// - LocaleSettings.setLocale(AppLocale.en) // set locale @@ -148,7 +148,10 @@ extension BuildContextTranslationsExtension on BuildContext { /// Manages all translation instances and the current locale class LocaleSettings extends BaseFlutterLocaleSettings { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); + LocaleSettings._() : super( + utils: AppLocaleUtils.instance, + lazy: true, + ); static final instance = LocaleSettings._(); diff --git a/slang/test/unit/api/locale_settings_test.dart b/slang/test/unit/api/locale_settings_test.dart index a12b4769..56464d55 100644 --- a/slang/test/unit/api/locale_settings_test.dart +++ b/slang/test/unit/api/locale_settings_test.dart @@ -76,5 +76,5 @@ class _AppLocaleUtils class _LocaleSettings extends BaseLocaleSettings { - _LocaleSettings() : super(utils: _AppLocaleUtils()); + _LocaleSettings() : super(utils: _AppLocaleUtils(), lazy: false); } diff --git a/slang_flutter/lib/slang_flutter.dart b/slang_flutter/lib/slang_flutter.dart index b0cf025e..534b6693 100644 --- a/slang_flutter/lib/slang_flutter.dart +++ b/slang_flutter/lib/slang_flutter.dart @@ -48,6 +48,7 @@ class BaseFlutterLocaleSettings, T extends BaseTranslations> extends BaseLocaleSettings { BaseFlutterLocaleSettings({ required super.utils, + required super.lazy, }); @override diff --git a/slang_flutter/test/integration/multi_package/gen/strings_a.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_a.g.dart index 6e6fad35..aefd44fe 100644 --- a/slang_flutter/test/integration/multi_package/gen/strings_a.g.dart +++ b/slang_flutter/test/integration/multi_package/gen/strings_a.g.dart @@ -1,25 +1,23 @@ /// Generated file. Do not edit. /// -/// Original: i18n +/// Source: i18n /// To regenerate, run: `dart run slang` /// /// Locales: 2 /// Strings: 2 (1 per locale) // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; -import 'strings_a_de.g.dart' deferred as strings_a_de; +import 'strings_a_de.g.dart' deferred as _$de; part 'strings_a_en.g.dart'; -const AppLocale _baseLocale = AppLocale.en; - -/// Supported locales, see extension methods below. +/// Supported locales. /// /// Usage: /// - LocaleSettings.setLocale(AppLocale.en) // set locale @@ -50,14 +48,14 @@ enum AppLocale with BaseAppLocale { }) async { switch (this) { case AppLocale.en: - return StringsAEn.build( + return TranslationsEn( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, ); case AppLocale.de: - await strings_a_de.loadLibrary(); - return strings_a_de.StringsADe.build( + await _$de.loadLibrary(); + return _$de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -73,13 +71,13 @@ enum AppLocale with BaseAppLocale { }) { switch (this) { case AppLocale.en: - return StringsAEn.build( + return TranslationsEn( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, ); case AppLocale.de: - return strings_a_de.StringsADe.build( + return _$de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -140,7 +138,11 @@ extension BuildContextTranslationsExtension on BuildContext { /// Manages all translation instances and the current locale class LocaleSettings extends BaseFlutterLocaleSettings { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); + LocaleSettings._() + : super( + utils: AppLocaleUtils.instance, + lazy: true, + ); static final instance = LocaleSettings._(); @@ -193,7 +195,10 @@ class LocaleSettings /// Provides utility functions without any side effects. class AppLocaleUtils extends BaseAppLocaleUtils { AppLocaleUtils._() - : super(baseLocale: _baseLocale, locales: AppLocale.values); + : super( + baseLocale: AppLocale.en, + locales: AppLocale.values, + ); static final instance = AppLocaleUtils._(); diff --git a/slang_flutter/test/integration/multi_package/gen/strings_a_de.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_a_de.g.dart index 3309a561..b264a3eb 100644 --- a/slang_flutter/test/integration/multi_package/gen/strings_a_de.g.dart +++ b/slang_flutter/test/integration/multi_package/gen/strings_a_de.g.dart @@ -2,16 +2,17 @@ /// Generated file. Do not edit. /// // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import -import 'strings_a.g.dart'; +import 'package:flutter/widgets.dart'; import 'package:slang/node.dart'; +import 'strings_a.g.dart'; // Path: -class StringsADe implements Translations { +class TranslationsDe implements Translations { /// You can call this constructor and build your own translation instance of this locale. /// Constructing via the enum [AppLocale.build] is preferred. - StringsADe.build( + TranslationsDe( {Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) @@ -34,7 +35,7 @@ class StringsADe implements Translations { @override dynamic operator [](String key) => $meta.getTranslation(key); - late final StringsADe _root = this; // ignore: unused_field + late final TranslationsDe _root = this; // ignore: unused_field // Translations @override @@ -43,7 +44,7 @@ class StringsADe implements Translations { /// Flat map(s) containing all translations. /// Only for edge cases! For simple maps, use the map function of this library. -extension on StringsADe { +extension on TranslationsDe { dynamic _flatMapFunction(String path) { switch (path) { case 'title': diff --git a/slang_flutter/test/integration/multi_package/gen/strings_a_en.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_a_en.g.dart index 7150a85b..943e43f6 100644 --- a/slang_flutter/test/integration/multi_package/gen/strings_a_en.g.dart +++ b/slang_flutter/test/integration/multi_package/gen/strings_a_en.g.dart @@ -2,12 +2,12 @@ /// Generated file. Do not edit. /// // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import part of 'strings_a.g.dart'; // Path: -typedef StringsAEn = Translations; // ignore: unused_element +typedef TranslationsEn = Translations; // ignore: unused_element class Translations implements BaseTranslations { /// Returns the current translations of the given [context]. @@ -19,7 +19,7 @@ class Translations implements BaseTranslations { /// You can call this constructor and build your own translation instance of this locale. /// Constructing via the enum [AppLocale.build] is preferred. - Translations.build( + Translations( {Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) diff --git a/slang_flutter/test/integration/multi_package/gen/strings_b.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_b.g.dart index 539b2ac0..42420e22 100644 --- a/slang_flutter/test/integration/multi_package/gen/strings_b.g.dart +++ b/slang_flutter/test/integration/multi_package/gen/strings_b.g.dart @@ -1,25 +1,23 @@ /// Generated file. Do not edit. /// -/// Original: i18n +/// Source: i18n /// To regenerate, run: `dart run slang` /// /// Locales: 2 /// Strings: 2 (1 per locale) // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; -import 'strings_b_de.g.dart' deferred as strings_b_de; +import 'strings_b_de.g.dart' deferred as _$de; part 'strings_b_en.g.dart'; -const AppLocale _baseLocale = AppLocale.en; - -/// Supported locales, see extension methods below. +/// Supported locales. /// /// Usage: /// - LocaleSettings.setLocale(AppLocale.en) // set locale @@ -50,14 +48,14 @@ enum AppLocale with BaseAppLocale { }) async { switch (this) { case AppLocale.en: - return StringsBEn.build( + return TranslationsEn( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, ); case AppLocale.de: - await strings_b_de.loadLibrary(); - return strings_b_de.StringsBDe.build( + await _$de.loadLibrary(); + return _$de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -73,13 +71,13 @@ enum AppLocale with BaseAppLocale { }) { switch (this) { case AppLocale.en: - return StringsBEn.build( + return TranslationsEn( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, ); case AppLocale.de: - return strings_b_de.StringsBDe.build( + return _$de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -140,7 +138,11 @@ extension BuildContextTranslationsExtension on BuildContext { /// Manages all translation instances and the current locale class LocaleSettings extends BaseFlutterLocaleSettings { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); + LocaleSettings._() + : super( + utils: AppLocaleUtils.instance, + lazy: true, + ); static final instance = LocaleSettings._(); @@ -193,7 +195,10 @@ class LocaleSettings /// Provides utility functions without any side effects. class AppLocaleUtils extends BaseAppLocaleUtils { AppLocaleUtils._() - : super(baseLocale: _baseLocale, locales: AppLocale.values); + : super( + baseLocale: AppLocale.en, + locales: AppLocale.values, + ); static final instance = AppLocaleUtils._(); diff --git a/slang_flutter/test/integration/multi_package/gen/strings_b_de.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_b_de.g.dart index 04a80096..96731cee 100644 --- a/slang_flutter/test/integration/multi_package/gen/strings_b_de.g.dart +++ b/slang_flutter/test/integration/multi_package/gen/strings_b_de.g.dart @@ -2,16 +2,17 @@ /// Generated file. Do not edit. /// // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import -import 'strings_b.g.dart'; +import 'package:flutter/widgets.dart'; import 'package:slang/node.dart'; +import 'strings_b.g.dart'; // Path: -class StringsBDe implements Translations { +class TranslationsDe implements Translations { /// You can call this constructor and build your own translation instance of this locale. /// Constructing via the enum [AppLocale.build] is preferred. - StringsBDe.build( + TranslationsDe( {Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) @@ -34,7 +35,7 @@ class StringsBDe implements Translations { @override dynamic operator [](String key) => $meta.getTranslation(key); - late final StringsBDe _root = this; // ignore: unused_field + late final TranslationsDe _root = this; // ignore: unused_field // Translations @override @@ -43,7 +44,7 @@ class StringsBDe implements Translations { /// Flat map(s) containing all translations. /// Only for edge cases! For simple maps, use the map function of this library. -extension on StringsBDe { +extension on TranslationsDe { dynamic _flatMapFunction(String path) { switch (path) { case 'title': diff --git a/slang_flutter/test/integration/multi_package/gen/strings_b_en.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_b_en.g.dart index 0926b7af..496a0506 100644 --- a/slang_flutter/test/integration/multi_package/gen/strings_b_en.g.dart +++ b/slang_flutter/test/integration/multi_package/gen/strings_b_en.g.dart @@ -2,12 +2,12 @@ /// Generated file. Do not edit. /// // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import part of 'strings_b.g.dart'; // Path: -typedef StringsBEn = Translations; // ignore: unused_element +typedef TranslationsEn = Translations; // ignore: unused_element class Translations implements BaseTranslations { /// Returns the current translations of the given [context]. @@ -19,7 +19,7 @@ class Translations implements BaseTranslations { /// You can call this constructor and build your own translation instance of this locale. /// Constructing via the enum [AppLocale.build] is preferred. - Translations.build( + Translations( {Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) From 0b55a710bff8a1e2ba97537c402e867a78432759 Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Fri, 18 Oct 2024 17:01:17 +0300 Subject: [PATCH 074/118] fix: update plural resolver map (#252) Signed-off-by: Emmanuel Ferdman --- slang/README.md | 2 +- slang/lib/src/api/singleton.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/slang/README.md b/slang/README.md index 56c1bc0d..3118bcad 100644 --- a/slang/README.md +++ b/slang/README.md @@ -714,7 +714,7 @@ Optionally, you can escape linked translations by surrounding the path with `{}` This library uses the concept defined [here](https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html). -Some languages have support out of the box. See [here](https://github.com/slang-i18n/slang/blob/main/slang/lib/api/plural_resolver_map.dart). +Some languages have support out of the box. See [here](https://github.com/slang-i18n/slang/blob/main/slang/lib/src/api/plural_resolver_map.dart). Plurals are detected by the following keywords: `zero`, `one`, `two`, `few`, `many`, `other`. diff --git a/slang/lib/src/api/singleton.dart b/slang/lib/src/api/singleton.dart index a8823b59..4cfee16a 100644 --- a/slang/lib/src/api/singleton.dart +++ b/slang/lib/src/api/singleton.dart @@ -413,7 +413,7 @@ extension LocaleSettingsExt, /// Sets plural resolvers. /// See https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html - /// See https://github.com/slang-i18n/slang/blob/main/slang/lib/api/plural_resolver_map.dart + /// See https://github.com/slang-i18n/slang/blob/main/slang/lib/src/api/plural_resolver_map.dart /// Either specify [language], or [locale]. [locale] has precedence. Future setPluralResolver({ String? language, From 8ada4d3bcedfbfd9feb4264bc4e52c1562420eef Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sat, 19 Oct 2024 12:26:18 +0200 Subject: [PATCH 075/118] feat: add underscoreTag --- slang/lib/src/api/locale.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/slang/lib/src/api/locale.dart b/slang/lib/src/api/locale.dart index 7553f1c6..ae4f42c2 100644 --- a/slang/lib/src/api/locale.dart +++ b/slang/lib/src/api/locale.dart @@ -86,10 +86,19 @@ abstract mixin class BaseAppLocale, static final BaseAppLocale undefinedLocale = FakeAppLocale(languageCode: 'und'); + /// Concatenates language, script and country code with dashes. + /// Resembles [Locale.toLanguageTag] of dart:ui. String get languageTag => [languageCode, scriptCode, countryCode] .where((element) => element != null) .join('-'); + /// For whatever reason, the intl package uses underscores instead of dashes + /// that contradicts https://www.unicode.org/reports/tr35/ + /// that is used by the Locale class in dart:ui. + String get underscoreTag => [languageCode, scriptCode, countryCode] + .where((element) => element != null) + .join('_'); + bool sameLocale(BaseAppLocale other) { return languageCode == other.languageCode && scriptCode == other.scriptCode && @@ -123,12 +132,7 @@ class FakeAppLocale extends BaseAppLocale { PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, }) async => - FakeTranslations( - FakeAppLocale( - languageCode: languageCode, - scriptCode: scriptCode, - countryCode: countryCode, - ), + buildSync( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, From 0ba2142dbf3bfab11d1c7e375c6c3d2e7c8b0189 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 20 Oct 2024 02:44:02 +0200 Subject: [PATCH 076/118] feat: l10n --- slang/README.md | 54 ++++++++ slang/lib/src/api/singleton.dart | 7 +- .../src/builder/builder/text/l10n_parser.dart | 131 ++++++++++++++++++ .../param_parser.dart} | 17 ++- .../builder/translation_model_builder.dart | 19 +-- .../translation_model_list_builder.dart | 4 +- .../builder/generator/generate_header.dart | 1 + .../generator/generate_translations.dart | 21 ++- slang/lib/src/builder/model/i18n_locale.dart | 1 + slang/lib/src/builder/model/node.dart | 96 +++++++++---- slang/pubspec.yaml | 1 + .../resources/main/_expected_de.output | 1 + .../_expected_fallback_base_locale_de.output | 17 +-- .../_expected_fallback_base_locale_en.output | 20 +-- ..._expected_fallback_base_locale_main.output | 1 + ...ted_fallback_base_locale_special_de.output | 3 +- ...d_fallback_base_locale_special_main.output | 1 + .../resources/main/_expected_main.output | 1 + .../main/_expected_no_flutter.output | 1 + .../main/_expected_no_locale_handling.output | 1 + .../main/_expected_obfuscation_de.output | 1 + .../main/_expected_obfuscation_main.output | 1 + .../_expected_translation_overrides_de.output | 1 + ...expected_translation_overrides_main.output | 1 + .../unit/builder/text/l10n_parser_test.dart | 66 +++++++++ .../param_parser_test.dart} | 2 +- .../translation_model_builder_test.dart | 29 ++-- slang/test/unit/model/node_test.dart | 10 ++ slang/test/util/text_node_builder.dart | 5 + 29 files changed, 434 insertions(+), 80 deletions(-) create mode 100644 slang/lib/src/builder/builder/text/l10n_parser.dart rename slang/lib/src/builder/builder/{text_parser.dart => text/param_parser.dart} (71%) create mode 100644 slang/test/unit/builder/text/l10n_parser_test.dart rename slang/test/unit/builder/{text_parser_test.dart => text/param_parser_test.dart} (96%) diff --git a/slang/README.md b/slang/README.md index 3118bcad..b6c97d0c 100644 --- a/slang/README.md +++ b/slang/README.md @@ -64,6 +64,7 @@ dart run slang migrate arb src.arb dest.json # migrate arb to json - [Pluralization](#-pluralization) - [Custom Contexts / Enums](#-custom-contexts--enums) - [Typed Parameters](#-typed-parameters) + - [L10n](#-l10n) - [Interfaces](#-interfaces) - [Modifiers](#-modifiers) - [Locale Enum](#-locale-enum) @@ -902,6 +903,59 @@ You can specify the type using the `name: type` syntax to increase type safety. } ``` +### ➤ L10n + +To properly display numbers and dates, +Slang extends the [Typed Parameters](#-typed-parameters) feature +to support additional types like `currency`, `decimalPattern`, or `jm`. + +Internally, it uses the `NumberFormat` and `DateFormat` from [intl](https://pub.dev/packages/intl). + +```json +{ + "greet": "Hello {name: String}, you have {amount: currency} in your account", + "today": "Today is {date: yMd}" +} +``` + +There are several built-in types: + +| Long | Short | Example 1 | Example 2 | +|--------------------------------------|-------------------------|-------------|-------------| +| `NumberFormat.compact` | `compact` | 1.2M | 1,2 M | +| `NumberFormat.compactCurrency` | `compactCurrency` | $1.2M | 1,2M € | +| `NumberFormat.compactSimpleCurrency` | `compactSimpleCurrency` | $1.2M | 1,2M € | +| `NumberFormat.compactLong` | `compactLong` | 1.2 million | 1,2 million | +| `NumberFormat.currency` | `currency` | $1.23 | 1,23 € | +| `NumberFormat.decimalPattern` | `decimalPattern` | 1,234.56 | 1.234,56 | +| `NumberFormat.decimalPatternDigits` | `decimalPatternDigits` | 1,234.56 | 1.234,56 | +| `NumberFormat.decimalPercentPattern` | `decimalPercentPattern` | 12.34% | 12,34% | +| `NumberFormat.percentPattern` | `percentPattern` | 12.34% | 12,34% | +| `NumberFormat.scientificPattern` | `scientificPattern` | 1.23E6 | 1,23E6 | +| `NumberFormat.simpleCurrency` | `simpleCurrency` | $1.23 | 1,23 € | +| `DateFormat.yM` | `yM` | 2023-12 | 12/2023 | +| `DateFormat.yMd` | `yMd` | 2023-12-31 | 12/31/2023 | +| `DateFormat.Hm` | `Hm` | 14:30 | 14:30 | +| `DateFormat.Hms` | `Hms` | 14:30:15 | 14:30:15 | +| `DateFormat.jm` | `jm` | 2:30 PM | 14:30 | +| `DateFormat.jms` | `jms` | 2:30:15 PM | 14:30:15 | + +You can also provide custom formats: + +```json +{ + "today": "Today is {date: DateFormat('yyyy-MM-dd')}" +} +``` + +Or adjust built-in formats: + +```json +{ + "today": "Today is {date: currency(symbol: 'EUR')}" +} +``` + ### ➤ Interfaces Often, multiple objects have the same attributes. You can create a common super class for that. diff --git a/slang/lib/src/api/singleton.dart b/slang/lib/src/api/singleton.dart index 4cfee16a..7835803b 100644 --- a/slang/lib/src/api/singleton.dart +++ b/slang/lib/src/api/singleton.dart @@ -6,6 +6,7 @@ import 'package:slang/src/builder/builder/translation_model_builder.dart'; import 'package:slang/src/builder/decoder/base_decoder.dart'; import 'package:slang/src/builder/model/build_model_config.dart'; import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/utils/map_utils.dart'; import 'package:slang/src/builder/utils/node_utils.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; @@ -227,7 +228,11 @@ extension AppLocaleUtilsExt, map: digestedMap, handleLinks: false, shouldEscapeText: false, - localeDebug: locale.languageTag, + locale: I18nLocale( + language: locale.languageCode, + script: locale.scriptCode, + country: locale.countryCode, + ), ); } } diff --git a/slang/lib/src/builder/builder/text/l10n_parser.dart b/slang/lib/src/builder/builder/text/l10n_parser.dart new file mode 100644 index 00000000..0ebc8371 --- /dev/null +++ b/slang/lib/src/builder/builder/text/l10n_parser.dart @@ -0,0 +1,131 @@ +import 'package:slang/src/builder/model/i18n_locale.dart'; + +class ParseL10nResult { + /// The actual parameter type. + /// Is [num] for [NumberFormat] and [DateTime] for [DateFormat]. + final String paramType; + + /// The format string that will be rendered as is. + final String format; + + ParseL10nResult({ + required this.paramType, + required this.format, + }); +} + +const _numberFormats = { + 'compact', + 'compactCurrency', + 'compactSimpleCurrency', + 'compactLong', + 'currency', + 'decimalPattern', + 'decimalPatternDigits', + 'decimalPercentPattern', + 'percentPattern', + 'scientificPattern', + 'simpleCurrency', +}; + +const _numberFormatsWithNamedParameters = { + 'NumberFormat.compactCurrency', + 'NumberFormat.compactSimpleCurrency', + 'NumberFormat.currency', + 'NumberFormat.decimalPatternDigits', + 'NumberFormat.decimalPercentPattern', + 'NumberFormat.simpleCurrency', +}; + +final _numberFormatsWithClass = { + for (final format in _numberFormats) 'NumberFormat.$format', + 'NumberFormat', +}; + +const _dateFormats = { + 'yM', + 'yMd', + 'Hm', + 'Hms', + 'jm', + 'jms', +}; + +final _dateFormatsWithClass = { + for (final format in _dateFormats) 'DateFormat.$format', + 'DateFormat', +}; + +// Parses "currency(symbol: '€')" +// -> paramType: num, format: NumberFormat.currency(symbol: '€', locale: locale).format(value) +ParseL10nResult? parseL10n({ + required I18nLocale locale, + required String paramName, + required String type, +}) { + final bracketStart = type.indexOf('('); + + // The type without parameters. + // E.g. currency(symbol: '€') -> currency + final digestedType = + bracketStart == -1 ? type : type.substring(0, bracketStart); + + final String paramType; + if (_numberFormats.contains(digestedType) || + _numberFormatsWithClass.contains(digestedType)) { + paramType = 'num'; + } else if (_dateFormats.contains(digestedType) || + _dateFormatsWithClass.contains(digestedType)) { + paramType = 'DateTime'; + } else { + return null; + } + + String methodName; + String arguments; + + if (bracketStart != -1 && type.endsWith(')')) { + methodName = type.substring(0, bracketStart); + arguments = type.substring(bracketStart + 1, type.length - 1); + } else { + methodName = type; + arguments = ''; + } + + // Prepend class if necessary + if (!type.startsWith('NumberFormat(') && + !type.startsWith('DateFormat(') && + !methodName.startsWith('NumberFormat.') && + !methodName.startsWith('DateFormat.')) { + if (paramType == 'num') { + methodName = 'NumberFormat.$methodName'; + } else if (paramType == 'DateTime') { + methodName = 'DateFormat.$methodName'; + } + } + + // Add locale + if (paramType == 'num' && + _numberFormatsWithNamedParameters.contains(methodName)) { + // add locale as named parameter + if (arguments.isEmpty) { + arguments = "locale: '${locale.underscoreTag}'"; + } else { + arguments = "$arguments, locale: '${locale.underscoreTag}'"; + } + } else { + // add locale as positional parameter + if (!arguments.contains('locale:')) { + if (arguments.isEmpty) { + arguments = "'${locale.underscoreTag}'"; + } else { + arguments = "$arguments, '${locale.underscoreTag}'"; + } + } + } + + return ParseL10nResult( + paramType: paramType, + format: '$methodName($arguments).format($paramName)', + ); +} diff --git a/slang/lib/src/builder/builder/text_parser.dart b/slang/lib/src/builder/builder/text/param_parser.dart similarity index 71% rename from slang/lib/src/builder/builder/text_parser.dart rename to slang/lib/src/builder/builder/text/param_parser.dart index 21b17448..f787d2bd 100644 --- a/slang/lib/src/builder/builder/text_parser.dart +++ b/slang/lib/src/builder/builder/text/param_parser.dart @@ -12,12 +12,16 @@ class ParseParamResult { 'ParseParamResult(paramName: $paramName, paramType: $paramType)'; } +/// Parses a parameter string. +/// E.g. `p: int` -> `ParseParamResult(paramName: 'p', paramType: 'int')` ParseParamResult parseParam({ required String rawParam, required String defaultType, required CaseStyle? caseStyle, }) { - if (rawParam.endsWith(')')) { + final colonIndex = rawParam.indexOf(':'); + final bracketIndex = rawParam.indexOf('('); + if (bracketIndex != -1 && (colonIndex == -1 || bracketIndex < colonIndex)) { // rich text parameter with default value // this will be parsed by parseParamWithArg return ParseParamResult( @@ -25,11 +29,14 @@ ParseParamResult parseParam({ '', ); } - final split = rawParam.split(':'); - if (split.length == 1) { - return ParseParamResult(split[0].toCase(caseStyle), defaultType); + + if (colonIndex == -1) { + return ParseParamResult(rawParam.toCase(caseStyle), defaultType); } - return ParseParamResult(split[0].trim().toCase(caseStyle), split[1].trim()); + return ParseParamResult( + rawParam.substring(0, colonIndex).trim().toCase(caseStyle), + rawParam.substring(colonIndex + 1).trim(), + ); } class ParamWithArg { diff --git a/slang/lib/src/builder/builder/translation_model_builder.dart b/slang/lib/src/builder/builder/translation_model_builder.dart index 155e397d..8ebf48f0 100644 --- a/slang/lib/src/builder/builder/translation_model_builder.dart +++ b/slang/lib/src/builder/builder/translation_model_builder.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:slang/src/builder/model/build_model_config.dart'; import 'package:slang/src/builder/model/context_type.dart'; import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/model/interface.dart'; import 'package:slang/src/builder/model/node.dart'; import 'package:slang/src/builder/model/pluralization.dart'; @@ -41,12 +42,12 @@ class TranslationModelBuilder { /// e.g. "Let's go" will be "Let's go" instead of "Let\'s go". /// Similar to [handleLinks], this is used for "Translation Overrides". static BuildModelResult build({ + required I18nLocale locale, required BuildModelConfig buildConfig, required Map map, BuildModelResult? baseData, bool handleLinks = true, bool shouldEscapeText = true, - required String localeDebug, }) { // flat map for leaves (TextNode, PluralNode, ContextNode) final Map leavesMap = {}; @@ -77,7 +78,7 @@ class TranslationModelBuilder { // Assumption: They are basic linked translations without parameters // Reason: Not all TextNodes are built, so final parameters are unknown final resultNodeTree = _parseMapNode( - localeDebug: localeDebug, + locale: locale, parentPath: '', parentRawPath: '', curr: map, @@ -112,7 +113,7 @@ class TranslationModelBuilder { final currLink = pathQueue.removeFirst(); final linkedNode = leavesMap[currLink]; if (linkedNode == null) { - throw '"$key" in <$localeDebug> is linked to "$currLink" but "$currLink" is undefined.'; + throw '"$key" in <${locale.languageTag}> is linked to "$currLink" but "$currLink" is undefined.'; } visitedLinks.add(currLink); @@ -220,7 +221,7 @@ class TranslationModelBuilder { /// Takes the [curr] map which is (a part of) the raw tree from json / yaml /// and returns the node model. Map _parseMapNode({ - required String localeDebug, + required I18nLocale locale, required String parentPath, required String parentRawPath, required Map curr, @@ -265,6 +266,7 @@ Map _parseMapNode({ path: currPath, rawPath: currRawPath, modifiers: modifiers, + locale: locale, raw: value.toString(), comment: comment, shouldEscape: shouldEscapeText, @@ -275,6 +277,7 @@ Map _parseMapNode({ path: currPath, rawPath: currRawPath, modifiers: modifiers, + locale: locale, raw: value.toString(), comment: comment, shouldEscape: shouldEscapeText, @@ -293,7 +296,7 @@ Map _parseMapNode({ for (int i = 0; i < value.length; i++) i.toString(): value[i], }; children = _parseMapNode( - localeDebug: localeDebug, + locale: locale, parentPath: currPath, parentRawPath: currRawPath, curr: listAsMap, @@ -319,7 +322,7 @@ Map _parseMapNode({ } else { // key: { ...value } children = _parseMapNode( - localeDebug: localeDebug, + locale: locale, parentPath: currPath, parentRawPath: currRawPath, curr: value, @@ -347,7 +350,7 @@ Map _parseMapNode({ if (children.isEmpty) { switch (config.fallbackStrategy) { case FallbackStrategy.none: - throw '"$currPath" in <$localeDebug> is empty but it is marked for pluralization / context. Define "fallback_strategy: base_locale" to ignore this node.'; + throw '"$currPath" in <${locale.languageTag}> is empty but it is marked for pluralization / context. Define "fallback_strategy: base_locale" to ignore this node.'; case FallbackStrategy.baseLocale: case FallbackStrategy.baseLocaleEmptyString: return; @@ -375,7 +378,7 @@ Map _parseMapNode({ if (rich) { // rebuild children as RichText digestedMap = _parseMapNode( - localeDebug: localeDebug, + locale: locale, parentPath: currPath, parentRawPath: currRawPath, curr: { diff --git a/slang/lib/src/builder/builder/translation_model_list_builder.dart b/slang/lib/src/builder/builder/translation_model_list_builder.dart index e9197c62..899d8c0e 100644 --- a/slang/lib/src/builder/builder/translation_model_list_builder.dart +++ b/slang/lib/src/builder/builder/translation_model_list_builder.dart @@ -26,7 +26,7 @@ class TranslationModelListBuilder { final baseResult = TranslationModelBuilder.build( buildConfig: buildConfig, map: rawConfig.namespaces ? namespaces : namespaces.values.first, - localeDebug: baseEntry.key.languageTag, + locale: baseEntry.key, ); return translationMap.getInternalMap().entries.map((localeEntry) { @@ -48,7 +48,7 @@ class TranslationModelListBuilder { buildConfig: buildConfig, map: rawConfig.namespaces ? namespaces : namespaces.values.first, baseData: baseResult, - localeDebug: locale.languageTag, + locale: locale, ); return I18nData( diff --git a/slang/lib/src/builder/generator/generate_header.dart b/slang/lib/src/builder/generator/generate_header.dart index 35876acc..9454ee7d 100644 --- a/slang/lib/src/builder/generator/generate_header.dart +++ b/slang/lib/src/builder/generator/generate_header.dart @@ -127,6 +127,7 @@ void _generateImports(GenerateConfig config, StringBuffer buffer) { buffer.writeln(); final imports = [ ...config.imports, + 'package:intl/intl.dart', 'package:slang/node.dart', if (config.obfuscation.enabled) 'package:slang/secret.dart', if (config.translationOverrides) 'package:slang/overrides.dart', diff --git a/slang/lib/src/builder/generator/generate_translations.dart b/slang/lib/src/builder/generator/generate_translations.dart index d4e506ba..8c4e5eed 100644 --- a/slang/lib/src/builder/generator/generate_translations.dart +++ b/slang/lib/src/builder/generator/generate_translations.dart @@ -40,6 +40,7 @@ String generateTranslations(GenerateConfig config, I18nData localeData) { final imports = [ config.outputFileName, ...config.imports, + 'package:intl/intl.dart', 'package:slang/node.dart', if (config.obfuscation.enabled) 'package:slang/secret.dart', if (config.translationOverrides) 'package:slang/overrides.dart', @@ -220,7 +221,7 @@ void _generateClass( if (callSuperConstructor) { buffer.write( - ',\n\t\t super.build(cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver)'); + ',\n\t\t super(cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver)'); } if (config.renderFlatMap) { @@ -263,9 +264,13 @@ void _generateClass( } else { if (callSuperConstructor) { buffer.writeln( - '\t$finalClassName._($rootClassName root) : this._root = root, super._(root);'); + '\t$finalClassName._($rootClassName root) : this._root = root, super.internal(root);'); } else { - buffer.writeln('\t$finalClassName._(this._root);'); + if (config.fallbackStrategy == GenerateFallbackStrategy.baseLocale) { + buffer.writeln('\t$finalClassName.internal(this._root);'); + } else { + buffer.writeln('\t$finalClassName._(this._root);'); + } } } @@ -389,8 +394,14 @@ void _generateClass( childName: key, locale: localeData.locale, ); - buffer.writeln( - 'late final $childClassWithLocale$optional $key = $childClassWithLocale._(_root);'); + + buffer.write('late final $childClassWithLocale$optional $key = '); + if (localeData.base && + config.fallbackStrategy == GenerateFallbackStrategy.baseLocale) { + buffer.writeln('$childClassWithLocale.internal(_root);'); + } else { + buffer.writeln('$childClassWithLocale._(_root);'); + } } } else if (value is PluralNode) { final returnType = value.rich ? 'TextSpan' : 'String'; diff --git a/slang/lib/src/builder/model/i18n_locale.dart b/slang/lib/src/builder/model/i18n_locale.dart index 3ffcf088..b71d88e2 100644 --- a/slang/lib/src/builder/model/i18n_locale.dart +++ b/slang/lib/src/builder/model/i18n_locale.dart @@ -10,6 +10,7 @@ class I18nLocale { final String? script; final String? country; late String languageTag = _toLanguageTag(); + late String underscoreTag = languageTag.replaceAll('-', '_'); late String enumConstant = _toEnumConstant(); I18nLocale({required this.language, this.script, this.country}); diff --git a/slang/lib/src/builder/model/node.dart b/slang/lib/src/builder/model/node.dart index c0a2413f..55288eaf 100644 --- a/slang/lib/src/builder/model/node.dart +++ b/slang/lib/src/builder/model/node.dart @@ -1,6 +1,8 @@ -import 'package:slang/src/builder/builder/text_parser.dart'; +import 'package:slang/src/builder/builder/text/l10n_parser.dart'; +import 'package:slang/src/builder/builder/text/param_parser.dart'; import 'package:slang/src/builder/model/context_type.dart'; import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/model/interface.dart'; import 'package:slang/src/builder/model/pluralization.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; @@ -213,6 +215,9 @@ class ContextNode extends Node implements LeafNode { } abstract class TextNode extends Node implements LeafNode { + /// The locale of the text node + final I18nLocale locale; + /// The original string final String raw; @@ -227,6 +232,9 @@ abstract class TextNode extends Node implements LeafNode { /// the type must be specified and cannot be [Object]. Map get paramTypeMap; + /// Map of parameters to their format in raw string. + Map get paramFormatMap; + /// Set of paths to [TextNode]s /// Will be used for 2nd round, determining the final set of parameters Set get links; @@ -242,6 +250,7 @@ abstract class TextNode extends Node implements LeafNode { required super.rawPath, required super.modifiers, required super.comment, + required this.locale, required this.raw, required this.shouldEscape, required this.interpolation, @@ -278,10 +287,16 @@ class StringTextNode extends TextNode { @override Map get paramTypeMap => _paramTypeMap; + final _paramFormatMap = {}; + + @override + Map get paramFormatMap => _paramFormatMap; + StringTextNode({ required super.path, required super.rawPath, required super.modifiers, + required super.locale, required super.raw, required super.comment, required super.shouldEscape, @@ -290,6 +305,7 @@ class StringTextNode extends TextNode { Map>? linkParamMap, }) { final parsedResult = _parseInterpolation( + locale: locale, raw: shouldEscape ? _escapeContent(raw, interpolation) : raw, interpolation: interpolation, defaultType: 'Object', @@ -324,6 +340,7 @@ class StringTextNode extends TextNode { path: path, rawPath: rawPath, modifiers: modifiers, + locale: locale, raw: raw, comment: comment, shouldEscape: shouldEscape, @@ -365,10 +382,16 @@ class RichTextNode extends TextNode { @override Map get paramTypeMap => _paramTypeMap; + final _paramFormatMap = {}; + + @override + Map get paramFormatMap => _paramFormatMap; + RichTextNode({ required super.path, required super.rawPath, required super.modifiers, + required super.locale, required super.raw, required super.comment, required super.shouldEscape, @@ -377,9 +400,11 @@ class RichTextNode extends TextNode { Map>? linkParamMap, }) { final rawParsedResult = _parseInterpolation( + locale: locale, raw: shouldEscape ? _escapeContent(raw, interpolation) : raw, interpolation: interpolation, - defaultType: 'ignored', // types are ignored + defaultType: 'ignored', + // types are ignored paramCase: null, // param case will be applied later ); @@ -451,6 +476,7 @@ class RichTextNode extends TextNode { path: path, rawPath: rawPath, modifiers: modifiers, + locale: locale, raw: raw, comment: comment, shouldEscape: shouldEscape, @@ -500,14 +526,22 @@ class _ParseInterpolationResult { /// Map of parameter name -> parameter type final Map params; - _ParseInterpolationResult(this.parsedContent, this.params); + /// Map of parameter name -> parameter format + final Map formats; + + _ParseInterpolationResult({ + required this.parsedContent, + required this.params, + required this.formats, + }); @override String toString() => - '_ParseInterpolationResult{parsedContent: $parsedContent, params: $params}'; + '_ParseInterpolationResult(parsedContent: $parsedContent, params: $params, formats: $formats)'; } _ParseInterpolationResult _parseInterpolation({ + required I18nLocale locale, required String raw, required StringInterpolation interpolation, required String defaultType, @@ -515,6 +549,30 @@ _ParseInterpolationResult _parseInterpolation({ }) { final String parsedContent; final params = {}; + final formats = {}; + + String convertInnerParam(String inner) { + final parsedParam = parseParam( + rawParam: inner, + defaultType: defaultType, + caseStyle: paramCase, + ); + + final parsedL10n = parseL10n( + locale: locale, + paramName: parsedParam.paramName, + type: parsedParam.paramType.replaceAll(r"\'", "'"), // unescape + ); + + if (parsedL10n != null) { + params[parsedParam.paramName] = parsedL10n.paramType; + formats[parsedParam.paramName] = parsedL10n.format; + return '\${${parsedL10n.format}}'; + } else { + params[parsedParam.paramName] = parsedParam.paramType; + return '\${${parsedParam.paramName}}'; + } + } switch (interpolation) { case StringInterpolation.dart: @@ -522,41 +580,27 @@ _ParseInterpolationResult _parseInterpolation({ final rawParam = match.startsWith(r'${') ? match.substring(2, match.length - 1) : match.substring(1, match.length); - final parsedParam = parseParam( - rawParam: rawParam, - defaultType: defaultType, - caseStyle: paramCase, - ); - params[parsedParam.paramName] = parsedParam.paramType; - return '\${${parsedParam.paramName}}'; + return convertInnerParam(rawParam); }); break; case StringInterpolation.braces: parsedContent = raw.replaceBracesInterpolation(replacer: (match) { final rawParam = match.substring(1, match.length - 1); - final parsedParam = parseParam( - rawParam: rawParam, - defaultType: defaultType, - caseStyle: paramCase, - ); - params[parsedParam.paramName] = parsedParam.paramType; - return '\${${parsedParam.paramName}}'; + return convertInnerParam(rawParam); }); break; case StringInterpolation.doubleBraces: parsedContent = raw.replaceDoubleBracesInterpolation(replacer: (match) { final rawParam = match.substring(2, match.length - 2); - final parsedParam = parseParam( - rawParam: rawParam, - defaultType: defaultType, - caseStyle: paramCase, - ); - params[parsedParam.paramName] = parsedParam.paramType; - return '\${${parsedParam.paramName}}'; + return convertInnerParam(rawParam); }); } - return _ParseInterpolationResult(parsedContent, params); + return _ParseInterpolationResult( + parsedContent: parsedContent, + params: params, + formats: formats, + ); } class _ParseLinksResult { diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index 9c24278d..227e086d 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -19,6 +19,7 @@ environment: dependencies: collection: ^1.15.0 csv: ">=5.0.1 <7.0.0" + intl: ">=0.18.1 <2.0.0" json2yaml: ^3.0.0 watcher: ^1.0.2 yaml: ^3.1.0 diff --git a/slang/test/integration/resources/main/_expected_de.output b/slang/test/integration/resources/main/_expected_de.output index ca32cdc1..e8e94c70 100644 --- a/slang/test/integration/resources/main/_expected_de.output +++ b/slang/test/integration/resources/main/_expected_de.output @@ -5,6 +5,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'translations.cgm.dart'; diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output index c4856b2f..4fe13bde 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output @@ -5,6 +5,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'translations.cgm.dart'; @@ -20,7 +21,7 @@ class TranslationsDe extends Translations { cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, ), - super.build(cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver) { + super(cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver) { super.$meta.setFlatMapFunction($meta.getTranslation); // copy base translations to super.$meta $meta.setFlatMapFunction(_flatMapFunction); } @@ -53,7 +54,7 @@ class TranslationsDe extends Translations { // Path: onboarding class _TranslationsOnboardingDe extends TranslationsOnboardingEn { - _TranslationsOnboardingDe._(TranslationsDe root) : this._root = root, super._(root); + _TranslationsOnboardingDe._(TranslationsDe root) : this._root = root, super.internal(root); final TranslationsDe _root; // ignore: unused_field @@ -102,7 +103,7 @@ class _TranslationsOnboardingDe extends TranslationsOnboardingEn { // Path: group class _TranslationsGroupDe extends TranslationsGroupEn { - _TranslationsGroupDe._(TranslationsDe root) : this._root = root, super._(root); + _TranslationsGroupDe._(TranslationsDe root) : this._root = root, super.internal(root); final TranslationsDe _root; // ignore: unused_field @@ -116,7 +117,7 @@ class _TranslationsGroupDe extends TranslationsGroupEn { // Path: end class _TranslationsEndDe extends TranslationsEndEn with EndData { - _TranslationsEndDe._(TranslationsDe root) : this._root = root, super._(root); + _TranslationsEndDe._(TranslationsDe root) : this._root = root, super.internal(root); final TranslationsDe _root; // ignore: unused_field @@ -138,7 +139,7 @@ class _TranslationsEndDe extends TranslationsEndEn with EndData { // Path: onboarding.pages.0 class _TranslationsOnboarding$pages$0i0$De extends TranslationsOnboarding$pages$0i0$En with PageData { - _TranslationsOnboarding$pages$0i0$De._(TranslationsDe root) : this._root = root, super._(root); + _TranslationsOnboarding$pages$0i0$De._(TranslationsDe root) : this._root = root, super.internal(root); final TranslationsDe _root; // ignore: unused_field @@ -149,7 +150,7 @@ class _TranslationsOnboarding$pages$0i0$De extends TranslationsOnboarding$pages$ // Path: onboarding.pages.1 class _TranslationsOnboarding$pages$0i1$De extends TranslationsOnboarding$pages$0i1$En with PageData { - _TranslationsOnboarding$pages$0i1$De._(TranslationsDe root) : this._root = root, super._(root); + _TranslationsOnboarding$pages$0i1$De._(TranslationsDe root) : this._root = root, super.internal(root); final TranslationsDe _root; // ignore: unused_field @@ -159,7 +160,7 @@ class _TranslationsOnboarding$pages$0i1$De extends TranslationsOnboarding$pages$ // Path: onboarding.modifierPages.0 class _TranslationsOnboarding$modifierPages$0i0$De extends TranslationsOnboarding$modifierPages$0i0$En with MPage { - _TranslationsOnboarding$modifierPages$0i0$De._(TranslationsDe root) : this._root = root, super._(root); + _TranslationsOnboarding$modifierPages$0i0$De._(TranslationsDe root) : this._root = root, super.internal(root); final TranslationsDe _root; // ignore: unused_field @@ -170,7 +171,7 @@ class _TranslationsOnboarding$modifierPages$0i0$De extends TranslationsOnboardin // Path: onboarding.modifierPages.1 class _TranslationsOnboarding$modifierPages$0i1$De extends TranslationsOnboarding$modifierPages$0i1$En with MPage { - _TranslationsOnboarding$modifierPages$0i1$De._(TranslationsDe root) : this._root = root, super._(root); + _TranslationsOnboarding$modifierPages$0i1$De._(TranslationsDe root) : this._root = root, super.internal(root); final TranslationsDe _root; // ignore: unused_field diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output index b9c4a8b1..26b037a7 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output @@ -37,9 +37,9 @@ class Translations implements BaseTranslations { late final Translations _root = this; // ignore: unused_field // Translations - late final TranslationsOnboardingEn onboarding = TranslationsOnboardingEn._(_root); - late final TranslationsGroupEn group = TranslationsGroupEn._(_root); - late final TranslationsEndEn end = TranslationsEndEn._(_root); + late final TranslationsOnboardingEn onboarding = TranslationsOnboardingEn.internal(_root); + late final TranslationsGroupEn group = TranslationsGroupEn.internal(_root); + late final TranslationsEndEn end = TranslationsEndEn.internal(_root); TextSpan advancedPlural({required num count, required InlineSpan Function(num) countBuilder, required GenderContext gender}) => RichPluralResolvers.bridge( n: count, resolver: _root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'), @@ -56,7 +56,7 @@ class Translations implements BaseTranslations { // Path: onboarding class TranslationsOnboardingEn { - TranslationsOnboardingEn._(this._root); + TranslationsOnboardingEn.internal(this._root); final Translations _root; // ignore: unused_field @@ -105,7 +105,7 @@ class TranslationsOnboardingEn { // Path: group class TranslationsGroupEn { - TranslationsGroupEn._(this._root); + TranslationsGroupEn.internal(this._root); final Translations _root; // ignore: unused_field @@ -119,7 +119,7 @@ class TranslationsGroupEn { // Path: end class TranslationsEndEn with EndData { - TranslationsEndEn._(this._root); + TranslationsEndEn.internal(this._root); final Translations _root; // ignore: unused_field @@ -141,7 +141,7 @@ class TranslationsEndEn with EndData { // Path: onboarding.pages.0 class TranslationsOnboarding$pages$0i0$En with PageData { - TranslationsOnboarding$pages$0i0$En._(this._root); + TranslationsOnboarding$pages$0i0$En.internal(this._root); final Translations _root; // ignore: unused_field @@ -152,7 +152,7 @@ class TranslationsOnboarding$pages$0i0$En with PageData { // Path: onboarding.pages.1 class TranslationsOnboarding$pages$0i1$En with PageData { - TranslationsOnboarding$pages$0i1$En._(this._root); + TranslationsOnboarding$pages$0i1$En.internal(this._root); final Translations _root; // ignore: unused_field @@ -162,7 +162,7 @@ class TranslationsOnboarding$pages$0i1$En with PageData { // Path: onboarding.modifierPages.0 class TranslationsOnboarding$modifierPages$0i0$En with MPage { - TranslationsOnboarding$modifierPages$0i0$En._(this._root); + TranslationsOnboarding$modifierPages$0i0$En.internal(this._root); final Translations _root; // ignore: unused_field @@ -173,7 +173,7 @@ class TranslationsOnboarding$modifierPages$0i0$En with MPage { // Path: onboarding.modifierPages.1 class TranslationsOnboarding$modifierPages$0i1$En with MPage { - TranslationsOnboarding$modifierPages$0i1$En._(this._root); + TranslationsOnboarding$modifierPages$0i1$En.internal(this._root); final Translations _root; // ignore: unused_field diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output index cf4e1c5e..d343ddea 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output @@ -10,6 +10,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_de.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_de.output index 9ce3d000..d2a4279e 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_de.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_de.output @@ -5,6 +5,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'translations.cgm.dart'; @@ -20,7 +21,7 @@ class TranslationsDe extends Translations { cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, ), - super.build(cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver) { + super(cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver) { super.$meta.setFlatMapFunction($meta.getTranslation); // copy base translations to super.$meta $meta.setFlatMapFunction(_flatMapFunction); } diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output index 4b356912..212f7169 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output @@ -10,6 +10,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang/test/integration/resources/main/_expected_main.output b/slang/test/integration/resources/main/_expected_main.output index cf4e1c5e..d343ddea 100644 --- a/slang/test/integration/resources/main/_expected_main.output +++ b/slang/test/integration/resources/main/_expected_main.output @@ -10,6 +10,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang/test/integration/resources/main/_expected_no_flutter.output b/slang/test/integration/resources/main/_expected_no_flutter.output index 2e0523f8..0d1f0c66 100644 --- a/slang/test/integration/resources/main/_expected_no_flutter.output +++ b/slang/test/integration/resources/main/_expected_no_flutter.output @@ -9,6 +9,7 @@ // coverage:ignore-file // ignore_for_file: type=lint, unused_import +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'package:slang/slang.dart'; export 'package:slang/slang.dart'; diff --git a/slang/test/integration/resources/main/_expected_no_locale_handling.output b/slang/test/integration/resources/main/_expected_no_locale_handling.output index 676d99aa..e2b8f94f 100644 --- a/slang/test/integration/resources/main/_expected_no_locale_handling.output +++ b/slang/test/integration/resources/main/_expected_no_locale_handling.output @@ -10,6 +10,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang/test/integration/resources/main/_expected_obfuscation_de.output b/slang/test/integration/resources/main/_expected_obfuscation_de.output index b5b80033..ecde0366 100644 --- a/slang/test/integration/resources/main/_expected_obfuscation_de.output +++ b/slang/test/integration/resources/main/_expected_obfuscation_de.output @@ -5,6 +5,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'package:slang/secret.dart'; import 'translations.cgm.dart'; diff --git a/slang/test/integration/resources/main/_expected_obfuscation_main.output b/slang/test/integration/resources/main/_expected_obfuscation_main.output index 03c02257..cfb47293 100644 --- a/slang/test/integration/resources/main/_expected_obfuscation_main.output +++ b/slang/test/integration/resources/main/_expected_obfuscation_main.output @@ -10,6 +10,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'package:slang/secret.dart'; import 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang/test/integration/resources/main/_expected_translation_overrides_de.output b/slang/test/integration/resources/main/_expected_translation_overrides_de.output index 95f4ded8..5d38c7a7 100644 --- a/slang/test/integration/resources/main/_expected_translation_overrides_de.output +++ b/slang/test/integration/resources/main/_expected_translation_overrides_de.output @@ -5,6 +5,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'package:slang/overrides.dart'; import 'translations.cgm.dart'; diff --git a/slang/test/integration/resources/main/_expected_translation_overrides_main.output b/slang/test/integration/resources/main/_expected_translation_overrides_main.output index 93ca22c2..dd54d2d9 100644 --- a/slang/test/integration/resources/main/_expected_translation_overrides_main.output +++ b/slang/test/integration/resources/main/_expected_translation_overrides_main.output @@ -10,6 +10,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'package:slang/overrides.dart'; import 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang/test/unit/builder/text/l10n_parser_test.dart b/slang/test/unit/builder/text/l10n_parser_test.dart new file mode 100644 index 00000000..ceb16b7f --- /dev/null +++ b/slang/test/unit/builder/text/l10n_parser_test.dart @@ -0,0 +1,66 @@ +import 'package:slang/src/builder/builder/text/l10n_parser.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:test/test.dart'; + +final _locale = I18nLocale(language: 'en'); + +void main() { + test('Should skip unknown type', () { + final p = parseL10n(locale: _locale, paramName: 'p', type: 'int'); + expect(p, isNull); + }); + + test('Should parse shorthand number format', () { + final p = parseL10n(locale: _locale, paramName: 'p', type: 'currency'); + expect(p!.paramType, 'num'); + expect(p.format, "NumberFormat.currency(locale: 'en').format(p)"); + }); + + test('Should parse full number format', () { + final p = parseL10n( + locale: _locale, + paramName: 'p', + type: 'NumberFormat.currency', + ); + expect(p!.paramType, 'num'); + expect(p.format, "NumberFormat.currency(locale: 'en').format(p)"); + }); + + test('Should parse shorthand date format', () { + final p = parseL10n(locale: _locale, paramName: 'p', type: 'yMd'); + expect(p!.paramType, 'DateTime'); + expect(p.format, "DateFormat.yMd('en').format(p)"); + }); + + test('Should keep named parameter', () { + final p = parseL10n( + locale: _locale, + paramName: 'p', + type: 'NumberFormat.currency(decimalDigits: 2)', + ); + expect(p!.paramType, 'num'); + expect(p.format, + "NumberFormat.currency(decimalDigits: 2, locale: 'en').format(p)"); + }); + + test('Should keep named string parameter', () { + final p = parseL10n( + locale: _locale, + paramName: 'p', + type: "NumberFormat.currency(symbol: '€')", + ); + expect(p!.paramType, 'num'); + expect( + p.format, "NumberFormat.currency(symbol: '€', locale: 'en').format(p)"); + }); + + test('Should keep positional parameter', () { + final p = parseL10n( + locale: _locale, + paramName: 'p', + type: "DateFormat('yMd')", + ); + expect(p!.paramType, 'DateTime'); + expect(p.format, "DateFormat('yMd', 'en').format(p)"); + }); +} diff --git a/slang/test/unit/builder/text_parser_test.dart b/slang/test/unit/builder/text/param_parser_test.dart similarity index 96% rename from slang/test/unit/builder/text_parser_test.dart rename to slang/test/unit/builder/text/param_parser_test.dart index f7a5da51..aa9442fe 100644 --- a/slang/test/unit/builder/text_parser_test.dart +++ b/slang/test/unit/builder/text/param_parser_test.dart @@ -1,4 +1,4 @@ -import 'package:slang/src/builder/builder/text_parser.dart'; +import 'package:slang/src/builder/builder/text/param_parser.dart'; import 'package:slang/src/builder/model/enums.dart'; import 'package:test/test.dart'; diff --git a/slang/test/unit/builder/translation_model_builder_test.dart b/slang/test/unit/builder/translation_model_builder_test.dart index 1c88354c..88b18997 100644 --- a/slang/test/unit/builder/translation_model_builder_test.dart +++ b/slang/test/unit/builder/translation_model_builder_test.dart @@ -2,17 +2,20 @@ import 'package:slang/src/builder/builder/build_model_config_builder.dart'; import 'package:slang/src/builder/builder/translation_model_builder.dart'; import 'package:slang/src/builder/model/context_type.dart'; import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/model/interface.dart'; import 'package:slang/src/builder/model/node.dart'; import 'package:slang/src/builder/model/raw_config.dart'; import 'package:test/test.dart'; +final _locale = I18nLocale(language: 'en'); + void main() { group('TranslationModelBuilder.build', () { test('1 StringTextNode', () { final result = TranslationModelBuilder.build( buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, map: { 'test': 'a', }, @@ -28,7 +31,7 @@ void main() { keyCase: CaseStyle.snake, keyMapCase: CaseStyle.camel, ).toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, map: { 'myMap': {'my_value': 'cool'}, }, @@ -43,7 +46,7 @@ void main() { maps: ['my_map'], keyCase: CaseStyle.snake, ).toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, map: { 'myMap': {'my_value 3': 'cool'}, }, @@ -55,7 +58,7 @@ void main() { test('one link no parameters', () { final result = TranslationModelBuilder.build( buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, map: { 'a': 'A', 'b': 'Hello @:a', @@ -69,7 +72,7 @@ void main() { test('one link 2 parameters straight', () { final result = TranslationModelBuilder.build( buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, map: { 'a': r'A $p1 $p1 $p2', 'b': 'Hello @:a', @@ -83,7 +86,7 @@ void main() { test('linked translations with parameters recursive', () { final result = TranslationModelBuilder.build( buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, map: { 'a': r'A $p1 $p1 $p2 @:b @:c', 'b': r'Hello $p3 @:a', @@ -99,7 +102,7 @@ void main() { test('linked translation with plural', () { final result = TranslationModelBuilder.build( buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, map: { 'a': { 'one': 'ONE', @@ -117,7 +120,7 @@ void main() { test('linked translation with plural and custom number type', () { final result = TranslationModelBuilder.build( buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, map: { 'a': { 'one': 'ONE', @@ -141,7 +144,7 @@ void main() { generateEnum: true, ), ]).toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, map: { 'a(context=GenderCon)': { 'male': 'MALE', @@ -184,7 +187,7 @@ void main() { }, ), ]).toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, map: { 'myEntry': { 'myList': [], @@ -215,7 +218,7 @@ void main() { generateEnum: true, ), ]).toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, map: { 'a': 'b', }, @@ -249,7 +252,7 @@ void main() { }, } }, - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, ); _checkInterfaceResult(resultUsingModifiers); @@ -295,7 +298,7 @@ void main() { }, } }, - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, ); _checkInterfaceResult(resultUsingConfig); diff --git a/slang/test/unit/model/node_test.dart b/slang/test/unit/model/node_test.dart index f1cd6855..b70dafa4 100644 --- a/slang/test/unit/model/node_test.dart +++ b/slang/test/unit/model/node_test.dart @@ -288,6 +288,16 @@ void main() { expect(node.content, r'${_root.a} ${_root.b(c: c, d: d)}'); expect(node.params, {'c', 'd'}); }); + + test('with number format', () { + final test = r"Your price is ${price: currency(symbol: '€')}"; + final node = textNode(test, StringInterpolation.dart); + expect( + node.content, + r"Your price is ${NumberFormat.currency(symbol: '€', locale: 'en').format(price)}", + ); + expect(node.params, {'price'}); + }); }); group(StringInterpolation.braces, () { diff --git a/slang/test/util/text_node_builder.dart b/slang/test/util/text_node_builder.dart index c4b7e185..3fd38aa8 100644 --- a/slang/test/util/text_node_builder.dart +++ b/slang/test/util/text_node_builder.dart @@ -1,6 +1,9 @@ import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/model/node.dart'; +final _locale = I18nLocale(language: 'en'); + StringTextNode textNode( String raw, StringInterpolation interpolation, { @@ -11,6 +14,7 @@ StringTextNode textNode( path: '', rawPath: '', modifiers: {}, + locale: _locale, raw: raw, comment: null, interpolation: interpolation, @@ -31,6 +35,7 @@ RichTextNode richTextNode( rawPath: '', modifiers: {}, comment: null, + locale: _locale, raw: raw, interpolation: interpolation, paramCase: paramCase, From 34f84abd5d712e34b48d080c9dfb328d5735fab8 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 20 Oct 2024 15:51:57 +0200 Subject: [PATCH 077/118] feat: user defined types --- slang/README.md | 21 +++++++++ slang/lib/node.dart | 1 + slang/lib/src/api/formatter.dart | 26 +++++++++++ slang/lib/src/api/locale.dart | 3 ++ .../builder/translation_model_builder.dart | 46 +++++++++++++++++++ .../translation_model_list_builder.dart | 2 + .../generator/generate_translations.dart | 9 ++++ slang/lib/src/builder/model/i18n_data.dart | 2 + slang/lib/src/builder/model/node.dart | 18 ++++++++ .../src/builder/model/translation_map.dart | 10 ++++ slang/test/unit/model/i18n_data_test.dart | 1 + slang/test/util/text_node_builder.dart | 2 + 12 files changed, 141 insertions(+) create mode 100644 slang/lib/src/api/formatter.dart diff --git a/slang/README.md b/slang/README.md index b6c97d0c..cc9d4358 100644 --- a/slang/README.md +++ b/slang/README.md @@ -956,6 +956,27 @@ Or adjust built-in formats: } ``` +To avoid repetition, you can define custom types via `@@types`. +Please note that the types are locale-specific. If you use [namespaces](#-namespaces), all definitions are merged. + +```json +{ + "@@types": { + "price": "currency(symbol: 'USD')", + "dateOnly": "DateFormat('MM/dd/yyyy')" + }, + "account": "You have {amount: price} in your account", + "today": "Today is {today: dateOnly}", + "tomorrow": "Tomorrow is {tomorrow: dateOnly}" +} +``` + +```dart +String a = t.account(amount: 1234.56); // You have $1,234.56 in your account +String b = t.today(today: DateTime(2023, 3, 2)); // Today is 03/02/2023 +String c = t.tomorrow(tomorrow: DateTime(2023, 3, 5)); // Tomorrow is 03/05/2023 +``` + ### ➤ Interfaces Often, multiple objects have the same attributes. You can create a common super class for that. diff --git a/slang/lib/node.dart b/slang/lib/node.dart index 596db32b..0dd6a9a4 100644 --- a/slang/lib/node.dart +++ b/slang/lib/node.dart @@ -1 +1,2 @@ +export 'package:slang/src/api/formatter.dart'; export 'package:slang/src/builder/model/node.dart'; diff --git a/slang/lib/src/api/formatter.dart b/slang/lib/src/api/formatter.dart new file mode 100644 index 00000000..fd98e84b --- /dev/null +++ b/slang/lib/src/api/formatter.dart @@ -0,0 +1,26 @@ +import 'package:intl/intl.dart'; + +class ValueFormatter { + /// Is either [NumberFormat] or [DateFormat]. + /// Unfortunately, there is no super class for both. + final Object Function() _formatter; + + /// The actual formatter. + /// We delay the initialization to ensure that intl is already initialized + /// by Flutter before we create the formatter. + late final Object formatter = _formatter(); + + ValueFormatter(this._formatter); + + /// Formats the given [value] with the formatter. + String format(Object value) { + switch (formatter) { + case NumberFormat formatter: + return formatter.format(value as num); + case DateFormat formatter: + return formatter.format(value as DateTime); + default: + throw Exception('Unknown formatter: $formatter'); + } + } +} diff --git a/slang/lib/src/api/locale.dart b/slang/lib/src/api/locale.dart index ae4f42c2..e23ff8ea 100644 --- a/slang/lib/src/api/locale.dart +++ b/slang/lib/src/api/locale.dart @@ -1,3 +1,4 @@ +import 'package:slang/src/api/formatter.dart'; import 'package:slang/src/api/pluralization.dart'; import 'package:slang/src/builder/model/node.dart'; @@ -17,6 +18,7 @@ class TranslationMetadata, final Map overrides; final PluralResolver? cardinalResolver; final PluralResolver? ordinalResolver; + final Map types; /// The secret. /// Used to decrypt obfuscated translation strings. @@ -29,6 +31,7 @@ class TranslationMetadata, required this.overrides, required this.cardinalResolver, required this.ordinalResolver, + this.types = const {}, this.s = 0, }); diff --git a/slang/lib/src/builder/builder/translation_model_builder.dart b/slang/lib/src/builder/builder/translation_model_builder.dart index 8ebf48f0..010e1f91 100644 --- a/slang/lib/src/builder/builder/translation_model_builder.dart +++ b/slang/lib/src/builder/builder/translation_model_builder.dart @@ -1,6 +1,7 @@ import 'dart:collection'; import 'package:collection/collection.dart'; +import 'package:slang/src/builder/builder/text/l10n_parser.dart'; import 'package:slang/src/builder/model/build_model_config.dart'; import 'package:slang/src/builder/model/context_type.dart'; import 'package:slang/src/builder/model/enums.dart'; @@ -16,11 +17,13 @@ class BuildModelResult { final ObjectNode root; // the actual strings final List interfaces; // detected interfaces final List contexts; // detected context types + final Map types; // detected types, values are rendered as is BuildModelResult({ required this.root, required this.interfaces, required this.contexts, + required this.types, }); } @@ -71,6 +74,28 @@ class TranslationModelBuilder { context.enumName: context.toPending(), }; + final types = {}; + final typesNode = map['@@types']; + if (typesNode != null && typesNode is Map) { + for (final entry in typesNode.entries) { + final key = entry.key; + final value = entry.value; + if (value is String) { + final typeInfo = parseL10n( + locale: locale, + paramName: 'value', + type: value, + ); + if (typeInfo != null) { + types[key] = FormatTypeInfo( + paramType: typeInfo.paramType, + implementation: typeInfo.format, + ); + } + } + } + } + // 1st iteration: Build nodes according to given map // // Linked Translations: @@ -79,6 +104,7 @@ class TranslationModelBuilder { // Reason: Not all TextNodes are built, so final parameters are unknown final resultNodeTree = _parseMapNode( locale: locale, + types: types, parentPath: '', parentRawPath: '', curr: map, @@ -214,6 +240,10 @@ class TranslationModelBuilder { generateEnum: c.generateEnum, )) .toList(), + types: { + for (final entry in types.entries) + entry.key: entry.value.implementation, + }, ); } } @@ -222,6 +252,7 @@ class TranslationModelBuilder { /// and returns the node model. Map _parseMapNode({ required I18nLocale locale, + required Map types, required String parentPath, required String parentRawPath, required Map curr, @@ -267,6 +298,7 @@ Map _parseMapNode({ rawPath: currRawPath, modifiers: modifiers, locale: locale, + types: types, raw: value.toString(), comment: comment, shouldEscape: shouldEscapeText, @@ -278,6 +310,7 @@ Map _parseMapNode({ rawPath: currRawPath, modifiers: modifiers, locale: locale, + types: types, raw: value.toString(), comment: comment, shouldEscape: shouldEscapeText, @@ -297,6 +330,7 @@ Map _parseMapNode({ }; children = _parseMapNode( locale: locale, + types: types, parentPath: currPath, parentRawPath: currRawPath, curr: listAsMap, @@ -323,6 +357,7 @@ Map _parseMapNode({ // key: { ...value } children = _parseMapNode( locale: locale, + types: types, parentPath: currPath, parentRawPath: currRawPath, curr: value, @@ -379,6 +414,7 @@ Map _parseMapNode({ // rebuild children as RichText digestedMap = _parseMapNode( locale: locale, + types: types, parentPath: currPath, parentRawPath: currRawPath, curr: { @@ -921,3 +957,13 @@ extension on BuildModelConfig { ); } } + +class FormatTypeInfo { + final String paramType; // num or DateTime + final String implementation; // raw string that will be rendered as is + + FormatTypeInfo({ + required this.paramType, + required this.implementation, + }); +} diff --git a/slang/lib/src/builder/builder/translation_model_list_builder.dart b/slang/lib/src/builder/builder/translation_model_list_builder.dart index 899d8c0e..11ed719a 100644 --- a/slang/lib/src/builder/builder/translation_model_list_builder.dart +++ b/slang/lib/src/builder/builder/translation_model_list_builder.dart @@ -42,6 +42,7 @@ class TranslationModelListBuilder { root: baseResult.root, contexts: baseResult.contexts, interfaces: baseResult.interfaces, + types: baseResult.types, ); } else { final result = TranslationModelBuilder.build( @@ -57,6 +58,7 @@ class TranslationModelListBuilder { root: result.root, contexts: result.contexts, interfaces: result.interfaces, + types: result.types, ); } }).toList() diff --git a/slang/lib/src/builder/generator/generate_translations.dart b/slang/lib/src/builder/generator/generate_translations.dart index 8c4e5eed..33f8954f 100644 --- a/slang/lib/src/builder/generator/generate_translations.dart +++ b/slang/lib/src/builder/generator/generate_translations.dart @@ -203,6 +203,15 @@ void _generateClass( buffer.writeln('\t\t overrides: overrides ?? {},'); buffer.writeln('\t\t cardinalResolver: cardinalResolver,'); buffer.writeln('\t\t ordinalResolver: ordinalResolver,'); + if (localeData.types.isNotEmpty) { + buffer.writeln('\t\t types: {'); + for (final entry in localeData.types.entries) { + buffer.writeln( + '\t\t \'${entry.key}\': ValueFormatter(() => ${entry.value.substring(0, entry.value.length - 14)}),', + ); + } + buffer.writeln('\t\t },'); + } if (config.obfuscation.enabled) { final String method; final List parts; diff --git a/slang/lib/src/builder/model/i18n_data.dart b/slang/lib/src/builder/model/i18n_data.dart index c43e05bb..c4433e8e 100644 --- a/slang/lib/src/builder/model/i18n_data.dart +++ b/slang/lib/src/builder/model/i18n_data.dart @@ -12,6 +12,7 @@ class I18nData { final ObjectNode root; // the actual strings final List contexts; // detected context types final List interfaces; // detected interfaces + final Map types; // detected types, values are rendered as is I18nData({ required this.base, @@ -19,6 +20,7 @@ class I18nData { required this.root, required this.contexts, required this.interfaces, + required this.types, }); /// sorts base locale first, then alphabetically diff --git a/slang/lib/src/builder/model/node.dart b/slang/lib/src/builder/model/node.dart index 55288eaf..6182b536 100644 --- a/slang/lib/src/builder/model/node.dart +++ b/slang/lib/src/builder/model/node.dart @@ -1,5 +1,6 @@ import 'package:slang/src/builder/builder/text/l10n_parser.dart'; import 'package:slang/src/builder/builder/text/param_parser.dart'; +import 'package:slang/src/builder/builder/translation_model_builder.dart'; import 'package:slang/src/builder/model/context_type.dart'; import 'package:slang/src/builder/model/enums.dart'; import 'package:slang/src/builder/model/i18n_locale.dart'; @@ -218,6 +219,9 @@ abstract class TextNode extends Node implements LeafNode { /// The locale of the text node final I18nLocale locale; + /// User-defined types for the locale + final Map types; + /// The original string final String raw; @@ -251,6 +255,7 @@ abstract class TextNode extends Node implements LeafNode { required super.modifiers, required super.comment, required this.locale, + required this.types, required this.raw, required this.shouldEscape, required this.interpolation, @@ -297,6 +302,7 @@ class StringTextNode extends TextNode { required super.rawPath, required super.modifiers, required super.locale, + required super.types, required super.raw, required super.comment, required super.shouldEscape, @@ -306,6 +312,7 @@ class StringTextNode extends TextNode { }) { final parsedResult = _parseInterpolation( locale: locale, + types: types, raw: shouldEscape ? _escapeContent(raw, interpolation) : raw, interpolation: interpolation, defaultType: 'Object', @@ -341,6 +348,7 @@ class StringTextNode extends TextNode { rawPath: rawPath, modifiers: modifiers, locale: locale, + types: types, raw: raw, comment: comment, shouldEscape: shouldEscape, @@ -392,6 +400,7 @@ class RichTextNode extends TextNode { required super.rawPath, required super.modifiers, required super.locale, + required super.types, required super.raw, required super.comment, required super.shouldEscape, @@ -401,6 +410,7 @@ class RichTextNode extends TextNode { }) { final rawParsedResult = _parseInterpolation( locale: locale, + types: types, raw: shouldEscape ? _escapeContent(raw, interpolation) : raw, interpolation: interpolation, defaultType: 'ignored', @@ -477,6 +487,7 @@ class RichTextNode extends TextNode { rawPath: rawPath, modifiers: modifiers, locale: locale, + types: types, raw: raw, comment: comment, shouldEscape: shouldEscape, @@ -542,6 +553,7 @@ class _ParseInterpolationResult { _ParseInterpolationResult _parseInterpolation({ required I18nLocale locale, + required Map types, required String raw, required StringInterpolation interpolation, required String defaultType, @@ -558,6 +570,12 @@ _ParseInterpolationResult _parseInterpolation({ caseStyle: paramCase, ); + final existingType = types[parsedParam.paramType]; + if (existingType != null) { + params[parsedParam.paramName] = existingType.paramType; + return '\${_root.\$meta.types[\'${parsedParam.paramType}\']!.format(${parsedParam.paramName})}'; + } + final parsedL10n = parseL10n( locale: locale, paramName: parsedParam.paramName, diff --git a/slang/lib/src/builder/model/translation_map.dart b/slang/lib/src/builder/model/translation_map.dart index 22a6170a..de111881 100644 --- a/slang/lib/src/builder/model/translation_map.dart +++ b/slang/lib/src/builder/model/translation_map.dart @@ -25,6 +25,16 @@ class TranslationMap { } _internalMap[locale]![namespace] = translations; + + // Copy types of each namespace to the global types map, + // merging them with existing types. + final typesMap = translations['@@types'] as Map?; + if (typesMap != null) { + _internalMap[locale]!['@@types'] = { + ...?_internalMap[locale]!['@@types'], + ...typesMap, + }; + } } /// Return all locales specified in this map diff --git a/slang/test/unit/model/i18n_data_test.dart b/slang/test/unit/model/i18n_data_test.dart index 29e5de8b..ac3e2f0f 100644 --- a/slang/test/unit/model/i18n_data_test.dart +++ b/slang/test/unit/model/i18n_data_test.dart @@ -17,6 +17,7 @@ I18nData _i18n(String locale, [bool base = false]) { ), contexts: [], interfaces: [], + types: {}, ); } diff --git a/slang/test/util/text_node_builder.dart b/slang/test/util/text_node_builder.dart index 3fd38aa8..603abc73 100644 --- a/slang/test/util/text_node_builder.dart +++ b/slang/test/util/text_node_builder.dart @@ -15,6 +15,7 @@ StringTextNode textNode( rawPath: '', modifiers: {}, locale: _locale, + types: {}, raw: raw, comment: null, interpolation: interpolation, @@ -36,6 +37,7 @@ RichTextNode richTextNode( modifiers: {}, comment: null, locale: _locale, + types: {}, raw: raw, interpolation: interpolation, paramCase: paramCase, From 7b7bcbda2bda60097686e1da54a9011ebe9680ca Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 00:25:35 +0200 Subject: [PATCH 078/118] feat: override and arb integration for l10n --- slang/lib/src/api/locale.dart | 6 + slang/lib/src/api/singleton.dart | 1 + slang/lib/src/api/translation_overrides.dart | 36 ++++- .../builder/text/l10n_override_parser.dart | 142 ++++++++++++++++++ .../src/builder/builder/text/l10n_parser.dart | 91 ++++++----- .../builder/builder/text/param_parser.dart | 22 ++- .../builder/translation_model_builder.dart | 13 ++ .../lib/src/builder/decoder/arb_decoder.dart | 70 ++++++++- slang/lib/src/builder/model/node.dart | 14 ++ slang/lib/src/builder/utils/regex_utils.dart | 42 ++++-- .../unit/api/translation_overrides_test.dart | 121 ++++++++++++--- .../text/l10n_override_parser_test.dart | 79 ++++++++++ .../unit/builder/text/param_parser_test.dart | 10 -- slang/test/unit/decoder/arb_decoder_test.dart | 62 ++++++++ slang/test/unit/utils/regex_utils_test.dart | 21 +++ slang/test/util/text_node_builder.dart | 2 + 16 files changed, 629 insertions(+), 103 deletions(-) create mode 100644 slang/lib/src/builder/builder/text/l10n_override_parser.dart create mode 100644 slang/test/unit/builder/text/l10n_override_parser_test.dart diff --git a/slang/lib/src/api/locale.dart b/slang/lib/src/api/locale.dart index e23ff8ea..a774510a 100644 --- a/slang/lib/src/api/locale.dart +++ b/slang/lib/src/api/locale.dart @@ -123,10 +123,13 @@ class FakeAppLocale extends BaseAppLocale { @override final String? countryCode; + final Map? types; + FakeAppLocale({ required this.languageCode, this.scriptCode, this.countryCode, + this.types, }); @override @@ -156,6 +159,7 @@ class FakeAppLocale extends BaseAppLocale { overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, + types: types, ); } } @@ -167,12 +171,14 @@ class FakeTranslations Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, + Map? types, int? s, }) : $meta = TranslationMetadata( locale: locale, overrides: overrides ?? {}, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, + types: types ?? {}, s: s ?? 0, ), providedNullOverrides = overrides == null; diff --git a/slang/lib/src/api/singleton.dart b/slang/lib/src/api/singleton.dart index 7835803b..829d8779 100644 --- a/slang/lib/src/api/singleton.dart +++ b/slang/lib/src/api/singleton.dart @@ -227,6 +227,7 @@ extension AppLocaleUtilsExt, buildConfig: buildConfig!, map: digestedMap, handleLinks: false, + handleTypes: false, shouldEscapeText: false, locale: I18nLocale( language: locale.languageCode, diff --git a/slang/lib/src/api/translation_overrides.dart b/slang/lib/src/api/translation_overrides.dart index 6104d8ac..81eb86c4 100644 --- a/slang/lib/src/api/translation_overrides.dart +++ b/slang/lib/src/api/translation_overrides.dart @@ -1,5 +1,7 @@ +import 'package:slang/src/api/formatter.dart'; import 'package:slang/src/api/locale.dart'; import 'package:slang/src/api/pluralization.dart'; +import 'package:slang/src/builder/builder/text/l10n_override_parser.dart'; import 'package:slang/src/builder/generator/helper.dart'; import 'package:slang/src/builder/model/node.dart'; import 'package:slang/src/builder/model/pluralization.dart'; @@ -119,14 +121,39 @@ class TranslationOverrides { extension TranslationOverridesStringExt on String { /// Replaces every ${param} with the given parameter - String applyParams(Map param) { + String applyParams( + Map existingTypes, + String locale, + Map param, + ) { return replaceDartNormalizedInterpolation(replacer: (match) { final nodeParam = match.substring(2, match.length - 1); - final providedParam = param[nodeParam]; + + final colonIndex = nodeParam.indexOf(':'); + if (colonIndex == -1) { + // parameter without type + final providedParam = param[nodeParam]; + if (providedParam == null) { + return match; // do not replace, keep as is + } + return providedParam.toString(); + } + + final paramName = nodeParam.substring(0, colonIndex).trim(); + final paramType = nodeParam.substring(colonIndex + 1).trim(); + + final providedParam = param[paramName]; if (providedParam == null) { return match; // do not replace, keep as is } - return providedParam.toString(); + + return digestL10nOverride( + locale: locale, + existingTypes: existingTypes, + type: paramType, + value: providedParam, + ) ?? + providedParam.toString(); }); } @@ -171,6 +198,7 @@ extension TranslationOverridesStringExt on String { /// Shortcut to call both at once. String applyParamsAndLinks( TranslationMetadata meta, Map param) { - return applyParams(param).applyLinks(meta, param); + return applyParams(meta.types, meta.locale.underscoreTag, param) + .applyLinks(meta, param); } } diff --git a/slang/lib/src/builder/builder/text/l10n_override_parser.dart b/slang/lib/src/builder/builder/text/l10n_override_parser.dart new file mode 100644 index 00000000..4e01e030 --- /dev/null +++ b/slang/lib/src/builder/builder/text/l10n_override_parser.dart @@ -0,0 +1,142 @@ +import 'package:intl/intl.dart'; +import 'package:slang/src/api/formatter.dart'; +import 'package:slang/src/builder/builder/text/l10n_parser.dart'; + +class L10nOverrideResult { + final String methodName; + final Map params; + + L10nOverrideResult({ + required this.methodName, + required this.params, + }); + + String format(Object value) { + final Function f = NumberFormat.currency; + final dynamic formatter = Function.apply(f, const [], params); + return formatter.format(value); + } +} + +/// Converts a type definition to an actual value. +/// e.g. +/// - currency -> $3.14 +/// - currency(symbol: '€') -> €3.14 +String? digestL10nOverride({ + required Map existingTypes, + required String locale, + required String type, + required Object value, +}) { + final existingType = existingTypes[type]; + if (existingType != null) { + // Use existing type formatter directly + return existingType.format(value); + } + + final parsed = parseL10nIntermediate(type); + if (parsed == null) { + return null; + } + + // Let's parse the method name and arguments + + if (numberFormatsWithNamedParameters.contains(parsed.methodName)) { + // named arguments + final arguments = switch (parsed.arguments) { + String args => parseArguments(args), + null => const {}, + }; + final Function formatterBuilder = switch (parsed.methodName) { + 'NumberFormat.compact' => NumberFormat.compact, + 'NumberFormat.compactCurrency' => NumberFormat.compactCurrency, + 'NumberFormat.compactSimpleCurrency' => + NumberFormat.compactSimpleCurrency, + 'NumberFormat.compactLong' => NumberFormat.compactLong, + 'NumberFormat.currency' => NumberFormat.currency, + 'NumberFormat.decimalPatternDigits' => NumberFormat.decimalPatternDigits, + 'NumberFormat.decimalPercentPattern' => + NumberFormat.decimalPercentPattern, + 'NumberFormat.simpleCurrency' => NumberFormat.simpleCurrency, + _ => throw UnimplementedError('Unknown formatter: ${parsed.methodName}'), + }; + + final formatter = Function.apply(formatterBuilder, [], { + ...arguments, + #locale: locale, + }); + + return formatter.format(value); + } else { + // positional arguments + final argument = switch (parsed.arguments) { + String args => parseSinglePositionalArgument(args), + null => null, + }; + final Function formatterBuilder = switch (parsed.methodName) { + 'NumberFormat.decimalPattern' => NumberFormat.decimalPattern, + 'NumberFormat.percentPattern' => NumberFormat.percentPattern, + 'NumberFormat.scientificPattern' => NumberFormat.scientificPattern, + 'NumberFormat' => _numberFormatBuilder, + 'DateFormat.yM' => DateFormat.yM, + 'DateFormat.yMd' => DateFormat.yMd, + 'DateFormat.Hm' => DateFormat.Hm, + 'DateFormat.Hms' => DateFormat.Hms, + 'DateFormat.jm' => DateFormat.jm, + 'DateFormat.jms' => DateFormat.jms, + 'DateFormat' => _dateFormatBuilder, + _ => throw UnimplementedError('Unknown formatter: ${parsed.methodName}'), + }; + + final formatter = Function.apply( + formatterBuilder, + [ + if (argument != null) argument, + locale, + ], + ); + return formatter.format(value); + } +} + +Map parseArguments(String arguments) { + final result = {}; + final parts = arguments.split(','); + for (final part in parts) { + final keyValue = part.split(':'); + if (keyValue.length != 2) { + continue; + } + final key = keyValue[0].trim(); + final value = keyValue[1].trim(); + + if ((value.startsWith("'") && value.endsWith("'")) || + (value.startsWith('"') && value.endsWith('"'))) { + result[Symbol(key)] = value.substring(1, value.length - 1); + } else { + final number = num.tryParse(value); + if (number != null) { + result[Symbol(key)] = number; + } + } + } + return result; +} + +Object? parseSinglePositionalArgument(String argument) { + if ((argument.startsWith("'") && argument.endsWith("'") || + argument.startsWith('"') && argument.endsWith('"'))) { + return argument.substring(1, argument.length - 1); + } else { + final number = num.tryParse(argument); + return number; + } +} + +NumberFormat _numberFormatBuilder(String pattern, String locale) { + return NumberFormat(pattern, locale); +} + +DateFormat _dateFormatBuilder(String pattern, String locale) { + return DateFormat(pattern, locale); +} diff --git a/slang/lib/src/builder/builder/text/l10n_parser.dart b/slang/lib/src/builder/builder/text/l10n_parser.dart index 0ebc8371..eec76671 100644 --- a/slang/lib/src/builder/builder/text/l10n_parser.dart +++ b/slang/lib/src/builder/builder/text/l10n_parser.dart @@ -1,4 +1,5 @@ import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; class ParseL10nResult { /// The actual parameter type. @@ -14,7 +15,7 @@ class ParseL10nResult { }); } -const _numberFormats = { +const numberFormats = { 'compact', 'compactCurrency', 'compactSimpleCurrency', @@ -28,17 +29,19 @@ const _numberFormats = { 'simpleCurrency', }; -const _numberFormatsWithNamedParameters = { +const numberFormatsWithNamedParameters = { + 'NumberFormat.compact', 'NumberFormat.compactCurrency', 'NumberFormat.compactSimpleCurrency', + 'NumberFormat.compactLong', 'NumberFormat.currency', 'NumberFormat.decimalPatternDigits', 'NumberFormat.decimalPercentPattern', 'NumberFormat.simpleCurrency', }; -final _numberFormatsWithClass = { - for (final format in _numberFormats) 'NumberFormat.$format', +final numberFormatsWithClass = { + for (final format in numberFormats) 'NumberFormat.$format', 'NumberFormat', }; @@ -56,40 +59,36 @@ final _dateFormatsWithClass = { 'DateFormat', }; -// Parses "currency(symbol: '€')" -// -> paramType: num, format: NumberFormat.currency(symbol: '€', locale: locale).format(value) -ParseL10nResult? parseL10n({ - required I18nLocale locale, - required String paramName, - required String type, -}) { - final bracketStart = type.indexOf('('); +class L10nIntermediateResult { + final String paramType; + final String methodName; + final String? arguments; - // The type without parameters. - // E.g. currency(symbol: '€') -> currency - final digestedType = - bracketStart == -1 ? type : type.substring(0, bracketStart); + L10nIntermediateResult({ + required this.paramType, + required this.methodName, + required this.arguments, + }); +} - final String paramType; - if (_numberFormats.contains(digestedType) || - _numberFormatsWithClass.contains(digestedType)) { - paramType = 'num'; - } else if (_dateFormats.contains(digestedType) || - _dateFormatsWithClass.contains(digestedType)) { - paramType = 'DateTime'; - } else { +L10nIntermediateResult? parseL10nIntermediate(String type) { + final parsed = RegexUtils.formatTypeRegex.firstMatch(type); + if (parsed == null) { return null; } - String methodName; - String arguments; + String methodName = parsed.group(1)!; + final arguments = parsed.group(2); - if (bracketStart != -1 && type.endsWith(')')) { - methodName = type.substring(0, bracketStart); - arguments = type.substring(bracketStart + 1, type.length - 1); + final String paramType; + if (numberFormats.contains(methodName) || + numberFormatsWithClass.contains(methodName)) { + paramType = 'num'; + } else if (_dateFormats.contains(methodName) || + _dateFormatsWithClass.contains(methodName)) { + paramType = 'DateTime'; } else { - methodName = type; - arguments = ''; + return null; } // Prepend class if necessary @@ -104,11 +103,31 @@ ParseL10nResult? parseL10n({ } } + return L10nIntermediateResult( + paramType: paramType, + methodName: methodName, + arguments: arguments?.trim(), + ); +} + +// Parses "currency(symbol: '€')" +// -> paramType: num, format: NumberFormat.currency(symbol: '€', locale: locale).format(value) +ParseL10nResult? parseL10n({ + required I18nLocale locale, + required String paramName, + required String type, +}) { + final parsed = parseL10nIntermediate(type); + if (parsed == null) { + return null; + } + // Add locale - if (paramType == 'num' && - _numberFormatsWithNamedParameters.contains(methodName)) { + String arguments = parsed.arguments ?? ''; + if (parsed.paramType == 'num' && + numberFormatsWithNamedParameters.contains(parsed.methodName)) { // add locale as named parameter - if (arguments.isEmpty) { + if (parsed.arguments == null) { arguments = "locale: '${locale.underscoreTag}'"; } else { arguments = "$arguments, locale: '${locale.underscoreTag}'"; @@ -125,7 +144,7 @@ ParseL10nResult? parseL10n({ } return ParseL10nResult( - paramType: paramType, - format: '$methodName($arguments).format($paramName)', + paramType: parsed.paramType, + format: '${parsed.methodName}($arguments).format($paramName)', ); } diff --git a/slang/lib/src/builder/builder/text/param_parser.dart b/slang/lib/src/builder/builder/text/param_parser.dart index f787d2bd..bcd7d8f5 100644 --- a/slang/lib/src/builder/builder/text/param_parser.dart +++ b/slang/lib/src/builder/builder/text/param_parser.dart @@ -20,16 +20,6 @@ ParseParamResult parseParam({ required CaseStyle? caseStyle, }) { final colonIndex = rawParam.indexOf(':'); - final bracketIndex = rawParam.indexOf('('); - if (bracketIndex != -1 && (colonIndex == -1 || bracketIndex < colonIndex)) { - // rich text parameter with default value - // this will be parsed by parseParamWithArg - return ParseParamResult( - rawParam, - '', - ); - } - if (colonIndex == -1) { return ParseParamResult(rawParam.toCase(caseStyle), defaultType); } @@ -53,12 +43,18 @@ ParamWithArg parseParamWithArg({ required String rawParam, required CaseStyle? paramCase, }) { - final end = rawParam.lastIndexOf(')'); - if (end == -1) { + final colonIndex = rawParam.indexOf(':'); + final start = rawParam.indexOf('('); + if (colonIndex != -1 && (start == -1 || colonIndex < start)) { + // ignore type for rich text + rawParam = rawParam.substring(0, colonIndex); + } + + if (start == -1) { return ParamWithArg(rawParam.toCase(paramCase), null); } - final start = rawParam.indexOf('('); + final end = rawParam.lastIndexOf(')'); final parameterName = rawParam.substring(0, start).toCase(paramCase); return ParamWithArg(parameterName, rawParam.substring(start + 1, end)); } diff --git a/slang/lib/src/builder/builder/translation_model_builder.dart b/slang/lib/src/builder/builder/translation_model_builder.dart index 010e1f91..99364586 100644 --- a/slang/lib/src/builder/builder/translation_model_builder.dart +++ b/slang/lib/src/builder/builder/translation_model_builder.dart @@ -41,6 +41,11 @@ class TranslationModelBuilder { /// This is used for "Translation Overrides" where the links are resolved /// on invocation. /// + /// [handleTypes] can be set false to ignore type resolution. + /// e.g. ${price: currency(symbol: 'USD')} stays as is. + /// This is used for "Translation Overrides" where the types are resolved + /// on invocation. + /// /// [shouldEscapeText] can be set false to ignore escaping of text nodes /// e.g. "Let's go" will be "Let's go" instead of "Let\'s go". /// Similar to [handleLinks], this is used for "Translation Overrides". @@ -50,6 +55,7 @@ class TranslationModelBuilder { required Map map, BuildModelResult? baseData, bool handleLinks = true, + bool handleTypes = true, bool shouldEscapeText = true, }) { // flat map for leaves (TextNode, PluralNode, ContextNode) @@ -115,6 +121,7 @@ class TranslationModelBuilder { baseData: baseData, baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, + handleTypes: handleTypes, ); // 2nd iteration: Handle parameterized linked translations @@ -263,6 +270,7 @@ Map _parseMapNode({ required BuildModelResult? baseData, required Map? baseContexts, required bool shouldEscapeText, + required bool handleTypes, }) { final Map resultNodeTree = {}; @@ -302,6 +310,7 @@ Map _parseMapNode({ raw: value.toString(), comment: comment, shouldEscape: shouldEscapeText, + handleTypes: handleTypes, interpolation: config.stringInterpolation, paramCase: config.paramCase, ) @@ -314,6 +323,7 @@ Map _parseMapNode({ raw: value.toString(), comment: comment, shouldEscape: shouldEscapeText, + handleTypes: handleTypes, interpolation: config.stringInterpolation, paramCase: config.paramCase, ); @@ -341,6 +351,7 @@ Map _parseMapNode({ baseData: baseData, baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, + handleTypes: handleTypes, ); // finally only take their values, ignoring keys @@ -372,6 +383,7 @@ Map _parseMapNode({ baseData: baseData, baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, + handleTypes: handleTypes, ); final Node finalNode; @@ -428,6 +440,7 @@ Map _parseMapNode({ baseData: baseData, baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, + handleTypes: handleTypes, ).cast(); } diff --git a/slang/lib/src/builder/decoder/arb_decoder.dart b/slang/lib/src/builder/decoder/arb_decoder.dart index 21b11915..40f24ba3 100644 --- a/slang/lib/src/builder/decoder/arb_decoder.dart +++ b/slang/lib/src/builder/decoder/arb_decoder.dart @@ -38,6 +38,7 @@ class ArbDecoder extends BaseDecoder { const _EntryMetadata( description: null, paramTypeMap: {}, + paramFormatMap: {}, ); final value = sourceMap[key]; @@ -86,7 +87,11 @@ void _addEntry({ map: resultMap, destinationPath: '$key(${isPlural ? 'plural' : 'context=${variable.toCase(CaseStyle.pascal)}'}, param=$variable).$partName', - item: _digestLeafText(partContent, metadata.paramTypeMap), + item: _digestLeafText( + partContent, + metadata.paramTypeMap, + metadata.paramFormatMap, + ), ); } return; @@ -127,16 +132,44 @@ void _addEntry({ brackets = BracketsUtils.findTopLevelBrackets(result); } - resultMap[key] = _digestLeafText(result, metadata.paramTypeMap); + resultMap[key] = _digestLeafText( + result, + metadata.paramTypeMap, + metadata.paramFormatMap, + ); } /// Transforms arguments to camel case /// Adds 'arg' to every positional argument -String _digestLeafText(String text, Map paramTypeMap) { +String _digestLeafText( + String text, + Map paramTypeMap, + Map paramFormatMap, +) { return text.replaceBracesInterpolation(replacer: (match) { final param = match.substring(1, match.length - 1); - final paramType = - paramTypeMap[param] != null ? ': ${paramTypeMap[param]}' : ''; + + final paramType = switch (paramFormatMap[param]) { + _EntryFormat format => switch (paramTypeMap[param]) { + 'DateTime' => ': DateFormat("${format.methodName}")', + _ => switch (format.parameters.isEmpty) { + true => ': NumberFormat.${format.methodName}', + false => + ': NumberFormat.${format.methodName}(${format.parameters.entries.map((e) { + final digestedValue = switch (e.value) { + String s => "'$s'", + _ => e.value, + }; + return '${e.key}: $digestedValue'; + }).join(', ')})', + }, + }, + _ => switch (paramTypeMap[param]) { + String type => ': $type', + _ => '', + }, + }; + final number = int.tryParse(param); if (number != null) { return '{arg$number$paramType}'; @@ -164,10 +197,12 @@ String _digestPluralKey(String key) { class _EntryMetadata { final String? description; final Map paramTypeMap; + final Map paramFormatMap; const _EntryMetadata({ required this.description, required this.paramTypeMap, + required this.paramFormatMap, }); static _EntryMetadata parseEntry(Map map) { @@ -178,6 +213,7 @@ class _EntryMetadata { return _EntryMetadata( description: description, paramTypeMap: {}, + paramFormatMap: {}, ); } @@ -190,13 +226,37 @@ class _EntryMetadata { } } + final paramFormatMap = {}; + for (final key in placeholders.keys) { + final value = placeholders[key] as Map; + final format = value['format'] as String?; + if (format != null) { + final parameters = value['optionalParameters'] as Map?; + paramFormatMap[key] = _EntryFormat( + methodName: format, + parameters: parameters ?? {}, + ); + } + } + return _EntryMetadata( description: description, paramTypeMap: paramTypeMap, + paramFormatMap: paramFormatMap, ); } } +class _EntryFormat { + final String methodName; + final Map parameters; + + _EntryFormat({ + required this.methodName, + required this.parameters, + }); +} + class _DistinctNameFactory { final existingNames = {}; diff --git a/slang/lib/src/builder/model/node.dart b/slang/lib/src/builder/model/node.dart index 6182b536..0559f684 100644 --- a/slang/lib/src/builder/model/node.dart +++ b/slang/lib/src/builder/model/node.dart @@ -246,6 +246,7 @@ abstract class TextNode extends Node implements LeafNode { /// Several configs, persisted into node to make it easier to copy /// See [updateWithLinkParams] final bool shouldEscape; + final bool handleTypes; final StringInterpolation interpolation; final CaseStyle? paramCase; @@ -258,6 +259,7 @@ abstract class TextNode extends Node implements LeafNode { required this.types, required this.raw, required this.shouldEscape, + required this.handleTypes, required this.interpolation, required this.paramCase, }); @@ -306,6 +308,7 @@ class StringTextNode extends TextNode { required super.raw, required super.comment, required super.shouldEscape, + required super.handleTypes, required super.interpolation, required super.paramCase, Map>? linkParamMap, @@ -317,6 +320,7 @@ class StringTextNode extends TextNode { interpolation: interpolation, defaultType: 'Object', paramCase: paramCase, + digestParameter: handleTypes && true, ); _params = parsedResult.params.keys.toSet(); _paramTypeMap.addAll(parsedResult.params); @@ -352,6 +356,7 @@ class StringTextNode extends TextNode { raw: raw, comment: comment, shouldEscape: shouldEscape, + handleTypes: handleTypes, interpolation: interpolation, paramCase: paramCase, linkParamMap: linkParamMap, @@ -404,6 +409,7 @@ class RichTextNode extends TextNode { required super.raw, required super.comment, required super.shouldEscape, + required super.handleTypes, required super.interpolation, required super.paramCase, Map>? linkParamMap, @@ -416,6 +422,7 @@ class RichTextNode extends TextNode { defaultType: 'ignored', // types are ignored paramCase: null, // param case will be applied later + digestParameter: false, ); _params = {}; @@ -491,6 +498,7 @@ class RichTextNode extends TextNode { raw: raw, comment: comment, shouldEscape: shouldEscape, + handleTypes: handleTypes, interpolation: interpolation, paramCase: paramCase, linkParamMap: linkParamMap, @@ -558,12 +566,18 @@ _ParseInterpolationResult _parseInterpolation({ required StringInterpolation interpolation, required String defaultType, required CaseStyle? paramCase, + required bool digestParameter, }) { final String parsedContent; final params = {}; final formats = {}; String convertInnerParam(String inner) { + if (!digestParameter) { + params[inner] = 'is ignored'; + return '\${$inner}'; + } + final parsedParam = parseParam( rawParam: inner, defaultType: defaultType, diff --git a/slang/lib/src/builder/utils/regex_utils.dart b/slang/lib/src/builder/utils/regex_utils.dart index 2d602927..8e33a1f9 100644 --- a/slang/lib/src/builder/utils/regex_utils.dart +++ b/slang/lib/src/builder/utils/regex_utils.dart @@ -2,20 +2,21 @@ class RegexUtils { /// matches $argument or ${argument} but not \$argument /// 1 = argument of $argument /// 2 = argument of ${argument} - static RegExp argumentsDartRegex = RegExp(r'(?|,)+)(\?)? (\w+)(\(.+\))?$'); /// Matches the generic of the list /// List /// 1 - MyGeneric - static RegExp genericRegex = RegExp(r'^List<((?:\w| |<|>)+)>$'); + static final RegExp genericRegex = RegExp(r'^List<((?:\w| |<|>)+)>$'); /// Matches the modifier part in a key if it exists /// greet(plural, param=gender) /// 1 - greet /// 2 - plural, param=gender - static RegExp modifierRegex = RegExp(r'^(\w+)\((.+)\)$'); + static final RegExp modifierRegex = RegExp(r'^(\w+)\((.+)\)$'); - static RegExp spaceRegex = RegExp(r'\s+'); + /// Matches a format type expression with optional parameters + /// + /// NumberFormat.currency(cool: 334) + /// 1 - NumberFormat.currency + /// 2 - cool: 334 + /// + /// currency + /// 1 - currency + static final RegExp formatTypeRegex = RegExp(r'^([\w.]+)(?:\((.+)\))?$'); + + static final RegExp spaceRegex = RegExp(r'\s+'); - static RegExp linkPathRegex = RegExp(r'^_root\.((?:[.\w])+)\(?'); + static final RegExp linkPathRegex = RegExp(r'^_root\.((?:[.\w])+)\(?'); /// Matches plurals or selects of format (variable,type,content) /// {sex, select, male{His birthday} female{Her birthday} other{Their birthday}} /// 1 - sex /// 2 - select /// 3 - male{His birthday} female{Her birthday} other{Their birthday} - static RegExp arbComplexNode = RegExp(r'^{((?: |\w)+),((?: |\w)+),(.+)}$'); + static final RegExp arbComplexNode = + RegExp(r'^{((?: |\w)+),((?: |\w)+),(.+)}$'); /// Matches the parts of the content /// male{His birthday} female{Her birthday} other{Their birthday} @@ -78,7 +90,7 @@ class RegexUtils { /// /// 1 - male /// 2 - His birthday - static RegExp arbComplexNodeContent = + static final RegExp arbComplexNodeContent = RegExp(r'((?:=|\w)+) *{((?:[^}{]+|{[^}]+})+)}'); /// Matches any missing translations file @@ -88,6 +100,6 @@ class RegexUtils { /// 1 - missing_translations or unused_translations /// 2 - de-DE /// 3 - json - static RegExp analysisFileRegex = RegExp( + static final RegExp analysisFileRegex = RegExp( r'^_(missing_translations|unused_translations)(?:_(.*))?\.(json|yaml|csv)$'); } diff --git a/slang/test/unit/api/translation_overrides_test.dart b/slang/test/unit/api/translation_overrides_test.dart index 7190300d..920f31b5 100644 --- a/slang/test/unit/api/translation_overrides_test.dart +++ b/slang/test/unit/api/translation_overrides_test.dart @@ -1,3 +1,6 @@ +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/node.dart'; import 'package:slang/src/api/locale.dart'; import 'package:slang/src/api/singleton.dart'; import 'package:slang/src/api/translation_overrides.dart'; @@ -6,33 +9,37 @@ import 'package:slang/src/builder/model/raw_config.dart'; import 'package:test/test.dart'; void main() { + setUpAll(() { + initializeDateFormatting(); + }); + group('string', () { - test('Should return a plain string', () async { - final meta = await _buildMetaWithOverrides({ + test('Should return a plain string', () { + final meta = _buildMetaWithOverrides({ 'aboutPage.title': 'About', }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', {}); expect(parsed, 'About'); }); - test('Should not escape new line', () async { - final meta = await _buildMetaWithOverrides({ + test('Should not escape new line', () { + final meta = _buildMetaWithOverrides({ 'aboutPage.title': 'About\nPage', }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', {}); expect(parsed, 'About\nPage'); }); - test('Should return a plain string without escaping', () async { - final meta = await _buildMetaWithOverrides({ + test('Should return a plain string without escaping', () { + final meta = _buildMetaWithOverrides({ 'aboutPage.title': 'About \' \$ {arg}', }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', {}); expect(parsed, 'About \' \$ {arg}'); }); - test('Should return an interpolated string', () async { - final meta = await _buildMetaWithOverrides({ + test('Should return an interpolated string', () { + final meta = _buildMetaWithOverrides({ 'aboutPage.title': r'About ${arg}', }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { @@ -41,8 +48,8 @@ void main() { expect(parsed, 'About Page'); }); - test('Should ignore type in interpolated string', () async { - final meta = await _buildMetaWithOverrides({ + test('Should ignore type in interpolated string', () { + final meta = _buildMetaWithOverrides({ 'aboutPage.title': r'About ${arg: int}', }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { @@ -51,8 +58,8 @@ void main() { expect(parsed, 'About Page'); }); - test('Should return an interpolated string with dollar only', () async { - final meta = await _buildMetaWithOverrides({ + test('Should return an interpolated string with dollar only', () { + final meta = _buildMetaWithOverrides({ 'aboutPage.title': r'About $arg', }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { @@ -60,16 +67,90 @@ void main() { }); expect(parsed, 'About Page'); }); + + test('Should return string with custom DateFormat', () { + final meta = _buildMetaWithOverrides({ + 'aboutPage.title': r"About ${date: DateFormat('dd-MM')}", + }); + final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { + 'date': DateTime(2022, 3, 12), + }); + expect(parsed, 'About 12-03'); + }); + + test('Should return string with built-in DateFormat', () { + final meta = _buildMetaWithOverrides({ + 'aboutPage.title': r'About ${date: yMd}', + }); + final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { + 'date': DateTime(2022, 3, 12), + }); + expect(parsed, 'About 3/12/2022'); + }); + + test('Should return string with predefined type', () { + final meta = _buildMetaWithOverrides({ + 'aboutPage.title': r'About ${date: predefined}', + }); + final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { + 'date': DateTime(2022, 3, 12), + }); + expect(parsed, 'About 2022'); + }); + + test('Should return string with custom NumberFormat', () { + final meta = _buildMetaWithOverrides({ + 'aboutPage.title': r'About ${number: NumberFormat("000.##")}', + }); + final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { + 'number': 3.14, + }); + expect(parsed, 'About 003.14'); + }); + + test('Should return string with built-in NumberFormat', () { + final meta = _buildMetaWithOverrides({ + 'aboutPage.title': r'About ${number: decimalPattern}', + }); + final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { + 'number': 3.14, + }); + expect(parsed, 'About 3.14'); + }); + + test('Should return string with built-in NumberFormat with parameters', () { + final meta = _buildMetaWithOverrides({ + 'aboutPage.title': + r'About ${number: NumberFormat.currency(symbol: "RR")}', + }); + final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { + 'number': 3.14, + }); + expect(parsed, 'About RR3.14'); + }); + + test('Should return string with NumberFormat with parameters in DE', () { + final meta = _buildMetaWithOverrides({ + 'aboutPage.title': + r'About ${number: NumberFormat.currency(symbol: "RR")}', + }, locale: 'de'); + final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { + 'number': 3.14, + }); + expect(parsed, 'About 3,14 RR'); + }); }); } -Future> - _buildMetaWithOverrides( - Map overrides, -) async { +TranslationMetadata _buildMetaWithOverrides( + Map overrides, { + String? locale, +}) { final utils = _Utils(); - final translations = await utils.buildWithOverridesFromMap( - locale: FakeAppLocale(languageCode: 'und'), + final translations = utils.buildWithOverridesFromMapSync( + locale: FakeAppLocale(languageCode: locale ?? 'en', types: { + 'predefined': ValueFormatter(() => DateFormat('yyyy')), + }), isFlatMap: false, map: overrides, ); @@ -79,8 +160,8 @@ Future> class _Utils extends BaseAppLocaleUtils { _Utils() : super( - baseLocale: FakeAppLocale(languageCode: 'und'), - locales: [FakeAppLocale(languageCode: 'und')], + baseLocale: FakeAppLocale(languageCode: 'en'), + locales: [FakeAppLocale(languageCode: 'en')], buildConfig: _defaultConfig, ); } diff --git a/slang/test/unit/builder/text/l10n_override_parser_test.dart b/slang/test/unit/builder/text/l10n_override_parser_test.dart new file mode 100644 index 00000000..3948d924 --- /dev/null +++ b/slang/test/unit/builder/text/l10n_override_parser_test.dart @@ -0,0 +1,79 @@ +import 'package:intl/date_symbol_data_local.dart'; +import 'package:slang/src/builder/builder/text/l10n_override_parser.dart'; +import 'package:test/test.dart'; + +void main() { + setUpAll(() { + initializeDateFormatting(); + }); + + test('Should skip unknown type', () { + final p = digestL10nOverride( + existingTypes: {}, + locale: 'en', + type: 'lol', + value: 33, + ); + expect(p, isNull); + }); + + test('Should parse shorthand number format', () { + final p = digestL10nOverride( + existingTypes: {}, + locale: 'en', + type: 'currency', + value: 33, + ); + expect(p!, r'USD33.00'); + }); + + test('Should parse full number format', () { + final p = digestL10nOverride( + existingTypes: {}, + locale: 'en', + type: 'NumberFormat.currency', + value: 33, + ); + expect(p!, r'USD33.00'); + }); + + test('Should parse custom number format', () { + final p = digestL10nOverride( + existingTypes: {}, + locale: 'en', + type: 'NumberFormat("000.00")', + value: 33, + ); + expect(p!, r'033.00'); + }); + + test('Should parse number format with parameters', () { + final p = digestL10nOverride( + existingTypes: {}, + locale: 'en', + type: 'NumberFormat.currency(decimalDigits: 3, symbol: "€")', + value: 33, + ); + expect(p!, r'€33.000'); + }); + + test('Should parse shorthand date format', () { + final p = digestL10nOverride( + existingTypes: {}, + locale: 'en', + type: 'yMd', + value: DateTime(2021, 12, 31), + ); + expect(p!, '12/31/2021'); + }); + + test('Should parse custom date format', () { + final p = digestL10nOverride( + existingTypes: {}, + locale: 'en', + type: 'DateFormat("yyyy-MM-dd")', + value: DateTime(2021, 12, 31), + ); + expect(p!, '2021-12-31'); + }); +} diff --git a/slang/test/unit/builder/text/param_parser_test.dart b/slang/test/unit/builder/text/param_parser_test.dart index aa9442fe..8026bb58 100644 --- a/slang/test/unit/builder/text/param_parser_test.dart +++ b/slang/test/unit/builder/text/param_parser_test.dart @@ -33,16 +33,6 @@ void main() { expect(result.paramName, 'MyName'); expect(result.paramType, 'DefaultType'); }); - - test('Should ignore rich text default parameter', () { - final result = parseParam( - rawParam: 'hello(Hi)', - caseStyle: null, - defaultType: 'DefaultType', - ); - expect(result.paramName, 'hello(Hi)'); - expect(result.paramType, ''); - }); }); group('parseParamWithArg', () { diff --git a/slang/test/unit/decoder/arb_decoder_test.dart b/slang/test/unit/decoder/arb_decoder_test.dart index 15aea872..3b677ad4 100644 --- a/slang/test/unit/decoder/arb_decoder_test.dart +++ b/slang/test/unit/decoder/arb_decoder_test.dart @@ -57,6 +57,68 @@ void main() { ); }); + test('Should decode with DateFormat', () { + expect( + _decodeArb({ + 'today': 'Today is {date}', + '@today': { + 'placeholders': { + 'date': { + 'type': 'DateTime', + 'format': 'yMd', + }, + }, + } + }), + { + 'today': 'Today is {date: DateFormat("yMd")}', + }, + ); + }); + + test('Should decode with NumberFormat', () { + expect( + _decodeArb({ + 'price': 'Price: {price}', + '@price': { + 'placeholders': { + 'price': { + 'type': 'num', + 'format': 'currency', + }, + }, + } + }), + { + 'price': 'Price: {price: NumberFormat.currency}', + }, + ); + }); + + test('Should decode with NumberFormat and parameters', () { + expect( + _decodeArb({ + 'price': 'Price: {price}', + '@price': { + 'placeholders': { + 'price': { + 'type': 'num', + 'format': 'currency', + 'optionalParameters': { + 'symbol': '€', + 'decimalDigits': 4, + }, + }, + }, + } + }), + { + 'price': + "Price: {price: NumberFormat.currency(symbol: '€', decimalDigits: 4)}", + }, + ); + }); + test('Should decode plural string identifiers', () { expect( _decodeArb({ diff --git a/slang/test/unit/utils/regex_utils_test.dart b/slang/test/unit/utils/regex_utils_test.dart index cf5c2e8d..af52be9f 100644 --- a/slang/test/unit/utils/regex_utils_test.dart +++ b/slang/test/unit/utils/regex_utils_test.dart @@ -185,6 +185,27 @@ void main() { }); }); + group('formatTypeRegex', () { + RegExp regex = RegexUtils.formatTypeRegex; + + test('NumberFormat.currency', () { + RegExpMatch? match = regex.firstMatch('NumberFormat.currency'); + expect(match?.group(1), 'NumberFormat.currency'); + expect(match?.group(2), null); + }); + + test('NumberFormat.currency(cool: 334)', () { + RegExpMatch? match = regex.firstMatch('NumberFormat.currency(cool: 334)'); + expect(match?.group(1), 'NumberFormat.currency'); + expect(match?.group(2), 'cool: 334'); + }); + + test('NumberFormat.currency()', () { + RegExpMatch? match = regex.firstMatch('NumberFormat.currency()'); + expect(match, isNull); + }); + }); + group('arbComplexNode', () { RegExp regex = RegexUtils.arbComplexNode; diff --git a/slang/test/util/text_node_builder.dart b/slang/test/util/text_node_builder.dart index 603abc73..d007b9e9 100644 --- a/slang/test/util/text_node_builder.dart +++ b/slang/test/util/text_node_builder.dart @@ -21,6 +21,7 @@ StringTextNode textNode( interpolation: interpolation, paramCase: paramCase, shouldEscape: true, + handleTypes: true, linkParamMap: linkParamMap, ); } @@ -42,6 +43,7 @@ RichTextNode richTextNode( interpolation: interpolation, paramCase: paramCase, shouldEscape: true, + handleTypes: true, linkParamMap: linkParamMap, ); } From fb24c20b6866260886808b8277a36635e58afe6a Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 00:56:07 +0200 Subject: [PATCH 079/118] fix: compilation error on web with complex interfaces --- .../builder/generator/generate_header.dart | 46 ++++++---- slang/lib/src/builder/generator/helper.dart | 2 +- ..._expected_fallback_base_locale_main.output | 92 +++++++++++++++++-- ...d_fallback_base_locale_special_main.output | 8 +- .../resources/main/_expected_main.output | 92 +++++++++++++++++-- .../main/_expected_obfuscation_main.output | 92 +++++++++++++++++-- ...expected_translation_overrides_main.output | 92 +++++++++++++++++-- 7 files changed, 363 insertions(+), 61 deletions(-) diff --git a/slang/lib/src/builder/generator/generate_header.dart b/slang/lib/src/builder/generator/generate_header.dart index 9454ee7d..a82bea18 100644 --- a/slang/lib/src/builder/generator/generate_header.dart +++ b/slang/lib/src/builder/generator/generate_header.dart @@ -588,28 +588,42 @@ void _generateInterfaces({ // equals override buffer.writeln(); buffer.writeln('\t@override'); - buffer.write( - '\tbool operator ==(Object other) => other is ${interface.name}'); - for (final attribute in interface.attributes) { - buffer.write( - ' && ${attribute.attributeName} == other.${attribute.attributeName}'); - } - buffer.writeln(';'); + buffer.writeln('\tbool operator ==(Object other) {'); + buffer.writeln('\t\tif (identical(this, other)) return true;'); + buffer.writeln('\t\tif (other is! ${interface.name}) return false;'); + + buffer.writeln(); + buffer.writeln('\t\tfinal fields = _fields;'); + buffer.writeln('\t\tfinal otherFields = other._fields;'); + buffer.writeln('\t\tfor (int i = 0; i < fields.length; i++) {'); + buffer.writeln('\t\t\tif (fields[i] != otherFields[i]) return false;'); + buffer.writeln('\t\t}'); + + buffer.writeln(); + buffer.writeln('\t\treturn true;'); + buffer.writeln('\t}'); // hashCode override buffer.writeln(); buffer.writeln('\t@override'); - buffer.write('\tint get hashCode => '); - bool multiply = false; + buffer.writeln('\tint get hashCode {'); + buffer.writeln('\t\tfinal fields = _fields;'); + buffer.writeln('\t\tint result = fields.first.hashCode;'); + buffer.writeln('\t\tfor (final element in fields.skip(1)) {'); + buffer.writeln('\t\t\tresult *= element.hashCode;'); + buffer.writeln('\t\t}'); + + buffer.writeln(); + buffer.writeln('\t\treturn result;'); + buffer.writeln('\t}'); + + // fields + buffer.writeln(); + buffer.writeln('\tList get _fields => ['); for (final attribute in interface.attributes) { - if (multiply) { - buffer.write(' * '); - } - buffer.write(attribute.attributeName); - buffer.write('.hashCode'); - multiply = true; + buffer.writeln('\t\t${attribute.attributeName},'); } - buffer.writeln(';'); + buffer.writeln('\t];'); buffer.writeln('}'); } diff --git a/slang/lib/src/builder/generator/helper.dart b/slang/lib/src/builder/generator/helper.dart index cd16b302..63919a78 100644 --- a/slang/lib/src/builder/generator/helper.dart +++ b/slang/lib/src/builder/generator/helper.dart @@ -11,7 +11,7 @@ const String characteristicLinkPrefix = '_root.'; String getImportName({ required I18nLocale locale, }) { - return '_\$${locale.languageTag.replaceAll('-', '_')}'; + return 'l_${locale.languageTag.replaceAll('-', '_')}'; } /// Returns the class name of the root translation class. diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output index d343ddea..4018327c 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output @@ -15,7 +15,7 @@ import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; -import 'translations_de.g.dart' deferred as _$de; +import 'translations_de.g.dart' deferred as l_de; part 'translations_en.g.dart'; /// Supported locales. @@ -52,8 +52,8 @@ enum AppLocale with BaseAppLocale { ordinalResolver: ordinalResolver, ); case AppLocale.de: - await _$de.loadLibrary(); - return _$de.TranslationsDe( + await l_de.loadLibrary(); + return l_de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -75,7 +75,7 @@ enum AppLocale with BaseAppLocale { ordinalResolver: ordinalResolver, ); case AppLocale.de: - return _$de.TranslationsDe( + return l_de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -193,10 +193,34 @@ mixin PageData { String? get content => null; @override - bool operator ==(Object other) => other is PageData && title == other.title && content == other.content; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! PageData) return false; + + final fields = _fields; + final otherFields = other._fields; + for (int i = 0; i < fields.length; i++) { + if (fields[i] != otherFields[i]) return false; + } + + return true; + } @override - int get hashCode => title.hashCode * content.hashCode; + int get hashCode { + final fields = _fields; + int result = fields.first.hashCode; + for (final element in fields.skip(1)) { + result *= element.hashCode; + } + + return result; + } + + List get _fields => [ + title, + content, + ]; } mixin MPage { @@ -204,10 +228,34 @@ mixin MPage { String? get content => null; @override - bool operator ==(Object other) => other is MPage && title == other.title && content == other.content; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! MPage) return false; + + final fields = _fields; + final otherFields = other._fields; + for (int i = 0; i < fields.length; i++) { + if (fields[i] != otherFields[i]) return false; + } + + return true; + } @override - int get hashCode => title.hashCode * content.hashCode; + int get hashCode { + final fields = _fields; + int result = fields.first.hashCode; + for (final element in fields.skip(1)) { + result *= element.hashCode; + } + + return result; + } + + List get _fields => [ + title, + content, + ]; } mixin EndData { @@ -215,8 +263,32 @@ mixin EndData { List> get pages; @override - bool operator ==(Object other) => other is EndData && stringPages == other.stringPages && pages == other.pages; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! EndData) return false; + + final fields = _fields; + final otherFields = other._fields; + for (int i = 0; i < fields.length; i++) { + if (fields[i] != otherFields[i]) return false; + } + + return true; + } @override - int get hashCode => stringPages.hashCode * pages.hashCode; + int get hashCode { + final fields = _fields; + int result = fields.first.hashCode; + for (final element in fields.skip(1)) { + result *= element.hashCode; + } + + return result; + } + + List get _fields => [ + stringPages, + pages, + ]; } diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output index 212f7169..c05391fd 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output @@ -15,7 +15,7 @@ import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; -import 'translations_de.g.dart' deferred as _$de; +import 'translations_de.g.dart' deferred as l_de; part 'translations_en.g.dart'; /// Supported locales. @@ -52,8 +52,8 @@ enum AppLocale with BaseAppLocale { ordinalResolver: ordinalResolver, ); case AppLocale.de: - await _$de.loadLibrary(); - return _$de.TranslationsDe( + await l_de.loadLibrary(); + return l_de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -75,7 +75,7 @@ enum AppLocale with BaseAppLocale { ordinalResolver: ordinalResolver, ); case AppLocale.de: - return _$de.TranslationsDe( + return l_de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, diff --git a/slang/test/integration/resources/main/_expected_main.output b/slang/test/integration/resources/main/_expected_main.output index d343ddea..4018327c 100644 --- a/slang/test/integration/resources/main/_expected_main.output +++ b/slang/test/integration/resources/main/_expected_main.output @@ -15,7 +15,7 @@ import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; -import 'translations_de.g.dart' deferred as _$de; +import 'translations_de.g.dart' deferred as l_de; part 'translations_en.g.dart'; /// Supported locales. @@ -52,8 +52,8 @@ enum AppLocale with BaseAppLocale { ordinalResolver: ordinalResolver, ); case AppLocale.de: - await _$de.loadLibrary(); - return _$de.TranslationsDe( + await l_de.loadLibrary(); + return l_de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -75,7 +75,7 @@ enum AppLocale with BaseAppLocale { ordinalResolver: ordinalResolver, ); case AppLocale.de: - return _$de.TranslationsDe( + return l_de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -193,10 +193,34 @@ mixin PageData { String? get content => null; @override - bool operator ==(Object other) => other is PageData && title == other.title && content == other.content; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! PageData) return false; + + final fields = _fields; + final otherFields = other._fields; + for (int i = 0; i < fields.length; i++) { + if (fields[i] != otherFields[i]) return false; + } + + return true; + } @override - int get hashCode => title.hashCode * content.hashCode; + int get hashCode { + final fields = _fields; + int result = fields.first.hashCode; + for (final element in fields.skip(1)) { + result *= element.hashCode; + } + + return result; + } + + List get _fields => [ + title, + content, + ]; } mixin MPage { @@ -204,10 +228,34 @@ mixin MPage { String? get content => null; @override - bool operator ==(Object other) => other is MPage && title == other.title && content == other.content; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! MPage) return false; + + final fields = _fields; + final otherFields = other._fields; + for (int i = 0; i < fields.length; i++) { + if (fields[i] != otherFields[i]) return false; + } + + return true; + } @override - int get hashCode => title.hashCode * content.hashCode; + int get hashCode { + final fields = _fields; + int result = fields.first.hashCode; + for (final element in fields.skip(1)) { + result *= element.hashCode; + } + + return result; + } + + List get _fields => [ + title, + content, + ]; } mixin EndData { @@ -215,8 +263,32 @@ mixin EndData { List> get pages; @override - bool operator ==(Object other) => other is EndData && stringPages == other.stringPages && pages == other.pages; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! EndData) return false; + + final fields = _fields; + final otherFields = other._fields; + for (int i = 0; i < fields.length; i++) { + if (fields[i] != otherFields[i]) return false; + } + + return true; + } @override - int get hashCode => stringPages.hashCode * pages.hashCode; + int get hashCode { + final fields = _fields; + int result = fields.first.hashCode; + for (final element in fields.skip(1)) { + result *= element.hashCode; + } + + return result; + } + + List get _fields => [ + stringPages, + pages, + ]; } diff --git a/slang/test/integration/resources/main/_expected_obfuscation_main.output b/slang/test/integration/resources/main/_expected_obfuscation_main.output index cfb47293..dc3c4317 100644 --- a/slang/test/integration/resources/main/_expected_obfuscation_main.output +++ b/slang/test/integration/resources/main/_expected_obfuscation_main.output @@ -16,7 +16,7 @@ import 'package:slang/secret.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; -import 'translations_de.g.dart' deferred as _$de; +import 'translations_de.g.dart' deferred as l_de; part 'translations_en.g.dart'; /// Supported locales. @@ -53,8 +53,8 @@ enum AppLocale with BaseAppLocale { ordinalResolver: ordinalResolver, ); case AppLocale.de: - await _$de.loadLibrary(); - return _$de.TranslationsDe( + await l_de.loadLibrary(); + return l_de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -76,7 +76,7 @@ enum AppLocale with BaseAppLocale { ordinalResolver: ordinalResolver, ); case AppLocale.de: - return _$de.TranslationsDe( + return l_de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -194,10 +194,34 @@ mixin PageData { String? get content => null; @override - bool operator ==(Object other) => other is PageData && title == other.title && content == other.content; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! PageData) return false; + + final fields = _fields; + final otherFields = other._fields; + for (int i = 0; i < fields.length; i++) { + if (fields[i] != otherFields[i]) return false; + } + + return true; + } @override - int get hashCode => title.hashCode * content.hashCode; + int get hashCode { + final fields = _fields; + int result = fields.first.hashCode; + for (final element in fields.skip(1)) { + result *= element.hashCode; + } + + return result; + } + + List get _fields => [ + title, + content, + ]; } mixin MPage { @@ -205,10 +229,34 @@ mixin MPage { String? get content => null; @override - bool operator ==(Object other) => other is MPage && title == other.title && content == other.content; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! MPage) return false; + + final fields = _fields; + final otherFields = other._fields; + for (int i = 0; i < fields.length; i++) { + if (fields[i] != otherFields[i]) return false; + } + + return true; + } @override - int get hashCode => title.hashCode * content.hashCode; + int get hashCode { + final fields = _fields; + int result = fields.first.hashCode; + for (final element in fields.skip(1)) { + result *= element.hashCode; + } + + return result; + } + + List get _fields => [ + title, + content, + ]; } mixin EndData { @@ -216,8 +264,32 @@ mixin EndData { List> get pages; @override - bool operator ==(Object other) => other is EndData && stringPages == other.stringPages && pages == other.pages; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! EndData) return false; + + final fields = _fields; + final otherFields = other._fields; + for (int i = 0; i < fields.length; i++) { + if (fields[i] != otherFields[i]) return false; + } + + return true; + } @override - int get hashCode => stringPages.hashCode * pages.hashCode; + int get hashCode { + final fields = _fields; + int result = fields.first.hashCode; + for (final element in fields.skip(1)) { + result *= element.hashCode; + } + + return result; + } + + List get _fields => [ + stringPages, + pages, + ]; } diff --git a/slang/test/integration/resources/main/_expected_translation_overrides_main.output b/slang/test/integration/resources/main/_expected_translation_overrides_main.output index dd54d2d9..2833746f 100644 --- a/slang/test/integration/resources/main/_expected_translation_overrides_main.output +++ b/slang/test/integration/resources/main/_expected_translation_overrides_main.output @@ -16,7 +16,7 @@ import 'package:slang/overrides.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; -import 'translations_de.g.dart' deferred as _$de; +import 'translations_de.g.dart' deferred as l_de; part 'translations_en.g.dart'; /// Generated by the "Translation Overrides" feature. @@ -71,8 +71,8 @@ enum AppLocale with BaseAppLocale { ordinalResolver: ordinalResolver, ); case AppLocale.de: - await _$de.loadLibrary(); - return _$de.TranslationsDe( + await l_de.loadLibrary(); + return l_de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -94,7 +94,7 @@ enum AppLocale with BaseAppLocale { ordinalResolver: ordinalResolver, ); case AppLocale.de: - return _$de.TranslationsDe( + return l_de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -221,10 +221,34 @@ mixin PageData { String? get content => null; @override - bool operator ==(Object other) => other is PageData && title == other.title && content == other.content; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! PageData) return false; + + final fields = _fields; + final otherFields = other._fields; + for (int i = 0; i < fields.length; i++) { + if (fields[i] != otherFields[i]) return false; + } + + return true; + } @override - int get hashCode => title.hashCode * content.hashCode; + int get hashCode { + final fields = _fields; + int result = fields.first.hashCode; + for (final element in fields.skip(1)) { + result *= element.hashCode; + } + + return result; + } + + List get _fields => [ + title, + content, + ]; } mixin MPage { @@ -232,10 +256,34 @@ mixin MPage { String? get content => null; @override - bool operator ==(Object other) => other is MPage && title == other.title && content == other.content; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! MPage) return false; + + final fields = _fields; + final otherFields = other._fields; + for (int i = 0; i < fields.length; i++) { + if (fields[i] != otherFields[i]) return false; + } + + return true; + } @override - int get hashCode => title.hashCode * content.hashCode; + int get hashCode { + final fields = _fields; + int result = fields.first.hashCode; + for (final element in fields.skip(1)) { + result *= element.hashCode; + } + + return result; + } + + List get _fields => [ + title, + content, + ]; } mixin EndData { @@ -243,8 +291,32 @@ mixin EndData { List> get pages; @override - bool operator ==(Object other) => other is EndData && stringPages == other.stringPages && pages == other.pages; + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! EndData) return false; + + final fields = _fields; + final otherFields = other._fields; + for (int i = 0; i < fields.length; i++) { + if (fields[i] != otherFields[i]) return false; + } + + return true; + } @override - int get hashCode => stringPages.hashCode * pages.hashCode; + int get hashCode { + final fields = _fields; + int result = fields.first.hashCode; + for (final element in fields.skip(1)) { + result *= element.hashCode; + } + + return result; + } + + List get _fields => [ + stringPages, + pages, + ]; } From a912469dbab997b7f5e12f9c11200f8fd36e4cbb Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 01:02:31 +0200 Subject: [PATCH 080/118] docs: update example --- slang/example/lib/i18n/strings.g.dart | 19 ++++++++++--------- slang/example/lib/i18n/strings.i18n.json | 2 +- slang/example/lib/i18n/strings_de.g.dart | 5 +++-- slang/example/lib/i18n/strings_de.i18n.json | 2 +- slang/example/lib/i18n/strings_en.g.dart | 4 ++-- slang/example/lib/i18n/strings_fr_FR.g.dart | 5 +++-- .../example/lib/i18n/strings_fr_FR.i18n.json | 2 +- slang/example/pubspec.yaml | 2 +- 8 files changed, 22 insertions(+), 19 deletions(-) diff --git a/slang/example/lib/i18n/strings.g.dart b/slang/example/lib/i18n/strings.g.dart index fa491fc7..752b061e 100644 --- a/slang/example/lib/i18n/strings.g.dart +++ b/slang/example/lib/i18n/strings.g.dart @@ -6,18 +6,19 @@ /// Locales: 3 /// Strings: 21 (7 per locale) /// -/// Built on 2024-10-18 at 01:08 UTC +/// Built on 2024-10-20 at 23:01 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; -import 'strings_de.g.dart' deferred as _$de; -import 'strings_fr_FR.g.dart' deferred as _$fr_FR; +import 'strings_de.g.dart' deferred as l_de; +import 'strings_fr_FR.g.dart' deferred as l_fr_FR; part 'strings_en.g.dart'; /// Supported locales. @@ -55,15 +56,15 @@ enum AppLocale with BaseAppLocale { ordinalResolver: ordinalResolver, ); case AppLocale.de: - await _$de.loadLibrary(); - return _$de.TranslationsDe( + await l_de.loadLibrary(); + return l_de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, ); case AppLocale.frFr: - await _$fr_FR.loadLibrary(); - return _$fr_FR.TranslationsFrFr( + await l_fr_FR.loadLibrary(); + return l_fr_FR.TranslationsFrFr( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, @@ -85,13 +86,13 @@ enum AppLocale with BaseAppLocale { ordinalResolver: ordinalResolver, ); case AppLocale.de: - return _$de.TranslationsDe( + return l_de.TranslationsDe( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, ); case AppLocale.frFr: - return _$fr_FR.TranslationsFrFr( + return l_fr_FR.TranslationsFrFr( overrides: overrides, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, diff --git a/slang/example/lib/i18n/strings.i18n.json b/slang/example/lib/i18n/strings.i18n.json index 45b233bb..814873b0 100644 --- a/slang/example/lib/i18n/strings.i18n.json +++ b/slang/example/lib/i18n/strings.i18n.json @@ -10,6 +10,6 @@ "locales(map)": { "en": "English", "de": "German", - "fr": "French" + "fr-FR": "French" } } \ No newline at end of file diff --git a/slang/example/lib/i18n/strings_de.g.dart b/slang/example/lib/i18n/strings_de.g.dart index 984c236f..e75cf975 100644 --- a/slang/example/lib/i18n/strings_de.g.dart +++ b/slang/example/lib/i18n/strings_de.g.dart @@ -5,6 +5,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'strings.g.dart'; @@ -36,7 +37,7 @@ class TranslationsDe implements Translations { @override Map get locales => { 'en': 'Englisch', 'de': 'Deutsch', - 'fr': 'Französisch', + 'fr-FR': 'Französisch', }; } @@ -68,7 +69,7 @@ extension on TranslationsDe { case 'mainScreen.tapMe': return 'Drück mich'; case 'locales.en': return 'Englisch'; case 'locales.de': return 'Deutsch'; - case 'locales.fr': return 'Französisch'; + case 'locales.fr-FR': return 'Französisch'; default: return null; } } diff --git a/slang/example/lib/i18n/strings_de.i18n.json b/slang/example/lib/i18n/strings_de.i18n.json index 3bb4ea9d..5ee4fb6d 100644 --- a/slang/example/lib/i18n/strings_de.i18n.json +++ b/slang/example/lib/i18n/strings_de.i18n.json @@ -10,6 +10,6 @@ "locales(map)": { "en": "Englisch", "de": "Deutsch", - "fr": "Französisch" + "fr-FR": "Französisch" } } \ No newline at end of file diff --git a/slang/example/lib/i18n/strings_en.g.dart b/slang/example/lib/i18n/strings_en.g.dart index 16e41df4..90738ae7 100644 --- a/slang/example/lib/i18n/strings_en.g.dart +++ b/slang/example/lib/i18n/strings_en.g.dart @@ -41,7 +41,7 @@ class Translations implements BaseTranslations { Map get locales => { 'en': 'English', 'de': 'German', - 'fr': 'French', + 'fr-FR': 'French', }; } @@ -73,7 +73,7 @@ extension on Translations { case 'mainScreen.tapMe': return 'Tap me'; case 'locales.en': return 'English'; case 'locales.de': return 'German'; - case 'locales.fr': return 'French'; + case 'locales.fr-FR': return 'French'; default: return null; } } diff --git a/slang/example/lib/i18n/strings_fr_FR.g.dart b/slang/example/lib/i18n/strings_fr_FR.g.dart index c0e2c8d1..362e4c2e 100644 --- a/slang/example/lib/i18n/strings_fr_FR.g.dart +++ b/slang/example/lib/i18n/strings_fr_FR.g.dart @@ -5,6 +5,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:slang/node.dart'; import 'strings.g.dart'; @@ -36,7 +37,7 @@ class TranslationsFrFr implements Translations { @override Map get locales => { 'en': 'Anglais', 'de': 'Allemand', - 'fr': 'Français', + 'fr-FR': 'Français', }; } @@ -68,7 +69,7 @@ extension on TranslationsFrFr { case 'mainScreen.tapMe': return 'Appuyez-moi'; case 'locales.en': return 'Anglais'; case 'locales.de': return 'Allemand'; - case 'locales.fr': return 'Français'; + case 'locales.fr-FR': return 'Français'; default: return null; } } diff --git a/slang/example/lib/i18n/strings_fr_FR.i18n.json b/slang/example/lib/i18n/strings_fr_FR.i18n.json index 2d03a48d..cd6db2ee 100644 --- a/slang/example/lib/i18n/strings_fr_FR.i18n.json +++ b/slang/example/lib/i18n/strings_fr_FR.i18n.json @@ -10,6 +10,6 @@ "locales(map)": { "en": "Anglais", "de": "Allemand", - "fr": "Français" + "fr-FR": "Français" } } \ No newline at end of file diff --git a/slang/example/pubspec.yaml b/slang/example/pubspec.yaml index ff677fd4..ebf20015 100644 --- a/slang/example/pubspec.yaml +++ b/slang/example/pubspec.yaml @@ -6,7 +6,7 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=3.3.0 <4.0.0" dependencies: flutter: From e94f93ad62f9b6dbc9a504bb15faa21216af9ff2 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 01:16:34 +0200 Subject: [PATCH 081/118] fix: trim context enum keys --- .../builder/builder/translation_model_builder.dart | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/slang/lib/src/builder/builder/translation_model_builder.dart b/slang/lib/src/builder/builder/translation_model_builder.dart index 99364586..e18499ff 100644 --- a/slang/lib/src/builder/builder/translation_model_builder.dart +++ b/slang/lib/src/builder/builder/translation_model_builder.dart @@ -416,7 +416,7 @@ Map _parseMapNode({ // split! // {one,two: hi} -> {one: hi, two: hi} for (final newChild in split) { - digestedMap[newChild] = entry.value as StringTextNode; + digestedMap[newChild.trim()] = entry.value as StringTextNode; } } } @@ -464,6 +464,7 @@ Map _parseMapNode({ final baseContext = baseContexts?[context.enumName]; if (baseContext != null) { digestedMap = _digestContextEntries( + locale: locale, baseTranslation: baseData!.root, baseContext: baseContext, path: currPath, @@ -877,6 +878,7 @@ void _fixEmptyLists({ /// Makes sure that every enum value in [baseContext] is also present in [entries]. /// If a value is missing, the base translation is used. Map _digestContextEntries({ + required I18nLocale locale, required ObjectNode baseTranslation, required PopulatedContextType baseContext, required String path, @@ -887,10 +889,18 @@ Map _digestContextEntries({ _findContextNode(baseTranslation, path.split('.')); return { for (final value in baseContext.enumValues) - value: entries[value] ?? baseContextNode.entries[value]!, + value: entries[value] ?? + baseContextNode.entries[value] ?? + _throwError( + 'In <${locale.languageTag}>, the value for $value in $path is missing (required by ${baseContext.enumName})', + ), }; } +Never _throwError(String message) { + throw message; +} + /// Recursively find the [ContextNode] using the given [path]. ContextNode _findContextNode(ObjectNode node, List path) { final child = node.entries[path[0]]; From 116d01e26c35cb311c39dab7b2569a2509e14a26 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 01:35:30 +0200 Subject: [PATCH 082/118] fix: analyze command should detect missing enums --- slang/lib/src/runner/analyze.dart | 33 +++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/slang/lib/src/runner/analyze.dart b/slang/lib/src/runner/analyze.dart index 860e9cd6..8fc8fbd2 100644 --- a/slang/lib/src/runner/analyze.dart +++ b/slang/lib/src/runner/analyze.dart @@ -180,12 +180,25 @@ void _getMissingTranslationsForOneLocaleRecursive({ final isOutdated = handleOutdated && currChild?.modifiers.containsKey(NodeModifiers.outdated) == true; if (isOutdated || !_checkEquality(baseChild, currChild)) { - // Add whole base node which is expected - _addNodeRecursive( - node: baseChild, - resultMap: resultMap, - addOutdatedModifier: isOutdated, - ); + if (baseChild is ContextNode && currChild is ContextNode) { + // Only add missing enums + for (final baseEnum in baseChild.entries.keys) { + if (!currChild.entries.containsKey(baseEnum)) { + _addNodeRecursive( + node: baseChild.entries[baseEnum]!, + resultMap: resultMap, + addOutdatedModifier: isOutdated, + ); + } + } + } else { + // Add whole base node which is expected + _addNodeRecursive( + node: baseChild, + resultMap: resultMap, + addOutdatedModifier: isOutdated, + ); + } } else if (baseChild is ObjectNode && !baseChild.isMap) { // [currChild] passed the previous equality check. // In this case, both [baseChild] and [currChild] are ObjectNodes @@ -282,6 +295,14 @@ bool _checkEquality(Node? a, Node? b) { if (a is TextNode && b is TextNode && !_setEquality.equals(a.params, b.params)) { + // different params + return false; + } + + if (a is ContextNode && + b is ContextNode && + !_setEquality.equals(a.entries.keys.toSet(), b.entries.keys.toSet())) { + // different enums return false; } From 146296178c99ad3254f59e2a128b3e46eb34481c Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 02:03:32 +0200 Subject: [PATCH 083/118] fix: analyze command should ignore unused translations that are linked --- slang/lib/src/runner/analyze.dart | 47 +++++++++++++++++--- slang/test/unit/runner/analyze_test.dart | 55 ++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/slang/lib/src/runner/analyze.dart b/slang/lib/src/runner/analyze.dart index 8fc8fbd2..b1b2585f 100644 --- a/slang/lib/src/runner/analyze.dart +++ b/slang/lib/src/runner/analyze.dart @@ -70,7 +70,7 @@ void runAnalyzeTranslations({ result: missingTranslationsResult, ); - final unusedTranslationsResult = _getUnusedTranslations( + final unusedTranslationsResult = getUnusedTranslations( rawConfig: rawConfig, translations: translationModelList, full: full, @@ -121,6 +121,7 @@ Map> getMissingTranslations({ curr: currTranslations.root, resultMap: resultMap, handleOutdated: true, + ignorePaths: const {}, ); result[currTranslations.locale] = resultMap; } @@ -128,7 +129,7 @@ Map> getMissingTranslations({ return result; } -Map> _getUnusedTranslations({ +Map> getUnusedTranslations({ required RawConfig rawConfig, required List translations, required bool full, @@ -150,11 +151,16 @@ Map> _getUnusedTranslations({ } final resultMap = {}; + final linkedPaths = {}; + _getReferredPaths(localeData.root, linkedPaths); + + // { } = localeData - baseTranslations _getMissingTranslationsForOneLocaleRecursive( baseNode: localeData.root, curr: baseTranslations.root, resultMap: resultMap, handleOutdated: false, + ignorePaths: linkedPaths, ); result[localeData.locale] = resultMap; } @@ -169,17 +175,21 @@ void _getMissingTranslationsForOneLocaleRecursive({ required ObjectNode curr, required Map resultMap, required bool handleOutdated, + required Set ignorePaths, }) { for (final baseEntry in baseNode.entries.entries) { final baseChild = baseEntry.value; - if (baseChild.modifiers.containsKey(NodeModifiers.ignoreMissing)) { + if (baseChild.modifiers.containsKey(NodeModifiers.ignoreMissing) || + ignorePaths.contains(baseChild.path)) { continue; } final currChild = curr.entries[baseEntry.key]; final isOutdated = handleOutdated && currChild?.modifiers.containsKey(NodeModifiers.outdated) == true; - if (isOutdated || !_checkEquality(baseChild, currChild)) { + if (isOutdated || + currChild == null || + !_checkEquality(baseChild, currChild)) { if (baseChild is ContextNode && currChild is ContextNode) { // Only add missing enums for (final baseEnum in baseChild.entries.keys) { @@ -208,6 +218,7 @@ void _getMissingTranslationsForOneLocaleRecursive({ curr: currChild as ObjectNode, resultMap: resultMap, handleOutdated: handleOutdated, + ignorePaths: ignorePaths, ); } } @@ -287,7 +298,7 @@ void _addNodeRecursive({ /// Both nodes are considered the same /// when they have the same type and the same parameters. -bool _checkEquality(Node? a, Node? b) { +bool _checkEquality(Node a, Node b) { if (a.runtimeType != b.runtimeType) { return false; } @@ -402,6 +413,32 @@ I18nData _findBaseTranslations(RawConfig rawConfig, List i18nData) { return baseTranslations; } +/// Populates [paths] with all paths that are referred in +/// linked translations. +void _getReferredPaths(ObjectNode root, Set paths) { + for (final entry in root.entries.entries) { + final child = entry.value; + switch (child) { + case ObjectNode() when !child.isMap: + _getReferredPaths(child, paths); + break; + case PluralNode(): + for (final quantity in child.quantities.values) { + paths.addAll(quantity.links); + } + break; + case ContextNode(): + for (final context in child.entries.values) { + paths.addAll(context.links); + } + break; + case TextNode(): + paths.addAll(child.links); + break; + } + } +} + void _writeMap({ required String outDir, required String fileNamePrefix, diff --git a/slang/test/unit/runner/analyze_test.dart b/slang/test/unit/runner/analyze_test.dart index fe4f75af..b6715446 100644 --- a/slang/test/unit/runner/analyze_test.dart +++ b/slang/test/unit/runner/analyze_test.dart @@ -1,3 +1,7 @@ +import 'package:slang/src/builder/builder/translation_model_list_builder.dart'; +import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/translation_map.dart'; import 'package:slang/src/runner/analyze.dart'; import 'package:test/test.dart'; @@ -53,4 +57,55 @@ void main() { expect(result, 'ADEG'); }); }); + + group('getUnusedTranslations', () { + test('Should find unused but translations', () { + final result = _getUnusedTranslations({ + 'en': { + 'a': 'A', + }, + 'de': { + 'a': 'A', + 'b': 'B', + }, + }); + + expect(result[I18nLocale(language: 'de')], {'b': 'B'}); + }); + + test('Should ignore unused but linked translations', () { + final result = _getUnusedTranslations({ + 'en': { + 'a': 'A', + }, + 'de': { + 'a': 'A @:b', + 'b': 'B', + }, + }); + + expect(result[I18nLocale(language: 'de')], isEmpty); + }); + }); +} + +Map> _getUnusedTranslations( + Map> translations, +) { + final map = TranslationMap(); + for (final entry in translations.entries) { + map.addTranslations( + locale: I18nLocale(language: entry.key), + translations: entry.value, + ); + } + + return getUnusedTranslations( + rawConfig: RawConfig.defaultConfig, + translations: TranslationModelListBuilder.build( + RawConfig.defaultConfig, + map, + ), + full: false, + ); } From 0efaed7cdeccb77c5f419836eb614122eb832c53 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 02:12:15 +0200 Subject: [PATCH 084/118] docs: update changelog --- slang/CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 78b45395..7c81af06 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,9 +1,17 @@ ## 4.0.0 -**Number formats and Lazy loading** +**DateFormat, NumberFormat and Lazy loading** + +Format translations with `DateFormat` and `NumberFormat`: `Hello {name}, today is {today: yMd}. You have {money: currency(symbol: '€')}.` On web, [Deferred loading](https://dart.dev/language/libraries#lazily-loading-a-library) is used to reduce initial load time. +- feat: add `DateFormat` and `NumberFormat` support +- feat: add `lazy: true` config which is enabled by default +- fix: `slang analyze` should not treat translations as unused if they are used in linked translations +- fix: `slang analyze` should detect missing enums +- fix: trim enum keys in compressed format while parsing (e.g. `"male, female": "..."` to `"male,female": "..."`) +- fix: compilation error on web when using large interfaces - **Breaking:** `setLocale`, `setLocaleRaw`, and `useDeviceLocale` returns a Future, use `-Sync` suffix for synchronous calls - **Breaking:** `output_format` removed, always generates multiple files now - **Breaking:** deprecated functions in `LocaleSettings` (`supportedLocales`, `supportedLocalesRaw`) removed From e189f0f2d55d4c47584eeb6dd9ca507928acd433 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 02:18:04 +0200 Subject: [PATCH 085/118] release: 4.0.0-dev.0 --- slang/pubspec.yaml | 2 +- slang_build_runner/CHANGELOG.md | 4 ++++ slang_build_runner/pubspec.yaml | 4 ++-- slang_flutter/CHANGELOG.md | 4 ++++ slang_flutter/pubspec.yaml | 4 ++-- slang_gpt/CHANGELOG.md | 4 ++++ slang_gpt/pubspec.yaml | 6 +++--- 7 files changed, 20 insertions(+), 8 deletions(-) diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index 227e086d..c47e5c7b 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -1,6 +1,6 @@ name: slang description: Localization / Internationalization (i18n) solution. Use JSON, YAML, CSV, or ARB files to create typesafe translations via source generation. -version: 3.30.2 +version: 4.0.0-dev.0 repository: https://github.com/slang-i18n/slang topics: - i18n diff --git a/slang_build_runner/CHANGELOG.md b/slang_build_runner/CHANGELOG.md index a6582d64..f1360629 100644 --- a/slang_build_runner/CHANGELOG.md +++ b/slang_build_runner/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.0.0 + +- Bump `slang` to `4.0.0` + ## 3.30.0 - Bump `slang` to `3.30.0` diff --git a/slang_build_runner/pubspec.yaml b/slang_build_runner/pubspec.yaml index 9bbd118c..6c075392 100644 --- a/slang_build_runner/pubspec.yaml +++ b/slang_build_runner/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_build_runner description: build_runner integration for slang. This library ensures that slang is recognized by build_runner. -version: 3.30.0 +version: 4.0.0-dev.0 repository: https://github.com/slang-i18n/slang environment: @@ -11,7 +11,7 @@ dependencies: glob: ^2.0.2 # Use a tight version to ensure that all features are available - slang: '>=3.30.0 <3.31.0' + slang: '>=4.0.0-dev.0 <4.1.0' dev_dependencies: lints: ^2.0.0 diff --git a/slang_flutter/CHANGELOG.md b/slang_flutter/CHANGELOG.md index 5a7b8973..541f47f6 100644 --- a/slang_flutter/CHANGELOG.md +++ b/slang_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.0.0 + +- Bump `slang` to `4.0.0` + ## 3.30.0 - Bump `slang` to `3.30.0` diff --git a/slang_flutter/pubspec.yaml b/slang_flutter/pubspec.yaml index 2b431610..52d1c939 100644 --- a/slang_flutter/pubspec.yaml +++ b/slang_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_flutter description: Flutter support for slang. This library provides helpful Flutter API. -version: 3.30.0 +version: 4.0.0-dev.0 repository: https://github.com/slang-i18n/slang environment: @@ -14,7 +14,7 @@ dependencies: sdk: flutter # Use a tight version to ensure that all features are available - slang: '>=3.30.0 <3.31.0' + slang: '>=4.0.0-dev.0 <4.1.0' dev_dependencies: flutter_test: diff --git a/slang_gpt/CHANGELOG.md b/slang_gpt/CHANGELOG.md index 6f3bcf28..c6cf2b8c 100644 --- a/slang_gpt/CHANGELOG.md +++ b/slang_gpt/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.11.0 + +- deps: bump slang to 4.0.0 + ## 0.10.3 - feat: add `gpt-4o-mini` model diff --git a/slang_gpt/pubspec.yaml b/slang_gpt/pubspec.yaml index 97b1fe4f..cbef85d2 100644 --- a/slang_gpt/pubspec.yaml +++ b/slang_gpt/pubspec.yaml @@ -1,15 +1,15 @@ name: slang_gpt description: Use GPT to automatically translate at compile time. A tool for slang. -version: 0.10.3 +version: 0.11.0 repository: https://github.com/slang-i18n/slang environment: - sdk: '>=2.17.0 <4.0.0' + sdk: '>=3.3.0 <4.0.0' dependencies: collection: ^1.15.0 http: '>=0.13.0 <2.0.0' - slang: ^3.30.1 + slang: '>=4.0.0-dev.0 <5.0.0' dev_dependencies: lints: ^2.0.0 From 9f1b2210a3d260e73e4108d5525d550459d903f9 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 02:37:20 +0200 Subject: [PATCH 086/118] docs: update migration guide --- slang/CHANGELOG.md | 8 ++++++-- slang/MIGRATION.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 7c81af06..b9889ad1 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,8 +1,9 @@ ## 4.0.0 -**DateFormat, NumberFormat and Lazy loading** +**DateFormat, NumberFormat, and Lazy loading** -Format translations with `DateFormat` and `NumberFormat`: `Hello {name}, today is {today: yMd}. You have {money: currency(symbol: '€')}.` +Format translations with `DateFormat` and `NumberFormat`: +`Hello {name}, today is {today: yMd}. You have {money: currency(symbol: '€')}.` On web, [Deferred loading](https://dart.dev/language/libraries#lazily-loading-a-library) is used to reduce initial load time. @@ -12,12 +13,15 @@ On web, [Deferred loading](https://dart.dev/language/libraries#lazily-loading-a- - fix: `slang analyze` should detect missing enums - fix: trim enum keys in compressed format while parsing (e.g. `"male, female": "..."` to `"male,female": "..."`) - fix: compilation error on web when using large interfaces +- **Breaking:** Require Dart 3.3 and Flutter 3.19 - **Breaking:** `setLocale`, `setLocaleRaw`, and `useDeviceLocale` returns a Future, use `-Sync` suffix for synchronous calls - **Breaking:** `output_format` removed, always generates multiple files now - **Breaking:** deprecated functions in `LocaleSettings` (`supportedLocales`, `supportedLocalesRaw`) removed - **Breaking:** defining contexts (enums) is no longer allowed in `build.yaml` or `slang.yaml` (deprecated in v3.19.0) - **Breaking:** enums specified in `context` are no longer transformed into pascal case keeping the original case +You can read the detailed migration guide [here](https://github.com/slang-i18n/slang/blob/main/slang/MIGRATION.md). + ## 3.32.0 - feat: add syntax to escape linked translations (#248) @Fasust diff --git a/slang/MIGRATION.md b/slang/MIGRATION.md index 16748263..1bf97c46 100644 --- a/slang/MIGRATION.md +++ b/slang/MIGRATION.md @@ -1,5 +1,33 @@ # Migration Guides +## slang 3.0 to 4.0 + +### Lazy Loading + +The 4.0 release expects the translations to be loaded asynchronously by default. +This makes it easy to support lazy loading of translations on Web. + +If you don't want to load translations asynchronously, you can set `lazy: false` in the `slang.yaml` or `build.yaml`. +Then, you are able to call `LocaleSettings.setLocaleSync`, `LocaleSettings.setLocaleRawSync` (and other sync methods) without any issues. + +### Context Type conversion + +Previously, context types are converted to pascal case. This is no longer the case. + +```yaml +contexts: + gender_context: # Previously, converted to GenderContext + default_parameter: gender +``` + +Now, you should use the exact context type. + +```yaml +contexts: + GenderContext: + default_parameter: gender +``` + ## Use context modifier (since 3.19.0) Since 3.19, slang supports context enum inference (i.e. you don't need to specify the exact enum values in the config). From 203fc297f813e8c559fa429ccb4f62475e53cd3d Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 03:02:16 +0200 Subject: [PATCH 087/118] test: more analyze command tests --- slang/lib/src/runner/analyze.dart | 6 +- slang/test/unit/runner/analyze_test.dart | 143 +++++++++++++++++++++-- 2 files changed, 140 insertions(+), 9 deletions(-) diff --git a/slang/lib/src/runner/analyze.dart b/slang/lib/src/runner/analyze.dart index b1b2585f..e5372cd7 100644 --- a/slang/lib/src/runner/analyze.dart +++ b/slang/lib/src/runner/analyze.dart @@ -121,6 +121,7 @@ Map> getMissingTranslations({ curr: currTranslations.root, resultMap: resultMap, handleOutdated: true, + ignoreModifierFlag: NodeModifiers.ignoreMissing, ignorePaths: const {}, ); result[currTranslations.locale] = resultMap; @@ -160,6 +161,7 @@ Map> getUnusedTranslations({ curr: baseTranslations.root, resultMap: resultMap, handleOutdated: false, + ignoreModifierFlag: NodeModifiers.ignoreUnused, ignorePaths: linkedPaths, ); result[localeData.locale] = resultMap; @@ -175,11 +177,12 @@ void _getMissingTranslationsForOneLocaleRecursive({ required ObjectNode curr, required Map resultMap, required bool handleOutdated, + required String ignoreModifierFlag, required Set ignorePaths, }) { for (final baseEntry in baseNode.entries.entries) { final baseChild = baseEntry.value; - if (baseChild.modifiers.containsKey(NodeModifiers.ignoreMissing) || + if (baseChild.modifiers.containsKey(ignoreModifierFlag) || ignorePaths.contains(baseChild.path)) { continue; } @@ -218,6 +221,7 @@ void _getMissingTranslationsForOneLocaleRecursive({ curr: currChild as ObjectNode, resultMap: resultMap, handleOutdated: handleOutdated, + ignoreModifierFlag: ignoreModifierFlag, ignorePaths: ignorePaths, ); } diff --git a/slang/test/unit/runner/analyze_test.dart b/slang/test/unit/runner/analyze_test.dart index b6715446..62b9e003 100644 --- a/slang/test/unit/runner/analyze_test.dart +++ b/slang/test/unit/runner/analyze_test.dart @@ -1,4 +1,5 @@ import 'package:slang/src/builder/builder/translation_model_list_builder.dart'; +import 'package:slang/src/builder/model/i18n_data.dart'; import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/model/raw_config.dart'; import 'package:slang/src/builder/model/translation_map.dart'; @@ -58,8 +59,92 @@ void main() { }); }); + group('getMissingTranslations', () { + test('Should find missing translations', () { + final result = _getMissingTranslations({ + 'en': { + 'a': 'A', + 'b': 'B', + }, + 'de': { + 'a': 'A', + }, + }); + + expect(result[I18nLocale(language: 'de')], {'b': 'B'}); + }); + + test('Should respect ignoreMissing flag', () { + final result = _getMissingTranslations({ + 'en': { + 'a': 'A', + 'b(ignoreMissing)': 'B', + }, + 'de': { + 'a': 'A', + }, + }); + + expect(result[I18nLocale(language: 'de')], isEmpty); + }); + + test('Should respect OUTDATED flag', () { + final result = _getMissingTranslations({ + 'en': { + 'a': 'A EN', + }, + 'de': { + 'a(OUTDATED)': 'A DE', + }, + }); + + expect(result[I18nLocale(language: 'de')], {'a(OUTDATED)': 'A EN'}); + }); + + test('Should ignore ignoreUnused flag', () { + final result = _getMissingTranslations({ + 'en': { + 'a': 'A', + 'b(ignoreUnused)': 'B', + }, + 'de': { + 'a': 'A', + }, + }); + + expect(result[I18nLocale(language: 'de')], {'b(ignoreUnused)': 'B'}); + }); + + test('Should find missing enum', () { + final result = _getMissingTranslations({ + 'en': { + 'a': 'A', + 'greet(context=Gender)': { + 'male': 'Hello Mr', + 'female': 'Hello Mrs', + }, + }, + 'de': { + 'a': 'A', + 'greet(context=Gender)': { + 'male': 'Hello Herr', + }, + }, + }); + + expect( + result[I18nLocale(language: 'de')], + { + 'greet(context=Gender)': { + 'female': 'Hello Mrs', + }, + }, + ); + }); + }); + group('getUnusedTranslations', () { - test('Should find unused but translations', () { + test('Should find unused translations', () { final result = _getUnusedTranslations({ 'en': { 'a': 'A', @@ -73,6 +158,34 @@ void main() { expect(result[I18nLocale(language: 'de')], {'b': 'B'}); }); + test('Should respect ignoreUnused flag', () { + final result = _getUnusedTranslations({ + 'en': { + 'a': 'A', + }, + 'de': { + 'a': 'A', + 'b(ignoreUnused)': 'B', + }, + }); + + expect(result[I18nLocale(language: 'de')], isEmpty); + }); + + test('Should ignore ignoreMissing flag', () { + final result = _getUnusedTranslations({ + 'en': { + 'a': 'A', + }, + 'de': { + 'a': 'A', + 'b(ignoreMissing)': 'B', + }, + }); + + expect(result[I18nLocale(language: 'de')], {'b(ignoreMissing)': 'B'}); + }); + test('Should ignore unused but linked translations', () { final result = _getUnusedTranslations({ 'en': { @@ -89,9 +202,27 @@ void main() { }); } +Map> _getMissingTranslations( + Map> translations, +) { + return getMissingTranslations( + rawConfig: RawConfig.defaultConfig, + translations: _buildTranslations(translations), + ); +} + Map> _getUnusedTranslations( Map> translations, ) { + return getUnusedTranslations( + rawConfig: RawConfig.defaultConfig, + translations: _buildTranslations(translations), + full: false, + ); +} + +List _buildTranslations( + Map> translations) { final map = TranslationMap(); for (final entry in translations.entries) { map.addTranslations( @@ -100,12 +231,8 @@ Map> _getUnusedTranslations( ); } - return getUnusedTranslations( - rawConfig: RawConfig.defaultConfig, - translations: TranslationModelListBuilder.build( - RawConfig.defaultConfig, - map, - ), - full: false, + return TranslationModelListBuilder.build( + RawConfig.defaultConfig, + map, ); } From 9e6efd22bea16aec1695604da821733dd76af575 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 03:11:16 +0200 Subject: [PATCH 088/118] fix: use correct constructor in base locale --- .../generator/generate_translations.dart | 17 +++++++++++++++-- .../_expected_fallback_base_locale_en.output | 8 ++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/slang/lib/src/builder/generator/generate_translations.dart b/slang/lib/src/builder/generator/generate_translations.dart index 33f8954f..05e9d840 100644 --- a/slang/lib/src/builder/generator/generate_translations.dart +++ b/slang/lib/src/builder/generator/generate_translations.dart @@ -514,7 +514,14 @@ void _generateMap({ childName: key, locale: locale, ); - buffer.writeln('\'$key\': $childClassWithLocale._(_root),'); + + buffer.write('\'$key\': '); + if (base && + config.fallbackStrategy == GenerateFallbackStrategy.baseLocale) { + buffer.writeln('$childClassWithLocale.internal(_root),'); + } else { + buffer.writeln('$childClassWithLocale._(_root),'); + } } } else if (value is PluralNode) { buffer.write('\'$key\': '); @@ -630,7 +637,13 @@ void _generateList({ childName: key, locale: locale, ); - buffer.writeln('$childClassWithLocale._(_root),'); + + if (base && + config.fallbackStrategy == GenerateFallbackStrategy.baseLocale) { + buffer.writeln('$childClassWithLocale.internal(_root),'); + } else { + buffer.writeln('$childClassWithLocale._(_root),'); + } } } else if (value is PluralNode) { _addPluralCall( diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output index 26b037a7..169c16d5 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output @@ -74,12 +74,12 @@ class TranslationsOnboardingEn { TextSpan(text: ' and ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), ]); List get pages => [ - TranslationsOnboarding$pages$0i0$En._(_root), - TranslationsOnboarding$pages$0i1$En._(_root), + TranslationsOnboarding$pages$0i0$En.internal(_root), + TranslationsOnboarding$pages$0i1$En.internal(_root), ]; List get modifierPages => [ - TranslationsOnboarding$modifierPages$0i0$En._(_root), - TranslationsOnboarding$modifierPages$0i1$En._(_root), + TranslationsOnboarding$modifierPages$0i0$En.internal(_root), + TranslationsOnboarding$modifierPages$0i1$En.internal(_root), ]; String greet({required GenderContext context, required Object lastName, required Object fullName, required Object firstName}) { switch (context) { From 9bb663f660954b4b521d8f86f3617b15ddf345ec Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 03:14:17 +0200 Subject: [PATCH 089/118] fix: make fields variable in interface public --- .../builder/generator/generate_header.dart | 10 ++++---- ..._expected_fallback_base_locale_main.output | 24 +++++++++---------- .../resources/main/_expected_main.output | 24 +++++++++---------- .../main/_expected_obfuscation_main.output | 24 +++++++++---------- ...expected_translation_overrides_main.output | 24 +++++++++---------- 5 files changed, 54 insertions(+), 52 deletions(-) diff --git a/slang/lib/src/builder/generator/generate_header.dart b/slang/lib/src/builder/generator/generate_header.dart index a82bea18..aa05e45f 100644 --- a/slang/lib/src/builder/generator/generate_header.dart +++ b/slang/lib/src/builder/generator/generate_header.dart @@ -561,6 +561,8 @@ void _generateInterfaces({ buffer.writeln('// interfaces generated as mixins'); } + const fieldsVar = r'$fields'; + for (final interface in config.interface) { buffer.writeln(); buffer.writeln('mixin ${interface.name} {'); @@ -593,8 +595,8 @@ void _generateInterfaces({ buffer.writeln('\t\tif (other is! ${interface.name}) return false;'); buffer.writeln(); - buffer.writeln('\t\tfinal fields = _fields;'); - buffer.writeln('\t\tfinal otherFields = other._fields;'); + buffer.writeln('\t\tfinal fields = $fieldsVar;'); + buffer.writeln('\t\tfinal otherFields = other.$fieldsVar;'); buffer.writeln('\t\tfor (int i = 0; i < fields.length; i++) {'); buffer.writeln('\t\t\tif (fields[i] != otherFields[i]) return false;'); buffer.writeln('\t\t}'); @@ -607,7 +609,7 @@ void _generateInterfaces({ buffer.writeln(); buffer.writeln('\t@override'); buffer.writeln('\tint get hashCode {'); - buffer.writeln('\t\tfinal fields = _fields;'); + buffer.writeln('\t\tfinal fields = $fieldsVar;'); buffer.writeln('\t\tint result = fields.first.hashCode;'); buffer.writeln('\t\tfor (final element in fields.skip(1)) {'); buffer.writeln('\t\t\tresult *= element.hashCode;'); @@ -619,7 +621,7 @@ void _generateInterfaces({ // fields buffer.writeln(); - buffer.writeln('\tList get _fields => ['); + buffer.writeln('\tList get $fieldsVar => ['); for (final attribute in interface.attributes) { buffer.writeln('\t\t${attribute.attributeName},'); } diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output index 4018327c..631de184 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output @@ -197,8 +197,8 @@ mixin PageData { if (identical(this, other)) return true; if (other is! PageData) return false; - final fields = _fields; - final otherFields = other._fields; + final fields = $fields; + final otherFields = other.$fields; for (int i = 0; i < fields.length; i++) { if (fields[i] != otherFields[i]) return false; } @@ -208,7 +208,7 @@ mixin PageData { @override int get hashCode { - final fields = _fields; + final fields = $fields; int result = fields.first.hashCode; for (final element in fields.skip(1)) { result *= element.hashCode; @@ -217,7 +217,7 @@ mixin PageData { return result; } - List get _fields => [ + List get $fields => [ title, content, ]; @@ -232,8 +232,8 @@ mixin MPage { if (identical(this, other)) return true; if (other is! MPage) return false; - final fields = _fields; - final otherFields = other._fields; + final fields = $fields; + final otherFields = other.$fields; for (int i = 0; i < fields.length; i++) { if (fields[i] != otherFields[i]) return false; } @@ -243,7 +243,7 @@ mixin MPage { @override int get hashCode { - final fields = _fields; + final fields = $fields; int result = fields.first.hashCode; for (final element in fields.skip(1)) { result *= element.hashCode; @@ -252,7 +252,7 @@ mixin MPage { return result; } - List get _fields => [ + List get $fields => [ title, content, ]; @@ -267,8 +267,8 @@ mixin EndData { if (identical(this, other)) return true; if (other is! EndData) return false; - final fields = _fields; - final otherFields = other._fields; + final fields = $fields; + final otherFields = other.$fields; for (int i = 0; i < fields.length; i++) { if (fields[i] != otherFields[i]) return false; } @@ -278,7 +278,7 @@ mixin EndData { @override int get hashCode { - final fields = _fields; + final fields = $fields; int result = fields.first.hashCode; for (final element in fields.skip(1)) { result *= element.hashCode; @@ -287,7 +287,7 @@ mixin EndData { return result; } - List get _fields => [ + List get $fields => [ stringPages, pages, ]; diff --git a/slang/test/integration/resources/main/_expected_main.output b/slang/test/integration/resources/main/_expected_main.output index 4018327c..631de184 100644 --- a/slang/test/integration/resources/main/_expected_main.output +++ b/slang/test/integration/resources/main/_expected_main.output @@ -197,8 +197,8 @@ mixin PageData { if (identical(this, other)) return true; if (other is! PageData) return false; - final fields = _fields; - final otherFields = other._fields; + final fields = $fields; + final otherFields = other.$fields; for (int i = 0; i < fields.length; i++) { if (fields[i] != otherFields[i]) return false; } @@ -208,7 +208,7 @@ mixin PageData { @override int get hashCode { - final fields = _fields; + final fields = $fields; int result = fields.first.hashCode; for (final element in fields.skip(1)) { result *= element.hashCode; @@ -217,7 +217,7 @@ mixin PageData { return result; } - List get _fields => [ + List get $fields => [ title, content, ]; @@ -232,8 +232,8 @@ mixin MPage { if (identical(this, other)) return true; if (other is! MPage) return false; - final fields = _fields; - final otherFields = other._fields; + final fields = $fields; + final otherFields = other.$fields; for (int i = 0; i < fields.length; i++) { if (fields[i] != otherFields[i]) return false; } @@ -243,7 +243,7 @@ mixin MPage { @override int get hashCode { - final fields = _fields; + final fields = $fields; int result = fields.first.hashCode; for (final element in fields.skip(1)) { result *= element.hashCode; @@ -252,7 +252,7 @@ mixin MPage { return result; } - List get _fields => [ + List get $fields => [ title, content, ]; @@ -267,8 +267,8 @@ mixin EndData { if (identical(this, other)) return true; if (other is! EndData) return false; - final fields = _fields; - final otherFields = other._fields; + final fields = $fields; + final otherFields = other.$fields; for (int i = 0; i < fields.length; i++) { if (fields[i] != otherFields[i]) return false; } @@ -278,7 +278,7 @@ mixin EndData { @override int get hashCode { - final fields = _fields; + final fields = $fields; int result = fields.first.hashCode; for (final element in fields.skip(1)) { result *= element.hashCode; @@ -287,7 +287,7 @@ mixin EndData { return result; } - List get _fields => [ + List get $fields => [ stringPages, pages, ]; diff --git a/slang/test/integration/resources/main/_expected_obfuscation_main.output b/slang/test/integration/resources/main/_expected_obfuscation_main.output index dc3c4317..e2dafce7 100644 --- a/slang/test/integration/resources/main/_expected_obfuscation_main.output +++ b/slang/test/integration/resources/main/_expected_obfuscation_main.output @@ -198,8 +198,8 @@ mixin PageData { if (identical(this, other)) return true; if (other is! PageData) return false; - final fields = _fields; - final otherFields = other._fields; + final fields = $fields; + final otherFields = other.$fields; for (int i = 0; i < fields.length; i++) { if (fields[i] != otherFields[i]) return false; } @@ -209,7 +209,7 @@ mixin PageData { @override int get hashCode { - final fields = _fields; + final fields = $fields; int result = fields.first.hashCode; for (final element in fields.skip(1)) { result *= element.hashCode; @@ -218,7 +218,7 @@ mixin PageData { return result; } - List get _fields => [ + List get $fields => [ title, content, ]; @@ -233,8 +233,8 @@ mixin MPage { if (identical(this, other)) return true; if (other is! MPage) return false; - final fields = _fields; - final otherFields = other._fields; + final fields = $fields; + final otherFields = other.$fields; for (int i = 0; i < fields.length; i++) { if (fields[i] != otherFields[i]) return false; } @@ -244,7 +244,7 @@ mixin MPage { @override int get hashCode { - final fields = _fields; + final fields = $fields; int result = fields.first.hashCode; for (final element in fields.skip(1)) { result *= element.hashCode; @@ -253,7 +253,7 @@ mixin MPage { return result; } - List get _fields => [ + List get $fields => [ title, content, ]; @@ -268,8 +268,8 @@ mixin EndData { if (identical(this, other)) return true; if (other is! EndData) return false; - final fields = _fields; - final otherFields = other._fields; + final fields = $fields; + final otherFields = other.$fields; for (int i = 0; i < fields.length; i++) { if (fields[i] != otherFields[i]) return false; } @@ -279,7 +279,7 @@ mixin EndData { @override int get hashCode { - final fields = _fields; + final fields = $fields; int result = fields.first.hashCode; for (final element in fields.skip(1)) { result *= element.hashCode; @@ -288,7 +288,7 @@ mixin EndData { return result; } - List get _fields => [ + List get $fields => [ stringPages, pages, ]; diff --git a/slang/test/integration/resources/main/_expected_translation_overrides_main.output b/slang/test/integration/resources/main/_expected_translation_overrides_main.output index 2833746f..50fe998d 100644 --- a/slang/test/integration/resources/main/_expected_translation_overrides_main.output +++ b/slang/test/integration/resources/main/_expected_translation_overrides_main.output @@ -225,8 +225,8 @@ mixin PageData { if (identical(this, other)) return true; if (other is! PageData) return false; - final fields = _fields; - final otherFields = other._fields; + final fields = $fields; + final otherFields = other.$fields; for (int i = 0; i < fields.length; i++) { if (fields[i] != otherFields[i]) return false; } @@ -236,7 +236,7 @@ mixin PageData { @override int get hashCode { - final fields = _fields; + final fields = $fields; int result = fields.first.hashCode; for (final element in fields.skip(1)) { result *= element.hashCode; @@ -245,7 +245,7 @@ mixin PageData { return result; } - List get _fields => [ + List get $fields => [ title, content, ]; @@ -260,8 +260,8 @@ mixin MPage { if (identical(this, other)) return true; if (other is! MPage) return false; - final fields = _fields; - final otherFields = other._fields; + final fields = $fields; + final otherFields = other.$fields; for (int i = 0; i < fields.length; i++) { if (fields[i] != otherFields[i]) return false; } @@ -271,7 +271,7 @@ mixin MPage { @override int get hashCode { - final fields = _fields; + final fields = $fields; int result = fields.first.hashCode; for (final element in fields.skip(1)) { result *= element.hashCode; @@ -280,7 +280,7 @@ mixin MPage { return result; } - List get _fields => [ + List get $fields => [ title, content, ]; @@ -295,8 +295,8 @@ mixin EndData { if (identical(this, other)) return true; if (other is! EndData) return false; - final fields = _fields; - final otherFields = other._fields; + final fields = $fields; + final otherFields = other.$fields; for (int i = 0; i < fields.length; i++) { if (fields[i] != otherFields[i]) return false; } @@ -306,7 +306,7 @@ mixin EndData { @override int get hashCode { - final fields = _fields; + final fields = $fields; int result = fields.first.hashCode; for (final element in fields.skip(1)) { result *= element.hashCode; @@ -315,7 +315,7 @@ mixin EndData { return result; } - List get _fields => [ + List get $fields => [ stringPages, pages, ]; From 5cdacbb8f67473cfc3c16c78b676b35ad263daa4 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 03:18:57 +0200 Subject: [PATCH 090/118] release: 4.0.0-dev.1 --- slang/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index c47e5c7b..096ebd08 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -1,6 +1,6 @@ name: slang description: Localization / Internationalization (i18n) solution. Use JSON, YAML, CSV, or ARB files to create typesafe translations via source generation. -version: 4.0.0-dev.0 +version: 4.0.0-dev.1 repository: https://github.com/slang-i18n/slang topics: - i18n From 1b7e49c6bc930ecdfa0eeb0670dae0edcbcd5e1f Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 03:26:53 +0200 Subject: [PATCH 091/118] docs: update readme --- slang/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/slang/README.md b/slang/README.md index cc9d4358..05a03c7d 100644 --- a/slang/README.md +++ b/slang/README.md @@ -944,7 +944,8 @@ You can also provide custom formats: ```json { - "today": "Today is {date: DateFormat('yyyy-MM-dd')}" + "today": "Today is {date: DateFormat('yyyy-MM-dd')}", + "number": "The number is {number: NumberFormat('###,###.##')}" } ``` @@ -952,7 +953,7 @@ Or adjust built-in formats: ```json { - "today": "Today is {date: currency(symbol: 'EUR')}" + "price": "It costs {price: currency(symbol: 'EUR')}" } ``` From 0bb00047cf705c7917e9879cadf598e10e189287 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 13:26:29 +0200 Subject: [PATCH 092/118] fix: correctly transform keys with modifiers when `key_case` is set --- slang/CHANGELOG.md | 14 ++++++++------ slang/lib/src/builder/utils/regex_utils.dart | 2 +- slang/test/unit/utils/regex_utils_test.dart | 6 ++++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index b9889ad1..338d53cf 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -3,16 +3,18 @@ **DateFormat, NumberFormat, and Lazy loading** Format translations with `DateFormat` and `NumberFormat`: + `Hello {name}, today is {today: yMd}. You have {money: currency(symbol: '€')}.` On web, [Deferred loading](https://dart.dev/language/libraries#lazily-loading-a-library) is used to reduce initial load time. -- feat: add `DateFormat` and `NumberFormat` support -- feat: add `lazy: true` config which is enabled by default -- fix: `slang analyze` should not treat translations as unused if they are used in linked translations -- fix: `slang analyze` should detect missing enums -- fix: trim enum keys in compressed format while parsing (e.g. `"male, female": "..."` to `"male,female": "..."`) -- fix: compilation error on web when using large interfaces +- feat: add `DateFormat` and `NumberFormat` support (#112) +- feat: add `lazy: true` config which is enabled by default (#135) +- fix: `slang analyze` should not treat translations as unused if they are used in linked translations (#231) +- fix: `slang analyze` should detect missing enums (#234) +- fix: trim enum keys in compressed format while parsing (e.g. `"male, female": "..."` to `"male,female": "..."`) (#247) +- fix: compilation error on web when using large interfaces (#251) +- fix: correctly transform keys with modifiers when `key_case` is set (#253) - **Breaking:** Require Dart 3.3 and Flutter 3.19 - **Breaking:** `setLocale`, `setLocaleRaw`, and `useDeviceLocale` returns a Future, use `-Sync` suffix for synchronous calls - **Breaking:** `output_format` removed, always generates multiple files now diff --git a/slang/lib/src/builder/utils/regex_utils.dart b/slang/lib/src/builder/utils/regex_utils.dart index 8e33a1f9..1c161e3c 100644 --- a/slang/lib/src/builder/utils/regex_utils.dart +++ b/slang/lib/src/builder/utils/regex_utils.dart @@ -58,7 +58,7 @@ class RegexUtils { /// greet(plural, param=gender) /// 1 - greet /// 2 - plural, param=gender - static final RegExp modifierRegex = RegExp(r'^(\w+)\((.+)\)$'); + static final RegExp modifierRegex = RegExp(r'^([\w-]+)\((.+)\)$'); /// Matches a format type expression with optional parameters /// diff --git a/slang/test/unit/utils/regex_utils_test.dart b/slang/test/unit/utils/regex_utils_test.dart index af52be9f..99774365 100644 --- a/slang/test/unit/utils/regex_utils_test.dart +++ b/slang/test/unit/utils/regex_utils_test.dart @@ -167,6 +167,12 @@ void main() { expect(match?.group(2), 'abc'); }); + test('some-key(abc)', () { + RegExpMatch? match = regex.firstMatch('some-key(abc)'); + expect(match?.group(1), 'some-key'); + expect(match?.group(2), 'abc'); + }); + test('myKey(cool_parameter)', () { RegExpMatch? match = regex.firstMatch('myKey(cool_parameter)'); expect(match?.group(1), 'myKey'); From 3391373c28e9d3dab74da002c345d48698d81253 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 13:32:23 +0200 Subject: [PATCH 093/118] fix: generate List instead of List --- slang/lib/src/builder/generator/generate_header.dart | 2 +- .../main/_expected_fallback_base_locale_main.output | 6 +++--- slang/test/integration/resources/main/_expected_main.output | 6 +++--- .../resources/main/_expected_obfuscation_main.output | 6 +++--- .../main/_expected_translation_overrides_main.output | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/slang/lib/src/builder/generator/generate_header.dart b/slang/lib/src/builder/generator/generate_header.dart index aa05e45f..e0e5c184 100644 --- a/slang/lib/src/builder/generator/generate_header.dart +++ b/slang/lib/src/builder/generator/generate_header.dart @@ -621,7 +621,7 @@ void _generateInterfaces({ // fields buffer.writeln(); - buffer.writeln('\tList get $fieldsVar => ['); + buffer.writeln('\tList get $fieldsVar => ['); for (final attribute in interface.attributes) { buffer.writeln('\t\t${attribute.attributeName},'); } diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output index 631de184..68eb37a8 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output @@ -217,7 +217,7 @@ mixin PageData { return result; } - List get $fields => [ + List get $fields => [ title, content, ]; @@ -252,7 +252,7 @@ mixin MPage { return result; } - List get $fields => [ + List get $fields => [ title, content, ]; @@ -287,7 +287,7 @@ mixin EndData { return result; } - List get $fields => [ + List get $fields => [ stringPages, pages, ]; diff --git a/slang/test/integration/resources/main/_expected_main.output b/slang/test/integration/resources/main/_expected_main.output index 631de184..68eb37a8 100644 --- a/slang/test/integration/resources/main/_expected_main.output +++ b/slang/test/integration/resources/main/_expected_main.output @@ -217,7 +217,7 @@ mixin PageData { return result; } - List get $fields => [ + List get $fields => [ title, content, ]; @@ -252,7 +252,7 @@ mixin MPage { return result; } - List get $fields => [ + List get $fields => [ title, content, ]; @@ -287,7 +287,7 @@ mixin EndData { return result; } - List get $fields => [ + List get $fields => [ stringPages, pages, ]; diff --git a/slang/test/integration/resources/main/_expected_obfuscation_main.output b/slang/test/integration/resources/main/_expected_obfuscation_main.output index e2dafce7..bf0d0ea7 100644 --- a/slang/test/integration/resources/main/_expected_obfuscation_main.output +++ b/slang/test/integration/resources/main/_expected_obfuscation_main.output @@ -218,7 +218,7 @@ mixin PageData { return result; } - List get $fields => [ + List get $fields => [ title, content, ]; @@ -253,7 +253,7 @@ mixin MPage { return result; } - List get $fields => [ + List get $fields => [ title, content, ]; @@ -288,7 +288,7 @@ mixin EndData { return result; } - List get $fields => [ + List get $fields => [ stringPages, pages, ]; diff --git a/slang/test/integration/resources/main/_expected_translation_overrides_main.output b/slang/test/integration/resources/main/_expected_translation_overrides_main.output index 50fe998d..08375592 100644 --- a/slang/test/integration/resources/main/_expected_translation_overrides_main.output +++ b/slang/test/integration/resources/main/_expected_translation_overrides_main.output @@ -245,7 +245,7 @@ mixin PageData { return result; } - List get $fields => [ + List get $fields => [ title, content, ]; @@ -280,7 +280,7 @@ mixin MPage { return result; } - List get $fields => [ + List get $fields => [ title, content, ]; @@ -315,7 +315,7 @@ mixin EndData { return result; } - List get $fields => [ + List get $fields => [ stringPages, pages, ]; From e1305dd7e0b2df2ffa0aaa9d608062e93ff9f9d2 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 14:03:14 +0200 Subject: [PATCH 094/118] refactor: rename node.dart to generated.dart --- slang/example/lib/i18n/strings.g.dart | 4 ++-- slang/example/lib/i18n/strings_de.g.dart | 2 +- slang/example/lib/i18n/strings_fr_FR.g.dart | 2 +- slang/lib/{node.dart => generated.dart} | 0 slang/lib/src/builder/generator/generate_header.dart | 2 +- slang/lib/src/builder/generator/generate_translations.dart | 2 +- slang/test/integration/resources/main/_expected_de.output | 2 +- .../resources/main/_expected_fallback_base_locale_de.output | 2 +- .../resources/main/_expected_fallback_base_locale_main.output | 2 +- .../main/_expected_fallback_base_locale_special_de.output | 2 +- .../main/_expected_fallback_base_locale_special_main.output | 2 +- slang/test/integration/resources/main/_expected_main.output | 2 +- .../integration/resources/main/_expected_no_flutter.output | 2 +- .../resources/main/_expected_no_locale_handling.output | 2 +- .../resources/main/_expected_obfuscation_de.output | 2 +- .../resources/main/_expected_obfuscation_main.output | 2 +- .../resources/main/_expected_translation_overrides_de.output | 2 +- .../main/_expected_translation_overrides_main.output | 2 +- slang/test/unit/api/translation_overrides_test.dart | 2 +- slang_flutter/lib/slang_flutter.dart | 2 +- .../test/integration/multi_package/gen/strings_a.g.dart | 2 +- .../test/integration/multi_package/gen/strings_a_de.g.dart | 2 +- .../test/integration/multi_package/gen/strings_b.g.dart | 2 +- .../test/integration/multi_package/gen/strings_b_de.g.dart | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) rename slang/lib/{node.dart => generated.dart} (100%) diff --git a/slang/example/lib/i18n/strings.g.dart b/slang/example/lib/i18n/strings.g.dart index 752b061e..3e4d1220 100644 --- a/slang/example/lib/i18n/strings.g.dart +++ b/slang/example/lib/i18n/strings.g.dart @@ -6,14 +6,14 @@ /// Locales: 3 /// Strings: 21 (7 per locale) /// -/// Built on 2024-10-20 at 23:01 UTC +/// Built on 2024-10-21 at 12:02 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang/example/lib/i18n/strings_de.g.dart b/slang/example/lib/i18n/strings_de.g.dart index e75cf975..06b2746f 100644 --- a/slang/example/lib/i18n/strings_de.g.dart +++ b/slang/example/lib/i18n/strings_de.g.dart @@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'strings.g.dart'; // Path: diff --git a/slang/example/lib/i18n/strings_fr_FR.g.dart b/slang/example/lib/i18n/strings_fr_FR.g.dart index 362e4c2e..c46d02a7 100644 --- a/slang/example/lib/i18n/strings_fr_FR.g.dart +++ b/slang/example/lib/i18n/strings_fr_FR.g.dart @@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'strings.g.dart'; // Path: diff --git a/slang/lib/node.dart b/slang/lib/generated.dart similarity index 100% rename from slang/lib/node.dart rename to slang/lib/generated.dart diff --git a/slang/lib/src/builder/generator/generate_header.dart b/slang/lib/src/builder/generator/generate_header.dart index e0e5c184..a7ed04cc 100644 --- a/slang/lib/src/builder/generator/generate_header.dart +++ b/slang/lib/src/builder/generator/generate_header.dart @@ -128,7 +128,7 @@ void _generateImports(GenerateConfig config, StringBuffer buffer) { final imports = [ ...config.imports, 'package:intl/intl.dart', - 'package:slang/node.dart', + 'package:slang/generated.dart', if (config.obfuscation.enabled) 'package:slang/secret.dart', if (config.translationOverrides) 'package:slang/overrides.dart', if (config.flutterIntegration) ...[ diff --git a/slang/lib/src/builder/generator/generate_translations.dart b/slang/lib/src/builder/generator/generate_translations.dart index 05e9d840..4013cd8f 100644 --- a/slang/lib/src/builder/generator/generate_translations.dart +++ b/slang/lib/src/builder/generator/generate_translations.dart @@ -41,7 +41,7 @@ String generateTranslations(GenerateConfig config, I18nData localeData) { config.outputFileName, ...config.imports, 'package:intl/intl.dart', - 'package:slang/node.dart', + 'package:slang/generated.dart', if (config.obfuscation.enabled) 'package:slang/secret.dart', if (config.translationOverrides) 'package:slang/overrides.dart', if (config.flutterIntegration) 'package:flutter/widgets.dart', diff --git a/slang/test/integration/resources/main/_expected_de.output b/slang/test/integration/resources/main/_expected_de.output index e8e94c70..327d4184 100644 --- a/slang/test/integration/resources/main/_expected_de.output +++ b/slang/test/integration/resources/main/_expected_de.output @@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'translations.cgm.dart'; // Path: diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output index 4fe13bde..af4f49a4 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output @@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'translations.cgm.dart'; // Path: diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output index 68eb37a8..39b46186 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output @@ -11,7 +11,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_de.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_de.output index d2a4279e..304886f3 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_de.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_de.output @@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'translations.cgm.dart'; // Path: diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output index c05391fd..275ccdad 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output @@ -11,7 +11,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang/test/integration/resources/main/_expected_main.output b/slang/test/integration/resources/main/_expected_main.output index 68eb37a8..39b46186 100644 --- a/slang/test/integration/resources/main/_expected_main.output +++ b/slang/test/integration/resources/main/_expected_main.output @@ -11,7 +11,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang/test/integration/resources/main/_expected_no_flutter.output b/slang/test/integration/resources/main/_expected_no_flutter.output index 0d1f0c66..76f40368 100644 --- a/slang/test/integration/resources/main/_expected_no_flutter.output +++ b/slang/test/integration/resources/main/_expected_no_flutter.output @@ -10,7 +10,7 @@ // ignore_for_file: type=lint, unused_import import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang/slang.dart'; export 'package:slang/slang.dart'; diff --git a/slang/test/integration/resources/main/_expected_no_locale_handling.output b/slang/test/integration/resources/main/_expected_no_locale_handling.output index e2b8f94f..203e294c 100644 --- a/slang/test/integration/resources/main/_expected_no_locale_handling.output +++ b/slang/test/integration/resources/main/_expected_no_locale_handling.output @@ -11,7 +11,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang/test/integration/resources/main/_expected_obfuscation_de.output b/slang/test/integration/resources/main/_expected_obfuscation_de.output index ecde0366..293a8b1b 100644 --- a/slang/test/integration/resources/main/_expected_obfuscation_de.output +++ b/slang/test/integration/resources/main/_expected_obfuscation_de.output @@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang/secret.dart'; import 'translations.cgm.dart'; diff --git a/slang/test/integration/resources/main/_expected_obfuscation_main.output b/slang/test/integration/resources/main/_expected_obfuscation_main.output index bf0d0ea7..28b00269 100644 --- a/slang/test/integration/resources/main/_expected_obfuscation_main.output +++ b/slang/test/integration/resources/main/_expected_obfuscation_main.output @@ -11,7 +11,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang/secret.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang/test/integration/resources/main/_expected_translation_overrides_de.output b/slang/test/integration/resources/main/_expected_translation_overrides_de.output index 5d38c7a7..d571af61 100644 --- a/slang/test/integration/resources/main/_expected_translation_overrides_de.output +++ b/slang/test/integration/resources/main/_expected_translation_overrides_de.output @@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang/overrides.dart'; import 'translations.cgm.dart'; diff --git a/slang/test/integration/resources/main/_expected_translation_overrides_main.output b/slang/test/integration/resources/main/_expected_translation_overrides_main.output index 08375592..8cf34ec2 100644 --- a/slang/test/integration/resources/main/_expected_translation_overrides_main.output +++ b/slang/test/integration/resources/main/_expected_translation_overrides_main.output @@ -11,7 +11,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang/overrides.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang/test/unit/api/translation_overrides_test.dart b/slang/test/unit/api/translation_overrides_test.dart index 920f31b5..f053f255 100644 --- a/slang/test/unit/api/translation_overrides_test.dart +++ b/slang/test/unit/api/translation_overrides_test.dart @@ -1,6 +1,6 @@ import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang/src/api/locale.dart'; import 'package:slang/src/api/singleton.dart'; import 'package:slang/src/api/translation_overrides.dart'; diff --git a/slang_flutter/lib/slang_flutter.dart b/slang_flutter/lib/slang_flutter.dart index 534b6693..42e738d2 100644 --- a/slang_flutter/lib/slang_flutter.dart +++ b/slang_flutter/lib/slang_flutter.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:slang/slang.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang/overrides.dart'; // ignore: implementation_imports import 'package:slang/src/builder/model/pluralization.dart'; diff --git a/slang_flutter/test/integration/multi_package/gen/strings_a.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_a.g.dart index aefd44fe..80294e9c 100644 --- a/slang_flutter/test/integration/multi_package/gen/strings_a.g.dart +++ b/slang_flutter/test/integration/multi_package/gen/strings_a.g.dart @@ -10,7 +10,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang_flutter/test/integration/multi_package/gen/strings_a_de.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_a_de.g.dart index b264a3eb..1a9c475b 100644 --- a/slang_flutter/test/integration/multi_package/gen/strings_a_de.g.dart +++ b/slang_flutter/test/integration/multi_package/gen/strings_a_de.g.dart @@ -5,7 +5,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'strings_a.g.dart'; // Path: diff --git a/slang_flutter/test/integration/multi_package/gen/strings_b.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_b.g.dart index 42420e22..7b844764 100644 --- a/slang_flutter/test/integration/multi_package/gen/strings_b.g.dart +++ b/slang_flutter/test/integration/multi_package/gen/strings_b.g.dart @@ -10,7 +10,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; diff --git a/slang_flutter/test/integration/multi_package/gen/strings_b_de.g.dart b/slang_flutter/test/integration/multi_package/gen/strings_b_de.g.dart index 96731cee..13aa5ae5 100644 --- a/slang_flutter/test/integration/multi_package/gen/strings_b_de.g.dart +++ b/slang_flutter/test/integration/multi_package/gen/strings_b_de.g.dart @@ -5,7 +5,7 @@ // ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; -import 'package:slang/node.dart'; +import 'package:slang/generated.dart'; import 'strings_b.g.dart'; // Path: From 9faa29e9f197d76e89c79a5d5136d4b7b5d62bd4 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 21 Oct 2024 14:25:02 +0200 Subject: [PATCH 095/118] release: 4.0.0 --- slang/pubspec.yaml | 2 +- slang_build_runner/pubspec.yaml | 4 ++-- slang_flutter/pubspec.yaml | 4 ++-- slang_gpt/pubspec.yaml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index 096ebd08..caeb41a1 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -1,6 +1,6 @@ name: slang description: Localization / Internationalization (i18n) solution. Use JSON, YAML, CSV, or ARB files to create typesafe translations via source generation. -version: 4.0.0-dev.1 +version: 4.0.0 repository: https://github.com/slang-i18n/slang topics: - i18n diff --git a/slang_build_runner/pubspec.yaml b/slang_build_runner/pubspec.yaml index 6c075392..ca31c32c 100644 --- a/slang_build_runner/pubspec.yaml +++ b/slang_build_runner/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_build_runner description: build_runner integration for slang. This library ensures that slang is recognized by build_runner. -version: 4.0.0-dev.0 +version: 4.0.0 repository: https://github.com/slang-i18n/slang environment: @@ -11,7 +11,7 @@ dependencies: glob: ^2.0.2 # Use a tight version to ensure that all features are available - slang: '>=4.0.0-dev.0 <4.1.0' + slang: '>=4.0.0 <4.1.0' dev_dependencies: lints: ^2.0.0 diff --git a/slang_flutter/pubspec.yaml b/slang_flutter/pubspec.yaml index 52d1c939..d605bd95 100644 --- a/slang_flutter/pubspec.yaml +++ b/slang_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_flutter description: Flutter support for slang. This library provides helpful Flutter API. -version: 4.0.0-dev.0 +version: 4.0.0 repository: https://github.com/slang-i18n/slang environment: @@ -14,7 +14,7 @@ dependencies: sdk: flutter # Use a tight version to ensure that all features are available - slang: '>=4.0.0-dev.0 <4.1.0' + slang: '>=4.0.0 <4.1.0' dev_dependencies: flutter_test: diff --git a/slang_gpt/pubspec.yaml b/slang_gpt/pubspec.yaml index cbef85d2..c376cbd7 100644 --- a/slang_gpt/pubspec.yaml +++ b/slang_gpt/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: collection: ^1.15.0 http: '>=0.13.0 <2.0.0' - slang: '>=4.0.0-dev.0 <5.0.0' + slang: '>=4.0.0 <5.0.0' dev_dependencies: lints: ^2.0.0 From 13d64ff8b870b7eb331c0e58279a51731265a388 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Wed, 23 Oct 2024 00:35:26 +0200 Subject: [PATCH 096/118] fix: correctly follow enum_name and class_name --- slang/CHANGELOG.md | 4 ++++ .../builder/generator/generate_header.dart | 22 +++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 338d53cf..0797b555 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.0.1 + +- fix: correctly generate with `enum_name` and `class_name` different from `AppLocale` / `Translations` (#254) + ## 4.0.0 **DateFormat, NumberFormat, and Lazy loading** diff --git a/slang/lib/src/builder/generator/generate_header.dart b/slang/lib/src/builder/generator/generate_header.dart index a7ed04cc..79abaf2e 100644 --- a/slang/lib/src/builder/generator/generate_header.dart +++ b/slang/lib/src/builder/generator/generate_header.dart @@ -264,7 +264,7 @@ void _generateEnum({ buffer.writeln(); buffer.writeln('\t@override'); buffer.writeln( - '\t${sync ? 'Translations' : 'Future'} build${sync ? 'Sync' : ''}({'); + '\t${sync ? config.className : 'Future<${config.className}>'} build${sync ? 'Sync' : ''}({'); buffer.writeln('\t\tMap? overrides,'); buffer.writeln('\t\tPluralResolver? cardinalResolver,'); buffer.writeln('\t\tPluralResolver? ordinalResolver,'); @@ -437,7 +437,7 @@ void _generateLocaleSettings({ } buffer.writeln( - '\tstatic Future setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver('); + '\tstatic Future setPluralResolver({String? language, $enumName? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver('); buffer.writeln('\t\tlanguage: language,'); buffer.writeln('\t\tlocale: locale,'); buffer.writeln('\t\tcardinalResolver: cardinalResolver,'); @@ -445,9 +445,9 @@ void _generateLocaleSettings({ buffer.writeln('\t);'); if (config.translationOverrides) { buffer.writeln( - '\tstatic Future overrideTranslations({required AppLocale locale, required FileType fileType, required String content}) => instance.overrideTranslations(locale: locale, fileType: fileType, content: content);'); + '\tstatic Future overrideTranslations({required $enumName locale, required FileType fileType, required String content}) => instance.overrideTranslations(locale: locale, fileType: fileType, content: content);'); buffer.writeln( - '\tstatic Future overrideTranslationsFromMap({required AppLocale locale, required bool isFlatMap, required Map map}) => instance.overrideTranslationsFromMap(locale: locale, isFlatMap: isFlatMap, map: map);'); + '\tstatic Future overrideTranslationsFromMap({required $enumName locale, required bool isFlatMap, required Map map}) => instance.overrideTranslationsFromMap(locale: locale, isFlatMap: isFlatMap, map: map);'); } // sync versions @@ -463,7 +463,7 @@ void _generateLocaleSettings({ } buffer.writeln( - '\tstatic void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync('); + '\tstatic void setPluralResolverSync({String? language, $enumName? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync('); buffer.writeln('\t\tlanguage: language,'); buffer.writeln('\t\tlocale: locale,'); buffer.writeln('\t\tcardinalResolver: cardinalResolver,'); @@ -472,9 +472,9 @@ void _generateLocaleSettings({ if (config.translationOverrides) { buffer.writeln( - '\tstatic void overrideTranslationsSync({required AppLocale locale, required FileType fileType, required String content}) => instance.overrideTranslationsSync(locale: locale, fileType: fileType, content: content);'); + '\tstatic void overrideTranslationsSync({required $enumName locale, required FileType fileType, required String content}) => instance.overrideTranslationsSync(locale: locale, fileType: fileType, content: content);'); buffer.writeln( - '\tstatic void overrideTranslationsFromMapSync({required AppLocale locale, required bool isFlatMap, required Map map}) => instance.overrideTranslationsFromMapSync(locale: locale, isFlatMap: isFlatMap, map: map);'); + '\tstatic void overrideTranslationsFromMapSync({required $enumName locale, required bool isFlatMap, required Map map}) => instance.overrideTranslationsFromMapSync(locale: locale, isFlatMap: isFlatMap, map: map);'); } buffer.writeln('}'); @@ -519,13 +519,13 @@ void _generateUtil({ '\tstatic List get supportedLocalesRaw => instance.supportedLocalesRaw;'); if (config.translationOverrides) { buffer.writeln( - '\tstatic Future<${config.className}> buildWithOverrides({required AppLocale locale, required FileType fileType, required String content, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverrides(locale: locale, fileType: fileType, content: content, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);'); + '\tstatic Future<${config.className}> buildWithOverrides({required $enumName locale, required FileType fileType, required String content, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverrides(locale: locale, fileType: fileType, content: content, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);'); buffer.writeln( - '\tstatic Future<${config.className}> buildWithOverridesFromMap({required AppLocale locale, required bool isFlatMap, required Map map, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverridesFromMap(locale: locale, isFlatMap: isFlatMap, map: map, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);'); + '\tstatic Future<${config.className}> buildWithOverridesFromMap({required $enumName locale, required bool isFlatMap, required Map map, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverridesFromMap(locale: locale, isFlatMap: isFlatMap, map: map, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);'); buffer.writeln( - '\tstatic ${config.className} buildWithOverridesSync({required AppLocale locale, required FileType fileType, required String content, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverridesSync(locale: locale, fileType: fileType, content: content, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);'); + '\tstatic ${config.className} buildWithOverridesSync({required $enumName locale, required FileType fileType, required String content, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverridesSync(locale: locale, fileType: fileType, content: content, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);'); buffer.writeln( - '\tstatic ${config.className} buildWithOverridesFromMapSync({required AppLocale locale, required bool isFlatMap, required Map map, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverridesFromMapSync(locale: locale, isFlatMap: isFlatMap, map: map, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);'); + '\tstatic ${config.className} buildWithOverridesFromMapSync({required $enumName locale, required bool isFlatMap, required Map map, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverridesFromMapSync(locale: locale, isFlatMap: isFlatMap, map: map, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);'); } buffer.writeln('}'); From 663f711daaf91eddcaca693049cfda64fe2ba4cf Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Wed, 23 Oct 2024 14:53:32 +0200 Subject: [PATCH 097/118] feat: add format config --- slang/CHANGELOG.md | 3 +- slang/README.md | 22 +++++++++++++++ slang/bin/slang.dart | 28 ++++++++++++++++--- .../builder/builder/raw_config_builder.dart | 11 ++++++++ .../lib/src/builder/model/format_config.dart | 13 +++++++++ slang/lib/src/builder/model/raw_config.dart | 12 ++++++++ slang/lib/src/builder/utils/path_utils.dart | 9 ++++++ slang/lib/src/runner/utils/format.dart | 25 +++++++++++++++++ slang_build_runner/CHANGELOG.md | 5 ++++ .../lib/slang_build_runner.dart | 18 ++++++++++-- slang_build_runner/pubspec.yaml | 1 + slang_flutter/CHANGELOG.md | 4 +++ 12 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 slang/lib/src/builder/model/format_config.dart create mode 100644 slang/lib/src/runner/utils/format.dart diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 0797b555..fe3ac095 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,5 +1,6 @@ -## 4.0.1 +## 4.1.0 +- feat: add `format` config to automatically format generated files (#184) - fix: correctly generate with `enum_name` and `class_name` different from `AppLocale` / `Translations` (#254) ## 4.0.0 diff --git a/slang/README.md b/slang/README.md index 05a03c7d..c7866521 100644 --- a/slang/README.md +++ b/slang/README.md @@ -80,6 +80,7 @@ dart run slang migrate arb src.arb dest.json # migrate arb to json - [Comments](#-comments) - [Recasing](#-recasing) - [Obfuscation](#-obfuscation) + - [Formatting](#-formatting) - [Dart Only](#-dart-only) - [Tools](#tools) - [Main Command](#-main-command) @@ -332,6 +333,9 @@ interfaces: obfuscation: enabled: false secret: somekey +format: + enabled: true + width: 150 imports: - 'package:my_package/path_to_enum.dart' ``` @@ -398,6 +402,9 @@ targets: obfuscation: enabled: false secret: somekey + format: + enabled: true + width: 150 imports: - 'package:my_package/path_to_enum.dart' ``` @@ -438,6 +445,8 @@ targets: | `children of interfaces` | `Pairs of Alias:Path` | alias interfaces [(i)](#-interfaces) | `null` | | `obfuscation`/`enabled` | `Boolean` | enable obfuscation [(i)](#-obfuscation) | `false` | | `obfuscation`/`secret` | `String` | obfuscation secret (random if null) [(i)](#-obfuscation) | `null` | +| `format`/`enabled` | `Boolean` | enable auto format [(i)](#-formatting) | `false` | +| `format`/`width` | `String` | set line length / characters per line [(i)](#-formatting) | `null` | | `imports` | `List` | generate import statements | `[]` | ## Main Features @@ -1445,6 +1454,19 @@ Keep in mind that this only prevents simple string searches of the binary. An experienced reverse engineer can still find the strings given enough time. +### ➤ Formatting + +The generated code is not formatted by default to keep the algorithm fast and efficient. + +You can enable it: + +```yaml +# Config +format: + enabled: true + width: 150 # optional +``` + ### ➤ Dart Only You can use this library without flutter. diff --git a/slang/bin/slang.dart b/slang/bin/slang.dart index d74b799b..867993b0 100644 --- a/slang/bin/slang.dart +++ b/slang/bin/slang.dart @@ -14,6 +14,7 @@ import 'package:slang/src/runner/edit.dart'; import 'package:slang/src/runner/migrate.dart'; import 'package:slang/src/runner/normalize.dart'; import 'package:slang/src/runner/stats.dart'; +import 'package:slang/src/runner/utils/format.dart'; import 'package:watcher/watcher.dart'; /// Determines what the runner will do @@ -348,12 +349,31 @@ Future generateTranslations({ locale: locale, )}'); } - print(''); + } - if (stopwatch != null) { - print( - '${_GREEN}Translations generated successfully. ${stopwatch.elapsedSeconds}$_RESET'); + if (fileCollection.config.format.enabled) { + final formatDir = PathUtils.getParentPath(outputFilePath)!; + Stopwatch? formatStopwatch; + if (verbose) { + print(''); + print('Formatting "$formatDir" ...'); + if (stopwatch != null) { + formatStopwatch = Stopwatch()..start(); + } } + await runDartFormat( + dir: formatDir, + width: fileCollection.config.format.width, + ); + if (verbose && formatStopwatch != null) { + print('Format done. ${formatStopwatch.elapsedSeconds}'); + } + } + + if (verbose && stopwatch != null) { + print(''); + print( + '${_GREEN}Translations generated successfully. ${stopwatch.elapsedSeconds}$_RESET'); } } diff --git a/slang/lib/src/builder/builder/raw_config_builder.dart b/slang/lib/src/builder/builder/raw_config_builder.dart index f1623f54..2cd0eea2 100644 --- a/slang/lib/src/builder/builder/raw_config_builder.dart +++ b/slang/lib/src/builder/builder/raw_config_builder.dart @@ -1,5 +1,6 @@ import 'package:slang/src/builder/model/context_type.dart'; import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/format_config.dart'; import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/model/interface.dart'; import 'package:slang/src/builder/model/obfuscation_config.dart'; @@ -113,6 +114,8 @@ class RawConfigBuilder { obfuscation: (map['obfuscation'] as Map?) ?.toObfuscationConfig() ?? RawConfig.defaultObfuscationConfig, + format: (map['format'] as Map?)?.toFormatConfig() ?? + RawConfig.defaultFormatConfig, imports: map['imports']?.cast() ?? RawConfig.defaultImports, rawMap: map, ); @@ -218,6 +221,14 @@ extension on Map { secret: this['secret'], ); } + + /// Parses the 'format' config + FormatConfig toFormatConfig() { + return FormatConfig( + enabled: this['enabled'], + width: this['width'], + ); + } } extension on String { diff --git a/slang/lib/src/builder/model/format_config.dart b/slang/lib/src/builder/model/format_config.dart new file mode 100644 index 00000000..82550792 --- /dev/null +++ b/slang/lib/src/builder/model/format_config.dart @@ -0,0 +1,13 @@ +/// Configuration model for the "format" entry. +class FormatConfig { + static const bool defaultEnabled = false; + static const int? defaultWidth = null; + + final bool enabled; + final int? width; + + const FormatConfig({ + required this.enabled, + required this.width, + }); +} diff --git a/slang/lib/src/builder/model/raw_config.dart b/slang/lib/src/builder/model/raw_config.dart index d866ecdb..4e81e315 100644 --- a/slang/lib/src/builder/model/raw_config.dart +++ b/slang/lib/src/builder/model/raw_config.dart @@ -1,5 +1,6 @@ import 'package:slang/src/builder/model/context_type.dart'; import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/format_config.dart'; import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/model/interface.dart'; import 'package:slang/src/builder/model/obfuscation_config.dart'; @@ -39,6 +40,10 @@ class RawConfig { static const List defaultInterfaces = []; static final ObfuscationConfig defaultObfuscationConfig = ObfuscationConfig.disabled(); + static const FormatConfig defaultFormatConfig = FormatConfig( + enabled: FormatConfig.defaultEnabled, + width: FormatConfig.defaultWidth, + ); static const List defaultImports = []; final FileType fileType; @@ -72,6 +77,7 @@ class RawConfig { final List contexts; final List interfaces; final ObfuscationConfig obfuscation; + final FormatConfig format; final List imports; /// Used by external tools to access the raw config. (e.g. slang_gpt) @@ -108,6 +114,7 @@ class RawConfig { required this.contexts, required this.interfaces, required this.obfuscation, + required this.format, required this.imports, required this.rawMap, }) : fileType = _determineFileType(inputFilePattern), @@ -139,6 +146,7 @@ class RawConfig { List? contexts, List? interfaces, ObfuscationConfig? obfuscation, + FormatConfig? format, }) { return RawConfig( baseLocale: baseLocale ?? this.baseLocale, @@ -172,6 +180,7 @@ class RawConfig { contexts: contexts ?? this.contexts, interfaces: interfaces ?? this.interfaces, obfuscation: obfuscation ?? this.obfuscation, + format: format ?? this.format, imports: imports, rawMap: rawMap, ); @@ -249,6 +258,8 @@ class RawConfig { } } print(' -> obfuscation: ${obfuscation.enabled ? 'enabled' : 'disabled'}'); + print( + ' -> format: ${format.enabled ? 'enabled (width=${format.width})' : 'disabled'}'); print(' -> imports: $imports'); } @@ -285,6 +296,7 @@ class RawConfig { contexts: RawConfig.defaultContexts, interfaces: RawConfig.defaultInterfaces, obfuscation: RawConfig.defaultObfuscationConfig, + format: RawConfig.defaultFormatConfig, imports: RawConfig.defaultImports, className: RawConfig.defaultClassName, rawMap: {}, diff --git a/slang/lib/src/builder/utils/path_utils.dart b/slang/lib/src/builder/utils/path_utils.dart index 147df96a..1ca26aa2 100644 --- a/slang/lib/src/builder/utils/path_utils.dart +++ b/slang/lib/src/builder/utils/path_utils.dart @@ -39,6 +39,15 @@ class PathUtils { return segments[segments.length - 2]; } + /// Converts /a/b/file.json to /a/b + static String? getParentPath(String path) { + final segments = getPathSegments(path); + if (segments.length == 1) { + return null; + } + return segments.sublist(0, segments.length - 1).join('/'); + } + /// finds locale in directory path /// eg. /en-US/b/file.json will result in en-US static I18nLocale? findDirectoryLocale( diff --git a/slang/lib/src/runner/utils/format.dart b/slang/lib/src/runner/utils/format.dart new file mode 100644 index 00000000..e60a3dd5 --- /dev/null +++ b/slang/lib/src/runner/utils/format.dart @@ -0,0 +1,25 @@ +import 'dart:io'; + +/// Formats a given directory by running a separate dart format process. +Future runDartFormat({ + required String dir, + required int? width, +}) async { + final executable = Platform.resolvedExecutable; + + final status = Process.runSync( + executable, + [ + 'format', + dir, + if (width != null) ...[ + '--line-length', + width.toString(), + ], + ], + ); + + if (status.exitCode != 0) { + print('Dart format failed with exit code ${status.exitCode}'); + } +} diff --git a/slang_build_runner/CHANGELOG.md b/slang_build_runner/CHANGELOG.md index f1360629..e2adbbbc 100644 --- a/slang_build_runner/CHANGELOG.md +++ b/slang_build_runner/CHANGELOG.md @@ -1,3 +1,8 @@ +## 4.1.0 + +- feat: add `dart_style` dependency to auto format generated files +- bump `slang` to `4.1.0` + ## 4.0.0 - Bump `slang` to `4.0.0` diff --git a/slang_build_runner/lib/slang_build_runner.dart b/slang_build_runner/lib/slang_build_runner.dart index 5cf962a6..a28fd039 100644 --- a/slang_build_runner/lib/slang_build_runner.dart +++ b/slang_build_runner/lib/slang_build_runner.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:build/build.dart'; +import 'package:dart_style/dart_style.dart'; import 'package:glob/glob.dart'; // ignore: implementation_imports import 'package:slang/src/builder/builder/raw_config_builder.dart'; @@ -81,10 +82,15 @@ class I18nBuilder implements Builder { // STEP 4: write output to hard drive FileUtils.createMissingFolders(filePath: outputFilePath); + final formatter = DartFormatter( + pageWidth: config.format.width, + ); + FileUtils.writeFile( path: BuildResultPaths.mainPath(outputFilePath), - content: result.main, + content: result.main.formatted(config, formatter), ); + for (final entry in result.translations.entries) { final locale = entry.key; final localeTranslations = entry.value; @@ -93,7 +99,7 @@ class I18nBuilder implements Builder { outputPath: outputFilePath, locale: locale, ), - content: localeTranslations, + content: localeTranslations.formatted(config, formatter), ); } } @@ -109,4 +115,12 @@ extension on String { String getFileExtension() { return PathUtils.getFileExtension(this); } + + /// Conditionally formats the string using the provided [formatter]. + String formatted(RawConfig config, DartFormatter formatter) { + return switch (config.format.enabled) { + true => formatter.format(this), + false => this, + }; + } } diff --git a/slang_build_runner/pubspec.yaml b/slang_build_runner/pubspec.yaml index ca31c32c..bded4844 100644 --- a/slang_build_runner/pubspec.yaml +++ b/slang_build_runner/pubspec.yaml @@ -8,6 +8,7 @@ environment: dependencies: build: ^2.2.1 + dart_style: ^2.3.0 glob: ^2.0.2 # Use a tight version to ensure that all features are available diff --git a/slang_flutter/CHANGELOG.md b/slang_flutter/CHANGELOG.md index 541f47f6..44c75e59 100644 --- a/slang_flutter/CHANGELOG.md +++ b/slang_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.1.0 + +- bump `slang` to `4.1.0` + ## 4.0.0 - Bump `slang` to `4.0.0` From 7311f47cfdcb74510e3cd21b5364473cc760b138 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Wed, 23 Oct 2024 15:50:13 +0200 Subject: [PATCH 098/118] release: 4.1.0 --- slang/pubspec.yaml | 2 +- slang_build_runner/pubspec.yaml | 4 ++-- slang_flutter/pubspec.yaml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index caeb41a1..6a8dcae3 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -1,6 +1,6 @@ name: slang description: Localization / Internationalization (i18n) solution. Use JSON, YAML, CSV, or ARB files to create typesafe translations via source generation. -version: 4.0.0 +version: 4.1.0 repository: https://github.com/slang-i18n/slang topics: - i18n diff --git a/slang_build_runner/pubspec.yaml b/slang_build_runner/pubspec.yaml index bded4844..d4a5cbca 100644 --- a/slang_build_runner/pubspec.yaml +++ b/slang_build_runner/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_build_runner description: build_runner integration for slang. This library ensures that slang is recognized by build_runner. -version: 4.0.0 +version: 4.1.0 repository: https://github.com/slang-i18n/slang environment: @@ -12,7 +12,7 @@ dependencies: glob: ^2.0.2 # Use a tight version to ensure that all features are available - slang: '>=4.0.0 <4.1.0' + slang: '>=4.1.0 <4.2.0' dev_dependencies: lints: ^2.0.0 diff --git a/slang_flutter/pubspec.yaml b/slang_flutter/pubspec.yaml index d605bd95..4f403c85 100644 --- a/slang_flutter/pubspec.yaml +++ b/slang_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_flutter description: Flutter support for slang. This library provides helpful Flutter API. -version: 4.0.0 +version: 4.1.0 repository: https://github.com/slang-i18n/slang environment: @@ -14,7 +14,7 @@ dependencies: sdk: flutter # Use a tight version to ensure that all features are available - slang: '>=4.0.0 <4.1.0' + slang: '>=4.1.0 <4.2.0' dev_dependencies: flutter_test: From 0fdaf0228049cf60eb556e829f9d3490da9c43f6 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Wed, 23 Oct 2024 16:04:19 +0200 Subject: [PATCH 099/118] style: remove unnecessary null check --- slang_flutter/lib/slang_flutter.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slang_flutter/lib/slang_flutter.dart b/slang_flutter/lib/slang_flutter.dart index 42e738d2..c366a014 100644 --- a/slang_flutter/lib/slang_flutter.dart +++ b/slang_flutter/lib/slang_flutter.dart @@ -145,7 +145,7 @@ class _TranslationProviderState, final E localeTyped = widget.settings.utils.parseAppLocale(locale); await widget.settings.loadLocale(localeTyped); setState(() { - this.translations = widget.settings.translationMap[localeTyped]!; + this.translations = widget.settings.translationMap[localeTyped]; }); } From c5a75f02a38c13f116ad85df802f5b33f07545f7 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Wed, 23 Oct 2024 16:07:51 +0200 Subject: [PATCH 100/118] docs: add L10n feature --- slang/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/slang/README.md b/slang/README.md index c7866521..e7e062bf 100644 --- a/slang/README.md +++ b/slang/README.md @@ -28,14 +28,15 @@ String a = t.mainScreen.title; // simple use case String b = t.game.end.highscore(score: 32.6); // with parameters String c = t.items(n: 2); // with pluralization String d = t.greet(name: 'Tom', context: Gender.male); // with custom context -String e = t.intro.step[4]; // with index -String f = t.error.type['WARNING']; // with dynamic key -String g = t['mainScreen.title']; // with fully dynamic key -TextSpan h = t.greet(name: TextSpan(text: 'Tom')); // with RichText +String e = t.greet(today: DateTime.now()); // with L10n +String f = t.intro.step[4]; // with index +String g = t.error.type['WARNING']; // with dynamic key +String h = t['mainScreen.title']; // with fully dynamic key +TextSpan i = t.greet(name: TextSpan(text: 'Tom')); // with RichText PageData page0 = t.onboarding.pages[0]; // with interfaces PageData page1 = t.onboarding.pages[1]; -String i = page1.title; // type-safe call +String j = page1.title; // type-safe call ``` An extensive CLI will help you to manage the translations: From 835779445c5b176ea60412a085dd3698554852b5 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Fri, 25 Oct 2024 02:37:45 +0200 Subject: [PATCH 101/118] test: more l10n tests --- slang/lib/src/builder/model/node.dart | 13 ----------- .../unit/api/translation_overrides_test.dart | 22 ++++++++++++++++--- slang/test/unit/model/node_test.dart | 20 +++++++++++++++++ slang/test/util/text_node_builder.dart | 4 +++- 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/slang/lib/src/builder/model/node.dart b/slang/lib/src/builder/model/node.dart index 0559f684..dd9329bf 100644 --- a/slang/lib/src/builder/model/node.dart +++ b/slang/lib/src/builder/model/node.dart @@ -236,9 +236,6 @@ abstract class TextNode extends Node implements LeafNode { /// the type must be specified and cannot be [Object]. Map get paramTypeMap; - /// Map of parameters to their format in raw string. - Map get paramFormatMap; - /// Set of paths to [TextNode]s /// Will be used for 2nd round, determining the final set of parameters Set get links; @@ -294,11 +291,6 @@ class StringTextNode extends TextNode { @override Map get paramTypeMap => _paramTypeMap; - final _paramFormatMap = {}; - - @override - Map get paramFormatMap => _paramFormatMap; - StringTextNode({ required super.path, required super.rawPath, @@ -395,11 +387,6 @@ class RichTextNode extends TextNode { @override Map get paramTypeMap => _paramTypeMap; - final _paramFormatMap = {}; - - @override - Map get paramFormatMap => _paramFormatMap; - RichTextNode({ required super.path, required super.rawPath, diff --git a/slang/test/unit/api/translation_overrides_test.dart b/slang/test/unit/api/translation_overrides_test.dart index f053f255..54ba7435 100644 --- a/slang/test/unit/api/translation_overrides_test.dart +++ b/slang/test/unit/api/translation_overrides_test.dart @@ -91,6 +91,20 @@ void main() { test('Should return string with predefined type', () { final meta = _buildMetaWithOverrides({ 'aboutPage.title': r'About ${date: predefined}', + }, formatters: { + 'predefined': ValueFormatter(() => DateFormat('yyyy')), + }); + final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { + 'date': DateTime(2022, 3, 12), + }); + expect(parsed, 'About 2022'); + }); + + test('Should prefer predefined type over built-in type', () { + final meta = _buildMetaWithOverrides({ + 'aboutPage.title': r'About ${date: yMd}', + }, formatters: { + 'yMd': ValueFormatter(() => DateFormat('yyyy')), }); final parsed = TranslationOverrides.string(meta, 'aboutPage.title', { 'date': DateTime(2022, 3, 12), @@ -145,12 +159,14 @@ void main() { TranslationMetadata _buildMetaWithOverrides( Map overrides, { String? locale, + Map formatters = const {}, }) { final utils = _Utils(); final translations = utils.buildWithOverridesFromMapSync( - locale: FakeAppLocale(languageCode: locale ?? 'en', types: { - 'predefined': ValueFormatter(() => DateFormat('yyyy')), - }), + locale: FakeAppLocale( + languageCode: locale ?? 'en', + types: formatters, + ), isFlatMap: false, map: overrides, ); diff --git a/slang/test/unit/model/node_test.dart b/slang/test/unit/model/node_test.dart index b70dafa4..b1670111 100644 --- a/slang/test/unit/model/node_test.dart +++ b/slang/test/unit/model/node_test.dart @@ -1,3 +1,4 @@ +import 'package:slang/src/builder/builder/translation_model_builder.dart'; import 'package:slang/src/builder/model/enums.dart'; import 'package:slang/src/builder/model/node.dart'; import 'package:test/test.dart'; @@ -298,6 +299,25 @@ void main() { ); expect(node.params, {'price'}); }); + + test('with predefined format', () { + final test = r'Your price is ${price: eur}'; + final node = textNode( + test, + StringInterpolation.dart, + formatters: { + 'eur': FormatTypeInfo( + paramType: 'NumberFormat', + implementation: 'currency(symbol: "EUR")', + ), + }, + ); + expect( + node.content, + r"Your price is ${_root.$meta.types['eur']!.format(price)}", + ); + expect(node.params, {'price'}); + }); }); group(StringInterpolation.braces, () { diff --git a/slang/test/util/text_node_builder.dart b/slang/test/util/text_node_builder.dart index d007b9e9..1a18922c 100644 --- a/slang/test/util/text_node_builder.dart +++ b/slang/test/util/text_node_builder.dart @@ -1,3 +1,4 @@ +import 'package:slang/src/builder/builder/translation_model_builder.dart'; import 'package:slang/src/builder/model/enums.dart'; import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/model/node.dart'; @@ -9,13 +10,14 @@ StringTextNode textNode( StringInterpolation interpolation, { CaseStyle? paramCase, Map>? linkParamMap, + Map formatters = const {}, }) { return StringTextNode( path: '', rawPath: '', modifiers: {}, locale: _locale, - types: {}, + types: formatters, raw: raw, comment: null, interpolation: interpolation, From 2f11761ab6b0e6c757c404d39062c6cc3e31143c Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Fri, 25 Oct 2024 03:31:13 +0200 Subject: [PATCH 102/118] fix: do not override locale --- slang/CHANGELOG.md | 4 + .../builder/text/l10n_override_parser.dart | 19 ++- .../src/builder/builder/text/l10n_parser.dart | 26 +++- .../generator/generate_translations.dart | 3 + .../builder/utils/parameter_string_ext.dart | 31 +++++ .../string_interpolation_extensions.dart | 120 +++++++++--------- .../text/l10n_override_parser_test.dart | 30 +++++ .../unit/builder/text/l10n_parser_test.dart | 41 ++++++ .../unit/utils/parameter_string_ext_test.dart | 29 +++++ 9 files changed, 229 insertions(+), 74 deletions(-) create mode 100644 slang/lib/src/builder/utils/parameter_string_ext.dart create mode 100644 slang/test/unit/utils/parameter_string_ext_test.dart diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index fe3ac095..2261a01a 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.1.1 + +- fix: do not override `locale` in L10n format definition + ## 4.1.0 - feat: add `format` config to automatically format generated files (#184) diff --git a/slang/lib/src/builder/builder/text/l10n_override_parser.dart b/slang/lib/src/builder/builder/text/l10n_override_parser.dart index 4e01e030..14d78985 100644 --- a/slang/lib/src/builder/builder/text/l10n_override_parser.dart +++ b/slang/lib/src/builder/builder/text/l10n_override_parser.dart @@ -1,6 +1,7 @@ import 'package:intl/intl.dart'; import 'package:slang/src/api/formatter.dart'; import 'package:slang/src/builder/builder/text/l10n_parser.dart'; +import 'package:slang/src/builder/utils/parameter_string_ext.dart'; class L10nOverrideResult { final String methodName; @@ -62,16 +63,17 @@ String? digestL10nOverride({ }; final formatter = Function.apply(formatterBuilder, [], { - ...arguments, #locale: locale, + ...arguments, }); return formatter.format(value); } else { // positional arguments - final argument = switch (parsed.arguments) { - String args => parseSinglePositionalArgument(args), - null => null, + final arguments = switch (parsed.arguments) { + String args => + args.splitParameters().map((s) => parseSinglePositionalArgument(s)), + null => const [], }; final Function formatterBuilder = switch (parsed.methodName) { 'NumberFormat.decimalPattern' => NumberFormat.decimalPattern, @@ -88,11 +90,14 @@ String? digestL10nOverride({ _ => throw UnimplementedError('Unknown formatter: ${parsed.methodName}'), }; + final has2Arguments = positionalWith2Arguments.contains(parsed.methodName); final formatter = Function.apply( formatterBuilder, [ - if (argument != null) argument, - locale, + ...arguments, + if ((has2Arguments && arguments.length < 2) || + (!has2Arguments && arguments.isEmpty)) + locale, ], ); return formatter.format(value); @@ -101,7 +106,7 @@ String? digestL10nOverride({ Map parseArguments(String arguments) { final result = {}; - final parts = arguments.split(','); + final parts = arguments.splitParameters(); for (final part in parts) { final keyValue = part.split(':'); if (keyValue.length != 2) { diff --git a/slang/lib/src/builder/builder/text/l10n_parser.dart b/slang/lib/src/builder/builder/text/l10n_parser.dart index eec76671..502aa4e4 100644 --- a/slang/lib/src/builder/builder/text/l10n_parser.dart +++ b/slang/lib/src/builder/builder/text/l10n_parser.dart @@ -1,4 +1,5 @@ import 'package:slang/src/builder/model/i18n_locale.dart'; +import 'package:slang/src/builder/utils/parameter_string_ext.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; class ParseL10nResult { @@ -54,6 +55,11 @@ const _dateFormats = { 'jms', }; +const positionalWith2Arguments = { + 'DateFormat', + 'NumberFormat', +}; + final _dateFormatsWithClass = { for (final format in _dateFormats) 'DateFormat.$format', 'DateFormat', @@ -127,14 +133,24 @@ ParseL10nResult? parseL10n({ if (parsed.paramType == 'num' && numberFormatsWithNamedParameters.contains(parsed.methodName)) { // add locale as named parameter - if (parsed.arguments == null) { - arguments = "locale: '${locale.underscoreTag}'"; - } else { - arguments = "$arguments, locale: '${locale.underscoreTag}'"; + if (!arguments.contains('locale')) { + if (parsed.arguments == null) { + arguments = "locale: '${locale.underscoreTag}'"; + } else { + arguments = "$arguments, locale: '${locale.underscoreTag}'"; + } } } else { // add locale as positional parameter - if (!arguments.contains('locale:')) { + final has2Arguments = positionalWith2Arguments.contains(parsed.methodName); + final containsLocale = switch (has2Arguments) { + // If there is only 1 argument, then it is the locale + false => arguments.trim().isNotEmpty, + // If there are 2 arguments, then the locale is the second one + true => arguments.splitParameters().length >= 2, + }; + + if (!containsLocale) { if (arguments.isEmpty) { arguments = "'${locale.underscoreTag}'"; } else { diff --git a/slang/lib/src/builder/generator/generate_translations.dart b/slang/lib/src/builder/generator/generate_translations.dart index 4013cd8f..c22a82f7 100644 --- a/slang/lib/src/builder/generator/generate_translations.dart +++ b/slang/lib/src/builder/generator/generate_translations.dart @@ -206,6 +206,9 @@ void _generateClass( if (localeData.types.isNotEmpty) { buffer.writeln('\t\t types: {'); for (final entry in localeData.types.entries) { + // trim NumberFormat.currency(symbol: '€', locale: 'en').format(value) + // to NumberFormat.currency(symbol: '€', locale: 'en') + // removing 14 characters buffer.writeln( '\t\t \'${entry.key}\': ValueFormatter(() => ${entry.value.substring(0, entry.value.length - 14)}),', ); diff --git a/slang/lib/src/builder/utils/parameter_string_ext.dart b/slang/lib/src/builder/utils/parameter_string_ext.dart new file mode 100644 index 00000000..f88fedae --- /dev/null +++ b/slang/lib/src/builder/utils/parameter_string_ext.dart @@ -0,0 +1,31 @@ +extension OverrideStringExt on String { + /// Splits the string by comma, ignoring commas inside quotes, and + /// trims the parts. + List splitParameters() { + if (isEmpty) { + return []; + } + + final parts = []; + final characters = split(''); + final result = StringBuffer(); + bool inQuotes = false; + for (final c in characters) { + if (c == ',') { + if (inQuotes) { + result.write(c); + } else { + parts.add(result.toString().trim()); + result.clear(); + } + } else { + if (c == "'" || c == '"') { + inQuotes = !inQuotes; + } + result.write(c); + } + } + parts.add(result.toString().trim()); + return parts; + } +} diff --git a/slang/lib/src/builder/utils/string_interpolation_extensions.dart b/slang/lib/src/builder/utils/string_interpolation_extensions.dart index 3ab352a4..cb05ea53 100644 --- a/slang/lib/src/builder/utils/string_interpolation_extensions.dart +++ b/slang/lib/src/builder/utils/string_interpolation_extensions.dart @@ -75,8 +75,7 @@ extension StringInterpolationExtensions on String { String replaceDartNormalizedInterpolation({ required String Function(String match) replacer, }) { - return _replaceBetween( - input: this, + return replaceBetween( startCharacter: r'${', endCharacter: '}', replacer: replacer, @@ -87,8 +86,7 @@ extension StringInterpolationExtensions on String { String replaceBracesInterpolation({ required String Function(String match) replacer, }) { - return _replaceBetween( - input: this, + return replaceBetween( startCharacter: '{', endCharacter: '}', replacer: replacer, @@ -99,73 +97,71 @@ extension StringInterpolationExtensions on String { String replaceDoubleBracesInterpolation({ required String Function(String match) replacer, }) { - return _replaceBetween( - input: this, + return replaceBetween( startCharacter: '{{', endCharacter: '}}', replacer: replacer, ); } -} -String _replaceBetween({ - required String input, - required String startCharacter, - required String endCharacter, - required String Function(String match) replacer, -}) { - String curr = input; - final buffer = StringBuffer(); - final startCharacterLength = startCharacter.length; - final endCharacterLength = endCharacter.length; - - do { - int startIndex = curr.indexOf(startCharacter); - if (startIndex == -1) { - buffer.write(curr); - break; - } - if (startIndex >= 1 && curr[startIndex - 1] == '\\') { - // ignore because of preceding \ - buffer.write(curr.substring(0, startIndex - 1)); // do not include \ - buffer.write(startCharacter); - if (startIndex + 1 < curr.length) { - curr = curr.substring(startIndex + startCharacterLength); - continue; - } else { + String replaceBetween({ + required String startCharacter, + required String endCharacter, + required String Function(String match) replacer, + }) { + String curr = this; + final buffer = StringBuffer(); + final startCharacterLength = startCharacter.length; + final endCharacterLength = endCharacter.length; + + do { + int startIndex = curr.indexOf(startCharacter); + if (startIndex == -1) { + buffer.write(curr); break; } - } + if (startIndex >= 1 && curr[startIndex - 1] == '\\') { + // ignore because of preceding \ + buffer.write(curr.substring(0, startIndex - 1)); // do not include \ + buffer.write(startCharacter); + if (startIndex + 1 < curr.length) { + curr = curr.substring(startIndex + startCharacterLength); + continue; + } else { + break; + } + } - if (startIndex >= 2 && - curr[startIndex - 1] == ':' && - curr[startIndex - 2] == '@') { - // ignore because of preceding @: which indicates an escaped, linked translation - buffer.write(curr.substring(0, startIndex + 1)); - if (startIndex + 1 < curr.length) { - curr = curr.substring(startIndex + startCharacterLength); - continue; - } else { + if (startIndex >= 2 && + curr[startIndex - 1] == ':' && + curr[startIndex - 2] == '@') { + // ignore because of preceding @: which indicates an escaped, linked translation + buffer.write(curr.substring(0, startIndex + 1)); + if (startIndex + 1 < curr.length) { + curr = curr.substring(startIndex + startCharacterLength); + continue; + } else { + break; + } + } + + if (startIndex != 0) { + // add prefix + buffer.write(curr.substring(0, startIndex)); + } + + int endIndex = + curr.indexOf(endCharacter, startIndex + startCharacterLength); + if (endIndex == -1) { + buffer.write(curr.substring(startIndex)); break; } - } - - if (startIndex != 0) { - // add prefix - buffer.write(curr.substring(0, startIndex)); - } - - int endIndex = - curr.indexOf(endCharacter, startIndex + startCharacterLength); - if (endIndex == -1) { - buffer.write(curr.substring(startIndex)); - break; - } - - buffer.write( - replacer(curr.substring(startIndex, endIndex + endCharacterLength))); - curr = curr.substring(endIndex + endCharacterLength); - } while (curr.isNotEmpty); - - return buffer.toString(); + + buffer.write( + replacer(curr.substring(startIndex, endIndex + endCharacterLength))); + curr = curr.substring(endIndex + endCharacterLength); + } while (curr.isNotEmpty); + + return buffer.toString(); + } } diff --git a/slang/test/unit/builder/text/l10n_override_parser_test.dart b/slang/test/unit/builder/text/l10n_override_parser_test.dart index 3948d924..837a7dc9 100644 --- a/slang/test/unit/builder/text/l10n_override_parser_test.dart +++ b/slang/test/unit/builder/text/l10n_override_parser_test.dart @@ -76,4 +76,34 @@ void main() { ); expect(p!, '2021-12-31'); }); + + test('Should keep locale in positional argument', () { + final p = digestL10nOverride( + existingTypes: {}, + locale: 'en', + type: 'NumberFormat("000.00", "de")', + value: 33.4, + ); + expect(p!, '033,40'); + }); + + test('Should keep locale in single positional argument', () { + final p = digestL10nOverride( + existingTypes: {}, + locale: 'en', + type: 'yMd("de")', + value: DateTime(2021, 12, 31), + ); + expect(p!, '31.12.2021'); + }); + + test('Should keep locale in named argument', () { + final p = digestL10nOverride( + existingTypes: {}, + locale: 'en', + type: 'currency(symbol: "€", locale: "de")', + value: 33.4, + ); + expect(p!, '33,40 €'); + }); } diff --git a/slang/test/unit/builder/text/l10n_parser_test.dart b/slang/test/unit/builder/text/l10n_parser_test.dart index ceb16b7f..e725fa78 100644 --- a/slang/test/unit/builder/text/l10n_parser_test.dart +++ b/slang/test/unit/builder/text/l10n_parser_test.dart @@ -54,6 +54,47 @@ void main() { p.format, "NumberFormat.currency(symbol: '€', locale: 'en').format(p)"); }); + test('Should keep locale in named arguments', () { + final p = parseL10n( + locale: _locale, + paramName: 'p', + type: "NumberFormat.currency(symbol: '€', locale: 'de')", + ); + expect(p!.paramType, 'num'); + expect( + p.format, "NumberFormat.currency(symbol: '€', locale: 'de').format(p)"); + }); + + test('Should keep locale in positional arguments', () { + final p = parseL10n( + locale: _locale, + paramName: 'p', + type: "NumberFormat('###', 'fr')", + ); + expect(p!.paramType, 'num'); + expect(p.format, "NumberFormat('###', 'fr').format(p)"); + }); + + test('Should keep locale in single positional argument', () { + final p = parseL10n( + locale: _locale, + paramName: 'p', + type: "yMd('fr')", + ); + expect(p!.paramType, 'DateTime'); + expect(p.format, "DateFormat.yMd('fr').format(p)"); + }); + + test('Should add locale even if there is a comma (false positive)', () { + final p = parseL10n( + locale: _locale, + paramName: 'p', + type: "NumberFormat('###,###')", + ); + expect(p!.paramType, 'num'); + expect(p.format, "NumberFormat('###,###', 'en').format(p)"); + }); + test('Should keep positional parameter', () { final p = parseL10n( locale: _locale, diff --git a/slang/test/unit/utils/parameter_string_ext_test.dart b/slang/test/unit/utils/parameter_string_ext_test.dart new file mode 100644 index 00000000..4f08c40f --- /dev/null +++ b/slang/test/unit/utils/parameter_string_ext_test.dart @@ -0,0 +1,29 @@ +import 'package:slang/src/builder/utils/parameter_string_ext.dart'; +import 'package:test/test.dart'; + +void main() { + test('Should handle empty string', () { + final result = ''.splitParameters(); + expect(result, isEmpty); + }); + + test('Should handle single parameter', () { + final result = 'a'.splitParameters(); + expect(result, ['a']); + }); + + test('Should handle multiple parameters', () { + final result = 'a, b, c'.splitParameters(); + expect(result, ['a', 'b', 'c']); + }); + + test('Should handle quoted parameters', () { + final result = 'a, "b, c", d'.splitParameters(); + expect(result, ['a', '"b, c"', 'd']); + }); + + test('Should handle single quoted parameters', () { + final result = "a, 'b, c', d".splitParameters(); + expect(result, ['a', "'b, c'", 'd']); + }); +} From 9f890e6e7fb21a2ba782c2fb1999f5c508e5d749 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Fri, 15 Nov 2024 15:29:48 +0100 Subject: [PATCH 103/118] ci: bump flutter to 3.24.5 --- .fvmrc | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/publish_template.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.fvmrc b/.fvmrc index c62692b4..679f8e11 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,4 +1,4 @@ { - "flutter": "3.24.3", + "flutter": "3.24.5", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e373d2ba..cf61c61f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ on: env: FLUTTER_VERSION_OLDEST: "3.19.6" - FLUTTER_VERSION_NEWEST: "3.24.3" + FLUTTER_VERSION_NEWEST: "3.24.5" jobs: format: diff --git a/.github/workflows/publish_template.yml b/.github/workflows/publish_template.yml index f5f298f1..5741679d 100644 --- a/.github/workflows/publish_template.yml +++ b/.github/workflows/publish_template.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 with: - flutter-version: '3.19.5' + flutter-version: '3.24.5' channel: 'stable' - name: Dependencies (core) run: flutter pub get From db3d94081830bcc38f584ed02ffcec5d2301406c Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Fri, 15 Nov 2024 17:13:29 +0100 Subject: [PATCH 104/118] feat: sanitize invalid identifiers as keys --- slang/README.md | 46 +++++++ slang/lib/overrides.dart | 1 + .../builder/build_model_config_builder.dart | 1 + .../builder/builder/raw_config_builder.dart | 28 ++++- .../builder/translation_model_builder.dart | 15 ++- .../builder/generator/generate_header.dart | 2 + .../src/builder/model/build_model_config.dart | 3 + slang/lib/src/builder/model/raw_config.dart | 15 ++- .../builder/model/sanitization_config.dart | 17 +++ slang/lib/src/builder/utils/regex_utils.dart | 4 + .../utils/reserved_keyword_sanitizer.dart | 66 ++++++++++ .../resources/main/_expected_de.output | 4 + .../resources/main/_expected_en.output | 4 + .../_expected_fallback_base_locale_de.output | 4 + .../_expected_fallback_base_locale_en.output | 4 + ..._expected_fallback_base_locale_main.output | 2 +- .../resources/main/_expected_main.output | 2 +- .../main/_expected_obfuscation_de.output | 4 + .../main/_expected_obfuscation_en.output | 4 + .../main/_expected_obfuscation_main.output | 2 +- .../_expected_translation_overrides_de.output | 4 + .../_expected_translation_overrides_en.output | 4 + ...expected_translation_overrides_main.output | 3 +- .../resources/main/csv_compact.csv | 2 + .../integration/resources/main/csv_de.csv | 2 + .../integration/resources/main/csv_en.csv | 2 + .../integration/resources/main/json_de.json | 2 + .../integration/resources/main/json_en.json | 2 + .../integration/resources/main/yaml_de.yaml | 2 + .../integration/resources/main/yaml_en.yaml | 2 + slang/test/unit/api/locale_settings_test.dart | 6 + .../builder/build_config_builder_test.dart | 59 --------- .../unit/builder/raw_config_builder_test.dart | 115 ++++++++++++++++++ .../reserved_keyword_sanitizer_test.dart | 48 ++++++++ 34 files changed, 413 insertions(+), 68 deletions(-) create mode 100644 slang/lib/src/builder/model/sanitization_config.dart create mode 100644 slang/lib/src/builder/utils/reserved_keyword_sanitizer.dart delete mode 100644 slang/test/unit/builder/build_config_builder_test.dart create mode 100644 slang/test/unit/builder/raw_config_builder_test.dart create mode 100644 slang/test/unit/utils/reserved_keyword_sanitizer_test.dart diff --git a/slang/README.md b/slang/README.md index e7e062bf..a1bb1711 100644 --- a/slang/README.md +++ b/slang/README.md @@ -80,6 +80,7 @@ dart run slang migrate arb src.arb dest.json # migrate arb to json - [Lazy Loading](#-lazy-loading) - [Comments](#-comments) - [Recasing](#-recasing) + - [Sanitization](#-sanitization) - [Obfuscation](#-obfuscation) - [Formatting](#-formatting) - [Dart Only](#-dart-only) @@ -302,6 +303,10 @@ translation_class_visibility: private key_case: snake key_map_case: camel param_case: pascal +sanitization: + enabled: true + prefix: k + case: camel string_interpolation: double_braces flat_map: false translation_overrides: false @@ -371,6 +376,10 @@ targets: key_case: snake key_map_case: camel param_case: pascal + sanitization: + enabled: true + prefix: k + case: camel string_interpolation: double_braces flat_map: false translation_overrides: false @@ -431,6 +440,9 @@ targets: | `key_case` | `null`, `camel`, `pascal`, `snake` | transform keys (optional) [(i)](#-recasing) | `null` | | `key_map_case` | `null`, `camel`, `pascal`, `snake` | transform keys for maps (optional) [(i)](#-recasing) | `null` | | `param_case` | `null`, `camel`, `pascal`, `snake` | transform parameters (optional) [(i)](#-recasing) | `null` | +| `sanitization`/`enabled` | `Boolean` | enable sanitization [(i)](#-sanitization) | `true` | +| `sanitization`/`prefix` | `String` | prefix for sanitization [(i)](#-sanitization) | `k` | +| `sanitization`/`case` | `null`, `camel`, `pascal`, `snake` | case style for sanitization [(i)](#-sanitization) | `camel` | | `string_interpolation` | `dart`, `braces`, `double_braces` | string interpolation mode [(i)](#-string-interpolation) | `dart` | | `flat_map` | `Boolean` | generate flat map [(i)](#-dynamic-keys--flat-map) | `true` | | `translation_overrides` | `Boolean` | enable translation overrides [(i)](#-translation-overrides) | `false` | @@ -1420,6 +1432,40 @@ maps: - myMap # all paths must be cased accordingly ``` +### ➤ Sanitization + +All keys must be valid Dart identifiers. Slang will automatically sanitize them. + +By default, the prefix `k` is added if the key is one of the [reserved words](https://dart.dev/language/keywords) or starts with a number. + +As always, you can configure this behavior. + +```yaml +# Config +sanitization: + enabled: true + prefix: k + case: camel +``` + +Now the following key: + +```json +{ + "continue": "Continue" +} +``` + +will be sanitized to: + +```dart +String get kContinue => 'Continue'; +``` + +**Note:** +Sanitization is happening before resolving [Linked Translations](#-linked-translations). +Therefore, you need to use the sanitized key (e.g. `@:kContinue`). + ### ➤ Obfuscation Obfuscate the translation strings to make reverse engineering harder. diff --git a/slang/lib/overrides.dart b/slang/lib/overrides.dart index 12830b56..7ca8bc3d 100644 --- a/slang/lib/overrides.dart +++ b/slang/lib/overrides.dart @@ -2,3 +2,4 @@ export 'package:slang/src/api/translation_overrides.dart'; export 'package:slang/src/builder/model/build_model_config.dart'; export 'package:slang/src/builder/model/context_type.dart'; export 'package:slang/src/builder/model/enums.dart'; +export 'package:slang/src/builder/model/sanitization_config.dart'; diff --git a/slang/lib/src/builder/builder/build_model_config_builder.dart b/slang/lib/src/builder/builder/build_model_config_builder.dart index 7e249cd9..04bd69a4 100644 --- a/slang/lib/src/builder/builder/build_model_config_builder.dart +++ b/slang/lib/src/builder/builder/build_model_config_builder.dart @@ -8,6 +8,7 @@ extension BuildModelConfigBuilder on RawConfig { keyCase: keyCase, keyMapCase: keyMapCase, paramCase: paramCase, + sanitization: sanitization, stringInterpolation: stringInterpolation, maps: maps, pluralAuto: pluralAuto, diff --git a/slang/lib/src/builder/builder/raw_config_builder.dart b/slang/lib/src/builder/builder/raw_config_builder.dart index 2cd0eea2..be3c65ca 100644 --- a/slang/lib/src/builder/builder/raw_config_builder.dart +++ b/slang/lib/src/builder/builder/raw_config_builder.dart @@ -5,6 +5,7 @@ import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/model/interface.dart'; import 'package:slang/src/builder/model/obfuscation_config.dart'; import 'package:slang/src/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/sanitization_config.dart'; import 'package:slang/src/builder/utils/map_utils.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; import 'package:slang/src/builder/utils/string_extensions.dart'; @@ -54,6 +55,9 @@ class RawConfigBuilder { ); } + final keyCase = + (map['key_case'] as String?)?.toCaseStyle() ?? RawConfig.defaultKeyCase; + return RawConfig( baseLocale: I18nLocale.fromString( map['base_locale'] ?? RawConfig.defaultBaseLocale), @@ -82,12 +86,19 @@ class RawConfigBuilder { (map['translation_class_visibility'] as String?) ?.toTranslationClassVisibility() ?? RawConfig.defaultTranslationClassVisibility, - keyCase: (map['key_case'] as String?)?.toCaseStyle() ?? - RawConfig.defaultKeyCase, + keyCase: keyCase, keyMapCase: (map['key_map_case'] as String?)?.toCaseStyle() ?? RawConfig.defaultKeyMapCase, paramCase: (map['param_case'] as String?)?.toCaseStyle() ?? RawConfig.defaultParamCase, + sanitization: (map['sanitization'] as Map?) + ?.toSanitizationConfig( + keyCase ?? SanitizationConfig.defaultCaseStyle) ?? + SanitizationConfig( + enabled: SanitizationConfig.defaultEnabled, + prefix: SanitizationConfig.defaultPrefix, + caseStyle: keyCase ?? SanitizationConfig.defaultCaseStyle, + ), stringInterpolation: (map['string_interpolation'] as String?)?.toStringInterpolation() ?? RawConfig.defaultStringInterpolation, @@ -229,6 +240,19 @@ extension on Map { width: this['width'], ); } + + /// Parses the 'sanitization' config + SanitizationConfig toSanitizationConfig(CaseStyle fallbackCase) { + return SanitizationConfig( + enabled: this['enabled'] ?? SanitizationConfig.defaultEnabled, + prefix: this['prefix'] ?? SanitizationConfig.defaultPrefix, + caseStyle: switch (this['case'] as String?) { + String s => s.toCaseStyle() ?? fallbackCase, + // explicit null or not present + null => containsKey('case') ? null : fallbackCase, + }, + ); + } } extension on String { diff --git a/slang/lib/src/builder/builder/translation_model_builder.dart b/slang/lib/src/builder/builder/translation_model_builder.dart index e18499ff..562055c5 100644 --- a/slang/lib/src/builder/builder/translation_model_builder.dart +++ b/slang/lib/src/builder/builder/translation_model_builder.dart @@ -11,7 +11,7 @@ import 'package:slang/src/builder/model/node.dart'; import 'package:slang/src/builder/model/pluralization.dart'; import 'package:slang/src/builder/utils/node_utils.dart'; import 'package:slang/src/builder/utils/regex_utils.dart'; -import 'package:slang/src/builder/utils/string_extensions.dart'; +import 'package:slang/src/builder/utils/reserved_keyword_sanitizer.dart'; class BuildModelResult { final ObjectNode root; // the actual strings @@ -122,6 +122,7 @@ class TranslationModelBuilder { baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, handleTypes: handleTypes, + sanitizeKey: true, ); // 2nd iteration: Handle parameterized linked translations @@ -271,6 +272,7 @@ Map _parseMapNode({ required Map? baseContexts, required bool shouldEscapeText, required bool handleTypes, + required bool sanitizeKey, }) { final Map resultNodeTree = {}; @@ -283,7 +285,13 @@ Map _parseMapNode({ final originalKey = key; final nodePathInfo = NodeUtils.parseModifiers(originalKey); - key = nodePathInfo.path.toCase(keyCase); + key = sanitizeReservedKeyword( + name: nodePathInfo.path, + prefix: config.sanitization.prefix, + sanitizeCaseStyle: config.sanitization.caseStyle, + defaultCaseStyle: keyCase, + sanitize: sanitizeKey && config.sanitization.enabled, + ); final modifiers = nodePathInfo.modifiers; final currPath = parentPath.isNotEmpty ? '$parentPath.$key' : key; final currRawPath = @@ -352,6 +360,7 @@ Map _parseMapNode({ baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, handleTypes: handleTypes, + sanitizeKey: false, ); // finally only take their values, ignoring keys @@ -384,6 +393,7 @@ Map _parseMapNode({ baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, handleTypes: handleTypes, + sanitizeKey: true, ); final Node finalNode; @@ -441,6 +451,7 @@ Map _parseMapNode({ baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, handleTypes: handleTypes, + sanitizeKey: false, ).cast(); } diff --git a/slang/lib/src/builder/generator/generate_header.dart b/slang/lib/src/builder/generator/generate_header.dart index 79abaf2e..be28a351 100644 --- a/slang/lib/src/builder/generator/generate_header.dart +++ b/slang/lib/src/builder/generator/generate_header.dart @@ -186,6 +186,8 @@ void _generateBuildConfig({ '\tkeyMapCase: ${config.keyMapCase != null ? 'CaseStyle.${config.keyMapCase!.name}' : 'null'},'); buffer.writeln( '\tparamCase: ${config.paramCase != null ? 'CaseStyle.${config.paramCase!.name}' : 'null'},'); + buffer.writeln( + '\tsanitization: SanitizationConfig(enabled: ${config.sanitization.enabled}, prefix: \'${config.sanitization.prefix}\', caseStyle: ${config.sanitization.caseStyle}),'); buffer.writeln( '\tstringInterpolation: StringInterpolation.${config.stringInterpolation.name},'); buffer.writeln('\tmaps: [${config.maps.map((m) => "'$m'").join(', ')}],'); diff --git a/slang/lib/src/builder/model/build_model_config.dart b/slang/lib/src/builder/model/build_model_config.dart index 5a16ef69..321ebb24 100644 --- a/slang/lib/src/builder/model/build_model_config.dart +++ b/slang/lib/src/builder/model/build_model_config.dart @@ -2,6 +2,7 @@ import 'package:slang/src/builder/model/context_type.dart'; import 'package:slang/src/builder/model/enums.dart'; import 'package:slang/src/builder/model/interface.dart'; import 'package:slang/src/builder/model/raw_config.dart'; +import 'package:slang/src/builder/model/sanitization_config.dart'; /// Config to generate the model. /// A subset of [RawConfig]. @@ -10,6 +11,7 @@ class BuildModelConfig { final CaseStyle? keyCase; final CaseStyle? keyMapCase; final CaseStyle? paramCase; + final SanitizationConfig sanitization; final StringInterpolation stringInterpolation; final List maps; final PluralAuto pluralAuto; @@ -24,6 +26,7 @@ class BuildModelConfig { required this.keyCase, required this.keyMapCase, required this.paramCase, + required this.sanitization, required this.stringInterpolation, required this.maps, required this.pluralAuto, diff --git a/slang/lib/src/builder/model/raw_config.dart b/slang/lib/src/builder/model/raw_config.dart index 4e81e315..4e9b5e3f 100644 --- a/slang/lib/src/builder/model/raw_config.dart +++ b/slang/lib/src/builder/model/raw_config.dart @@ -4,6 +4,7 @@ import 'package:slang/src/builder/model/format_config.dart'; import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/model/interface.dart'; import 'package:slang/src/builder/model/obfuscation_config.dart'; +import 'package:slang/src/builder/model/sanitization_config.dart'; /// represents a build.yaml or a slang.yaml file class RawConfig { @@ -25,6 +26,11 @@ class RawConfig { static const CaseStyle? defaultKeyCase = null; static const CaseStyle? defaultKeyMapCase = null; static const CaseStyle? defaultParamCase = null; + static const SanitizationConfig defaultSanitization = SanitizationConfig( + enabled: SanitizationConfig.defaultEnabled, + prefix: SanitizationConfig.defaultPrefix, + caseStyle: SanitizationConfig.defaultCaseStyle, + ); static const StringInterpolation defaultStringInterpolation = StringInterpolation.dart; static const bool defaultRenderFlatMap = true; @@ -64,6 +70,7 @@ class RawConfig { final CaseStyle? keyCase; final CaseStyle? keyMapCase; final CaseStyle? paramCase; + final SanitizationConfig sanitization; final StringInterpolation stringInterpolation; final bool renderFlatMap; final bool translationOverrides; @@ -101,6 +108,7 @@ class RawConfig { required this.keyCase, required this.keyMapCase, required this.paramCase, + required this.sanitization, required StringInterpolation stringInterpolation, required this.renderFlatMap, required this.translationOverrides, @@ -135,6 +143,7 @@ class RawConfig { TranslationClassVisibility? translationClassVisibility, CaseStyle? keyCase, CaseStyle? keyMapCase, + CaseStyle? paramCase, bool? renderFlatMap, bool? translationOverrides, bool? renderTimestamp, @@ -166,7 +175,8 @@ class RawConfig { translationClassVisibility ?? this.translationClassVisibility, keyCase: keyCase ?? this.keyCase, keyMapCase: keyMapCase ?? this.keyMapCase, - paramCase: paramCase, + paramCase: paramCase ?? this.paramCase, + sanitization: sanitization, stringInterpolation: stringInterpolation, renderFlatMap: renderFlatMap ?? this.renderFlatMap, translationOverrides: translationOverrides ?? this.translationOverrides, @@ -228,6 +238,8 @@ class RawConfig { ' -> keyCase (for maps): ${keyMapCase != null ? keyMapCase?.name : 'null (no change)'}'); print( ' -> paramCase: ${paramCase != null ? paramCase?.name : 'null (no change)'}'); + print( + ' -> sanitization: ${sanitization.enabled ? 'enabled' : 'disabled'} / \'${sanitization.prefix}\' / caseStyle: ${sanitization.caseStyle},'); print(' -> stringInterpolation: ${stringInterpolation.name}'); print(' -> renderFlatMap: $renderFlatMap'); print(' -> translationOverrides: $translationOverrides'); @@ -283,6 +295,7 @@ class RawConfig { keyCase: RawConfig.defaultKeyCase, keyMapCase: RawConfig.defaultKeyMapCase, paramCase: RawConfig.defaultParamCase, + sanitization: RawConfig.defaultSanitization, stringInterpolation: RawConfig.defaultStringInterpolation, renderFlatMap: RawConfig.defaultRenderFlatMap, translationOverrides: RawConfig.defaultTranslationOverrides, diff --git a/slang/lib/src/builder/model/sanitization_config.dart b/slang/lib/src/builder/model/sanitization_config.dart new file mode 100644 index 00000000..e8228cff --- /dev/null +++ b/slang/lib/src/builder/model/sanitization_config.dart @@ -0,0 +1,17 @@ +import 'package:slang/src/builder/model/enums.dart'; + +class SanitizationConfig { + static const bool defaultEnabled = true; + static const String defaultPrefix = 'k'; + static const CaseStyle defaultCaseStyle = CaseStyle.camel; + + final bool enabled; + final String prefix; + final CaseStyle? caseStyle; + + const SanitizationConfig({ + required this.enabled, + required this.prefix, + required this.caseStyle, + }); +} diff --git a/slang/lib/src/builder/utils/regex_utils.dart b/slang/lib/src/builder/utils/regex_utils.dart index 1c161e3c..335239f6 100644 --- a/slang/lib/src/builder/utils/regex_utils.dart +++ b/slang/lib/src/builder/utils/regex_utils.dart @@ -102,4 +102,8 @@ class RegexUtils { /// 3 - json static final RegExp analysisFileRegex = RegExp( r'^_(missing_translations|unused_translations)(?:_(.*))?\.(json|yaml|csv)$'); + + /// Matches if the string starts with a number. + /// Example: 1hello, 2world + static final RegExp startsWithNumber = RegExp(r'^\d'); } diff --git a/slang/lib/src/builder/utils/reserved_keyword_sanitizer.dart b/slang/lib/src/builder/utils/reserved_keyword_sanitizer.dart new file mode 100644 index 00000000..ea711044 --- /dev/null +++ b/slang/lib/src/builder/utils/reserved_keyword_sanitizer.dart @@ -0,0 +1,66 @@ +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/string_extensions.dart'; + +/// Converts a reserved keyword to a valid identifier by +/// adding a prefix to it. +/// It also applies the specified [caseStyle] to the identifier. +String sanitizeReservedKeyword({ + required String name, + required String prefix, + required CaseStyle? sanitizeCaseStyle, + required CaseStyle? defaultCaseStyle, + bool sanitize = true, +}) { + if (sanitize && + (_reservedKeyWords.contains(name) || + RegexUtils.startsWithNumber.hasMatch(name))) { + if (sanitizeCaseStyle != null) { + // need to add space so that it treats the prefix as a separate word + return '$prefix $name'.toCase(sanitizeCaseStyle); + } else { + return '$prefix$name'; + } + } + return name.toCase(defaultCaseStyle); +} + +/// Reserved keywords that are not allowed to be used as identifiers. +/// Some reserved keywords are allowed to be used as identifiers! For example, +/// abstract, await, yield, etc. +/// See https://dart.dev/language/keywords +const _reservedKeyWords = { + 'assert', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'default', + 'do', + 'else', + 'enum', + 'extends', + 'false', + 'final', + 'finally', + 'for', + 'if', + 'in', + 'is', + 'new', + 'null', + 'rethrow', + 'return', + 'super', + 'switch', + 'this', + 'throw', + 'true', + 'try', + 'var', + 'void', + 'with', + 'while', +}; diff --git a/slang/test/integration/resources/main/_expected_de.output b/slang/test/integration/resources/main/_expected_de.output index 327d4184..e022ea46 100644 --- a/slang/test/integration/resources/main/_expected_de.output +++ b/slang/test/integration/resources/main/_expected_de.output @@ -64,6 +64,8 @@ class _TranslationsOnboardingDe implements TranslationsOnboardingEn { /// Bye text @override String bye({required Object firstName}) => 'Tschüss ${firstName}'; + @override String get kContinue => 'Weiter'; + @override String get linkContinue => _root.onboarding.kContinue; @override TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), name, @@ -186,6 +188,8 @@ extension on TranslationsDe { case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; case 'onboarding.bye': return ({required Object firstName}) => 'Tschüss ${firstName}'; + case 'onboarding.kContinue': return 'Weiter'; + case 'onboarding.linkContinue': return _root.onboarding.kContinue; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), name, diff --git a/slang/test/integration/resources/main/_expected_en.output b/slang/test/integration/resources/main/_expected_en.output index 865ccdcc..fd3d3085 100644 --- a/slang/test/integration/resources/main/_expected_en.output +++ b/slang/test/integration/resources/main/_expected_en.output @@ -68,6 +68,8 @@ class TranslationsOnboardingEn { /// Bye text String bye({required Object firstName}) => 'Bye ${firstName}'; + String get kContinue => 'Continue'; + String get linkContinue => _root.onboarding.kContinue; TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), name, @@ -190,6 +192,8 @@ extension on Translations { case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; case 'onboarding.bye': return ({required Object firstName}) => 'Bye ${firstName}'; + case 'onboarding.kContinue': return 'Continue'; + case 'onboarding.linkContinue': return _root.onboarding.kContinue; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), name, diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output index af4f49a4..69e3f225 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output @@ -66,6 +66,8 @@ class _TranslationsOnboardingDe extends TranslationsOnboardingEn { /// Bye text @override String bye({required Object firstName}) => 'Tschüss ${firstName}'; + @override String get kContinue => 'Weiter'; + @override String get linkContinue => _root.onboarding.kContinue; @override TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), name, @@ -188,6 +190,8 @@ extension on TranslationsDe { case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; case 'onboarding.bye': return ({required Object firstName}) => 'Tschüss ${firstName}'; + case 'onboarding.kContinue': return 'Weiter'; + case 'onboarding.linkContinue': return _root.onboarding.kContinue; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), name, diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output index 169c16d5..d47e3d2b 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output @@ -68,6 +68,8 @@ class TranslationsOnboardingEn { /// Bye text String bye({required Object firstName}) => 'Bye ${firstName}'; + String get kContinue => 'Continue'; + String get linkContinue => _root.onboarding.kContinue; TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), name, @@ -190,6 +192,8 @@ extension on Translations { case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => '${firstName}'; case 'onboarding.bye': return ({required Object firstName}) => 'Bye ${firstName}'; + case 'onboarding.kContinue': return 'Continue'; + case 'onboarding.linkContinue': return _root.onboarding.kContinue; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ const TextSpan(text: 'Hi '), name, diff --git a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output index 39b46186..76c65fb3 100644 --- a/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 58 (29 per locale) +/// Strings: 62 (31 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/slang/test/integration/resources/main/_expected_main.output b/slang/test/integration/resources/main/_expected_main.output index 39b46186..76c65fb3 100644 --- a/slang/test/integration/resources/main/_expected_main.output +++ b/slang/test/integration/resources/main/_expected_main.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 58 (29 per locale) +/// Strings: 62 (31 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/slang/test/integration/resources/main/_expected_obfuscation_de.output b/slang/test/integration/resources/main/_expected_obfuscation_de.output index 293a8b1b..153b81a2 100644 --- a/slang/test/integration/resources/main/_expected_obfuscation_de.output +++ b/slang/test/integration/resources/main/_expected_obfuscation_de.output @@ -66,6 +66,8 @@ class _TranslationsOnboardingDe implements TranslationsOnboardingEn { /// Bye text @override String bye({required Object firstName}) => _root.$meta.d([15, 40, 56, 51, 167, 40, 40, 123]) + firstName.toString(); + @override String get kContinue => _root.$meta.d([12, 62, 50, 47, 62, 41]); + @override String get linkContinue => _root.onboarding.kContinue; @override TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ TextSpan(text: _root.$meta.d([19, 50, 123])), name, @@ -188,6 +190,8 @@ extension on TranslationsDe { case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => firstName.toString(); case 'onboarding.bye': return ({required Object firstName}) => _root.$meta.d([15, 40, 56, 51, 167, 40, 40, 123]) + firstName.toString(); + case 'onboarding.kContinue': return _root.$meta.d([12, 62, 50, 47, 62, 41]); + case 'onboarding.linkContinue': return _root.onboarding.kContinue; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ TextSpan(text: _root.$meta.d([19, 50, 123])), name, diff --git a/slang/test/integration/resources/main/_expected_obfuscation_en.output b/slang/test/integration/resources/main/_expected_obfuscation_en.output index 6de9aeef..29a4df21 100644 --- a/slang/test/integration/resources/main/_expected_obfuscation_en.output +++ b/slang/test/integration/resources/main/_expected_obfuscation_en.output @@ -69,6 +69,8 @@ class TranslationsOnboardingEn { /// Bye text String bye({required Object firstName}) => _root.$meta.d([25, 34, 62, 123]) + firstName.toString(); + String get kContinue => _root.$meta.d([24, 52, 53, 47, 50, 53, 46, 62]); + String get linkContinue => _root.onboarding.kContinue; TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ TextSpan(text: _root.$meta.d([19, 50, 123])), name, @@ -191,6 +193,8 @@ extension on Translations { case 'onboarding.welcomeAlias': return ({required Object fullName}) => _root.onboarding.welcome(fullName: fullName); case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => firstName.toString(); case 'onboarding.bye': return ({required Object firstName}) => _root.$meta.d([25, 34, 62, 123]) + firstName.toString(); + case 'onboarding.kContinue': return _root.$meta.d([24, 52, 53, 47, 50, 53, 46, 62]); + case 'onboarding.linkContinue': return _root.onboarding.kContinue; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TextSpan(children: [ TextSpan(text: _root.$meta.d([19, 50, 123])), name, diff --git a/slang/test/integration/resources/main/_expected_obfuscation_main.output b/slang/test/integration/resources/main/_expected_obfuscation_main.output index 28b00269..c6f8974e 100644 --- a/slang/test/integration/resources/main/_expected_obfuscation_main.output +++ b/slang/test/integration/resources/main/_expected_obfuscation_main.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 58 (29 per locale) +/// Strings: 62 (31 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/slang/test/integration/resources/main/_expected_translation_overrides_de.output b/slang/test/integration/resources/main/_expected_translation_overrides_de.output index d571af61..b4b9fa58 100644 --- a/slang/test/integration/resources/main/_expected_translation_overrides_de.output +++ b/slang/test/integration/resources/main/_expected_translation_overrides_de.output @@ -65,6 +65,8 @@ class _TranslationsOnboardingDe implements TranslationsOnboardingEn { /// Bye text @override String bye({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Tschüss ${firstName}'; + @override String get kContinue => TranslationOverrides.string(_root.$meta, 'onboarding.kContinue', {}) ?? 'Weiter'; + @override String get linkContinue => TranslationOverrides.string(_root.$meta, 'onboarding.linkContinue', {}) ?? _root.onboarding.kContinue; @override TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TranslationOverridesFlutter.rich(_root.$meta, 'onboarding.hi', {'name': name, 'lastName': lastName, 'context': context, 'fullName': fullName, 'firstName': firstName}) ?? TextSpan(children: [ const TextSpan(text: 'Hi '), name, @@ -195,6 +197,8 @@ extension on TranslationsDe { case 'onboarding.welcomeAlias': return ({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeAlias', {'fullName': fullName}) ?? _root.onboarding.welcome(fullName: fullName); case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeOnlyParam', {'firstName': firstName}) ?? '${firstName}'; case 'onboarding.bye': return ({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Tschüss ${firstName}'; + case 'onboarding.kContinue': return TranslationOverrides.string(_root.$meta, 'onboarding.kContinue', {}) ?? 'Weiter'; + case 'onboarding.linkContinue': return TranslationOverrides.string(_root.$meta, 'onboarding.linkContinue', {}) ?? _root.onboarding.kContinue; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TranslationOverridesFlutter.rich(_root.$meta, 'onboarding.hi', {'name': name, 'lastName': lastName, 'context': context, 'fullName': fullName, 'firstName': firstName}) ?? TextSpan(children: [ const TextSpan(text: 'Hi '), name, diff --git a/slang/test/integration/resources/main/_expected_translation_overrides_en.output b/slang/test/integration/resources/main/_expected_translation_overrides_en.output index 5d5b1d7f..8f8bbbc3 100644 --- a/slang/test/integration/resources/main/_expected_translation_overrides_en.output +++ b/slang/test/integration/resources/main/_expected_translation_overrides_en.output @@ -68,6 +68,8 @@ class TranslationsOnboardingEn { /// Bye text String bye({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Bye ${firstName}'; + String get kContinue => TranslationOverrides.string(_root.$meta, 'onboarding.kContinue', {}) ?? 'Continue'; + String get linkContinue => TranslationOverrides.string(_root.$meta, 'onboarding.linkContinue', {}) ?? _root.onboarding.kContinue; TextSpan hi({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TranslationOverridesFlutter.rich(_root.$meta, 'onboarding.hi', {'name': name, 'lastName': lastName, 'context': context, 'fullName': fullName, 'firstName': firstName}) ?? TextSpan(children: [ const TextSpan(text: 'Hi '), name, @@ -198,6 +200,8 @@ extension on Translations { case 'onboarding.welcomeAlias': return ({required Object fullName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeAlias', {'fullName': fullName}) ?? _root.onboarding.welcome(fullName: fullName); case 'onboarding.welcomeOnlyParam': return ({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.welcomeOnlyParam', {'firstName': firstName}) ?? '${firstName}'; case 'onboarding.bye': return ({required Object firstName}) => TranslationOverrides.string(_root.$meta, 'onboarding.bye', {'firstName': firstName}) ?? 'Bye ${firstName}'; + case 'onboarding.kContinue': return TranslationOverrides.string(_root.$meta, 'onboarding.kContinue', {}) ?? 'Continue'; + case 'onboarding.linkContinue': return TranslationOverrides.string(_root.$meta, 'onboarding.linkContinue', {}) ?? _root.onboarding.kContinue; case 'onboarding.hi': return ({required InlineSpan name, required Object lastName, required GenderContext context, required Object fullName, required Object firstName}) => TranslationOverridesFlutter.rich(_root.$meta, 'onboarding.hi', {'name': name, 'lastName': lastName, 'context': context, 'fullName': fullName, 'firstName': firstName}) ?? TextSpan(children: [ const TextSpan(text: 'Hi '), name, diff --git a/slang/test/integration/resources/main/_expected_translation_overrides_main.output b/slang/test/integration/resources/main/_expected_translation_overrides_main.output index 8cf34ec2..b9b1dd2c 100644 --- a/slang/test/integration/resources/main/_expected_translation_overrides_main.output +++ b/slang/test/integration/resources/main/_expected_translation_overrides_main.output @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 58 (29 per locale) +/// Strings: 62 (31 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import @@ -27,6 +27,7 @@ final _buildConfig = BuildModelConfig( keyCase: null, keyMapCase: null, paramCase: null, + sanitization: SanitizationConfig(enabled: true, prefix: 'k', caseStyle: CaseStyle.camel), stringInterpolation: StringInterpolation.braces, maps: ['end.pages.0', 'end.pages.1'], pluralAuto: PluralAuto.cardinal, diff --git a/slang/test/integration/resources/main/csv_compact.csv b/slang/test/integration/resources/main/csv_compact.csv index 476b9e09..acf66699 100644 --- a/slang/test/integration/resources/main/csv_compact.csv +++ b/slang/test/integration/resources/main/csv_compact.csv @@ -3,6 +3,8 @@ onboarding.welcome,,Welcome {fullName},Willkommen {fullName} onboarding.welcomeAlias,,@:onboarding.welcome,@:onboarding.welcome onboarding.welcomeOnlyParam,,{firstName},{firstName} onboarding.bye,Bye text,Bye {firstName},Tschüss {firstName} +onboarding.continue,,Continue,Weiter +onboarding.linkContinue,,@:onboarding.kContinue,@:onboarding.kContinue onboarding.hi(rich),,Hi {name} and @:onboarding.greet,Hi {name} und @:onboarding.greet onboarding.pages.0.title,,First Page,Erste Seite onboarding.pages.0.content,,First Page Content,Erster Seiteninhalt diff --git a/slang/test/integration/resources/main/csv_de.csv b/slang/test/integration/resources/main/csv_de.csv index af59b9a1..68ec381c 100644 --- a/slang/test/integration/resources/main/csv_de.csv +++ b/slang/test/integration/resources/main/csv_de.csv @@ -3,6 +3,8 @@ onboarding.welcomeAlias,@:onboarding.welcome onboarding.welcomeOnlyParam,{firstName} onboarding.bye,Tschüss {firstName} onboarding.@bye,Bye text +onboarding.continue,Weiter +onboarding.linkContinue,@:onboarding.kContinue onboarding.hi(rich),Hi {name} und @:onboarding.greet onboarding.pages.0.title,Erste Seite onboarding.pages.0.content,Erster Seiteninhalt diff --git a/slang/test/integration/resources/main/csv_en.csv b/slang/test/integration/resources/main/csv_en.csv index 4aa35309..ae576c35 100644 --- a/slang/test/integration/resources/main/csv_en.csv +++ b/slang/test/integration/resources/main/csv_en.csv @@ -2,6 +2,8 @@ onboarding.welcome,Welcome {fullName} onboarding.welcomeAlias,@:onboarding.welcome onboarding.welcomeOnlyParam,{firstName} onboarding.bye,Bye {firstName} +onboarding.continue,Continue +onboarding.linkContinue,@:onboarding.kContinue onboarding.@bye,Bye text onboarding.hi(rich),Hi {name} and @:onboarding.greet onboarding.pages.0.title,First Page diff --git a/slang/test/integration/resources/main/json_de.json b/slang/test/integration/resources/main/json_de.json index 631a0d74..d320db9e 100644 --- a/slang/test/integration/resources/main/json_de.json +++ b/slang/test/integration/resources/main/json_de.json @@ -4,6 +4,8 @@ "welcomeAlias": "@:onboarding.welcome", "welcomeOnlyParam": "{firstName}", "bye": "Tschüss {firstName}", + "continue": "Weiter", + "linkContinue": "@:onboarding.kContinue", "@bye": "Bye text", "hi(rich)": "Hi {name} und @:onboarding.greet", "pages": [ diff --git a/slang/test/integration/resources/main/json_en.json b/slang/test/integration/resources/main/json_en.json index 3209a0ee..07e771b6 100644 --- a/slang/test/integration/resources/main/json_en.json +++ b/slang/test/integration/resources/main/json_en.json @@ -4,6 +4,8 @@ "welcomeAlias": "@:onboarding.welcome", "welcomeOnlyParam": "{firstName}", "bye": "Bye {firstName}", + "continue": "Continue", + "linkContinue": "@:onboarding.kContinue", "@bye": { "this should be ignored": "ignored", "description": "Bye text" diff --git a/slang/test/integration/resources/main/yaml_de.yaml b/slang/test/integration/resources/main/yaml_de.yaml index 321090ba..ee2a2a33 100644 --- a/slang/test/integration/resources/main/yaml_de.yaml +++ b/slang/test/integration/resources/main/yaml_de.yaml @@ -3,6 +3,8 @@ onboarding: welcomeAlias: "@:onboarding.welcome" welcomeOnlyParam: "{firstName}" bye: Tschüss {firstName} + continue: Weiter + linkContinue: "@:onboarding.kContinue" "@bye": Bye text hi(rich): Hi {name} und @:onboarding.greet pages: diff --git a/slang/test/integration/resources/main/yaml_en.yaml b/slang/test/integration/resources/main/yaml_en.yaml index 4032f778..52e80383 100644 --- a/slang/test/integration/resources/main/yaml_en.yaml +++ b/slang/test/integration/resources/main/yaml_en.yaml @@ -3,6 +3,8 @@ onboarding: welcomeAlias: "@:onboarding.welcome" welcomeOnlyParam: "{firstName}" bye: Bye {firstName} + continue: Continue + linkContinue: "@:onboarding.kContinue" "@bye": Bye text hi(rich): Hi {name} and @:onboarding.greet pages: diff --git a/slang/test/unit/api/locale_settings_test.dart b/slang/test/unit/api/locale_settings_test.dart index 56464d55..e05ce9b6 100644 --- a/slang/test/unit/api/locale_settings_test.dart +++ b/slang/test/unit/api/locale_settings_test.dart @@ -3,6 +3,7 @@ import 'package:slang/src/api/singleton.dart'; import 'package:slang/src/api/state.dart'; import 'package:slang/src/builder/model/build_model_config.dart'; import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/model/sanitization_config.dart'; import 'package:test/test.dart'; void main() { @@ -62,6 +63,11 @@ class _AppLocaleUtils keyCase: null, keyMapCase: null, paramCase: null, + sanitization: SanitizationConfig( + enabled: true, + prefix: 'k', + caseStyle: CaseStyle.camel, + ), stringInterpolation: StringInterpolation.braces, maps: [], pluralAuto: PluralAuto.cardinal, diff --git a/slang/test/unit/builder/build_config_builder_test.dart b/slang/test/unit/builder/build_config_builder_test.dart deleted file mode 100644 index 24daa216..00000000 --- a/slang/test/unit/builder/build_config_builder_test.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:slang/src/builder/builder/raw_config_builder.dart'; -import 'package:test/test.dart'; - -void main() { - group(RawConfigBuilder.fromYaml, () { - test('context gender default', () { - final result = RawConfigBuilder.fromYaml(r''' - targets: - $default: - builders: - slang_build_runner: - options: - input_directory: lib/i18n - fallback_strategy: base_locale - contexts: - GenderContext: - default_parameter: gender - render_enum: false - '''); - - expect(result, isNotNull); - expect(result!.contexts.length, 1); - expect(result.contexts.first.enumName, 'GenderContext'); - expect(result.contexts.first.defaultParameter, 'gender'); - }); - }); - - group(RawConfigBuilder.fromMap, () { - test('context gender default', () { - final result = RawConfigBuilder.fromMap( - { - 'contexts': { - 'GenderContext': { - 'enum': [ - 'male', - 'female', - 'neutral', - ], - }, - }, - }, - ); - - expect(result.contexts.length, 1); - expect(result.contexts.first.enumName, 'GenderContext'); - expect(result.contexts.first.defaultParameter, 'context'); - }); - - test('Should remove trailing slash', () { - final result = RawConfigBuilder.fromMap( - { - 'input_directory': 'lib/abc/', - }, - ); - - expect(result.inputDirectory, 'lib/abc'); - }); - }); -} diff --git a/slang/test/unit/builder/raw_config_builder_test.dart b/slang/test/unit/builder/raw_config_builder_test.dart new file mode 100644 index 00000000..f2d30fc9 --- /dev/null +++ b/slang/test/unit/builder/raw_config_builder_test.dart @@ -0,0 +1,115 @@ +import 'package:slang/src/builder/builder/raw_config_builder.dart'; +import 'package:slang/src/builder/model/enums.dart'; +import 'package:test/test.dart'; + +void main() { + group('RawConfigBuilder.fromYaml', () { + test('context gender default', () { + final result = RawConfigBuilder.fromYaml(r''' + targets: + $default: + builders: + slang_build_runner: + options: + input_directory: lib/i18n + fallback_strategy: base_locale + contexts: + GenderContext: + default_parameter: gender + render_enum: false + '''); + + expect(result, isNotNull); + expect(result!.contexts.length, 1); + expect(result.contexts.first.enumName, 'GenderContext'); + expect(result.contexts.first.defaultParameter, 'gender'); + }); + }); + + group('RawConfigBuilder.fromMap', () { + test('context gender default', () { + final result = RawConfigBuilder.fromMap( + { + 'contexts': { + 'GenderContext': { + 'enum': [ + 'male', + 'female', + 'neutral', + ], + }, + }, + }, + ); + + expect(result.contexts.length, 1); + expect(result.contexts.first.enumName, 'GenderContext'); + expect(result.contexts.first.defaultParameter, 'context'); + }); + + test('Should remove trailing slash', () { + final result = RawConfigBuilder.fromMap( + { + 'input_directory': 'lib/abc/', + }, + ); + + expect(result.inputDirectory, 'lib/abc'); + }); + + test('Not provided sanitization config should follow key_case', () { + final result = RawConfigBuilder.fromMap( + { + 'key_case': 'snake', + }, + ); + + expect(result.sanitization.caseStyle, CaseStyle.snake); + }); + + test('Not provided sanitization case should follow key_case', () { + final result = RawConfigBuilder.fromMap( + { + 'key_case': 'pascal', + 'sanitization': { + 'prefix': 'abc', + }, + }, + ); + + expect(result.sanitization.prefix, 'abc'); + expect(result.sanitization.caseStyle, CaseStyle.pascal); + }); + + test('Should respected sanitization case', () { + final result = RawConfigBuilder.fromMap( + { + 'key_case': 'snake', + 'sanitization': { + 'prefix': 'abc', + 'case': 'camel', + }, + }, + ); + + expect(result.sanitization.prefix, 'abc'); + expect(result.sanitization.caseStyle, CaseStyle.camel); + }); + + test('Should respected sanitization case of null', () { + // Sometimes, the user explicitly wants to disable recasing. + final result = RawConfigBuilder.fromMap( + { + 'key_case': 'snake', + 'sanitization': { + 'prefix': 'k_', + 'case': null, + }, + }, + ); + + expect(result.sanitization.prefix, 'k_'); + expect(result.sanitization.caseStyle, isNull); + }); + }); +} diff --git a/slang/test/unit/utils/reserved_keyword_sanitizer_test.dart b/slang/test/unit/utils/reserved_keyword_sanitizer_test.dart new file mode 100644 index 00000000..ac5e4304 --- /dev/null +++ b/slang/test/unit/utils/reserved_keyword_sanitizer_test.dart @@ -0,0 +1,48 @@ +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/utils/reserved_keyword_sanitizer.dart'; +import 'package:test/test.dart'; + +String _sanitizeDefault(String name) => sanitizeReservedKeyword( + name: name, + prefix: 'k_', + sanitizeCaseStyle: CaseStyle.camel, + defaultCaseStyle: CaseStyle.camel, + ); + +void main() { + test('Should sanitize keywords', () { + expect(_sanitizeDefault('continue'), 'kContinue'); + }); + + test('Should not sanitize non-keywords', () { + expect(_sanitizeDefault('hello'), 'hello'); + }); + + test('Should sanitize keywords with numbers', () { + expect(_sanitizeDefault('1continue'), 'k1continue'); + }); + + test('Should not recase', () { + expect( + sanitizeReservedKeyword( + name: 'continue', + prefix: 'k', + sanitizeCaseStyle: null, + defaultCaseStyle: CaseStyle.camel, + ), + 'kcontinue', + ); + }); + + test('Should recase correctly', () { + expect( + sanitizeReservedKeyword( + name: 'continue', + prefix: 'k', + sanitizeCaseStyle: CaseStyle.snake, + defaultCaseStyle: CaseStyle.camel, + ), + 'k_continue', + ); + }); +} From cd2213360841dabc7e3f1c751a1b42f5a3791912 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Fri, 15 Nov 2024 19:52:59 +0100 Subject: [PATCH 105/118] release: 4.2.0 --- slang/CHANGELOG.md | 3 ++- slang/pubspec.yaml | 2 +- slang_build_runner/CHANGELOG.md | 4 ++++ slang_build_runner/pubspec.yaml | 4 ++-- slang_flutter/CHANGELOG.md | 4 ++++ slang_flutter/pubspec.yaml | 4 ++-- 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 2261a01a..da66cdc6 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,5 +1,6 @@ -## 4.1.1 +## 4.2.0 +- feat: automatically sanitize invalid keys (e.g. `continue`, `123`) (#257) - fix: do not override `locale` in L10n format definition ## 4.1.0 diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index 6a8dcae3..85e3eea3 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -1,6 +1,6 @@ name: slang description: Localization / Internationalization (i18n) solution. Use JSON, YAML, CSV, or ARB files to create typesafe translations via source generation. -version: 4.1.0 +version: 4.2.0 repository: https://github.com/slang-i18n/slang topics: - i18n diff --git a/slang_build_runner/CHANGELOG.md b/slang_build_runner/CHANGELOG.md index e2adbbbc..6b690076 100644 --- a/slang_build_runner/CHANGELOG.md +++ b/slang_build_runner/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.2.0 + +- bump `slang` to `4.2.0` + ## 4.1.0 - feat: add `dart_style` dependency to auto format generated files diff --git a/slang_build_runner/pubspec.yaml b/slang_build_runner/pubspec.yaml index d4a5cbca..ce8d939d 100644 --- a/slang_build_runner/pubspec.yaml +++ b/slang_build_runner/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_build_runner description: build_runner integration for slang. This library ensures that slang is recognized by build_runner. -version: 4.1.0 +version: 4.2.0 repository: https://github.com/slang-i18n/slang environment: @@ -12,7 +12,7 @@ dependencies: glob: ^2.0.2 # Use a tight version to ensure that all features are available - slang: '>=4.1.0 <4.2.0' + slang: '>=4.2.0 <4.3.0' dev_dependencies: lints: ^2.0.0 diff --git a/slang_flutter/CHANGELOG.md b/slang_flutter/CHANGELOG.md index 44c75e59..72ef265c 100644 --- a/slang_flutter/CHANGELOG.md +++ b/slang_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.2.0 + +- bump `slang` to `4.2.0` + ## 4.1.0 - bump `slang` to `4.1.0` diff --git a/slang_flutter/pubspec.yaml b/slang_flutter/pubspec.yaml index 4f403c85..7e8b27b6 100644 --- a/slang_flutter/pubspec.yaml +++ b/slang_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_flutter description: Flutter support for slang. This library provides helpful Flutter API. -version: 4.1.0 +version: 4.2.0 repository: https://github.com/slang-i18n/slang environment: @@ -14,7 +14,7 @@ dependencies: sdk: flutter # Use a tight version to ensure that all features are available - slang: '>=4.1.0 <4.2.0' + slang: '>=4.2.0 <4.3.0' dev_dependencies: flutter_test: From c9b23ee676850f280a3a9409195d1c8537c364cf Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sat, 16 Nov 2024 04:25:09 +0100 Subject: [PATCH 106/118] fix: do not sanitize keys in maps --- slang/CHANGELOG.md | 4 +++ .../builder/translation_model_builder.dart | 21 +++++++++----- .../translation_model_builder_test.dart | 29 +++++++++++++++++++ 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index da66cdc6..5a8e1f58 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.2.1 + +- fix: do not sanitize keys in maps + ## 4.2.0 - feat: automatically sanitize invalid keys (e.g. `continue`, `123`) (#257) diff --git a/slang/lib/src/builder/builder/translation_model_builder.dart b/slang/lib/src/builder/builder/translation_model_builder.dart index 562055c5..40aa8d23 100644 --- a/slang/lib/src/builder/builder/translation_model_builder.dart +++ b/slang/lib/src/builder/builder/translation_model_builder.dart @@ -374,6 +374,12 @@ Map _parseMapNode({ _setParent(node, children.values); resultNodeTree[key] = node; } else { + _DetectionResult? detectedType = + modifiers.keys.contains(NodeModifiers.map) || + config.maps.contains(currPath) + ? const _DetectionResult(_DetectionType.map) + : null; + // key: { ...value } children = _parseMapNode( locale: locale, @@ -393,13 +399,14 @@ Map _parseMapNode({ baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, handleTypes: handleTypes, - sanitizeKey: true, + sanitizeKey: detectedType == null, ); - final Node finalNode; - final detectedType = + detectedType ??= _determineNodeType(config, currPath, modifiers, children); + final Node finalNode; + // split by comma if necessary if (detectedType.nodeType == _DetectionType.context || detectedType.nodeType == _DetectionType.pluralCardinal || @@ -572,6 +579,7 @@ void _setParent(Node parent, Iterable children) { } } +// Note: We already detected the map type, no need to check for it again _DetectionResult _determineNodeType( BuildModelConfig config, String nodePath, @@ -579,10 +587,7 @@ _DetectionResult _determineNodeType( Map children, ) { final modifierFlags = modifiers.keys.toSet(); - if (modifierFlags.contains(NodeModifiers.map) || - config.maps.contains(nodePath)) { - return _DetectionResult(_DetectionType.map); - } else if (modifierFlags.contains(NodeModifiers.plural) || + if (modifierFlags.contains(NodeModifiers.plural) || modifierFlags.contains(NodeModifiers.cardinal) || config.pluralCardinal.contains(nodePath)) { return _DetectionResult(_DetectionType.pluralCardinal); @@ -940,7 +945,7 @@ class _DetectionResult { final _DetectionType nodeType; final String? contextHint; - _DetectionResult(this.nodeType, [this.contextHint]); + const _DetectionResult(this.nodeType, [this.contextHint]); } class _InterfaceAttributesResult { diff --git a/slang/test/unit/builder/translation_model_builder_test.dart b/slang/test/unit/builder/translation_model_builder_test.dart index 88b18997..afe89701 100644 --- a/slang/test/unit/builder/translation_model_builder_test.dart +++ b/slang/test/unit/builder/translation_model_builder_test.dart @@ -55,6 +55,35 @@ void main() { expect((mapNode.entries['my_value 3'] as StringTextNode).content, 'cool'); }); + test('Should sanitize reserved keyword', () { + final result = TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), + locale: _locale, + map: { + 'continue': 'Continue', + }, + ); + + expect(result.root.entries['continue'], isNull); + expect(result.root.entries['kContinue'], isA()); + }); + + test('Should not sanitize keys in maps', () { + final result = TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), + locale: _locale, + map: { + 'a(map)': { + 'continue': 'Continue', + }, + }, + ); + + final mapNode = result.root.entries['a'] as ObjectNode; + expect(mapNode.entries['continue'], isA()); + expect(mapNode.entries['kContinue'], isNull); + }); + test('one link no parameters', () { final result = TranslationModelBuilder.build( buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), From f8f1590838b190afb7c6498cdb734c00c946e677 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sat, 16 Nov 2024 04:25:46 +0100 Subject: [PATCH 107/118] release: 4.2.1 --- slang/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index 85e3eea3..2ad2a4fe 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -1,6 +1,6 @@ name: slang description: Localization / Internationalization (i18n) solution. Use JSON, YAML, CSV, or ARB files to create typesafe translations via source generation. -version: 4.2.0 +version: 4.2.1 repository: https://github.com/slang-i18n/slang topics: - i18n From 2b4785ef8b9fe7f302ed723810ee8a731debbd2d Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 18 Nov 2024 16:25:04 +0100 Subject: [PATCH 108/118] feat: remove unnecessary comma --- slang/lib/src/builder/model/raw_config.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slang/lib/src/builder/model/raw_config.dart b/slang/lib/src/builder/model/raw_config.dart index 4e9b5e3f..2f82dc6e 100644 --- a/slang/lib/src/builder/model/raw_config.dart +++ b/slang/lib/src/builder/model/raw_config.dart @@ -239,7 +239,7 @@ class RawConfig { print( ' -> paramCase: ${paramCase != null ? paramCase?.name : 'null (no change)'}'); print( - ' -> sanitization: ${sanitization.enabled ? 'enabled' : 'disabled'} / \'${sanitization.prefix}\' / caseStyle: ${sanitization.caseStyle},'); + ' -> sanitization: ${sanitization.enabled ? 'enabled' : 'disabled'} / \'${sanitization.prefix}\' / caseStyle: ${sanitization.caseStyle}'); print(' -> stringInterpolation: ${stringInterpolation.name}'); print(' -> renderFlatMap: $renderFlatMap'); print(' -> translationOverrides: $translationOverrides'); From 0150fa0daf7f34bf135560dac3b97bae04a93c3e Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sat, 23 Nov 2024 01:32:40 +0100 Subject: [PATCH 109/118] docs: promote normalize command --- slang/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/slang/README.md b/slang/README.md index a1bb1711..aba2cf9a 100644 --- a/slang/README.md +++ b/slang/README.md @@ -44,6 +44,7 @@ An extensive CLI will help you to manage the translations: ```bash dart run slang # generate dart file dart run slang analyze # unused and missing translations +dart run slang normalize # sort translations according to base locale dart run slang edit move loginPage authPage # move or rename translations dart run slang migrate arb src.arb dest.json # migrate arb to json ``` From e25d4f7dbd9a476b98685286777310ae492b32e3 Mon Sep 17 00:00:00 2001 From: Emir Bolat <89213834+speeedev@users.noreply.github.com> Date: Mon, 25 Nov 2024 04:31:06 +0300 Subject: [PATCH 110/118] Update README.md (#264) --- slang/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/slang/README.md b/slang/README.md index aba2cf9a..46888844 100644 --- a/slang/README.md +++ b/slang/README.md @@ -2031,6 +2031,7 @@ The second one always returns a new instance. - [Medium (English)](https://medium.com/swlh/flutter-i18n-made-easy-1fd9ccd82cb3) - [Medium (English)](https://maruf-hassan.medium.com/handling-flutter-internationalization-like-a-pro-699ac2f6d856) +- [Medium (Turkish)](https://medium.com/@speedev/flutterda-lokalizasyon-i18n-nas%C4%B1l-yap%C4%B1l%C4%B1r-ad%C4%B1m-ad%C4%B1m-0c438fcb8537) - [Хабр (Russian)](https://habr.com/ru/post/718310/) - [Qiita (Japanese)](https://qiita.com/popy1017/items/3495be9fdc028161bef9) - [okaryo (Japanese)](https://blog.okaryo.io/20230104-split-and-manage-arb-files-for-internationalized-flutter-app-in-yaml-format) From a79b854ac4423c53d02119b6b0f608b8052a6ce5 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Fri, 29 Nov 2024 16:04:34 +0100 Subject: [PATCH 111/118] feat: simplify and normalize file names --- slang/CHANGELOG.md | 10 ++ slang/MIGRATION.md | 44 +++++++ slang/README.md | 20 ++- .../{strings_de.i18n.json => de.i18n.json} | 0 .../i18n/{strings.i18n.json => en.i18n.json} | 0 ...trings_fr_FR.i18n.json => fr_FR.i18n.json} | 0 .../slang_file_collection_builder.dart | 115 ++++++++++++++---- .../builder/model/slang_file_collection.dart | 2 + .../slang_file_collection_builder_test.dart | 31 ++++- 9 files changed, 186 insertions(+), 36 deletions(-) rename slang/example/lib/i18n/{strings_de.i18n.json => de.i18n.json} (100%) rename slang/example/lib/i18n/{strings.i18n.json => en.i18n.json} (100%) rename slang/example/lib/i18n/{strings_fr_FR.i18n.json => fr_FR.i18n.json} (100%) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 5a8e1f58..5331fe35 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,13 @@ +## 4.3.0 + +- feat: simplify file names without namespaces (#267) +- **DEPRECATED:** Do not use namespaces in file names when namespaces are disabled: `strings_de.json` -> `de.json` +- **DEPRECATED:** Always specify the locale in the file name (namespace enabled): `strings.json` -> `strings_en.json`, except the locale is specified in the directory name + +Note: This might make the files order in your IDE less pleasant if input and output files are in the same directory. +You can specify the output directory to a subdirectory to avoid this. +For example, `output_directory: lib/i18n/gen` + ## 4.2.1 - fix: do not sanitize keys in maps diff --git a/slang/MIGRATION.md b/slang/MIGRATION.md index 1bf97c46..73d47a40 100644 --- a/slang/MIGRATION.md +++ b/slang/MIGRATION.md @@ -1,5 +1,49 @@ # Migration Guides +## slang 4.0 to 5.0 + +### Remove namespace in file name + +When namespaces are disabled, remove the namespace in the file name. + +Before: +```text +lib/ + └── i18n/ + └── strings.i18n.json + └── strings_de.i18n.json +``` + +After: +```text +lib/ + └── i18n/ + └── en.i18n.json + └── de.i18n.json +``` + +### Add locale in file name + +When namespaces are enabled, always specify the locale in the file name. + +Before: +```text +i18n/ + └── widgets.i18n.json + └── widgets_fr.i18n.json + └── errorDialogs.i18n.json + └── errorDialogs_fr.i18n.json +``` + +After: +```text +i18n/ + └── widgets_en.i18n.json + └── widgets_fr.i18n.json + └── errorDialogs_en.i18n.json + └── errorDialogs_fr.i18n.json +``` + ## slang 3.0 to 4.0 ### Lazy Loading diff --git a/slang/README.md b/slang/README.md index 46888844..223a0536 100644 --- a/slang/README.md +++ b/slang/README.md @@ -133,24 +133,22 @@ dev_dependencies: Format: ```text -_. +. ``` -You can ignore the [namespace](#-namespaces) for this basic example, so just use a generic name like `strings`. - Most common i18n directories are `assets/i18n` and `lib/i18n`. (see [Assets](#-assets)). Example: ```text lib/ └── i18n/ - └── strings.i18n.json - └── strings_de.i18n.json - └── strings_zh-CN.i18n.json <-- example for country code + └── en.i18n.json + └── de.i18n.json + └── zh-CN.i18n.json <-- example for country code ``` ```json5 -// File: strings.i18n.json (mandatory, base locale) +// File: en.i18n.json { "hello": "Hello $name", "save": "Save", @@ -162,7 +160,7 @@ lib/ ``` ```json5 -// File: strings_de.i18n.json +// File: de.i18n.json { "hello": "Hallo $name", "save": "Speichern", @@ -1220,14 +1218,14 @@ output_file_name: translations.g.dart # set file name (mandatory) Let's create two namespaces called `widgets` and `errorDialogs`. Please use camel case for multiple words. ```text -_. +_. ``` ```text i18n/ - └── widgets.i18n.json + └── widgets_en.i18n.json └── widgets_fr.i18n.json - └── errorDialogs.i18n.json <-- camel case for multiple words + └── errorDialogs_en.i18n.json <-- camel case for multiple words └── errorDialogs_fr.i18n.json ``` diff --git a/slang/example/lib/i18n/strings_de.i18n.json b/slang/example/lib/i18n/de.i18n.json similarity index 100% rename from slang/example/lib/i18n/strings_de.i18n.json rename to slang/example/lib/i18n/de.i18n.json diff --git a/slang/example/lib/i18n/strings.i18n.json b/slang/example/lib/i18n/en.i18n.json similarity index 100% rename from slang/example/lib/i18n/strings.i18n.json rename to slang/example/lib/i18n/en.i18n.json diff --git a/slang/example/lib/i18n/strings_fr_FR.i18n.json b/slang/example/lib/i18n/fr_FR.i18n.json similarity index 100% rename from slang/example/lib/i18n/strings_fr_FR.i18n.json rename to slang/example/lib/i18n/fr_FR.i18n.json diff --git a/slang/lib/src/builder/builder/slang_file_collection_builder.dart b/slang/lib/src/builder/builder/slang_file_collection_builder.dart index e8565357..bd6622ee 100644 --- a/slang/lib/src/builder/builder/slang_file_collection_builder.dart +++ b/slang/lib/src/builder/builder/slang_file_collection_builder.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:slang/src/builder/builder/raw_config_builder.dart'; +import 'package:slang/src/builder/model/enums.dart'; import 'package:slang/src/builder/model/i18n_locale.dart'; import 'package:slang/src/builder/model/raw_config.dart'; import 'package:slang/src/builder/model/slang_file_collection.dart'; @@ -71,7 +72,9 @@ class SlangFileCollectionBuilder { static SlangFileCollection fromFileModel({ required RawConfig config, required Iterable files, + bool showWarning = true, }) { + // True, if (1) namespaces are enabled and (2) directory locale is used final includeUnderscore = config.namespaces && files.any((f) { final fileNameNoExtension = PathUtils.getFileNameNoExtension(f.path); @@ -95,11 +98,31 @@ class SlangFileCollectionBuilder { .map((f) { final fileNameNoExtension = PathUtils.getFileNameNoExtension(f.path); + + if (!config.namespaces) { + final localeMatch = + RegexUtils.localeRegex.firstMatch(fileNameNoExtension); + if (localeMatch != null) { + final locale = I18nLocale( + language: localeMatch.group(1)!, + script: localeMatch.group(2), + country: localeMatch.group(3), + ); + + return TranslationFile( + path: f.path, + locale: locale, + namespace: TranslationFile.DEFAULT_NAMESPACE, + read: f.read, + ); + } + } + final baseFileMatch = RegexUtils.baseFileRegex.firstMatch(fileNameNoExtension); if (includeUnderscore || baseFileMatch != null) { - // base file (file without locale, may be multiples due to namespaces!) - // could also be a non-base locale when directory name is a locale + // base file (file without locale) + // could also be a non-base locale when directory name is a locale (namespace only) // directory name could be a locale I18nLocale? directoryLocale; @@ -108,41 +131,69 @@ class SlangFileCollectionBuilder { filePath: f.path, inputDirectory: config.inputDirectory, ); + + if (showWarning && directoryLocale == null) { + _baseLocaleDeprecationWarning( + fileName: PathUtils.getFileName(f.path), + replacement: + '${fileNameNoExtension}_${config.baseLocale.languageTag.replaceAll('-', '_')}${config.inputFilePattern}', + ); + } + } + + if (showWarning && + !config.namespaces && + config.fileType != FileType.csv) { + // Note: Compact CSV files are still allowed to have a file name without locale. + _namespaceDeprecationWarning( + fileName: PathUtils.getFileName(f.path), + replacement: + '${config.baseLocale.languageTag.replaceAll('-', '_')}${config.inputFilePattern}', + ); } return TranslationFile( path: f.path, locale: directoryLocale ?? config.baseLocale, - namespace: fileNameNoExtension, + namespace: config.namespaces + ? fileNameNoExtension + : TranslationFile.DEFAULT_NAMESPACE, read: f.read, ); - } else { - // secondary files (strings_x) - final match = RegexUtils.fileWithLocaleRegex - .firstMatch(fileNameNoExtension); - if (match != null) { - final namespace = match.group(1)!; - final locale = I18nLocale( - language: match.group(2)!, - script: match.group(3), - country: match.group(4), - ); + } - return TranslationFile( - path: f.path, - locale: locale, - namespace: namespace, - read: f.read, + // secondary files (strings_x) + final match = + RegexUtils.fileWithLocaleRegex.firstMatch(fileNameNoExtension); + if (match != null) { + final namespace = match.group(1)!; + final locale = I18nLocale( + language: match.group(2)!, + script: match.group(3), + country: match.group(4), + ); + + if (showWarning && !config.namespaces) { + _namespaceDeprecationWarning( + fileName: PathUtils.getFileName(f.path), + replacement: + '${locale.languageTag.replaceAll('-', '_')}${config.inputFilePattern}', ); } + + return TranslationFile( + path: f.path, + locale: locale, + namespace: config.namespaces + ? namespace + : TranslationFile.DEFAULT_NAMESPACE, + read: f.read, + ); } return null; }) - // We cannot use "nonNulls" because this requires Dart 3.0 - // and slang currently supports Dart 2.17 - // ignore: deprecated_member_use - .whereNotNull() + .nonNulls .sortedBy((file) => '${file.locale}-${file.namespace}'), ); } @@ -244,3 +295,21 @@ extension on String { return PathUtils.getFileName(this); } } + +void _namespaceDeprecationWarning({ + required String fileName, + required String replacement, +}) { + print( + 'DEPRECATED(v4.3.0): Do not use namespaces in file names when namespaces are disabled: "$fileName" -> "$replacement"', + ); +} + +void _baseLocaleDeprecationWarning({ + required String fileName, + required String replacement, +}) { + print( + 'DEPRECATED(v4.3.0): Always specify locale: "$fileName" -> "$replacement"', + ); +} diff --git a/slang/lib/src/builder/model/slang_file_collection.dart b/slang/lib/src/builder/model/slang_file_collection.dart index 6e0cefcc..35c93df5 100644 --- a/slang/lib/src/builder/model/slang_file_collection.dart +++ b/slang/lib/src/builder/model/slang_file_collection.dart @@ -51,6 +51,8 @@ class SlangFileCollection { } class TranslationFile extends PlainTranslationFile { + static const DEFAULT_NAMESPACE = r'$default$'; + /// The inferred locale of this file (by file name, directory name, or config) final I18nLocale locale; diff --git a/slang/test/unit/builder/slang_file_collection_builder_test.dart b/slang/test/unit/builder/slang_file_collection_builder_test.dart index aa4763c5..c23dd9f8 100644 --- a/slang/test/unit/builder/slang_file_collection_builder_test.dart +++ b/slang/test/unit/builder/slang_file_collection_builder_test.dart @@ -10,20 +10,45 @@ PlainTranslationFile _file(String path) { void main() { group('SlangFileCollectionBuilder.fromFileModel', () { - test('should find base locale', () { + test('should find locales', () { + final model = SlangFileCollectionBuilder.fromFileModel( + config: RawConfig.defaultConfig + .copyWith(baseLocale: I18nLocale(language: 'de')), + files: [ + _file('lib/i18n/en.i18n.json'), + _file('lib/i18n/de.i18n.json'), + _file('lib/i18n/fr_FR.i18n.json'), + _file('lib/i18n/zh-CN.i18n.json'), + ], + ); + + expect(model.files.length, 4); + expect( + model.files.map((f) => f.locale.languageTag).toList(), + [ + 'de', + 'en', + 'fr-FR', + 'zh-CN', + ], + ); + }); + + test('should find base locale (legacy)', () { final model = SlangFileCollectionBuilder.fromFileModel( config: RawConfig.defaultConfig .copyWith(baseLocale: I18nLocale(language: 'de')), files: [ _file('lib/i18n/strings.i18n.json'), ], + showWarning: false, ); expect(model.files.length, 1); expect(model.files.first.locale.language, 'de'); }); - test('should find locale in file names', () { + test('should find locale in file names (legacy)', () { final model = SlangFileCollectionBuilder.fromFileModel( config: RawConfig.defaultConfig .copyWith(baseLocale: I18nLocale(language: 'en')), @@ -32,6 +57,7 @@ void main() { _file('lib/i18n/strings_de.i18n.json'), _file('lib/i18n/strings-fr-FR.i18n.json'), ], + showWarning: false, ); expect(model.files.length, 3); @@ -50,6 +76,7 @@ void main() { files: [ _file('lib/i18n/dialogs.i18n.json'), ], + showWarning: false, ); expect(model.files.length, 1); From 3c4d8af90a06a2fd8c6aea992b73763d3a8de576 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Fri, 29 Nov 2024 16:32:34 +0100 Subject: [PATCH 112/118] release: 4.3.0 --- slang/pubspec.yaml | 2 +- slang_build_runner/CHANGELOG.md | 4 ++++ slang_build_runner/pubspec.yaml | 4 ++-- slang_flutter/CHANGELOG.md | 4 ++++ slang_flutter/pubspec.yaml | 4 ++-- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index 2ad2a4fe..1d2ee047 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -1,6 +1,6 @@ name: slang description: Localization / Internationalization (i18n) solution. Use JSON, YAML, CSV, or ARB files to create typesafe translations via source generation. -version: 4.2.1 +version: 4.3.0 repository: https://github.com/slang-i18n/slang topics: - i18n diff --git a/slang_build_runner/CHANGELOG.md b/slang_build_runner/CHANGELOG.md index 6b690076..869ece02 100644 --- a/slang_build_runner/CHANGELOG.md +++ b/slang_build_runner/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.3.0 + +- bump `slang` to `4.3.0` + ## 4.2.0 - bump `slang` to `4.2.0` diff --git a/slang_build_runner/pubspec.yaml b/slang_build_runner/pubspec.yaml index ce8d939d..86565a4f 100644 --- a/slang_build_runner/pubspec.yaml +++ b/slang_build_runner/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_build_runner description: build_runner integration for slang. This library ensures that slang is recognized by build_runner. -version: 4.2.0 +version: 4.3.0 repository: https://github.com/slang-i18n/slang environment: @@ -12,7 +12,7 @@ dependencies: glob: ^2.0.2 # Use a tight version to ensure that all features are available - slang: '>=4.2.0 <4.3.0' + slang: '>=4.3.0 <4.4.0' dev_dependencies: lints: ^2.0.0 diff --git a/slang_flutter/CHANGELOG.md b/slang_flutter/CHANGELOG.md index 72ef265c..bebf01a9 100644 --- a/slang_flutter/CHANGELOG.md +++ b/slang_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.3.0 + +- bump `slang` to `4.3.0` + ## 4.2.0 - bump `slang` to `4.2.0` diff --git a/slang_flutter/pubspec.yaml b/slang_flutter/pubspec.yaml index 7e8b27b6..7eaa471e 100644 --- a/slang_flutter/pubspec.yaml +++ b/slang_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_flutter description: Flutter support for slang. This library provides helpful Flutter API. -version: 4.2.0 +version: 4.3.0 repository: https://github.com/slang-i18n/slang environment: @@ -14,7 +14,7 @@ dependencies: sdk: flutter # Use a tight version to ensure that all features are available - slang: '>=4.2.0 <4.3.0' + slang: '>=4.3.0 <4.4.0' dev_dependencies: flutter_test: From e5f934b9978019f450fc54e8f50131b60f978351 Mon Sep 17 00:00:00 2001 From: Tien Do Nam <38380847+Tienisto@users.noreply.github.com> Date: Sun, 1 Dec 2024 18:50:07 +0100 Subject: [PATCH 113/118] feat: add fallback modifier for maps (#269) --- slang/CHANGELOG.md | 4 + slang/README.md | 22 +- .../builder/translation_model_builder.dart | 53 +++- slang/lib/src/builder/model/node.dart | 191 +++++++++++- .../translation_model_builder_test.dart | 291 ++++++++++++++---- 5 files changed, 488 insertions(+), 73 deletions(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 5331fe35..9686a18d 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.4.0 + +- feat: add `(fallback)` modifier to fallback entries within a map (#268) + ## 4.3.0 - feat: simplify file names without namespaces (#267) diff --git a/slang/README.md b/slang/README.md index 223a0536..83c1b6dd 100644 --- a/slang/README.md +++ b/slang/README.md @@ -1088,6 +1088,7 @@ Available Modifiers: |----------------------------|-----------------------------------------------|---------------------------------| | `(rich)` | This is a rich text. | Leaves, Maps (Plural / Context) | | `(map)` | This is a map / dictionary (and not a class). | Maps | +| `(fallback)` | Should fallback. `(map)` required. | Maps | | `(plural)` | This is a plural (type: cardinal) | Maps | | `(cardinal)` | This is a plural (type: cardinal) | Maps | | `(ordinal)` | This is a plural (type: ordinal) | Maps | @@ -1300,6 +1301,14 @@ By default, you must provide all translations for all locales. Otherwise, you ca In case of rapid development, you can turn off this feature. Missing translations will fall back to base locale. +The following configurations are available: + +| Fallback Strategy | Description | +|----------------------------|-------------------------------------------------------------------| +| `none` | Don't fallback (default). | +| `base_locale` | Fallback to the base locale. | +| `base_locale_empty_string` | Fallback to the base locale. Also treat empty strings as missing. | + ```yaml # Config base_locale: en @@ -1322,7 +1331,18 @@ fallback_strategy: base_locale # add this } ``` -To also treat empty strings as missing translations, set `fallback_strategy: base_locale_empty_string`. +By default, entries inside `(map)` are not affected by the fallback strategy. +This allows you to provide different map entries for each locale. +To still apply the fallback strategy to maps, add the `(fallback)` modifier. + +```json5 +{ + "myMap(map, fallback)": { + "someKey": "Some value", + // missing keys will fallback to the base locale + } +} +``` ### ➤ Lazy Loading diff --git a/slang/lib/src/builder/builder/translation_model_builder.dart b/slang/lib/src/builder/builder/translation_model_builder.dart index 40aa8d23..83ce095f 100644 --- a/slang/lib/src/builder/builder/translation_model_builder.dart +++ b/slang/lib/src/builder/builder/translation_model_builder.dart @@ -28,6 +28,8 @@ class BuildModelResult { } class TranslationModelBuilder { + TranslationModelBuilder._(); + /// Builds the i18n model for ONE locale /// /// The [map] must be of type Map and all children may of type @@ -381,7 +383,7 @@ Map _parseMapNode({ : null; // key: { ...value } - children = _parseMapNode( + final tempChildren = _parseMapNode( locale: locale, types: types, parentPath: currPath, @@ -402,6 +404,19 @@ Map _parseMapNode({ sanitizeKey: detectedType == null, ); + if (detectedType?.nodeType == _DetectionType.map && + baseData != null && + modifiers.containsKey(NodeModifiers.fallback)) { + children = _digestMapEntries( + locale: locale, + baseTranslation: baseData.root, + path: currPath, + entries: tempChildren, + ); + } else { + children = tempChildren; + } + detectedType ??= _determineNodeType(config, currPath, modifiers, children); @@ -902,34 +917,54 @@ Map _digestContextEntries({ }) { // Using "late" keyword because we are optimistic that all values are present late ContextNode baseContextNode = - _findContextNode(baseTranslation, path.split('.')); + _findNode(baseTranslation, path.split('.')); return { for (final value in baseContext.enumValues) value: entries[value] ?? - baseContextNode.entries[value] ?? + baseContextNode.entries[value] + ?.clone(keepParent: false, locale: locale) ?? _throwError( 'In <${locale.languageTag}>, the value for $value in $path is missing (required by ${baseContext.enumName})', ), }; } +/// Makes sure that every map entry in [baseTranslation] is also present in [entries]. +/// If a value is missing, the base translation is used. +Map _digestMapEntries({ + required I18nLocale locale, + required ObjectNode baseTranslation, + required String path, + required Map entries, +}) { + // Using "late" keyword because we are optimistic that all values are present + late ObjectNode baseMapNode = + _findNode(baseTranslation, path.split('.')); + return { + for (final entry in baseMapNode.entries.entries) + entry.key: entries[entry.key] ?? + baseMapNode.entries[entry.key]! + .clone(keepParent: false, locale: locale), + }; +} + Never _throwError(String message) { throw message; } -/// Recursively find the [ContextNode] using the given [path]. -ContextNode _findContextNode(ObjectNode node, List path) { +/// Recursively find the [Node] using the given [path]. +T _findNode(ObjectNode node, List path) { final child = node.entries[path[0]]; if (path.length == 1) { - if (child is ContextNode) { + if (child is T) { return child; } else { - throw 'Parent node is not a ContextNode but a ${node.runtimeType} at path $path'; + throw 'Parent node is not a $T but a ${node.runtimeType} at path $path'; } } else if (child is ObjectNode) { - return _findContextNode(child, path.sublist(1)); + return _findNode(child, path.sublist(1)); } else { - throw 'Cannot find base ContextNode'; + throw 'Cannot find base $T'; } } diff --git a/slang/lib/src/builder/model/node.dart b/slang/lib/src/builder/model/node.dart index dd9329bf..38903afc 100644 --- a/slang/lib/src/builder/model/node.dart +++ b/slang/lib/src/builder/model/node.dart @@ -10,17 +10,44 @@ import 'package:slang/src/builder/utils/regex_utils.dart'; import 'package:slang/src/builder/utils/string_interpolation_extensions.dart'; class NodeModifiers { + /// Flag. Mark a node as rich text static const rich = 'rich'; + + /// Flag. Mark a node as a map static const map = 'map'; + + /// Flag. Missing keys should fallback to base map. + /// Only relevant when fallback_strategy is configured. + static const fallback = 'fallback'; + + /// Flag. Mark a plural as plural static const plural = 'plural'; + + /// Flag. Mark a plural as cardinal static const cardinal = 'cardinal'; + + /// Flag. Mark a plural as ordinal static const ordinal = 'ordinal'; + + /// Parameter. Set context name. static const context = 'context'; + + /// Parameter. Set a parameter name for a plural or context static const param = 'param'; + + /// Flag. Mark all children of an interface as descendants of an interface static const interface = 'interface'; + + /// Flag. Mark a single JSON object as a descendant of an interface static const singleInterface = 'singleInterface'; + + /// Analysis flag. Ignore during missing translation analysis static const ignoreMissing = 'ignoreMissing'; + + /// Analysis flag. Ignore during unused translation analysis static const ignoreUnused = 'ignoreUnused'; + + /// Analysis flag. Translation is outdated static const outdated = 'OUTDATED'; } @@ -47,13 +74,19 @@ abstract class Node { assert(_parent == null); _parent = parent; } + + /// Deep clones the node. + Node clone({ + required bool keepParent, + I18nLocale? locale, + }); } /// Flag for leaves -/// Leaves are: TextNode, PluralNode and ContextNode -abstract class LeafNode {} +/// Leaves are: [TextNode], [PluralNode] and [ContextNode] +abstract interface class LeafNode {} -/// the super class for list and object nodes +/// The super class for [ListNode] and [ObjectNode] abstract class IterableNode extends Node { /// The generic type of the container, i.e. Map or List String _genericType; @@ -104,6 +137,31 @@ class ObjectNode extends IterableNode { @override String toString() => entries.toString(); + + @override + ObjectNode clone({required bool keepParent, I18nLocale? locale}) { + final node = ObjectNode( + path: path, + rawPath: rawPath, + modifiers: modifiers, + comment: comment, + entries: entries.map( + (key, value) => MapEntry( + key, + value.clone(keepParent: keepParent, locale: locale), + ), + ), + isMap: isMap, + ); + if (keepParent && parent != null) { + node.setParent(parent!); + } + node.setGenericType(genericType); + if (interface != null) { + node.setInterface(interface!); + } + return node; + } } class ListNode extends IterableNode { @@ -122,6 +180,27 @@ class ListNode extends IterableNode { @override String toString() => entries.toString(); + + @override + ListNode clone({required bool keepParent, I18nLocale? locale}) { + final node = ListNode( + path: path, + rawPath: rawPath, + modifiers: modifiers, + comment: comment, + entries: entries + .map((e) => e.clone(keepParent: keepParent, locale: locale)) + .toList(), + ); + + if (keepParent && parent != null) { + node.setParent(parent!); + } + + node.setGenericType(genericType); + + return node; + } } enum PluralType { @@ -172,6 +251,32 @@ class PluralNode extends Node implements LeafNode { @override String toString() => quantities.toString(); + + @override + PluralNode clone({required bool keepParent, I18nLocale? locale}) { + final node = PluralNode( + path: path, + rawPath: rawPath, + modifiers: modifiers, + comment: comment, + pluralType: pluralType, + quantities: quantities.map( + (key, value) => MapEntry( + key, + value.clone(keepParent: keepParent, locale: locale), + ), + ), + paramName: paramName, + paramType: paramType, + rich: rich, + ); + + if (keepParent && parent != null) { + node.setParent(parent!); + } + + return node; + } } class ContextNode extends Node implements LeafNode { @@ -213,6 +318,31 @@ class ContextNode extends Node implements LeafNode { @override String toString() => entries.toString(); + + @override + ContextNode clone({required bool keepParent, I18nLocale? locale}) { + final node = ContextNode( + path: path, + rawPath: rawPath, + modifiers: modifiers, + comment: comment, + context: context, + entries: entries.map( + (key, value) => MapEntry( + key, + value.clone(keepParent: keepParent, locale: locale), + ), + ), + paramName: paramName, + rich: rich, + ); + + if (keepParent && parent != null) { + node.setParent(parent!); + } + + return node; + } } abstract class TextNode extends Node implements LeafNode { @@ -267,6 +397,12 @@ abstract class TextNode extends Node implements LeafNode { required Map> linkParamMap, required Map paramTypeMap, }); + + @override + TextNode clone({ + required bool keepParent, + I18nLocale? locale, + }); } class StringTextNode extends TextNode { @@ -365,6 +501,29 @@ class StringTextNode extends TextNode { return '$params => $content'; } } + + @override + StringTextNode clone({required bool keepParent, I18nLocale? locale}) { + final node = StringTextNode( + path: path, + rawPath: rawPath, + modifiers: modifiers, + locale: locale ?? this.locale, + types: types, + raw: raw, + comment: comment, + shouldEscape: shouldEscape, + handleTypes: handleTypes, + interpolation: interpolation, + paramCase: paramCase, + ); + + if (keepParent && parent != null) { + node.setParent(parent!); + } + + return node; + } } class RichTextNode extends TextNode { @@ -408,7 +567,8 @@ class RichTextNode extends TextNode { interpolation: interpolation, defaultType: 'ignored', // types are ignored - paramCase: null, // param case will be applied later + paramCase: null, + // param case will be applied later digestParameter: false, ); @@ -493,6 +653,29 @@ class RichTextNode extends TextNode { _spans = temp.spans; } + + @override + RichTextNode clone({required bool keepParent, I18nLocale? locale}) { + final node = RichTextNode( + path: path, + rawPath: rawPath, + modifiers: modifiers, + locale: locale ?? this.locale, + types: types, + raw: raw, + comment: comment, + shouldEscape: shouldEscape, + handleTypes: handleTypes, + interpolation: interpolation, + paramCase: paramCase, + ); + + if (keepParent && parent != null) { + node.setParent(parent!); + } + + return node; + } } String _escapeContent(String raw, StringInterpolation interpolation) { diff --git a/slang/test/unit/builder/translation_model_builder_test.dart b/slang/test/unit/builder/translation_model_builder_test.dart index afe89701..95d9d5de 100644 --- a/slang/test/unit/builder/translation_model_builder_test.dart +++ b/slang/test/unit/builder/translation_model_builder_test.dart @@ -11,19 +11,19 @@ import 'package:test/test.dart'; final _locale = I18nLocale(language: 'en'); void main() { - group('TranslationModelBuilder.build', () { - test('1 StringTextNode', () { - final result = TranslationModelBuilder.build( - buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), - locale: _locale, - map: { - 'test': 'a', - }, - ); - final map = result.root.entries; - expect((map['test'] as StringTextNode).content, 'a'); - }); + test('1 StringTextNode', () { + final result = TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), + locale: _locale, + map: { + 'test': 'a', + }, + ); + final map = result.root.entries; + expect((map['test'] as StringTextNode).content, 'a'); + }); + group('Recasing', () { test('keyCase=snake and keyMapCase=camel', () { final result = TranslationModelBuilder.build( buildConfig: RawConfig.defaultConfig.copyWith( @@ -54,36 +54,9 @@ void main() { final mapNode = result.root.entries['my_map'] as ObjectNode; expect((mapNode.entries['my_value 3'] as StringTextNode).content, 'cool'); }); + }); - test('Should sanitize reserved keyword', () { - final result = TranslationModelBuilder.build( - buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), - locale: _locale, - map: { - 'continue': 'Continue', - }, - ); - - expect(result.root.entries['continue'], isNull); - expect(result.root.entries['kContinue'], isA()); - }); - - test('Should not sanitize keys in maps', () { - final result = TranslationModelBuilder.build( - buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), - locale: _locale, - map: { - 'a(map)': { - 'continue': 'Continue', - }, - }, - ); - - final mapNode = result.root.entries['a'] as ObjectNode; - expect(mapNode.entries['continue'], isA()); - expect(mapNode.entries['kContinue'], isNull); - }); - + group('Linked Translations', () { test('one link no parameters', () { final result = TranslationModelBuilder.build( buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), @@ -187,7 +160,29 @@ void main() { expect(textNode.paramTypeMap, {'p1': 'Object', 'gender': 'GenderCon'}); expect(textNode.content, r'Hello ${_root.a(p1: p1, gender: gender)}'); }); + }); + + group('Context Type', () { + test('Should not include context type if values are unspecified', () { + final result = TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig.copyWith(contexts: [ + ContextType( + enumName: 'GenderCon', + defaultParameter: 'gender', + generateEnum: true, + ), + ]).toBuildModelConfig(), + locale: _locale, + map: { + 'a': 'b', + }, + ); + + expect(result.contexts, []); + }); + }); + group('Interface', () { test('empty lists should take generic type of interface', () { final result = TranslationModelBuilder.build( buildConfig: RawConfig.defaultConfig.copyWith(interfaces: [ @@ -238,24 +233,6 @@ void main() { (objectNode2.entries['myList'] as ListNode).genericType, 'MyType2'); }); - test('Should not include context type if values are unspecified', () { - final result = TranslationModelBuilder.build( - buildConfig: RawConfig.defaultConfig.copyWith(contexts: [ - ContextType( - enumName: 'GenderCon', - defaultParameter: 'gender', - generateEnum: true, - ), - ]).toBuildModelConfig(), - locale: _locale, - map: { - 'a': 'b', - }, - ); - - expect(result.contexts, []); - }); - test('Should handle nested interfaces specified via modifier', () { final resultUsingModifiers = TranslationModelBuilder.build( buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), @@ -333,6 +310,202 @@ void main() { _checkInterfaceResult(resultUsingConfig); }); }); + + group('Sanitization', () { + test('Should sanitize reserved keyword', () { + final result = TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), + locale: _locale, + map: { + 'continue': 'Continue', + }, + ); + + expect(result.root.entries['continue'], isNull); + expect(result.root.entries['kContinue'], isA()); + }); + + test('Should not sanitize keys in maps', () { + final result = TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), + locale: _locale, + map: { + 'a(map)': { + 'continue': 'Continue', + }, + }, + ); + + final mapNode = result.root.entries['a'] as ObjectNode; + expect(mapNode.entries['continue'], isA()); + expect(mapNode.entries['kContinue'], isNull); + }); + }); + + group('Fallback', () { + test('Should fallback context type cases', () { + final result = TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig + .copyWith( + fallbackStrategy: FallbackStrategy.baseLocale, + ) + .toBuildModelConfig(), + locale: _locale, + map: { + 'hello(context=GenderCon)': { + 'a': 'A', + 'c': 'C', + } + }, + baseData: TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig.copyWith( + contexts: [ + ContextType( + enumName: 'GenderCon', + defaultParameter: 'default', + generateEnum: true, + ), + ], + ).toBuildModelConfig(), + locale: _locale, + map: { + 'hello(context=GenderCon)': { + 'a': 'Base A', + 'b': 'Base B', + 'c': 'Base C', + }, + }, + ), + ); + + final textNode = result.root.entries['hello'] as ContextNode; + expect(textNode.entries.length, 3); + expect( + textNode.entries.values.map((e) => (e as StringTextNode).content), + [ + 'A', + 'Base B', + 'C', + ], + ); + }); + + test('Should not fallback map entry by default', () { + final result = TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig + .copyWith( + fallbackStrategy: FallbackStrategy.baseLocale, + ) + .toBuildModelConfig(), + locale: _locale, + map: { + 'myMap(map)': { + 'a': 'A', + 'c': 'C', + } + }, + baseData: TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig.copyWith().toBuildModelConfig(), + locale: _locale, + map: { + 'myMap(map)': { + 'a': 'Base A', + 'b': 'Base B', + 'c': 'Base C', + }, + }, + ), + ); + + final textNode = result.root.entries['myMap'] as ObjectNode; + expect(textNode.entries.length, 2); + expect( + textNode.entries.values.map((e) => (e as StringTextNode).content), + [ + 'A', + 'C', + ], + ); + }); + + test('Should fallback map entry when fallback modifier is added', () { + final result = TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig + .copyWith( + fallbackStrategy: FallbackStrategy.baseLocale, + ) + .toBuildModelConfig(), + locale: _locale, + map: { + 'myMap(map, fallback)': { + 'a': 'A', + 'c': 'C', + } + }, + baseData: TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig.copyWith().toBuildModelConfig(), + locale: _locale, + map: { + 'myMap(map, fallback)': { + 'a': 'Base A', + 'b': 'Base B', + 'c': 'Base C', + }, + }, + ), + ); + + final textNode = result.root.entries['myMap'] as ObjectNode; + expect(textNode.entries.length, 3); + expect( + textNode.entries.values.map((e) => (e as StringTextNode).content), + [ + 'A', + 'Base B', + 'C', + ], + ); + }); + + test('Should respect base_locale_empty_string in fallback map', () { + final result = TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig + .copyWith( + fallbackStrategy: FallbackStrategy.baseLocaleEmptyString, + ) + .toBuildModelConfig(), + locale: _locale, + map: { + 'myMap(map, fallback)': { + 'a': 'A', + 'c': '', + } + }, + baseData: TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig.copyWith().toBuildModelConfig(), + locale: _locale, + map: { + 'myMap(map, fallback)': { + 'a': 'Base A', + 'b': 'Base B', + 'c': 'Base C', + }, + }, + ), + ); + + final textNode = result.root.entries['myMap'] as ObjectNode; + expect(textNode.entries.length, 3); + expect( + textNode.entries.values.map((e) => (e as StringTextNode).content), + [ + 'A', + 'Base B', + 'Base C', + ], + ); + }); + }); } void _checkInterfaceResult(BuildModelResult result) { From bd9692de819e9a774ddc46739077f84cad21cbaf Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 1 Dec 2024 18:53:42 +0100 Subject: [PATCH 114/118] fix: empty strings in base translations should not be removed --- slang/CHANGELOG.md | 1 + slang/lib/src/builder/builder/translation_model_builder.dart | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/slang/CHANGELOG.md b/slang/CHANGELOG.md index 9686a18d..4b9dd623 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,6 +1,7 @@ ## 4.4.0 - feat: add `(fallback)` modifier to fallback entries within a map (#268) +- fix: empty strings in base translations should not be removed when using `fallback_strategy: base_locale_empty_string` ## 4.3.0 diff --git a/slang/lib/src/builder/builder/translation_model_builder.dart b/slang/lib/src/builder/builder/translation_model_builder.dart index 83ce095f..cb06c0fc 100644 --- a/slang/lib/src/builder/builder/translation_model_builder.dart +++ b/slang/lib/src/builder/builder/translation_model_builder.dart @@ -304,7 +304,8 @@ Map _parseMapNode({ // leaf // key: 'value' - if (config.fallbackStrategy == FallbackStrategy.baseLocaleEmptyString && + if (baseData != null && + config.fallbackStrategy == FallbackStrategy.baseLocaleEmptyString && value is String && value.isEmpty) { return; From 244e7b1b5084653a01b40c746fe2c349009666d7 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 1 Dec 2024 18:58:19 +0100 Subject: [PATCH 115/118] test: add base_locale_empty_string test --- .../builder/translation_model_builder_test.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/slang/test/unit/builder/translation_model_builder_test.dart b/slang/test/unit/builder/translation_model_builder_test.dart index 95d9d5de..0d7e7d6c 100644 --- a/slang/test/unit/builder/translation_model_builder_test.dart +++ b/slang/test/unit/builder/translation_model_builder_test.dart @@ -343,6 +343,22 @@ void main() { }); group('Fallback', () { + test('base_locale_empty_string: Do not remove empty strings in base locale', + () { + final result = TranslationModelBuilder.build( + buildConfig: RawConfig.defaultConfig + .copyWith( + fallbackStrategy: FallbackStrategy.baseLocaleEmptyString, + ) + .toBuildModelConfig(), + locale: _locale, + map: {'hello': ''}, + ); + + expect(result.root.entries['hello'], isA()); + expect((result.root.entries['hello'] as StringTextNode).content, ''); + }); + test('Should fallback context type cases', () { final result = TranslationModelBuilder.build( buildConfig: RawConfig.defaultConfig From 81187d2717cdcc83f6b852fa34ec10290644980f Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sat, 7 Dec 2024 02:52:09 +0100 Subject: [PATCH 116/118] fix: analyzer issues --- .../generator/generate_translations.dart | 4 ++- .../src/builder/utils/string_extensions.dart | 35 ++++++++----------- slang/pubspec.yaml | 2 +- .../integration/main/compilation_test.dart | 1 + slang_build_runner/pubspec.yaml | 2 +- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/slang/lib/src/builder/generator/generate_translations.dart b/slang/lib/src/builder/generator/generate_translations.dart index c22a82f7..d96d058c 100644 --- a/slang/lib/src/builder/generator/generate_translations.dart +++ b/slang/lib/src/builder/generator/generate_translations.dart @@ -320,7 +320,9 @@ void _generateClass( if (!localeData.base || node.interface?.attributes .any((attribute) => attribute.attributeName == key) == - true) buffer.write('@override '); + true) { + buffer.write('@override '); + } // even if this attribute exist, it has to satisfy the same signature as // specified in the interface diff --git a/slang/lib/src/builder/utils/string_extensions.dart b/slang/lib/src/builder/utils/string_extensions.dart index 741118b4..c9a236f0 100644 --- a/slang/lib/src/builder/utils/string_extensions.dart +++ b/slang/lib/src/builder/utils/string_extensions.dart @@ -14,22 +14,15 @@ extension StringExtensions on String { /// transforms the string to the specified case /// if case is null, then no transformation will be applied String toCase(CaseStyle? style) { - switch (style) { - case CaseStyle.camel: - return getWords() - .mapIndexed((index, word) => - index == 0 ? word.toLowerCase() : word.capitalize()) - .join(''); - case CaseStyle.pascal: - return getWords().map((word) => word.capitalize()).join(''); - case CaseStyle.snake: - return getWords().map((word) => word.toLowerCase()).join('_'); - case null: - return this; - default: - print('Unknown case: $style'); - return this; - } + return switch (style) { + CaseStyle.camel => getWords() + .mapIndexed((index, word) => + index == 0 ? word.toLowerCase() : word.capitalize()) + .join(''), + CaseStyle.pascal => getWords().map((word) => word.capitalize()).join(''), + CaseStyle.snake => getWords().map((word) => word.toLowerCase()).join('_'), + null => this, + }; } /// de-DE will be interpreted as [de,DE] @@ -42,9 +35,9 @@ extension StringExtensions on String { /// assume that words are separated by special characters or by camel case List getWords() { final input = this; - final StringBuffer buffer = StringBuffer(); - final List words = []; - final bool isAllCaps = input.toUpperCase() == input; + final buffer = StringBuffer(); + final words = []; + final isAllCaps = input.toUpperCase() == input; for (int i = 0; i < input.length; i++) { final String currChar = input[i]; @@ -70,5 +63,5 @@ extension StringExtensions on String { } } -final RegExp _upperAlphaRegex = RegExp(r'[A-Z]'); -final Set _symbolSet = {' ', '.', '_', '-', '/', '\\'}; +final _upperAlphaRegex = RegExp(r'[A-Z]'); +final _symbolSet = {' ', '.', '_', '-', '/', '\\'}; diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index 1d2ee047..c2dbcba6 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -26,5 +26,5 @@ dependencies: dev_dependencies: expect_error: ^1.0.7 - lints: ^2.0.0 + lints: any test: ^1.21.0 diff --git a/slang/test/integration/main/compilation_test.dart b/slang/test/integration/main/compilation_test.dart index 9debf8af..c6378e6a 100644 --- a/slang/test/integration/main/compilation_test.dart +++ b/slang/test/integration/main/compilation_test.dart @@ -1,4 +1,5 @@ @Skip('not updated for multiple files') +library; import 'package:expect_error/expect_error.dart'; import 'package:test/test.dart'; diff --git a/slang_build_runner/pubspec.yaml b/slang_build_runner/pubspec.yaml index 86565a4f..e61720ac 100644 --- a/slang_build_runner/pubspec.yaml +++ b/slang_build_runner/pubspec.yaml @@ -15,4 +15,4 @@ dependencies: slang: '>=4.3.0 <4.4.0' dev_dependencies: - lints: ^2.0.0 + lints: any From 1c7c5df0801ef578d6650f61392102e42e3fbff6 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sat, 7 Dec 2024 03:41:34 +0100 Subject: [PATCH 117/118] test: enable compilation test --- slang/pubspec.yaml | 2 +- .../integration/main/compilation_test.dart | 30 +++++-------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index c2dbcba6..926f15a3 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -25,6 +25,6 @@ dependencies: yaml: ^3.1.0 dev_dependencies: - expect_error: ^1.0.7 + analyzer: '>=6.0.0' lints: any test: ^1.21.0 diff --git a/slang/test/integration/main/compilation_test.dart b/slang/test/integration/main/compilation_test.dart index c6378e6a..8d50cb64 100644 --- a/slang/test/integration/main/compilation_test.dart +++ b/slang/test/integration/main/compilation_test.dart @@ -1,28 +1,18 @@ -@Skip('not updated for multiple files') -library; - -import 'package:expect_error/expect_error.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:test/test.dart'; import '../../util/resources_utils.dart'; /// These tests ensure that the generated code compiles. +/// It currently only checks for syntax errors. void main() { - late Library library; - - setUp(() async { - // A workaround so we have Flutter available in the analyzer. - // See: https://pub.dev/packages/expect_error#flutter-support - library = await Library.custom( - packageName: 'slang_flutter', - path: 'not used', - packageRoot: '../slang_flutter', - ); - }); - - Future expectCompiles(String path) { + void expectCompiles(String path) { final output = loadResource(path); - return expectLater(library.withCode(output), compiles); + final result = parseString( + path: 'path.dart', + content: output, + ); + expect(result.errors, isEmpty); } test('fallback base locale', () { @@ -41,10 +31,6 @@ void main() { expectCompiles('main/_expected_rich_text.output'); }); - test('single output', () { - expectCompiles('main/_expected_single.output'); - }); - test('translation overrides', () { expectCompiles('main/_expected_translation_overrides.output'); }); From fd736df96fc467bb6145eed97454269ea255e00a Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Sun, 15 Dec 2024 00:53:21 +0100 Subject: [PATCH 118/118] release: 4.4.0 --- .fvmrc | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/publish_template.yml | 2 +- slang/lib/src/api/translation_overrides.dart | 2 +- .../src/builder/builder/translation_model_builder.dart | 8 ++++---- slang/lib/src/builder/decoder/base_decoder.dart | 4 ++-- .../lib/src/builder/generator/generate_translations.dart | 7 +------ slang/lib/src/builder/model/node.dart | 2 +- slang/lib/src/builder/utils/map_utils.dart | 2 +- slang/lib/src/builder/utils/regex_utils.dart | 2 +- slang/pubspec.yaml | 2 +- slang_build_runner/CHANGELOG.md | 4 ++++ slang_build_runner/pubspec.yaml | 4 ++-- slang_flutter/CHANGELOG.md | 4 ++++ slang_flutter/pubspec.yaml | 4 ++-- 15 files changed, 27 insertions(+), 24 deletions(-) diff --git a/.fvmrc b/.fvmrc index 679f8e11..34136bbd 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,4 +1,4 @@ { - "flutter": "3.24.5", + "flutter": "3.27.0", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf61c61f..57494ea6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ on: env: FLUTTER_VERSION_OLDEST: "3.19.6" - FLUTTER_VERSION_NEWEST: "3.24.5" + FLUTTER_VERSION_NEWEST: "3.27.0" jobs: format: diff --git a/.github/workflows/publish_template.yml b/.github/workflows/publish_template.yml index 5741679d..b19c66f0 100644 --- a/.github/workflows/publish_template.yml +++ b/.github/workflows/publish_template.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 with: - flutter-version: '3.24.5' + flutter-version: '3.27.0' channel: 'stable' - name: Dependencies (core) run: flutter pub get diff --git a/slang/lib/src/api/translation_overrides.dart b/slang/lib/src/api/translation_overrides.dart index 81eb86c4..f02c963d 100644 --- a/slang/lib/src/api/translation_overrides.dart +++ b/slang/lib/src/api/translation_overrides.dart @@ -157,7 +157,7 @@ extension TranslationOverridesStringExt on String { }); } - /// Replaces every ${_root.} with the real string + /// Replaces every `${_root.}` with the real string String applyLinks(TranslationMetadata meta, Map param) { return replaceDartNormalizedInterpolation(replacer: (match) { final nodeParam = match.substring(2, match.length - 1); diff --git a/slang/lib/src/builder/builder/translation_model_builder.dart b/slang/lib/src/builder/builder/translation_model_builder.dart index cb06c0fc..3b64fac0 100644 --- a/slang/lib/src/builder/builder/translation_model_builder.dart +++ b/slang/lib/src/builder/builder/translation_model_builder.dart @@ -32,8 +32,8 @@ class TranslationModelBuilder { /// Builds the i18n model for ONE locale /// - /// The [map] must be of type Map and all children may of type - /// String, num, List or Map. + /// The [map] must be of type `Map` and all children may of type + /// `String`, `num`, `List` or `Map`. /// /// If [baseData] is set and [BuildModelConfig.fallbackStrategy] is [FallbackStrategy.baseLocale], /// then the base translations will be added to contexts where the translation is missing. @@ -889,8 +889,8 @@ Set _parseAttributes(ObjectNode node) { /// Applies the generic type defined in the interface for all empty lists. /// -/// By default, empty lists are considered to be List -/// But when interfaces are used, it can differ: e.g. List +/// By default, empty lists are considered to be `List` +/// But when interfaces are used, it can differ: e.g. `List` void _fixEmptyLists({ required ObjectNode node, required Interface interface, diff --git a/slang/lib/src/builder/decoder/base_decoder.dart b/slang/lib/src/builder/decoder/base_decoder.dart index c7c52cef..444579b4 100644 --- a/slang/lib/src/builder/decoder/base_decoder.dart +++ b/slang/lib/src/builder/decoder/base_decoder.dart @@ -6,9 +6,9 @@ import 'package:slang/src/builder/model/enums.dart'; abstract class BaseDecoder { /// Transforms the raw string (json, yaml, csv) - /// to a standardized map structure of Map + /// to a standardized map structure of `Map` /// - /// Children are Map, List or String + /// Children are `Map`, `List` or `String` /// /// No case transformations, etc! Only the raw data represented as a tree. Map decode(String raw); diff --git a/slang/lib/src/builder/generator/generate_translations.dart b/slang/lib/src/builder/generator/generate_translations.dart index d96d058c..8c93435f 100644 --- a/slang/lib/src/builder/generator/generate_translations.dart +++ b/slang/lib/src/builder/generator/generate_translations.dart @@ -608,12 +608,7 @@ void _generateList({ depth: depth + 1, ); } else if (value is ObjectNode) { - // ignore: prefer_interpolation_to_compose_strings - final String key = r'$' '${listName ?? ''}\$' + - depth.toString() + - 'i' + - i.toString() + - r'$'; + final key = '\$${listName ?? ''}\$${depth.toString()}i${i.toString()}\$'; final String childClassNoLocale = getClassName( base: base, visibility: config.translationClassVisibility, diff --git a/slang/lib/src/builder/model/node.dart b/slang/lib/src/builder/model/node.dart index 38903afc..d3e0b01d 100644 --- a/slang/lib/src/builder/model/node.dart +++ b/slang/lib/src/builder/model/node.dart @@ -88,7 +88,7 @@ abstract interface class LeafNode {} /// The super class for [ListNode] and [ObjectNode] abstract class IterableNode extends Node { - /// The generic type of the container, i.e. Map or List + /// The generic type of the container, i.e. `Map` or `List` String _genericType; String get genericType => _genericType; diff --git a/slang/lib/src/builder/utils/map_utils.dart b/slang/lib/src/builder/utils/map_utils.dart index d90a48ec..f10954d7 100644 --- a/slang/lib/src/builder/utils/map_utils.dart +++ b/slang/lib/src/builder/utils/map_utils.dart @@ -2,7 +2,7 @@ import 'package:collection/collection.dart'; import 'package:slang/src/builder/utils/node_utils.dart'; class MapUtils { - /// converts Map to Map for all children + /// converts `Map` to `Map` for all children /// forcing all keys to be strings static Map deepCast(Map source) { return source.map((key, value) { diff --git a/slang/lib/src/builder/utils/regex_utils.dart b/slang/lib/src/builder/utils/regex_utils.dart index 335239f6..691ef096 100644 --- a/slang/lib/src/builder/utils/regex_utils.dart +++ b/slang/lib/src/builder/utils/regex_utils.dart @@ -50,7 +50,7 @@ class RegexUtils { RegExp(r'^((\w|\<|\>|,)+)(\?)? (\w+)(\(.+\))?$'); /// Matches the generic of the list - /// List + /// `List` /// 1 - MyGeneric static final RegExp genericRegex = RegExp(r'^List<((?:\w| |<|>)+)>$'); diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index 926f15a3..753b4749 100644 --- a/slang/pubspec.yaml +++ b/slang/pubspec.yaml @@ -1,6 +1,6 @@ name: slang description: Localization / Internationalization (i18n) solution. Use JSON, YAML, CSV, or ARB files to create typesafe translations via source generation. -version: 4.3.0 +version: 4.4.0 repository: https://github.com/slang-i18n/slang topics: - i18n diff --git a/slang_build_runner/CHANGELOG.md b/slang_build_runner/CHANGELOG.md index 869ece02..cf5f1b25 100644 --- a/slang_build_runner/CHANGELOG.md +++ b/slang_build_runner/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.4.0 + +- bump `slang` to `4.4.0` + ## 4.3.0 - bump `slang` to `4.3.0` diff --git a/slang_build_runner/pubspec.yaml b/slang_build_runner/pubspec.yaml index e61720ac..4fee804e 100644 --- a/slang_build_runner/pubspec.yaml +++ b/slang_build_runner/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_build_runner description: build_runner integration for slang. This library ensures that slang is recognized by build_runner. -version: 4.3.0 +version: 4.4.0 repository: https://github.com/slang-i18n/slang environment: @@ -12,7 +12,7 @@ dependencies: glob: ^2.0.2 # Use a tight version to ensure that all features are available - slang: '>=4.3.0 <4.4.0' + slang: '>=4.4.0 <4.5.0' dev_dependencies: lints: any diff --git a/slang_flutter/CHANGELOG.md b/slang_flutter/CHANGELOG.md index bebf01a9..adf8ba85 100644 --- a/slang_flutter/CHANGELOG.md +++ b/slang_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.4.0 + +- bump `slang` to `4.4.0` + ## 4.3.0 - bump `slang` to `4.3.0` diff --git a/slang_flutter/pubspec.yaml b/slang_flutter/pubspec.yaml index 7eaa471e..cc1e5be9 100644 --- a/slang_flutter/pubspec.yaml +++ b/slang_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: slang_flutter description: Flutter support for slang. This library provides helpful Flutter API. -version: 4.3.0 +version: 4.4.0 repository: https://github.com/slang-i18n/slang environment: @@ -14,7 +14,7 @@ dependencies: sdk: flutter # Use a tight version to ensure that all features are available - slang: '>=4.3.0 <4.4.0' + slang: '>=4.4.0 <4.5.0' dev_dependencies: flutter_test: