diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json deleted file mode 100644 index b35b02a4..00000000 --- a/.fvm/fvm_config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "flutterSdkVersion": "3.16.5", - "flavors": {} -} \ No newline at end of file diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..34136bbd --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.27.0", + "flavors": {} +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0e603ad..57494ea6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,8 +12,8 @@ on: branches: [ main ] env: - FLUTTER_VERSION_OLDEST: "3.0.5" - FLUTTER_VERSION_NEWEST: "3.16.5" + FLUTTER_VERSION_OLDEST: "3.19.6" + FLUTTER_VERSION_NEWEST: "3.27.0" jobs: format: @@ -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/.github/workflows/publish_template.yml b/.github/workflows/publish_template.yml index 4bc3e6ee..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.7.12' + flutter-version: '3.27.0' channel: 'stable' - name: Dependencies (core) run: flutter pub get diff --git a/.gitignore b/.gitignore index 95d05dc0..c4c8e0ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -.fvm/flutter_sdk - # 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/ 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/CHANGELOG.md b/slang/CHANGELOG.md index fc371e73..4b9dd623 100644 --- a/slang/CHANGELOG.md +++ b/slang/CHANGELOG.md @@ -1,3 +1,102 @@ +## 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 + +- 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 + +## 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 + +- 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 + +**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 (#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 +- **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 +- 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 + +## 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 +- 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 + +- 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) + +## 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 +- 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/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/MIGRATION.md b/slang/MIGRATION.md index 16748263..73d47a40 100644 --- a/slang/MIGRATION.md +++ b/slang/MIGRATION.md @@ -1,5 +1,77 @@ # 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 + +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). diff --git a/slang/README.md b/slang/README.md index ba2d8380..83c1b6dd 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. @@ -30,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: @@ -45,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 ``` @@ -65,6 +65,8 @@ 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) + - [L10n](#-l10n) - [Interfaces](#-interfaces) - [Modifiers](#-modifiers) - [Locale Enum](#-locale-enum) @@ -73,13 +75,15 @@ 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) + - [Sanitization](#-sanitization) - [Obfuscation](#-obfuscation) + - [Formatting](#-formatting) - [Dart Only](#-dart-only) - [Tools](#tools) - [Main Command](#-main-command) @@ -87,6 +91,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) @@ -101,12 +106,15 @@ 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 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). @@ -125,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", @@ -154,7 +160,7 @@ lib/ ``` ```json5 -// File: strings_de.i18n.json +// File: de.i18n.json { "hello": "Hallo $name", "save": "Speichern", @@ -167,19 +173,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 +251,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 +268,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 @@ -285,7 +291,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 @@ -296,6 +302,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 @@ -313,12 +323,7 @@ pluralization: ordinal: - someKey.place contexts: - gender_context: - enum: - - male - - female - paths: - - my.path.to.greet + GenderContext: default_parameter: gender generate_enum: true interfaces: @@ -333,6 +338,9 @@ interfaces: obfuscation: enabled: false secret: somekey +format: + enabled: true + width: 150 imports: - 'package:my_package/path_to_enum.dart' ``` @@ -356,7 +364,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 @@ -367,6 +375,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 @@ -384,12 +396,7 @@ targets: ordinal: - someKey.place contexts: - gender_context: - enum: - - male - - female - paths: - - my.path.to.greet + GenderContext: default_parameter: gender generate_enum: true interfaces: @@ -404,6 +411,9 @@ targets: obfuscation: enabled: false secret: somekey + format: + enabled: true + width: 150 imports: - 'package:my_package/path_to_enum.dart' ``` @@ -418,7 +428,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` | @@ -429,6 +439,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` | @@ -439,13 +452,13 @@ 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` | | `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 @@ -510,8 +523,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" + } } } } @@ -705,11 +721,22 @@ 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 by surrounding the path with `{}`: + +```json +{ + "fields": { + "name": "my name is {firstName}" + }, + "introduce": "Hello, @:{fields.name}inator" +} +``` + ### ➤ 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). +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`. @@ -885,6 +912,93 @@ 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" +} +``` + +### ➤ 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')}", + "number": "The number is {number: NumberFormat('###,###.##')}" +} +``` + +Or adjust built-in formats: + +```json +{ + "price": "It costs {price: currency(symbol: 'EUR')}" +} +``` + +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. @@ -974,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 | @@ -1104,14 +1219,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 ``` @@ -1147,32 +1262,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: @@ -1212,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 @@ -1234,7 +1331,31 @@ 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 + +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 @@ -1330,6 +1451,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. @@ -1365,10 +1520,29 @@ 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. +```yaml +# pubspec.yaml +dependencies: + slang: +``` + ```yaml # Config flutter_integration: false # set this @@ -1469,6 +1643,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? @@ -1861,6 +2048,8 @@ 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) +- [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) @@ -1876,13 +2065,21 @@ 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) +- [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) @@ -1896,7 +2093,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/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/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/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..867993b0 100644 --- a/slang/bin/slang.dart +++ b/slang/bin/slang.dart @@ -1,20 +1,20 @@ 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/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'; 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:slang/builder/utils/file_utils.dart'; -import 'package:slang/builder/utils/path_utils.dart'; +import 'package:slang/src/runner/utils/format.dart'; import 'package:watcher/watcher.dart'; /// Determines what the runner will do @@ -29,6 +29,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 +69,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 +109,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 +173,12 @@ void main(List arguments) async { arguments: arguments, ); break; + case RunnerMode.normalize: + await runNormalize( + fileCollection: fileCollection, + arguments: arguments, + ); + break; } } @@ -302,79 +316,65 @@ 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, + )}'); } + } - 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(); + } } - - 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'); + 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'); + } } // returns current time in HH:mm:ss @@ -388,11 +388,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/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/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 @@ UIViewControllerBasedStatusBarAppearance + + + CFBundleLocalizations + + en + de + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/slang/example/lib/i18n/strings_de.i18n.json b/slang/example/lib/i18n/de.i18n.json similarity index 83% rename from slang/example/lib/i18n/strings_de.i18n.json rename to slang/example/lib/i18n/de.i18n.json index 2572eb71..5ee4fb6d 100644 --- a/slang/example/lib/i18n/strings_de.i18n.json +++ b/slang/example/lib/i18n/de.i18n.json @@ -9,6 +9,7 @@ }, "locales(map)": { "en": "Englisch", - "de": "Deutsch" + "de": "Deutsch", + "fr-FR": "Französisch" } } \ No newline at end of file diff --git a/slang/example/lib/i18n/strings.i18n.json b/slang/example/lib/i18n/en.i18n.json similarity index 84% rename from slang/example/lib/i18n/strings.i18n.json rename to slang/example/lib/i18n/en.i18n.json index c754733a..814873b0 100644 --- a/slang/example/lib/i18n/strings.i18n.json +++ b/slang/example/lib/i18n/en.i18n.json @@ -9,6 +9,7 @@ }, "locales(map)": { "en": "English", - "de": "German" + "de": "German", + "fr-FR": "French" } } \ No newline at end of file diff --git a/slang/example/lib/i18n/fr_FR.i18n.json b/slang/example/lib/i18n/fr_FR.i18n.json new file mode 100644 index 00000000..cd6db2ee --- /dev/null +++ b/slang/example/lib/i18n/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-FR": "Français" + } +} \ No newline at end of file diff --git a/slang/example/lib/i18n/strings.g.dart b/slang/example/lib/i18n/strings.g.dart index 93be7c6c..3e4d1220 100644 --- a/slang/example/lib/i18n/strings.g.dart +++ b/slang/example/lib/i18n/strings.g.dart @@ -1,42 +1,107 @@ /// Generated file. Do not edit. /// -/// Original: lib/i18n +/// Source: 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-21 at 12:02 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:intl/intl.dart'; +import 'package:slang/generated.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 l_de; +import 'strings_fr_FR.g.dart' deferred as l_fr_FR; +part 'strings_en.g.dart'; -/// Supported locales, see extension methods below. +/// Supported locales. /// /// 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: _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 l_de.loadLibrary(); + return l_de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.frFr: + await l_fr_FR.loadLibrary(); + return l_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 l_de.TranslationsDe( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.frFr: + return l_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 @@ -82,19 +147,31 @@ 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._(); // 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 +181,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 +195,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_de.g.dart b/slang/example/lib/i18n/strings_de.g.dart new file mode 100644 index 00000000..06b2746f --- /dev/null +++ b/slang/example/lib/i18n/strings_de.g.dart @@ -0,0 +1,77 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/generated.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-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-FR': return 'Französisch'; + default: return null; + } + } +} + 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..90738ae7 --- /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-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-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..c46d02a7 --- /dev/null +++ b/slang/example/lib/i18n/strings_fr_FR.g.dart @@ -0,0 +1,77 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/generated.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-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-FR': return 'Français'; + default: return null; + } + } +} + 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..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: @@ -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/builder/translation_model_list_builder.dart b/slang/lib/builder/builder/translation_model_list_builder.dart deleted file mode 100644 index b3d826f0..00000000 --- a/slang/lib/builder/builder/translation_model_list_builder.dart +++ /dev/null @@ -1,38 +0,0 @@ -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'; - -class TranslationModelListBuilder { - /// Combine all namespaces and build the internal model - /// The returned locales are sorted (base locale first) - /// - /// After this method call, information about the namespace is lost. - /// It will be just a normal parent. - static List build( - RawConfig rawConfig, - TranslationMap translationMap, - ) { - final buildConfig = rawConfig.toBuildModelConfig(); - - 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, - ); - - 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/decoder/arb_decoder.dart b/slang/lib/builder/decoder/arb_decoder.dart deleted file mode 100644 index cfb5dedf..00000000 --- a/slang/lib/builder/decoder/arb_decoder.dart +++ /dev/null @@ -1,162 +0,0 @@ -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'; - -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; - } - - 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, - ); - } - }); - - return resultMap; - } -} - -void _addEntry({ - required final String key, - required final String value, - required final Map resultMap, -}) { - List brackets = BracketsUtils.findTopLevelBrackets(value); - if (brackets.length == 1 && - brackets.first.start == 0 && - brackets.first.end == value.length - 1) { - // potential single complex node - final singleComplexMatch = RegexUtils.arbComplexNode.firstMatch(value); - if (singleComplexMatch != null) { - // this is a plural or a context node - // add additional nodes to this base path - - final variable = singleComplexMatch.group(1)!.trim(); - final type = singleComplexMatch.group(2)!.trim(); - final content = singleComplexMatch.group(3)!; - final isPlural = type == 'plural'; - for (final part in RegexUtils.arbComplexNodeContent.allMatches(content)) { - final partName = - isPlural ? _digestPluralKey(part.group(1)!) : part.group(1)!; - final partContent = part.group(2)!; - MapUtils.addItemToMap( - map: resultMap, - destinationPath: - '$key(${isPlural ? 'plural' : 'context=${variable.toCase(CaseStyle.pascal)}'}, param=$variable).$partName', - item: _digestLeafText(partContent), - ); - } - return; - } - } - - final nameFactory = _DistinctNameFactory(); - String result = value; - while (brackets.isNotEmpty) { - final currentBracket = brackets.first; - - final match = - RegexUtils.arbComplexNode.firstMatch(currentBracket.substring()); - - if (match == null) { - // invalid complex, continue to next bracket without any changes - // Likely just a placeholder for a variable - brackets.removeAt(0); - continue; - } - - // add linked complex expression - final originalParameter = match.group(1)!.trim(); - final parameter = nameFactory.getNewName(originalParameter); - - // create new key - _addEntry( - key: '${key}__$parameter', - value: match.group(0)!, - resultMap: resultMap, - ); - - // update string and refer to new key - result = currentBracket.replaceWith('@:${key}__$parameter'); - - // re-run because indices changed (old bracket list is invalid now) - brackets = BracketsUtils.findTopLevelBrackets(result); - } - - resultMap[key] = _digestLeafText(result); -} - -/// Transforms arguments to camel case -/// Adds 'arg' to every positional argument -String _digestLeafText(String text) { - return text.replaceBracesInterpolation(replacer: (match) { - final param = match.substring(1, match.length - 1); - final number = int.tryParse(param); - if (number != null) { - return '{arg$number}'; - } else { - return '{${param.toCase(CaseStyle.camel)}}'; - } - }); -} - -/// ARB files use '=0', '=1', and '=2' for 'zero', 'one', and 'two' -/// We need to normalize that. -String _digestPluralKey(String key) { - switch (key) { - case '=0': - return 'zero'; - case '=1': - return 'one'; - case '=2': - return 'two'; - default: - return key; - } -} - -class _DistinctNameFactory { - final existingNames = {}; - - /// Gets a name which is distinct from the previous ones - /// If [raw] already exists, then a number will be appended - /// - /// E.g. - /// apple, banana, apple2, apple3, banana2, ... - String getNewName(String raw) { - int number = 1; - String result = raw; - while (existingNames.contains(result)) { - number++; - result = raw + number.toString(); - } - existingNames.add(result); - return result; - } -} diff --git a/slang/lib/builder/generator/generator.dart b/slang/lib/builder/generator/generator.dart deleted file mode 100644 index 423ffe11..00000000 --- a/slang/lib/builder/generator/generator.dart +++ /dev/null @@ -1,31 +0,0 @@ -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'; - -class Generator { - /// main generate function - /// returns a string representing the content of the .g.dart file - static BuildResult generate({ - 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, - ); - } -} 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/builder/model/enums.dart b/slang/lib/builder/model/enums.dart deleted file mode 100644 index bf4e17eb..00000000 --- a/slang/lib/builder/model/enums.dart +++ /dev/null @@ -1,106 +0,0 @@ -enum FileType { json, yaml, csv, arb } - -enum FallbackStrategy { none, baseLocale, baseLocaleEmptyString } - -/// Similar to [FallbackStrategy] but [FallbackStrategy.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 } - -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) { - case FallbackStrategy.none: - return GenerateFallbackStrategy.none; - case FallbackStrategy.baseLocale: - return GenerateFallbackStrategy.baseLocale; - case FallbackStrategy.baseLocaleEmptyString: - return GenerateFallbackStrategy.baseLocale; - } - } -} diff --git a/slang/lib/builder/utils/file_utils.dart b/slang/lib/builder/utils/file_utils.dart deleted file mode 100644 index 2a3fa5cb..00000000 --- a/slang/lib/builder/utils/file_utils.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:json2yaml/json2yaml.dart'; -import 'package:slang/builder/model/enums.dart'; - -const String INFO_KEY = '@@info'; - -class FileUtils { - static void writeFile({required String path, required String content}) { - File(path).writeAsStringSync(content); - } - - static void writeFileOfType({ - required FileType fileType, - required String path, - required Map content, - }) { - FileUtils.writeFile( - path: path, - content: FileUtils.encodeContent( - fileType: fileType, - content: content, - ), - ); - } - - static String encodeContent({ - required FileType fileType, - required Map content, - }) { - switch (fileType) { - case FileType.json: - // this encoder does not append \n automatically - return JsonEncoder.withIndent(' ').convert(content) + '\n'; - case FileType.yaml: - if (content.containsKey(INFO_KEY)) { - // workaround - // https://github.com/alexei-sintotski/json2yaml/issues/23 - content = { - '"$INFO_KEY"': content[INFO_KEY], - ...content..remove(INFO_KEY), - }; - } - return json2yaml(content, yamlStyle: YamlStyle.generic); - case FileType.csv: - throw UnimplementedError('CSV is not supported yet'); - case FileType.arb: - // this encoder does not append \n automatically - return JsonEncoder.withIndent(' ').convert(content) + '\n'; - } - } - - static void createMissingFolders({required String filePath}) { - final index = filePath - .replaceAll('/', Platform.pathSeparator) - .replaceAll('\\', Platform.pathSeparator) - .lastIndexOf(Platform.pathSeparator); - if (index == -1) { - return; - } - - final directoryPath = filePath.substring(0, index); - Directory(directoryPath).createSync(recursive: true); - } -} diff --git a/slang/lib/generated.dart b/slang/lib/generated.dart new file mode 100644 index 00000000..0dd6a9a4 --- /dev/null +++ b/slang/lib/generated.dart @@ -0,0 +1,2 @@ +export 'package:slang/src/api/formatter.dart'; +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..7ca8bc3d --- /dev/null +++ b/slang/lib/overrides.dart @@ -0,0 +1,5 @@ +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/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/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/api/locale.dart b/slang/lib/src/api/locale.dart similarity index 62% rename from slang/lib/api/locale.dart rename to slang/lib/src/api/locale.dart index dcda6d41..a774510a 100644 --- a/slang/lib/api/locale.dart +++ b/slang/lib/src/api/locale.dart @@ -1,5 +1,6 @@ -import 'package:slang/api/pluralization.dart'; -import 'package:slang/builder/model/node.dart'; +import 'package:slang/src/api/formatter.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 @@ -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, }); @@ -50,19 +53,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,17 +69,39 @@ 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'); + /// 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 && @@ -107,24 +123,44 @@ class FakeAppLocale extends BaseAppLocale { @override final String? countryCode; + final Map? types; + FakeAppLocale({ required this.languageCode, this.scriptCode, this.countryCode, + this.types, }); @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 => + buildSync( + 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, + types: types, + ); } } @@ -135,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/api/plural_resolver_map.dart b/slang/lib/src/api/plural_resolver_map.dart similarity index 85% rename from slang/lib/api/plural_resolver_map.dart rename to slang/lib/src/api/plural_resolver_map.dart index 08c87fb7..8a364d93 100644 --- a/slang/lib/api/plural_resolver_map.dart +++ b/slang/lib/src/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}) { 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 52% rename from slang/lib/api/singleton.dart rename to slang/lib/src/api/singleton.dart index 68f7daa2..829d8779 100644 --- a/slang/lib/api/singleton.dart +++ b/slang/lib/src/api/singleton.dart @@ -1,14 +1,15 @@ 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/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/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/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'; /// Provides utility functions without any side effects. abstract class BaseAppLocaleUtils, @@ -89,25 +90,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. @@ -116,14 +120,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), @@ -133,7 +154,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, @@ -159,28 +223,34 @@ extension AppLocaleUtilsExt, digestedMap = MapUtils.deepCast(map); } - final buildResult = TranslationModelBuilder.build( + return TranslationModelBuilder.build( buildConfig: buildConfig!, map: digestedMap, handleLinks: false, + handleTypes: false, shouldEscapeText: false, - localeDebug: locale.languageTag, - ); - - return locale.build( - overrides: buildResult.root.toFlatMap(), - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, + locale: I18nLocale( + language: locale.languageCode, + script: locale.scriptCode, + country: locale.countryCode, + ), ); } } 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; @@ -190,7 +260,14 @@ abstract class BaseLocaleSettings, BaseLocaleSettings({ required this.utils, - }) : this.translationMap = _buildMap(utils.locales); + 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. @@ -203,12 +280,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, @@ -230,11 +365,6 @@ extension LocaleSettingsExt, }); } - /// Gets current translations - T get currentTranslations { - return translationMap[currentLocale]!; - } - /// Gets supported locales in string format. List get supportedLocalesRaw { return utils.supportedLocalesRaw; @@ -246,7 +376,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) { @@ -262,37 +403,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 await setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + } + + /// Sync version of [setLocaleRaw]. + E setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) { final E locale = utils.parse(rawLocale); - return setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + return setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale); } /// Sets plural resolvers. - /// See https://unicode-org.github.io/cldr-staging/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://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html + /// 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. - 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, @@ -301,6 +470,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. @@ -312,13 +496,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, @@ -339,13 +542,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, @@ -356,12 +559,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 72% rename from slang/lib/api/translation_overrides.dart rename to slang/lib/src/api/translation_overrides.dart index a3f44152..f02c963d 100644 --- a/slang/lib/api/translation_overrides.dart +++ b/slang/lib/src/api/translation_overrides.dart @@ -1,10 +1,13 @@ -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/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'; +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'; /// Utility class handling overridden translations class TranslationOverrides { @@ -118,18 +121,43 @@ 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(); }); } - /// 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); @@ -152,11 +180,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, }, ); } @@ -168,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/builder/builder/build_model_config_builder.dart b/slang/lib/src/builder/builder/build_model_config_builder.dart similarity index 76% 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..04bd69a4 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() { @@ -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/builder/builder/generate_config_builder.dart b/slang/lib/src/builder/builder/generate_config_builder.dart similarity index 59% rename from slang/lib/builder/builder/generate_config_builder.dart rename to slang/lib/src/builder/builder/generate_config_builder.dart index a8e54f8a..071ca667 100644 --- a/slang/lib/builder/builder/generate_config_builder.dart +++ b/slang/lib/src/builder/builder/generate_config_builder.dart @@ -1,25 +1,24 @@ -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/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, + required List contexts, required List interfaces, }) { return GenerateConfig( buildConfig: config.toBuildModelConfig(), inputDirectoryHint: inputDirectoryHint, - baseName: baseName, baseLocale: config.baseLocale, fallbackStrategy: config.fallbackStrategy.toGenerateFallbackStrategy(), - outputFormat: config.outputFormat, + outputFileName: config.outputFileName, + lazy: config.lazy, localeHandling: config.localeHandling, flutterIntegration: config.flutterIntegration, translateVariable: config.translateVar, @@ -30,13 +29,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/raw_config_builder.dart b/slang/lib/src/builder/builder/raw_config_builder.dart similarity index 66% rename from slang/lib/builder/builder/raw_config_builder.dart rename to slang/lib/src/builder/builder/raw_config_builder.dart index 8952f5cd..be3c65ca 100644 --- a/slang/lib/builder/builder/raw_config_builder.dart +++ b/slang/lib/src/builder/builder/raw_config_builder.dart @@ -1,12 +1,14 @@ -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/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/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'; +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'; import 'package:yaml/yaml.dart'; class RawConfigBuilder { @@ -47,6 +49,15 @@ 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.', + ); + } + + final keyCase = + (map['key_case'] as String?)?.toCaseStyle() ?? RawConfig.defaultKeyCase; + return RawConfig( baseLocale: I18nLocale.fromString( map['base_locale'] ?? RawConfig.defaultBaseLocale), @@ -63,8 +74,7 @@ class RawConfigBuilder { RawConfig.defaultOutputDirectory, outputFileName: map['output_file_name'] ?? RawConfig.defaultOutputFileName, - outputFormat: (map['output_format'] as String?)?.toOutputFormat() ?? - RawConfig.defaultOutputFormat, + lazy: map['lazy'] ?? RawConfig.defaultLazy, localeHandling: map['locale_handling'] ?? RawConfig.defaultLocaleHandling, flutterIntegration: map['flutter_integration'] ?? RawConfig.defaultFlutterIntegration, @@ -76,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, @@ -108,6 +125,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, ); @@ -117,18 +136,12 @@ class RawConfigBuilder { extension on Map { /// Parses the 'contexts' config List toContextTypes() { - return this.entries.map((e) { - final enumName = e.key.toCase(CaseStyle.pascal); + return entries.map((e) { + 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: @@ -139,7 +152,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 +170,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 +203,7 @@ extension on Map { ); attributes.add(parsedAttribute); - }); + } // parse paths final pathsConfig = interfaceConfig['paths'] as List? ?? []; @@ -219,10 +232,94 @@ extension on Map { secret: this['secret'], ); } + + /// Parses the 'format' config + FormatConfig toFormatConfig() { + return FormatConfig( + enabled: this['enabled'], + 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 { String removeTrailingSlash() { - return this.endsWith('/') ? this.substring(0, this.length - 1) : this; + 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; + } + } + + 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/builder/slang_file_collection_builder.dart b/slang/lib/src/builder/builder/slang_file_collection_builder.dart similarity index 61% rename from slang/lib/builder/builder/slang_file_collection_builder.dart rename to slang/lib/src/builder/builder/slang_file_collection_builder.dart index 48632327..bd6622ee 100644 --- a/slang/lib/builder/builder/slang_file_collection_builder.dart +++ b/slang/lib/src/builder/builder/slang_file_collection_builder.dart @@ -2,12 +2,13 @@ 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/builder/utils/path_utils.dart'; -import 'package:slang/builder/utils/regex_utils.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'; +import 'package:slang/src/builder/utils/path_utils.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; class SlangFileCollectionBuilder { static SlangFileCollection readFromFileSystem({ @@ -46,6 +47,7 @@ class SlangFileCollectionBuilder { '.flutter.git', '.dart_tool', '.symlinks', + 'cargokit_build', }, ); } @@ -70,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); @@ -94,52 +98,103 @@ 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 = null; + I18nLocale? directoryLocale; if (config.namespaces) { directoryLocale = PathUtils.findDirectoryLocale( 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; }) - .whereNotNull() - .toList(), + .nonNulls + .sortedBy((file) => '${file.locale}-${file.namespace}'), ); } } @@ -240,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/builder/text/l10n_override_parser.dart b/slang/lib/src/builder/builder/text/l10n_override_parser.dart new file mode 100644 index 00000000..14d78985 --- /dev/null +++ b/slang/lib/src/builder/builder/text/l10n_override_parser.dart @@ -0,0 +1,147 @@ +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; + 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, [], { + #locale: locale, + ...arguments, + }); + + return formatter.format(value); + } else { + // positional arguments + 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, + '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 has2Arguments = positionalWith2Arguments.contains(parsed.methodName); + final formatter = Function.apply( + formatterBuilder, + [ + ...arguments, + if ((has2Arguments && arguments.length < 2) || + (!has2Arguments && arguments.isEmpty)) + locale, + ], + ); + return formatter.format(value); + } +} + +Map parseArguments(String arguments) { + final result = {}; + final parts = arguments.splitParameters(); + 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 new file mode 100644 index 00000000..502aa4e4 --- /dev/null +++ b/slang/lib/src/builder/builder/text/l10n_parser.dart @@ -0,0 +1,166 @@ +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 { + /// 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.compact', + 'NumberFormat.compactCurrency', + 'NumberFormat.compactSimpleCurrency', + 'NumberFormat.compactLong', + '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', +}; + +const positionalWith2Arguments = { + 'DateFormat', + 'NumberFormat', +}; + +final _dateFormatsWithClass = { + for (final format in _dateFormats) 'DateFormat.$format', + 'DateFormat', +}; + +class L10nIntermediateResult { + final String paramType; + final String methodName; + final String? arguments; + + L10nIntermediateResult({ + required this.paramType, + required this.methodName, + required this.arguments, + }); +} + +L10nIntermediateResult? parseL10nIntermediate(String type) { + final parsed = RegexUtils.formatTypeRegex.firstMatch(type); + if (parsed == null) { + return null; + } + + String methodName = parsed.group(1)!; + final arguments = parsed.group(2); + + final String paramType; + if (numberFormats.contains(methodName) || + numberFormatsWithClass.contains(methodName)) { + paramType = 'num'; + } else if (_dateFormats.contains(methodName) || + _dateFormatsWithClass.contains(methodName)) { + paramType = 'DateTime'; + } else { + return null; + } + + // 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'; + } + } + + 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 + String arguments = parsed.arguments ?? ''; + if (parsed.paramType == 'num' && + numberFormatsWithNamedParameters.contains(parsed.methodName)) { + // add locale as named parameter + if (!arguments.contains('locale')) { + if (parsed.arguments == null) { + arguments = "locale: '${locale.underscoreTag}'"; + } else { + arguments = "$arguments, locale: '${locale.underscoreTag}'"; + } + } + } else { + // add locale as positional parameter + 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 { + arguments = "$arguments, '${locale.underscoreTag}'"; + } + } + } + + return ParseL10nResult( + 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 new file mode 100644 index 00000000..bcd7d8f5 --- /dev/null +++ b/slang/lib/src/builder/builder/text/param_parser.dart @@ -0,0 +1,60 @@ +import 'package:slang/src/builder/model/enums.dart'; +import 'package:slang/src/builder/utils/string_extensions.dart'; + +class ParseParamResult { + final String paramName; + final String paramType; + + const ParseParamResult(this.paramName, this.paramType); + + @override + String toString() => + '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, +}) { + final colonIndex = rawParam.indexOf(':'); + if (colonIndex == -1) { + return ParseParamResult(rawParam.toCase(caseStyle), defaultType); + } + return ParseParamResult( + rawParam.substring(0, colonIndex).trim().toCase(caseStyle), + rawParam.substring(colonIndex + 1).trim(), + ); +} + +class ParamWithArg { + final String paramName; + final String? arg; + + const ParamWithArg(this.paramName, this.arg); + + @override + String toString() => 'ParamWithArg(paramName: $paramName, arg: $arg)'; +} + +ParamWithArg parseParamWithArg({ + required String rawParam, + required CaseStyle? paramCase, +}) { + 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 end = rawParam.lastIndexOf(')'); + final parameterName = rawParam.substring(0, start).toCase(paramCase); + return ParamWithArg(parameterName, rawParam.substring(start + 1, end)); +} diff --git a/slang/lib/builder/builder/translation_map_builder.dart b/slang/lib/src/builder/builder/translation_map_builder.dart similarity index 87% rename from slang/lib/builder/builder/translation_map_builder.dart rename to slang/lib/src/builder/builder/translation_map_builder.dart index 353704d9..6e43e9ad 100644 --- a/slang/lib/builder/builder/translation_map_builder.dart +++ b/slang/lib/src/builder/builder/translation_map_builder.dart @@ -1,15 +1,15 @@ -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'; +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]. /// 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/builder/translation_model_builder.dart b/slang/lib/src/builder/builder/translation_model_builder.dart similarity index 67% rename from slang/lib/builder/builder/translation_model_builder.dart rename to slang/lib/src/builder/builder/translation_model_builder.dart index 5f053624..3b64fac0 100644 --- a/slang/lib/builder/builder/translation_model_builder.dart +++ b/slang/lib/src/builder/builder/translation_model_builder.dart @@ -1,56 +1,109 @@ 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/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/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'; +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'; +import 'package:slang/src/builder/utils/node_utils.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; +import 'package:slang/src/builder/utils/reserved_keyword_sanitizer.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 + final Map types; // detected types, values are rendered as is BuildModelResult({ required this.root, required this.interfaces, required this.contexts, + required this.types, }); } class TranslationModelBuilder { + 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. /// /// [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 /// 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". static BuildModelResult build({ + required I18nLocale locale, required BuildModelConfig buildConfig, required Map map, + BuildModelResult? baseData, bool handleLinks = true, + bool handleTypes = true, bool shouldEscapeText = true, - required String localeDebug, }) { // 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, + for (final context in buildConfig.contexts) + 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: @@ -58,7 +111,8 @@ 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, + types: types, parentPath: '', parentRawPath: '', curr: map, @@ -66,7 +120,11 @@ class TranslationModelBuilder { keyCase: buildConfig.keyCase, leavesMap: leavesMap, contextCollection: contextCollection, + baseData: baseData, + baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, + handleTypes: handleTypes, + sanitizeKey: true, ); // 2nd iteration: Handle parameterized linked translations @@ -81,7 +139,7 @@ class TranslationModelBuilder { final linkParamMap = >{}; final paramTypeMap = {}; - value.links.forEach((link) { + for (final link in value.links) { final paramSet = {}; final visitedLinks = {}; final pathQueue = Queue(); @@ -91,7 +149,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); @@ -101,63 +159,57 @@ 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 : (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 - 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 @@ -190,8 +242,18 @@ 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(), + types: { + for (final entry in types.entries) + entry.key: entry.value.implementation, + }, ); } } @@ -199,15 +261,20 @@ 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 Map types, required String parentPath, required String parentRawPath, required Map curr, required BuildModelConfig config, required CaseStyle? keyCase, required Map leavesMap, - required Map contextCollection, + required Map contextCollection, + required BuildModelResult? baseData, + required Map? baseContexts, required bool shouldEscapeText, + required bool handleTypes, + required bool sanitizeKey, }) { final Map resultNodeTree = {}; @@ -220,7 +287,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 = @@ -231,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; @@ -242,9 +316,12 @@ Map _parseMapNode({ path: currPath, rawPath: currRawPath, modifiers: modifiers, + locale: locale, + types: types, raw: value.toString(), comment: comment, shouldEscape: shouldEscapeText, + handleTypes: handleTypes, interpolation: config.stringInterpolation, paramCase: config.paramCase, ) @@ -252,9 +329,12 @@ Map _parseMapNode({ path: currPath, rawPath: currRawPath, modifiers: modifiers, + locale: locale, + types: types, raw: value.toString(), comment: comment, shouldEscape: shouldEscapeText, + handleTypes: handleTypes, interpolation: config.stringInterpolation, paramCase: config.paramCase, ); @@ -270,7 +350,8 @@ Map _parseMapNode({ for (int i = 0; i < value.length; i++) i.toString(): value[i], }; children = _parseMapNode( - localeDebug: localeDebug, + locale: locale, + types: types, parentPath: currPath, parentRawPath: currRawPath, curr: listAsMap, @@ -278,7 +359,11 @@ Map _parseMapNode({ keyCase: config.keyCase, leavesMap: leavesMap, contextCollection: contextCollection, + baseData: baseData, + baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, + handleTypes: handleTypes, + sanitizeKey: false, ); // finally only take their values, ignoring keys @@ -292,9 +377,16 @@ 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( - localeDebug: localeDebug, + final tempChildren = _parseMapNode( + locale: locale, + types: types, parentPath: currPath, parentRawPath: currRawPath, curr: value, @@ -306,13 +398,31 @@ Map _parseMapNode({ : config.keyCase, leavesMap: leavesMap, contextCollection: contextCollection, + baseData: baseData, + baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, + handleTypes: handleTypes, + sanitizeKey: detectedType == null, ); - final Node finalNode; - final detectedType = + 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); + final Node finalNode; + // split by comma if necessary if (detectedType.nodeType == _DetectionType.context || detectedType.nodeType == _DetectionType.pluralCardinal || @@ -320,7 +430,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; @@ -339,7 +449,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; } } } @@ -348,7 +458,8 @@ Map _parseMapNode({ if (rich) { // rebuild children as RichText digestedMap = _parseMapNode( - localeDebug: localeDebug, + locale: locale, + types: types, parentPath: currPath, parentRawPath: currRawPath, curr: { @@ -359,25 +470,42 @@ Map _parseMapNode({ keyCase: config.keyCase, leavesMap: leavesMap, contextCollection: contextCollection, + baseData: baseData, + baseContexts: baseContexts, shouldEscapeText: shouldEscapeText, + handleTypes: handleTypes, + sanitizeKey: false, ).cast(); } 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 == + FallbackStrategy.baseLocaleEmptyString) { + // add base context values if necessary + final baseContext = baseContexts?[context.enumName]; + if (baseContext != null) { + digestedMap = _digestContextEntries( + locale: locale, + baseTranslation: baseData!.root, + baseContext: baseContext, + path: currPath, + entries: digestedMap, + ); + } + } finalNode = ContextNode( path: currPath, @@ -391,6 +519,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, @@ -404,8 +545,8 @@ Map _parseMapNode({ // because detection was correct return MapEntry(key.toQuantity()!, value); }), - paramName: - modifiers[NodeModifiers.param] ?? config.pluralParameter, + paramName: paramName, + paramType: paramType, rich: rich, ); } @@ -449,9 +590,12 @@ String? _parseCommentNode(dynamic node) { } void _setParent(Node parent, Iterable children) { - children.forEach((child) => child.setParent(parent)); + for (final child in children) { + child.setParent(parent); + } } +// Note: We already detected the map type, no need to check for it again _DetectionResult _determineNodeType( BuildModelConfig config, String nodePath, @@ -459,10 +603,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); @@ -500,21 +641,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); } @@ -527,14 +653,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 @@ -545,7 +671,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; } } @@ -734,7 +861,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'; @@ -756,13 +889,13 @@ 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, }) { - 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); @@ -771,7 +904,69 @@ void _fixEmptyLists({ child.setGenericType(generic); } } - }); + } +} + +/// 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, + required Map entries, +}) { + // Using "late" keyword because we are optimistic that all values are present + late ContextNode baseContextNode = + _findNode(baseTranslation, path.split('.')); + return { + for (final value in baseContext.enumValues) + value: 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 [Node] using the given [path]. +T _findNode(ObjectNode node, List path) { + final child = node.entries[path[0]]; + if (path.length == 1) { + if (child is T) { + return child; + } else { + throw 'Parent node is not a $T but a ${node.runtimeType} at path $path'; + } + } else if (child is ObjectNode) { + return _findNode(child, path.sublist(1)); + } else { + throw 'Cannot find base $T'; + } } enum _DetectionType { @@ -786,7 +981,7 @@ class _DetectionResult { final _DetectionType nodeType; final String? contextHint; - _DetectionResult(this.nodeType, [this.contextHint]); + const _DetectionResult(this.nodeType, [this.contextHint]); } class _InterfaceAttributesResult { @@ -819,13 +1014,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( @@ -837,3 +1032,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 new file mode 100644 index 00000000..11ed719a --- /dev/null +++ b/slang/lib/src/builder/builder/translation_model_list_builder.dart @@ -0,0 +1,67 @@ +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 + /// The returned locales are sorted (base locale first) + /// + /// After this method call, information about the namespace is lost. + /// It will be just a normal parent. + static List build( + RawConfig rawConfig, + TranslationMap translationMap, + ) { + 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, + locale: baseEntry.key, + ); + + return translationMap.getInternalMap().entries.map((localeEntry) { + final locale = localeEntry.key; + final namespaces = localeEntry.value; + 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, + types: baseResult.types, + ); + } else { + final result = TranslationModelBuilder.build( + buildConfig: buildConfig, + map: rawConfig.namespaces ? namespaces : namespaces.values.first, + baseData: baseResult, + locale: locale, + ); + + return I18nData( + base: false, + locale: locale, + root: result.root, + contexts: result.contexts, + interfaces: result.interfaces, + types: result.types, + ); + } + }).toList() + ..sort(I18nData.generationComparator); + } +} diff --git a/slang/lib/src/builder/decoder/arb_decoder.dart b/slang/lib/src/builder/decoder/arb_decoder.dart new file mode 100644 index 00000000..40f24ba3 --- /dev/null +++ b/slang/lib/src/builder/decoder/arb_decoder.dart @@ -0,0 +1,278 @@ +import 'dart:convert'; + +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'; +import 'package:slang/src/builder/utils/string_extensions.dart'; +import 'package:slang/src/builder/utils/string_interpolation_extensions.dart'; + +class ArbDecoder extends BaseDecoder { + @override + Map decode(String raw) { + final sourceMap = json.decode(raw) as Map; + + final entryMetadata = {}; // key -> metadata + + // Parse metadata first + for (final key in sourceMap.keys) { + final value = sourceMap[key]; + if (key.length > 1 && + key.startsWith('@') && + value is Map) { + entryMetadata[key.substring(1)] = _EntryMetadata.parseEntry( + value, + ); + } + } + + final resultMap = {}; + + for (final key in sourceMap.keys) { + if (key.startsWith('@')) { + continue; + } + + final metadata = entryMetadata[key] ?? + const _EntryMetadata( + description: null, + paramTypeMap: {}, + paramFormatMap: {}, + ); + + final value = sourceMap[key]; + + _addEntry( + key: key, + metadata: metadata, + value: value, + resultMap: resultMap, + ); + + if (metadata.description != null) { + resultMap['@$key'] = metadata.description; + } + } + + return resultMap; + } +} + +void _addEntry({ + required final String key, + required final _EntryMetadata metadata, + required final String value, + required final Map resultMap, +}) { + List brackets = BracketsUtils.findTopLevelBrackets(value); + if (brackets.length == 1 && + brackets.first.start == 0 && + brackets.first.end == value.length - 1) { + // potential single complex node + final singleComplexMatch = RegexUtils.arbComplexNode.firstMatch(value); + if (singleComplexMatch != null) { + // this is a plural or a context node + // add additional nodes to this base path + + final variable = singleComplexMatch.group(1)!.trim(); + final type = singleComplexMatch.group(2)!.trim(); + final content = singleComplexMatch.group(3)!; + final isPlural = type == 'plural'; + for (final part in RegexUtils.arbComplexNodeContent.allMatches(content)) { + final partName = + isPlural ? _digestPluralKey(part.group(1)!) : part.group(1)!; + final partContent = part.group(2)!; + MapUtils.addItemToMap( + map: resultMap, + destinationPath: + '$key(${isPlural ? 'plural' : 'context=${variable.toCase(CaseStyle.pascal)}'}, param=$variable).$partName', + item: _digestLeafText( + partContent, + metadata.paramTypeMap, + metadata.paramFormatMap, + ), + ); + } + return; + } + } + + final nameFactory = _DistinctNameFactory(); + String result = value; + while (brackets.isNotEmpty) { + final currentBracket = brackets.first; + + final match = + RegexUtils.arbComplexNode.firstMatch(currentBracket.substring()); + + if (match == null) { + // invalid complex, continue to next bracket without any changes + // Likely just a placeholder for a variable + brackets.removeAt(0); + continue; + } + + // add linked complex expression + final originalParameter = match.group(1)!.trim(); + final parameter = nameFactory.getNewName(originalParameter); + + // create new key + _addEntry( + key: '${key}__$parameter', + metadata: metadata, + value: match.group(0)!, + resultMap: resultMap, + ); + + // update string and refer to new key + result = currentBracket.replaceWith('@:${key}__$parameter'); + + // re-run because indices changed (old bracket list is invalid now) + brackets = BracketsUtils.findTopLevelBrackets(result); + } + + 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, + Map paramFormatMap, +) { + return text.replaceBracesInterpolation(replacer: (match) { + final param = match.substring(1, match.length - 1); + + 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}'; + } else { + return '{${param.toCase(CaseStyle.camel)}$paramType}'; + } + }); +} + +/// ARB files use '=0', '=1', and '=2' for 'zero', 'one', and 'two' +/// We need to normalize that. +String _digestPluralKey(String key) { + switch (key) { + case '=0': + return 'zero'; + case '=1': + return 'one'; + case '=2': + return 'two'; + default: + return 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) { + final description = map['description'] as String?; + + final placeholders = map['placeholders'] as Map?; + if (placeholders == null) { + return _EntryMetadata( + description: description, + paramTypeMap: {}, + paramFormatMap: {}, + ); + } + + 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; + } + } + + 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 = {}; + + /// Gets a name which is distinct from the previous ones + /// If [raw] already exists, then a number will be appended + /// + /// E.g. + /// apple, banana, apple2, apple3, banana2, ... + String getNewName(String raw) { + int number = 1; + String result = raw; + while (existingNames.contains(result)) { + number++; + result = raw + number.toString(); + } + existingNames.add(result); + return result; + } +} diff --git a/slang/lib/builder/decoder/base_decoder.dart b/slang/lib/src/builder/decoder/base_decoder.dart similarity index 63% rename from slang/lib/builder/decoder/base_decoder.dart rename to slang/lib/src/builder/decoder/base_decoder.dart index df55c734..444579b4 100644 --- a/slang/lib/builder/decoder/base_decoder.dart +++ b/slang/lib/src/builder/decoder/base_decoder.dart @@ -1,14 +1,14 @@ -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'; +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/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 61% rename from slang/lib/builder/generator/generate_header.dart rename to slang/lib/src/builder/generator/generate_header.dart index a8bffeb6..be28a351 100644 --- a/slang/lib/builder/generator/generate_header.dart +++ b/slang/lib/src/builder/generator/generate_header.dart @@ -1,17 +1,15 @@ -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/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); @@ -127,26 +116,21 @@ 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 -// 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:intl/intl.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', 'package:slang_flutter/slang_flutter.dart', @@ -166,20 +150,22 @@ 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, + ); + final deferred = config.lazy ? ' deferred' : ''; 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({ @@ -200,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(', ')}],'); @@ -212,25 +200,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, @@ -241,7 +217,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( @@ -264,15 +240,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 +250,70 @@ 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, + {required bool sync, required bool proxySync}) { + buffer.writeln(); + buffer.writeln('\t@override'); + buffer.writeln( + '\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,'); + buffer.writeln('\t}) ${sync ? '' : 'async '}{'); + + 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}'); + } + + buffer.writeln('\t}'); + } + + generateBuildMethod(buffer, sync: false, proxySync: !config.lazy); + generateBuildMethod(buffer, sync: true, proxySync: false); + 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('}'); @@ -396,8 +414,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._();'); @@ -409,29 +429,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, $enumName? 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 $enumName 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 $enumName 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, $enumName? 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 $enumName 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 $enumName locale, required bool isFlatMap, required Map map}) => instance.overrideTranslationsFromMapSync(locale: locale, isFlatMap: isFlatMap, map: map);'); } buffer.writeln('}'); @@ -440,7 +485,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 +493,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 +521,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 $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 ${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 $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 $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('}'); @@ -509,6 +563,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} {'); @@ -536,28 +592,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 = $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}'); + + 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 = $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;'); + buffer.writeln('\t\t}'); + + buffer.writeln(); + buffer.writeln('\t\treturn result;'); + buffer.writeln('\t}'); + + // fields + buffer.writeln(); + buffer.writeln('\tList get $fieldsVar => ['); 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/builder/generator/generate_translation_map.dart b/slang/lib/src/builder/generator/generate_translation_map.dart similarity index 66% rename from slang/lib/builder/generator/generate_translation_map.dart rename to slang/lib/src/builder/generator/generate_translation_map.dart index 07bf9572..05e44d2f 100644 --- a/slang/lib/builder/generator/generate_translation_map.dart +++ b/slang/lib/src/builder/generator/generate_translation_map.dart @@ -1,41 +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('part of \'${config.baseName}.g.dart\';'); - 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( + 'extension on ${localeData.base ? config.className : getClassNameRoot(className: config.className, locale: localeData.locale)} {'); + buffer.writeln('\tdynamic _flatMapFunction(String path) {'); - 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('\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(); } @@ -50,7 +40,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;'); @@ -72,27 +63,27 @@ _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( + _addPluralCall( buffer: buffer, config: config, language: language, diff --git a/slang/lib/builder/generator/generate_translations.dart b/slang/lib/src/builder/generator/generate_translations.dart similarity index 78% rename from slang/lib/builder/generator/generate_translations.dart rename to slang/lib/src/builder/generator/generate_translations.dart index 1d1af762..8c93435f 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/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,15 +26,35 @@ String generateTranslations(GenerateConfig config, I18nData localeData) { final queue = Queue(); final buffer = StringBuffer(); - if (config.outputFormat == OutputFormat.multipleFiles) { - // this is a part file - buffer.writeln('part of \'${config.baseName}.g.dart\';'); + buffer.writeln(''' +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import +'''); + + if (localeData.base) { + buffer.writeln("part of '${config.outputFileName}';"); + } else { + final imports = [ + config.outputFileName, + ...config.imports, + 'package:intl/intl.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', + ]..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, )); @@ -46,11 +66,23 @@ String generateTranslations(GenerateConfig config, I18nData localeData) { 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(); } @@ -77,35 +109,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 { @@ -116,6 +150,8 @@ void _generateClass( final baseClassName = root ? config.className : getClassName( + base: true, + visibility: TranslationClassVisibility.public, parentName: className, locale: config.baseLocale, ); @@ -154,7 +190,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 '); @@ -167,6 +203,18 @@ 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) { + // 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)}),', + ); + } + buffer.writeln('\t\t },'); + } if (config.obfuscation.enabled) { final String method; final List parts; @@ -185,7 +233,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) { @@ -228,19 +276,19 @@ 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);'); + } } } // 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;'); @@ -272,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 @@ -289,7 +339,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;'); @@ -327,8 +378,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 @@ -347,14 +402,25 @@ void _generateClass( // generate a class later on queue.add(ClassTask(childClassNoLocale, value)); String childClassWithLocale = getClassName( - parentName: className, childName: key, locale: localeData.locale); - buffer.writeln( - 'late final $childClassWithLocale$optional $key = $childClassWithLocale._(_root);'); + base: localeData.base, + visibility: config.translationClassVisibility, + parentName: className, + childName: key, + locale: localeData.locale, + ); + + 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'; buffer.write('$returnType$optional $key'); - _addPluralizationCall( + _addPluralCall( buffer: buffer, config: config, language: localeData.locale.language, @@ -401,7 +467,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 { @@ -422,8 +489,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 @@ -441,13 +512,25 @@ void _generateMap({ } else { // generate a class later on queue.add(ClassTask(childClassNoLocale, value)); - String childClassWithLocale = - getClassName(parentName: className, childName: key, locale: locale); - buffer.writeln('\'$key\': $childClassWithLocale._(_root),'); + String childClassWithLocale = getClassName( + base: base, + visibility: config.translationClassVisibility, + parentName: className, + childName: key, + locale: locale, + ); + + 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\': '); - _addPluralizationCall( + _addPluralCall( buffer: buffer, config: config, language: locale.language, @@ -504,7 +587,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 { @@ -524,14 +608,12 @@ void _generateList({ depth: depth + 1, ); } else if (value is ObjectNode) { - final String key = r'$' + - '${listName ?? ''}\$' + - depth.toString() + - 'i' + - i.toString() + - r'$'; - final String childClassNoLocale = - getClassName(parentName: className, childName: key); + final key = '\$${listName ?? ''}\$${depth.toString()}i${i.toString()}\$'; + final String childClassNoLocale = getClassName( + base: base, + visibility: config.translationClassVisibility, + parentName: className, + childName: key); if (value.isMap) { // inline map @@ -549,14 +631,22 @@ 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, ); - 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) { - _addPluralizationCall( + _addPluralCall( buffer: buffer, config: config, language: locale.language, @@ -621,7 +711,7 @@ String _toParameterMap(Set params) { return buffer.toString(); } -void _addPluralizationCall({ +void _addPluralCall({ required StringBuffer buffer, required GenerateConfig config, required String language, @@ -642,14 +732,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'} '); @@ -700,8 +793,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)},'); } } @@ -746,7 +840,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) { @@ -756,7 +850,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(','); @@ -844,8 +938,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/generator.dart b/slang/lib/src/builder/generator/generator.dart new file mode 100644 index 00000000..be6bed6d --- /dev/null +++ b/slang/lib/src/builder/generator/generator.dart @@ -0,0 +1,21 @@ +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 + /// returns a string representing the content of the .g.dart file + static BuildResult generate({ + required GenerateConfig config, + required List translations, + }) { + return BuildResult( + main: generateHeader(config, translations), + translations: { + for (final t in translations) t.locale: generateTranslations(config, t), + }, + ); + } +} diff --git a/slang/lib/builder/generator/helper.dart b/slang/lib/src/builder/generator/helper.dart similarity index 64% rename from slang/lib/builder/generator/helper.dart rename to slang/lib/src/builder/generator/helper.dart index ac56a437..63919a78 100644 --- a/slang/lib/builder/generator/helper.dart +++ b/slang/lib/src/builder/generator/helper.dart @@ -1,28 +1,34 @@ -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/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'; /// Pragmatic way to detect links within interpolations. const String characteristicLinkPrefix = '_root.'; +String getImportName({ + required I18nLocale locale, +}) { + return 'l_${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; } @@ -40,10 +56,18 @@ 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 - return "'$value'"; + 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 { + // We need to add quotes + return "'$value'"; + } } // Return the obfuscated version diff --git a/slang/lib/builder/generator_facade.dart b/slang/lib/src/builder/generator_facade.dart similarity index 74% rename from slang/lib/builder/generator_facade.dart rename to slang/lib/src/builder/generator_facade.dart index 2de4c1fe..f7163269 100644 --- a/slang/lib/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/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/interface.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, }) { @@ -26,7 +25,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)) { @@ -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 71% rename from slang/lib/builder/model/build_model_config.dart rename to slang/lib/src/builder/model/build_model_config.dart index 096e4a78..321ebb24 100644 --- a/slang/lib/builder/model/build_model_config.dart +++ b/slang/lib/src/builder/model/build_model_config.dart @@ -1,7 +1,8 @@ -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/interface.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'; +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/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/src/builder/model/enums.dart b/slang/lib/src/builder/model/enums.dart new file mode 100644 index 00000000..ceef2623 --- /dev/null +++ b/slang/lib/src/builder/model/enums.dart @@ -0,0 +1,28 @@ +enum FileType { json, yaml, csv, arb } + +enum FallbackStrategy { none, baseLocale, baseLocaleEmptyString } + +/// Similar to [FallbackStrategy] but [FallbackStrategy.baseLocaleEmptyString] +/// has been already handled in the previous step. +enum GenerateFallbackStrategy { none, baseLocale } + +enum StringInterpolation { dart, braces, doubleBraces } + +enum TranslationClassVisibility { private, public } + +enum CaseStyle { camel, pascal, snake } + +enum PluralAuto { off, cardinal, ordinal } + +extension FallbackStrategyExt on FallbackStrategy { + GenerateFallbackStrategy toGenerateFallbackStrategy() { + switch (this) { + case FallbackStrategy.none: + return GenerateFallbackStrategy.none; + case FallbackStrategy.baseLocale: + return GenerateFallbackStrategy.baseLocale; + case FallbackStrategy.baseLocaleEmptyString: + return GenerateFallbackStrategy.baseLocale; + } + } +} 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/builder/model/generate_config.dart b/slang/lib/src/builder/model/generate_config.dart similarity index 74% rename from slang/lib/builder/model/generate_config.dart rename to slang/lib/src/builder/model/generate_config.dart index 9b1a7239..97ff7977 100644 --- a/slang/lib/builder/model/generate_config.dart +++ b/slang/lib/src/builder/model/generate_config.dart @@ -1,19 +1,19 @@ -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/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 OutputFormat outputFormat; + final String outputFileName; + final bool lazy; final bool localeHandling; final bool flutterIntegration; final String translateVariable; @@ -32,10 +32,10 @@ class GenerateConfig { GenerateConfig({ required this.buildConfig, required this.inputDirectoryHint, - required this.baseName, required this.baseLocale, required this.fallbackStrategy, - required this.outputFormat, + required this.outputFileName, + required this.lazy, 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 69% rename from slang/lib/builder/model/i18n_data.dart rename to slang/lib/src/builder/model/i18n_data.dart index 7854c630..c4433e8e 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); @@ -10,8 +10,9 @@ 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 + 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/builder/model/i18n_locale.dart b/slang/lib/src/builder/model/i18n_locale.dart similarity index 86% rename from slang/lib/builder/model/i18n_locale.dart rename to slang/lib/src/builder/model/i18n_locale.dart index d6b419d1..b71d88e2 100644 --- a/slang/lib/builder/model/i18n_locale.dart +++ b/slang/lib/src/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/model/enums.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 { @@ -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/builder/model/interface.dart b/slang/lib/src/builder/model/interface.dart similarity index 99% rename from slang/lib/builder/model/interface.dart rename to slang/lib/src/builder/model/interface.dart index 215f3c31..df7970c2 100644 --- a/slang/lib/builder/model/interface.dart +++ b/slang/lib/src/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/src/builder/model/node.dart similarity index 60% rename from slang/lib/builder/model/node.dart rename to slang/lib/src/builder/model/node.dart index d07f8580..d3e0b01d 100644 --- a/slang/lib/builder/model/node.dart +++ b/slang/lib/src/builder/model/node.dart @@ -1,23 +1,53 @@ -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/context_type.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/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'; +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'; 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'; } @@ -44,15 +74,21 @@ 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 + /// The generic type of the container, i.e. `Map` or `List` String _genericType; String get genericType => _genericType; @@ -101,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 { @@ -119,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 { @@ -130,6 +212,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 +223,7 @@ class PluralNode extends Node implements LeafNode { required this.pluralType, required this.quantities, required this.paramName, + required this.paramType, required this.rich, }); @@ -151,24 +235,52 @@ 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(); } @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 { - final ContextType context; + final PendingContextType context; final Map entries; final String paramName; // name of the context parameter final bool rich; @@ -206,9 +318,40 @@ 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 { + /// The locale of the text node + final I18nLocale locale; + + /// User-defined types for the locale + final Map types; + /// The original string final String raw; @@ -230,6 +373,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; @@ -238,8 +382,11 @@ abstract class TextNode extends Node implements LeafNode { required super.rawPath, required super.modifiers, required super.comment, + required this.locale, + required this.types, required this.raw, required this.shouldEscape, + required this.handleTypes, required this.interpolation, required this.paramCase, }); @@ -250,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 { @@ -278,19 +431,27 @@ class StringTextNode extends TextNode { required super.path, required super.rawPath, required super.modifiers, + required super.locale, + required super.types, required super.raw, required super.comment, required super.shouldEscape, + required super.handleTypes, required super.interpolation, required super.paramCase, Map>? linkParamMap, }) { final parsedResult = _parseInterpolation( + locale: locale, + types: types, raw: shouldEscape ? _escapeContent(raw, interpolation) : raw, interpolation: interpolation, + defaultType: 'Object', paramCase: paramCase, + digestParameter: handleTypes && true, ); - _params = parsedResult.params; + _params = parsedResult.params.keys.toSet(); + _paramTypeMap.addAll(parsedResult.params); if (linkParamMap != null) { _params.addAll(linkParamMap.values.expand((e) => e)); @@ -301,8 +462,8 @@ class StringTextNode extends TextNode { linkParamMap: linkParamMap, ); - this._links = parsedLinksResult.links; - this._content = parsedLinksResult.parsedContent; + _links = parsedLinksResult.links; + _content = parsedLinksResult.parsedContent; } @override @@ -310,23 +471,26 @@ 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( path: path, rawPath: rawPath, modifiers: modifiers, + locale: locale, + types: types, raw: raw, comment: comment, shouldEscape: shouldEscape, + handleTypes: handleTypes, interpolation: interpolation, paramCase: paramCase, linkParamMap: linkParamMap, ); - this._content = temp.content; + _content = temp.content; } @override @@ -337,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 { @@ -354,7 +541,7 @@ class RichTextNode extends TextNode { @override Set get links => _links; - Map _paramTypeMap = {}; + final Map _paramTypeMap = {}; @override Map get paramTypeMap => _paramTypeMap; @@ -363,32 +550,43 @@ class RichTextNode extends TextNode { required super.path, required super.rawPath, required super.modifiers, + required super.locale, + required super.types, required super.raw, required super.comment, required super.shouldEscape, + required super.handleTypes, required super.interpolation, required super.paramCase, Map>? linkParamMap, }) { final rawParsedResult = _parseInterpolation( + locale: locale, + types: types, raw: shouldEscape ? _escapeContent(raw, interpolation) : raw, interpolation: interpolation, - paramCase: null, // param case will be applied later + defaultType: 'ignored', + // types are ignored + paramCase: null, + // param case will be applied later + digestParameter: false, ); - 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, @@ -402,11 +600,12 @@ class RichTextNode extends TextNode { return LiteralSpan( literal: parsedLinksResult.parsedContent, isConstant: parsedLinksResult.links.isEmpty, + links: parsedLinksResult.links, ); }, onMatch: (match) { - final parsed = _parseParamWithArg( - input: (match.group(1) ?? match.group(2))!, + final parsed = parseParamWithArg( + rawParam: (match.group(1) ?? match.group(2))!, paramCase: paramCase, ); final parsedArg = parsed.arg; @@ -417,8 +616,9 @@ class RichTextNode extends TextNode { ); _links.addAll(parsedLinksResult.links); return FunctionSpan( - parsed.paramName, - parsedLinksResult.parsedContent, + functionName: parsed.paramName, + arg: parsedLinksResult.parsedContent, + links: parsedLinksResult.links, ); } else { return VariableSpan(parsed.paramName); @@ -432,23 +632,49 @@ 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( path: path, rawPath: rawPath, modifiers: modifiers, + locale: locale, + types: types, raw: raw, comment: comment, shouldEscape: shouldEscape, + handleTypes: handleTypes, interpolation: interpolation, paramCase: paramCase, linkParamMap: linkParamMap, ); - this._spans = temp.spans; + _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; } } @@ -485,22 +711,70 @@ String _escapeContent(String raw, StringInterpolation interpolation) { class _ParseInterpolationResult { final String parsedContent; - final Set params; - _ParseInterpolationResult(this.parsedContent, this.params); + /// Map of parameter name -> parameter type + final Map 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 Map types, required String raw, required StringInterpolation interpolation, + required String defaultType, required CaseStyle? paramCase, + required bool digestParameter, }) { final String parsedContent; - final params = Set(); + final params = {}; + final formats = {}; + + String convertInnerParam(String inner) { + if (!digestParameter) { + params[inner] = 'is ignored'; + return '\${$inner}'; + } + + final parsedParam = parseParam( + rawParam: inner, + defaultType: defaultType, + 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, + 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: @@ -508,27 +782,27 @@ _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}'; + return convertInnerParam(rawParam); }); 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); + return convertInnerParam(rawParam); }); 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); + return convertInnerParam(rawParam); }); } - return _ParseInterpolationResult(parsedContent, params); + return _ParseInterpolationResult( + parsedContent: parsedContent, + params: params, + formats: formats, + ); } class _ParseLinksResult { @@ -546,9 +820,9 @@ _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)!; + final linkedPath = (match.group(1) ?? match.group(2))!; links.add(linkedPath); if (linkParamMap == null) { @@ -588,41 +862,30 @@ Iterable _splitWithMatchAndNonMatch( } } -_ParamWithArg _parseParamWithArg({ - required String input, - 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'; - } - return _ParamWithArg(match.group(1)!.toCase(paramCase), match.group(3)); -} - -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 { final String literal; final bool isConstant; + final Set links; - LiteralSpan({required this.literal, required this.isConstant}); + LiteralSpan({ + required this.literal, + required this.isConstant, + required this.links, + }); } class FunctionSpan extends BaseSpan { final String functionName; final String arg; + final Set links; - FunctionSpan(this.functionName, this.arg); + FunctionSpan({ + required this.functionName, + required this.arg, + required this.links, + }); } class VariableSpan extends BaseSpan { 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 85% rename from slang/lib/builder/model/raw_config.dart rename to slang/lib/src/builder/model/raw_config.dart index 2383a2c5..2f82dc6e 100644 --- a/slang/lib/builder/model/raw_config.dart +++ b/slang/lib/src/builder/model/raw_config.dart @@ -1,10 +1,12 @@ -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/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 +/// represents a build.yaml or a slang.yaml file class RawConfig { static const String defaultBaseLocale = 'en'; static const FallbackStrategy defaultFallbackStrategy = FallbackStrategy.none; @@ -12,7 +14,7 @@ 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 defaultLazy = true; static const bool defaultLocaleHandling = true; static const bool defaultFlutterIntegration = true; static const bool defaultNamespaces = false; @@ -24,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; @@ -39,6 +46,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; @@ -48,7 +59,7 @@ class RawConfig { final String inputFilePattern; final String? outputDirectory; final String outputFileName; - final OutputFormat outputFormat; + final bool lazy; final bool localeHandling; final bool flutterIntegration; final bool namespaces; @@ -59,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; @@ -72,6 +84,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) @@ -84,7 +97,7 @@ class RawConfig { required this.inputFilePattern, required this.outputDirectory, required this.outputFileName, - required this.outputFormat, + required this.lazy, required this.localeHandling, required this.flutterIntegration, required this.namespaces, @@ -95,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, @@ -108,6 +122,7 @@ class RawConfig { required this.contexts, required this.interfaces, required this.obfuscation, + required this.format, required this.imports, required this.rawMap, }) : fileType = _determineFileType(inputFilePattern), @@ -120,13 +135,15 @@ class RawConfig { I18nLocale? baseLocale, FallbackStrategy? fallbackStrategy, String? inputFilePattern, - OutputFormat? outputFormat, + String? outputFileName, + bool? lazy, bool? localeHandling, bool? flutterIntegration, bool? namespaces, TranslationClassVisibility? translationClassVisibility, CaseStyle? keyCase, CaseStyle? keyMapCase, + CaseStyle? paramCase, bool? renderFlatMap, bool? translationOverrides, bool? renderTimestamp, @@ -138,6 +155,7 @@ class RawConfig { List? contexts, List? interfaces, ObfuscationConfig? obfuscation, + FormatConfig? format, }) { return RawConfig( baseLocale: baseLocale ?? this.baseLocale, @@ -145,8 +163,8 @@ class RawConfig { inputDirectory: inputDirectory, inputFilePattern: inputFilePattern ?? this.inputFilePattern, outputDirectory: outputDirectory, - outputFileName: outputFileName, - outputFormat: outputFormat ?? this.outputFormat, + outputFileName: outputFileName ?? this.outputFileName, + lazy: lazy ?? this.lazy, localeHandling: localeHandling ?? this.localeHandling, flutterIntegration: flutterIntegration ?? this.flutterIntegration, namespaces: namespaces ?? this.namespaces, @@ -157,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, @@ -171,6 +190,7 @@ class RawConfig { contexts: contexts ?? this.contexts, interfaces: interfaces ?? this.interfaces, obfuscation: obfuscation ?? this.obfuscation, + format: format ?? this.format, imports: imports, rawMap: rawMap, ); @@ -200,13 +220,12 @@ 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(' -> lazy: $lazy'); print(' -> localeHandling: $localeHandling'); print(' -> flutterIntegration: $flutterIntegration'); print(' -> namespaces: $namespaces'); @@ -219,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'); @@ -231,8 +252,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) { @@ -250,6 +270,8 @@ class RawConfig { } } print(' -> obfuscation: ${obfuscation.enabled ? 'enabled' : 'disabled'}'); + print( + ' -> format: ${format.enabled ? 'enabled (width=${format.width})' : 'disabled'}'); print(' -> imports: $imports'); } @@ -263,7 +285,7 @@ class RawConfig { inputFilePattern: RawConfig.defaultInputFilePattern, outputDirectory: RawConfig.defaultOutputDirectory, outputFileName: RawConfig.defaultOutputFileName, - outputFormat: RawConfig.defaultOutputFormat, + lazy: RawConfig.defaultLazy, localeHandling: RawConfig.defaultLocaleHandling, flutterIntegration: RawConfig.defaultFlutterIntegration, namespaces: RawConfig.defaultNamespaces, @@ -273,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, @@ -286,6 +309,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/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/builder/model/slang_file_collection.dart b/slang/lib/src/builder/model/slang_file_collection.dart similarity index 78% rename from slang/lib/builder/model/slang_file_collection.dart rename to slang/lib/src/builder/model/slang_file_collection.dart index ae9ade1f..35c93df5 100644 --- a/slang/lib/builder/model/slang_file_collection.dart +++ b/slang/lib/src/builder/model/slang_file_collection.dart @@ -1,9 +1,12 @@ -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/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. +/// 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; @@ -16,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; @@ -48,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; @@ -79,7 +84,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/builder/model/translation_map.dart b/slang/lib/src/builder/model/translation_map.dart similarity index 72% rename from slang/lib/builder/model/translation_map.dart rename to slang/lib/src/builder/model/translation_map.dart index 97faf94c..de111881 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 @@ -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/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/src/builder/utils/file_utils.dart b/slang/lib/src/builder/utils/file_utils.dart new file mode 100644 index 00000000..fe15a965 --- /dev/null +++ b/slang/lib/src/builder/utils/file_utils.dart @@ -0,0 +1,118 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:json2yaml/json2yaml.dart'; +import 'package:slang/src/builder/model/enums.dart'; + +const String INFO_KEY = '@@info'; + +class FileUtils { + static void writeFile({required String path, required String content}) { + File(path).writeAsStringSync(content); + } + + static void writeFileOfType({ + required FileType fileType, + required String path, + required Map content, + }) { + FileUtils.writeFile( + path: path, + content: FileUtils.encodeContent( + fileType: fileType, + content: content, + ), + ); + } + + static String encodeContent({ + required FileType fileType, + required Map content, + }) { + switch (fileType) { + case FileType.json: + // this encoder does not append \n automatically + return '${JsonEncoder.withIndent(' ').convert(content)}\n'; + case FileType.yaml: + if (content.containsKey(INFO_KEY)) { + // workaround + // https://github.com/alexei-sintotski/json2yaml/issues/23 + content = { + '"$INFO_KEY"': content[INFO_KEY], + ...content..remove(INFO_KEY), + }; + } + return json2yaml(content, yamlStyle: YamlStyle.generic); + case FileType.csv: + String escapeRow(String value) { + final escaped = value.replaceAll('"', '""'); + if (escaped.contains(RegExp(r'[,"]'))) { + return '"$escaped"'; + } + return escaped; + } + + void encodeCsvRows({ + String key = '', + required dynamic value, + required Map result, + }) { + if (value is Map) { + 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) { + result[key] = escapeRow(value); + } + } + + final Map> columns = {}; + if (content.containsKey(INFO_KEY)) { + final info = content.remove(INFO_KEY); + columns[INFO_KEY] = {INFO_KEY: escapeRow(info.join('\\n'))}; + } + for (final e in content.entries) { + final result = {}; + encodeCsvRows(result: result, value: e.value); + + columns[e.key] = result; + } + + // 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'; + } + } + + static void createMissingFolders({required String filePath}) { + final index = filePath + .replaceAll('/', Platform.pathSeparator) + .replaceAll('\\', Platform.pathSeparator) + .lastIndexOf(Platform.pathSeparator); + if (index == -1) { + return; + } + + final directoryPath = filePath.substring(0, index); + Directory(directoryPath).createSync(recursive: true); + } +} diff --git a/slang/lib/builder/utils/map_utils.dart b/slang/lib/src/builder/utils/map_utils.dart similarity index 98% rename from slang/lib/builder/utils/map_utils.dart rename to slang/lib/src/builder/utils/map_utils.dart index f8e3c2e5..f10954d7 100644 --- a/slang/lib/builder/utils/map_utils.dart +++ b/slang/lib/src/builder/utils/map_utils.dart @@ -1,8 +1,8 @@ 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 + /// converts `Map` to `Map` for all children /// forcing all keys to be strings static Map deepCast(Map source) { return source.map((key, value) { @@ -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 90% rename from slang/lib/builder/utils/node_utils.dart rename to slang/lib/src/builder/utils/node_utils.dart index 3e4c6c8f..b71b8944 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/model/node.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; class NodeUtils { /// Returns a map containing modifiers @@ -80,7 +80,11 @@ class NodePathInfo { extension StringModifierExt on String { /// Returns the key without modifiers. String get withoutModifiers { - return this.split('(').first; + final index = indexOf('('); + if (index == -1) { + return this; + } + return substring(0, index); } String withModifier(String modifierKey, [String? modifierValue]) { @@ -99,9 +103,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/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/builder/utils/path_utils.dart b/slang/lib/src/builder/utils/path_utils.dart similarity index 90% rename from slang/lib/builder/utils/path_utils.dart rename to slang/lib/src/builder/utils/path_utils.dart index ed698f2f..1ca26aa2 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/model/i18n_locale.dart'; +import 'package:slang/src/builder/utils/regex_utils.dart'; /// Operations on paths class PathUtils { @@ -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( @@ -46,7 +55,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/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/lib/builder/utils/regex_utils.dart b/slang/lib/src/builder/utils/regex_utils.dart similarity index 55% rename from slang/lib/builder/utils/regex_utils.dart rename to slang/lib/src/builder/utils/regex_utils.dart index f3dfc98f..691ef096 100644 --- a/slang/lib/builder/utils/regex_utils.dart +++ b/slang/lib/src/builder/utils/regex_utils.dart @@ -2,19 +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 + /// `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 RegExp linkPathRegex = RegExp(r'^_root\.((?:[.\w])+)\(?'); + static final RegExp spaceRegex = RegExp(r'\s+'); + + 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} @@ -77,8 +90,8 @@ class RegexUtils { /// /// 1 - male /// 2 - His birthday - static RegExp arbComplexNodeContent = - RegExp(r'((?:=|\w)+){((?:[^}{]+|{[^}]+})+)}'); + static final RegExp arbComplexNodeContent = + RegExp(r'((?:=|\w)+) *{((?:[^}{]+|{[^}]+})+)}'); /// Matches any missing translations file /// _missing_translations.json, _missing_translations_de-DE.json @@ -87,6 +100,10 @@ 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)$'); + + /// 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/lib/builder/utils/string_extensions.dart b/slang/lib/src/builder/utils/string_extensions.dart similarity index 55% rename from slang/lib/builder/utils/string_extensions.dart rename to slang/lib/src/builder/utils/string_extensions.dart index efb9cfd0..c9a236f0 100644 --- a/slang/lib/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 @@ -7,44 +7,37 @@ 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 /// 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] /// 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 /// 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/lib/builder/utils/string_interpolation_extensions.dart b/slang/lib/src/builder/utils/string_interpolation_extensions.dart similarity index 62% rename from slang/lib/builder/utils/string_interpolation_extensions.dart rename to slang/lib/src/builder/utils/string_interpolation_extensions.dart index ac0b5937..cb05ea53 100644 --- a/slang/lib/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,60 +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 { + 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/lib/src/runner/analyze.dart b/slang/lib/src/runner/analyze.dart index 1418ed0b..e5372cd7 100644 --- a/slang/lib/src/runner/analyze.dart +++ b/slang/lib/src/runner/analyze.dart @@ -1,17 +1,17 @@ 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/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/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'; +import 'package:slang/src/builder/utils/path_utils.dart'; final _setEquality = SetEquality(); @@ -70,7 +70,7 @@ void runAnalyzeTranslations({ result: missingTranslationsResult, ); - final unusedTranslationsResult = _getUnusedTranslations( + final unusedTranslationsResult = getUnusedTranslations( rawConfig: rawConfig, translations: translationModelList, full: full, @@ -121,6 +121,8 @@ Map> getMissingTranslations({ curr: currTranslations.root, resultMap: resultMap, handleOutdated: true, + ignoreModifierFlag: NodeModifiers.ignoreMissing, + ignorePaths: const {}, ); result[currTranslations.locale] = resultMap; } @@ -128,7 +130,7 @@ Map> getMissingTranslations({ return result; } -Map> _getUnusedTranslations({ +Map> getUnusedTranslations({ required RawConfig rawConfig, required List translations, required bool full, @@ -150,11 +152,17 @@ Map> _getUnusedTranslations({ } final resultMap = {}; + final linkedPaths = {}; + _getReferredPaths(localeData.root, linkedPaths); + + // { } = localeData - baseTranslations _getMissingTranslationsForOneLocaleRecursive( baseNode: localeData.root, curr: baseTranslations.root, resultMap: resultMap, handleOutdated: false, + ignoreModifierFlag: NodeModifiers.ignoreUnused, + ignorePaths: linkedPaths, ); result[localeData.locale] = resultMap; } @@ -169,23 +177,41 @@ 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; } final currChild = curr.entries[baseEntry.key]; 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 (isOutdated || + currChild == null || + !_checkEquality(baseChild, currChild)) { + 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 @@ -195,6 +221,8 @@ void _getMissingTranslationsForOneLocaleRecursive({ curr: currChild as ObjectNode, resultMap: resultMap, handleOutdated: handleOutdated, + ignoreModifierFlag: ignoreModifierFlag, + ignorePaths: ignorePaths, ); } } @@ -225,37 +253,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'; } @@ -274,7 +302,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; } @@ -282,6 +310,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; } @@ -331,7 +367,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, @@ -347,9 +394,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'/\*.*?\*/', dotAll: 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(); @@ -363,6 +417,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, @@ -377,10 +457,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..b84fcd5d 100644 --- a/slang/lib/src/runner/apply.dart +++ b/slang/lib/src/runner/apply.dart @@ -1,15 +1,15 @@ 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/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/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'; import 'package:slang/src/runner/analyze.dart'; import 'package:slang/src/runner/utils/read_analysis_file.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/lib/src/runner/clean.dart b/slang/lib/src/runner/clean.dart index 55296b5b..48c3dbad 100644 --- a/slang/lib/src/runner/clean.dart +++ b/slang/lib/src/runner/clean.dart @@ -1,9 +1,9 @@ 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/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'; /// Reads the "_unused_translations" file and removes the specified keys @@ -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, diff --git a/slang/lib/src/runner/edit.dart b/slang/lib/src/runner/edit.dart index 23d82950..095fa1ed 100644 --- a/slang/lib/src/runner/edit.dart +++ b/slang/lib/src/runner/edit.dart @@ -1,14 +1,14 @@ 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/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/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'; 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..b1911297 100644 --- a/slang/lib/src/runner/migrate_arb.dart +++ b/slang/lib/src/runner/migrate_arb.dart @@ -2,13 +2,13 @@ import 'dart:convert'; 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/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'; +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/normalize.dart b/slang/lib/src/runner/normalize.dart new file mode 100644 index 00000000..c822988c --- /dev/null +++ b/slang/lib/src/runner/normalize.dart @@ -0,0 +1,114 @@ +import 'package:collection/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'; + +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, + ); +} diff --git a/slang/lib/src/runner/stats.dart b/slang/lib/src/runner/stats.dart index fc9b3e0f..6c66f9f3 100644 --- a/slang/lib/src/runner/stats.dart +++ b/slang/lib/src/runner/stats.dart @@ -1,9 +1,9 @@ -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/builder/utils/regex_utils.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({ 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/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/lib/src/runner/utils/read_analysis_file.dart b/slang/lib/src/runner/utils/read_analysis_file.dart index f8cee271..be10e485 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/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'; import 'package:slang/src/runner/apply.dart'; const _supportedFiles = [FileType.json, FileType.yaml]; diff --git a/slang/pubspec.yaml b/slang/pubspec.yaml index 840b456c..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: 3.28.0 +version: 4.4.0 repository: https://github.com/slang-i18n/slang topics: - i18n @@ -10,17 +10,21 @@ topics: screenshots: - description: The slang logo. path: resources/icon.png +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 - csv: ^5.0.1 - yaml: ^3.1.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 dev_dependencies: - lints: ^2.0.0 + 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 new file mode 100644 index 00000000..8d50cb64 --- /dev/null +++ b/slang/test/integration/main/compilation_test.dart @@ -0,0 +1,37 @@ +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() { + void expectCompiles(String path) { + final output = loadResource(path); + final result = parseString( + path: 'path.dart', + content: output, + ); + expect(result.errors, isEmpty); + } + + 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('translation overrides', () { + expectCompiles('main/_expected_translation_overrides.output'); + }); +} diff --git a/slang/test/integration/main/csv_compact_test.dart b/slang/test/integration/main/csv_compact_test.dart index b572d827..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/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/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 b21b5bc7..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/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/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 b5e2e792..17d9b07e 100644 --- a/slang/test/integration/main/fallback_base_locale_test.dart +++ b/slang/test/integration/main/fallback_base_locale_test.dart @@ -1,9 +1,10 @@ -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/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'; @@ -11,24 +12,49 @@ 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 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'); + 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', ); }); - test('translation overrides', () { + test('fallback with generic integration data', () { final parsed = CsvDecoder().decode(compactInput); final result = GeneratorFacade.generate( rawConfig: RawConfigBuilder.fromYaml(buildYaml)!.copyWith( fallbackStrategy: FallbackStrategy.baseLocale, ), - baseName: 'translations', translationMap: TranslationMap() ..addTranslations( locale: I18nLocale.fromString('en'), @@ -41,6 +67,36 @@ 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', () { + final result = GeneratorFacade.generate( + rawConfig: RawConfigBuilder.fromYaml(buildYaml)!.copyWith( + fallbackStrategy: FallbackStrategy.baseLocale, + ), + 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.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 797035d7..00000000 --- a/slang/test/integration/main/json_multiple_files_test.dart +++ /dev/null @@ -1,53 +0,0 @@ -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: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, - ), - 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 11a4ebd5..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/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/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 68ccee94..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/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/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 7a79c818..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/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/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 976c7f5e..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/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/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 c5c9274b..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/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: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 cb238f5d..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/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/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 4bd5454a..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/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/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 ea45709d..e022ea46 100644 --- a/slang/test/integration/resources/main/_expected_de.output +++ b/slang/test/integration/resources/main/_expected_de.output @@ -1,10 +1,19 @@ -part of 'translations.g.dart'; +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/generated.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, @@ -21,7 +30,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); @@ -42,17 +51,21 @@ 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}'; + @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 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, @@ -89,10 +102,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, @@ -103,10 +116,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 => [ @@ -125,10 +138,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'; @@ -136,20 +149,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'; @@ -157,11 +170,81 @@ 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.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, + 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 0e4b6baa..fd3d3085 100644 --- a/slang/test/integration/resources/main/_expected_en.output +++ b/slang/test/integration/resources/main/_expected_en.output @@ -1,6 +1,13 @@ -part of 'translations.g.dart'; +/// +/// 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]. /// @@ -10,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, @@ -30,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'), @@ -48,29 +55,33 @@ class Translations implements BaseTranslations { } // Path: onboarding -class _TranslationsOnboardingEn { - _TranslationsOnboardingEn._(this._root); +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}'; + 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, 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) { @@ -95,8 +106,8 @@ class _TranslationsOnboardingEn { } // Path: group -class _TranslationsGroupEn { - _TranslationsGroupEn._(this._root); +class TranslationsGroupEn { + TranslationsGroupEn._(this._root); final Translations _root; // ignore: unused_field @@ -109,8 +120,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 @@ -131,8 +142,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 @@ -142,8 +153,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 @@ -152,8 +163,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 @@ -163,11 +174,81 @@ 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.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, + 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.output b/slang/test/integration/resources/main/_expected_fallback_base_locale.output index cee50f27..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: 54 (27 per locale) +/// Strings: 58 (29 per locale) // coverage:ignore-file // ignore_for_file: type=lint @@ -213,6 +213,8 @@ 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 +383,8 @@ 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}'; @@ -505,6 +509,8 @@ 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 '), @@ -568,6 +574,8 @@ 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 '), 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..69e3f225 --- /dev/null +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_de.output @@ -0,0 +1,252 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/generated.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(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.internal(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 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, + 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.internal(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.internal(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.internal(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.internal(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.internal(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.internal(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.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, + 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..d47e3d2b --- /dev/null +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_en.output @@ -0,0 +1,254 @@ +/// +/// 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.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'), + 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.internal(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}'; + + 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, + TextSpan(text: ' and ${_root.onboarding.greet(lastName: lastName, context: context, fullName: fullName, firstName: firstName)}'), + ]); + List get pages => [ + TranslationsOnboarding$pages$0i0$En.internal(_root), + TranslationsOnboarding$pages$0i1$En.internal(_root), + ]; + List get modifierPages => [ + 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) { + 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.internal(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.internal(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.internal(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.internal(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.internal(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.internal(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.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, + 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..76c65fb3 --- /dev/null +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_main.output @@ -0,0 +1,294 @@ +/// Generated file. Do not edit. +/// +/// Source: fake/path/integration +/// To regenerate, run: `dart run slang` +/// +/// Locales: 2 +/// Strings: 62 (31 per locale) + +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/generated.dart'; +import 'package:slang_flutter/slang_flutter.dart'; +export 'package:slang_flutter/slang_flutter.dart'; + +import 'translations_de.g.dart' deferred as l_de; +part 'translations_en.g.dart'; + +/// Supported locales. +/// +/// 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 l_de.loadLibrary(); + return l_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 l_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, + lazy: true, + ); + + 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) { + 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 { + 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 { + String get title; + String? get content => null; + + @override + 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 { + 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 { + List get stringPages; + List> get pages; + + @override + 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 { + 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.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/_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..304886f3 --- /dev/null +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_de.output @@ -0,0 +1,65 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/generated.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(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..275ccdad --- /dev/null +++ b/slang/test/integration/resources/main/_expected_fallback_base_locale_special_main.output @@ -0,0 +1,187 @@ +/// Generated file. Do not edit. +/// +/// Source: 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:intl/intl.dart'; +import 'package:slang/generated.dart'; +import 'package:slang_flutter/slang_flutter.dart'; +export 'package:slang_flutter/slang_flutter.dart'; + +import 'translations_de.g.dart' deferred as l_de; +part 'translations_en.g.dart'; + +/// Supported locales. +/// +/// 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 l_de.loadLibrary(); + return l_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 l_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, + lazy: true, + ); + + 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 79b175ca..76c65fb3 100644 --- a/slang/test/integration/resources/main/_expected_main.output +++ b/slang/test/integration/resources/main/_expected_main.output @@ -1,44 +1,90 @@ /// Generated file. Do not edit. /// -/// Original: fake/path/integration +/// Source: fake/path/integration /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 54 (27 per locale) +/// Strings: 62 (31 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:intl/intl.dart'; +import 'package:slang/generated.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; +import 'translations_de.g.dart' deferred as l_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. +/// Supported locales. /// /// 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); + 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 l_de.loadLibrary(); + return l_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 l_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 @@ -84,19 +130,31 @@ 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._(); // 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 +164,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._(); @@ -132,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 { @@ -143,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 { @@ -154,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_map.output b/slang/test/integration/resources/main/_expected_map.output index 057cab17..bd3b4e63 100644 --- a/slang/test/integration/resources/main/_expected_map.output +++ b/slang/test/integration/resources/main/_expected_map.output @@ -1,4 +1,10 @@ -part of 'translations.g.dart'; +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint + +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. @@ -7,6 +13,8 @@ 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 '), @@ -70,6 +78,8 @@ 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 '), diff --git a/slang/test/integration/resources/main/_expected_no_flutter.output b/slang/test/integration/resources/main/_expected_no_flutter.output index 0cb40850..76f40368 100644 --- a/slang/test/integration/resources/main/_expected_no_flutter.output +++ b/slang/test/integration/resources/main/_expected_no_flutter.output @@ -1,38 +1,74 @@ /// Generated file. Do not edit. /// -/// Original: fake/path/integration +/// Source: fake/path/integration /// To regenerate, run: `dart run slang` /// /// Locales: 1 /// 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:intl/intl.dart'; +import 'package:slang/generated.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. +/// Supported locales. /// /// 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); + 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 @@ -48,17 +84,29 @@ 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._(); // 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 +116,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 +128,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..203e294c 100644 --- a/slang/test/integration/resources/main/_expected_no_locale_handling.output +++ b/slang/test/integration/resources/main/_expected_no_locale_handling.output @@ -1,41 +1,80 @@ /// Generated file. Do not edit. /// -/// Original: fake/path/integration +/// Source: fake/path/integration /// To regenerate, run: `dart run slang` /// /// Locales: 1 /// 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:intl/intl.dart'; +import 'package:slang/generated.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. +/// Supported locales. /// /// 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); + 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 +85,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.output b/slang/test/integration/resources/main/_expected_obfuscation.output index 372790f8..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: 54 (27 per locale) +/// Strings: 58 (29 per locale) // coverage:ignore-file // ignore_for_file: type=lint @@ -215,6 +215,8 @@ 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(); @@ -382,6 +384,8 @@ 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(); @@ -506,6 +510,8 @@ 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])), @@ -569,6 +575,8 @@ 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])), 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..153b81a2 --- /dev/null +++ b/slang/test/integration/resources/main/_expected_obfuscation_de.output @@ -0,0 +1,252 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/generated.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 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, + 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.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, + 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..29a4df21 --- /dev/null +++ b/slang/test/integration/resources/main/_expected_obfuscation_en.output @@ -0,0 +1,255 @@ +/// +/// 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(); + + 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, + 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.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, + 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..c6f8974e --- /dev/null +++ b/slang/test/integration/resources/main/_expected_obfuscation_main.output @@ -0,0 +1,295 @@ +/// Generated file. Do not edit. +/// +/// Source: fake/path/integration +/// To regenerate, run: `dart run slang` +/// +/// Locales: 2 +/// Strings: 62 (31 per locale) + +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.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'; + +import 'translations_de.g.dart' deferred as l_de; +part 'translations_en.g.dart'; + +/// Supported locales. +/// +/// 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 l_de.loadLibrary(); + return l_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 l_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, + lazy: true, + ); + + 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) { + 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 { + 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 { + String get title; + String? get content => null; + + @override + 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 { + 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 { + List get stringPages; + List> get pages; + + @override + 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 { + 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_rich_text.output b/slang/test/integration/resources/main/_expected_rich_text.output index 45415b23..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: 16 - // 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, @@ -152,6 +37,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', @@ -226,10 +132,30 @@ 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) { + 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', @@ -304,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 5621ea2f..00000000 --- a/slang/test/integration/resources/main/_expected_single.output +++ /dev/null @@ -1,626 +0,0 @@ -/// Generated file. Do not edit. -/// -/// Original: fake/path/integration -/// To regenerate, run: `dart run slang` -/// -/// Locales: 2 -/// Strings: 54 (27 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}'; - - /// 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}'; - - /// 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.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.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.output b/slang/test/integration/resources/main/_expected_translation_overrides.output index 846b6e8c..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: 54 (27 per locale) +/// Strings: 58 (29 per locale) // coverage:ignore-file // ignore_for_file: type=lint @@ -239,6 +239,8 @@ 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}'; @@ -413,6 +415,8 @@ 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}'; @@ -545,6 +549,8 @@ 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 '), @@ -616,6 +622,8 @@ 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 '), 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..b4b9fa58 --- /dev/null +++ b/slang/test/integration/resources/main/_expected_translation_overrides_de.output @@ -0,0 +1,267 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/generated.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 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, + 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.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, + 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..8f8bbbc3 --- /dev/null +++ b/slang/test/integration/resources/main/_expected_translation_overrides_en.output @@ -0,0 +1,270 @@ +/// +/// 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}'; + + 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, + 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.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, + 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..b9b1dd2c --- /dev/null +++ b/slang/test/integration/resources/main/_expected_translation_overrides_main.output @@ -0,0 +1,323 @@ +/// Generated file. Do not edit. +/// +/// Source: fake/path/integration +/// To regenerate, run: `dart run slang` +/// +/// Locales: 2 +/// Strings: 62 (31 per locale) + +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.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'; + +import 'translations_de.g.dart' deferred as l_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, + sanitization: SanitizationConfig(enabled: true, prefix: 'k', caseStyle: CaseStyle.camel), + stringInterpolation: StringInterpolation.braces, + maps: ['end.pages.0', 'end.pages.1'], + pluralAuto: PluralAuto.cardinal, + pluralParameter: 'n', + pluralCardinal: [], + pluralOrdinal: [], + contexts: [], + interfaces: [], // currently not supported +); + +/// Supported locales. +/// +/// 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 l_de.loadLibrary(); + return l_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 l_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, + lazy: true, + ); + + 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) { + 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 { + 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 { + String get title; + String? get content => null; + + @override + 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 { + 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 { + List get stringPages; + List> get pages; + + @override + 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 { + 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/build_config.yaml b/slang/test/integration/resources/main/build_config.yaml index 5bd59550..c179266d 100644 --- a/slang/test/integration/resources/main/build_config.yaml +++ b/slang/test/integration/resources/main/build_config.yaml @@ -6,8 +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_format: single_file # may get changed programmatically + output_file_name: translations.cgm.dart # currently set manually for each test locale_handling: true # may get changed programmatically string_interpolation: braces timestamp: false # make every test deterministic diff --git a/slang/test/integration/resources/main/csv_compact.csv b/slang/test/integration/resources/main/csv_compact.csv index 58ecace4..acf66699 100644 --- a/slang/test/integration/resources/main/csv_compact.csv +++ b/slang/test/integration/resources/main/csv_compact.csv @@ -1,6 +1,10 @@ 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.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 a4dd5b2c..68ec381c 100644 --- a/slang/test/integration/resources/main/csv_de.csv +++ b/slang/test/integration/resources/main/csv_de.csv @@ -1,6 +1,10 @@ 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.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 1eda4804..ae576c35 100644 --- a/slang/test/integration/resources/main/csv_en.csv +++ b/slang/test/integration/resources/main/csv_en.csv @@ -1,5 +1,9 @@ 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/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/resources/main/json_de.json b/slang/test/integration/resources/main/json_de.json index b74b822e..d320db9e 100644 --- a/slang/test/integration/resources/main/json_de.json +++ b/slang/test/integration/resources/main/json_de.json @@ -1,7 +1,11 @@ { "onboarding": { "welcome": "Willkommen {fullName}", + "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 daa03cc1..07e771b6 100644 --- a/slang/test/integration/resources/main/json_en.json +++ b/slang/test/integration/resources/main/json_en.json @@ -1,7 +1,11 @@ { "onboarding": { "welcome": "Welcome {fullName}", + "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/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" diff --git a/slang/test/integration/resources/main/yaml_de.yaml b/slang/test/integration/resources/main/yaml_de.yaml index 1dafd030..ee2a2a33 100644 --- a/slang/test/integration/resources/main/yaml_de.yaml +++ b/slang/test/integration/resources/main/yaml_de.yaml @@ -1,6 +1,10 @@ onboarding: welcome: Willkommen {fullName} + 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 f95ad860..52e80383 100644 --- a/slang/test/integration/resources/main/yaml_en.yaml +++ b/slang/test/integration/resources/main/yaml_en.yaml @@ -1,6 +1,10 @@ onboarding: welcome: Welcome {fullName} + 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/integration/update.dart b/slang/test/integration/update.dart index 7d25112a..0a55ac04 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/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/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'; @@ -23,14 +23,16 @@ 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); - generateMainSplitIntegration(buildConfig, en, de); generateNoFlutter(buildConfig, simple); generateNoLocaleHandling(buildConfig, simple); generateTranslationOverrides(buildConfig, en, de); generateFallbackBaseLocale(buildConfig, en, de); + generateFallbackBaseLocaleSpecial(buildConfig, fallbackEn, fallbackDe); generateObfuscation(buildConfig, en, de); generateRichText(); @@ -44,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( @@ -92,7 +72,7 @@ void generateMainSplitIntegration( _write( path: 'main/_expected_main', - content: result.header, + content: result.main, ); _write( @@ -104,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) { @@ -120,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, ); } @@ -140,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, ); } @@ -161,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')]!, ); } @@ -184,11 +169,58 @@ 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_en', + content: result.translations[I18nLocale.fromString('en')]!, + ); + + _write( + path: 'main/_expected_fallback_base_locale_de', + content: result.translations[I18nLocale.fromString('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), + ), + ); + + _write( + path: 'main/_expected_fallback_base_locale_special_main', + content: result.main, + ); _write( - path: 'main/_expected_fallback_base_locale', - 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')]!, ); } @@ -210,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_main', + content: result.main, + ); + + _write( + path: 'main/_expected_obfuscation_en', + content: result.translations[I18nLocale.fromString('en')]!, + ); _write( - path: 'main/_expected_obfuscation', - content: result, + path: 'main/_expected_obfuscation_de', + content: result.translations[I18nLocale.fromString('de')]!, ); } @@ -230,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..e05ce9b6 100644 --- a/slang/test/unit/api/locale_settings_test.dart +++ b/slang/test/unit/api/locale_settings_test.dart @@ -1,8 +1,9 @@ -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:slang/src/builder/model/sanitization_config.dart'; import 'package:test/test.dart'; void main() { @@ -24,16 +25,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!; @@ -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, @@ -76,5 +82,5 @@ class _AppLocaleUtils class _LocaleSettings extends BaseLocaleSettings { - _LocaleSettings() : super(utils: _AppLocaleUtils()); + _LocaleSettings() : super(utils: _AppLocaleUtils(), lazy: false); } diff --git a/slang/test/unit/api/secret_test.dart b/slang/test/unit/api/secret_test.dart index 3b291563..69bf93c3 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/api/secret.dart'; +import 'package:slang/src/builder/utils/encryption_utils.dart'; import 'package:test/test.dart'; void main() { diff --git a/slang/test/unit/api/singleton_test.dart b/slang/test/unit/api/singleton_test.dart index d4f87f59..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 @@ -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, diff --git a/slang/test/unit/api/translation_overrides_test.dart b/slang/test/unit/api/translation_overrides_test.dart index 825919db..54ba7435 100644 --- a/slang/test/unit/api/translation_overrides_test.dart +++ b/slang/test/unit/api/translation_overrides_test.dart @@ -1,11 +1,18 @@ -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:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.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'; +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() { + setUpAll(() { + initializeDateFormatting(); + }); + group('string', () { test('Should return a plain string', () { final meta = _buildMetaWithOverrides({ @@ -15,6 +22,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}', @@ -25,7 +40,17 @@ 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', + }); + 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', @@ -35,34 +60,124 @@ 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', }); 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}', + }, 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), + }); + 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'); + }); }); } TranslationMetadata _buildMetaWithOverrides( - Map overrides, -) { + Map overrides, { + String? locale, + Map formatters = const {}, +}) { final utils = _Utils(); - return utils - .buildWithOverridesFromMap( - locale: FakeAppLocale(languageCode: 'und'), - isFlatMap: false, - map: overrides, - ) - .$meta; + final translations = utils.buildWithOverridesFromMapSync( + locale: FakeAppLocale( + languageCode: locale ?? 'en', + types: formatters, + ), + isFlatMap: false, + map: overrides, + ); + return translations.$meta; } 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/build_config_builder_test.dart b/slang/test/unit/builder/build_config_builder_test.dart deleted file mode 100644 index b56cda33..00000000 --- a/slang/test/unit/builder/build_config_builder_test.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:slang/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: - enum: - - male - - female - default_parameter: gender - render_enum: false - '''); - - 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, []); - }); - }); - - group(RawConfigBuilder.fromMap, () { - test('context gender default', () { - final result = RawConfigBuilder.fromMap( - { - 'contexts': { - 'gender_context': { - 'enum': [ - 'male', - 'female', - 'neutral', - ], - }, - }, - }, - ); - - 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': { - 'gender_context': { - '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', () { - 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/builder/slang_file_collection_builder_test.dart b/slang/test/unit/builder/slang_file_collection_builder_test.dart index 000c5453..c23dd9f8 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) { @@ -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,11 +57,12 @@ void main() { _file('lib/i18n/strings_de.i18n.json'), _file('lib/i18n/strings-fr-FR.i18n.json'), ], + showWarning: false, ); 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'); }); @@ -50,6 +76,7 @@ void main() { files: [ _file('lib/i18n/dialogs.i18n.json'), ], + showWarning: false, ); expect(model.files.length, 1); @@ -97,9 +124,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'); }); }); } 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..837a7dc9 --- /dev/null +++ b/slang/test/unit/builder/text/l10n_override_parser_test.dart @@ -0,0 +1,109 @@ +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'); + }); + + 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 new file mode 100644 index 00000000..e725fa78 --- /dev/null +++ b/slang/test/unit/builder/text/l10n_parser_test.dart @@ -0,0 +1,107 @@ +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 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, + 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/param_parser_test.dart b/slang/test/unit/builder/text/param_parser_test.dart new file mode 100644 index 00000000..8026bb58 --- /dev/null +++ b/slang/test/unit/builder/text/param_parser_test.dart @@ -0,0 +1,66 @@ +import 'package:slang/src/builder/builder/text/param_parser.dart'; +import 'package:slang/src/builder/model/enums.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'); + }); + }); + + 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!'); + }); + }); +} diff --git a/slang/test/unit/builder/translation_model_builder_test.dart b/slang/test/unit/builder/translation_model_builder_test.dart index 24119c08..0d7e7d6c 100644 --- a/slang/test/unit/builder/translation_model_builder_test.dart +++ b/slang/test/unit/builder/translation_model_builder_test.dart @@ -1,26 +1,29 @@ -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/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/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, - 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( @@ -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'}, }, @@ -51,11 +54,13 @@ void main() { final mapNode = result.root.entries['my_map'] as ObjectNode; expect((mapNode.entries['my_value 3'] as StringTextNode).content, 'cool'); }); + }); + group('Linked Translations', () { 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 +74,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 +88,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,35 +104,51 @@ void main() { test('linked translation with plural', () { final result = TranslationModelBuilder.build( buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, 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(), + locale: _locale, + 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: [ ContextType( enumName: 'GenderCon', - enumValues: ['male', 'female'], - paths: [], defaultParameter: 'gender', generateEnum: true, ), ]).toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, map: { - 'a': { + 'a(context=GenderCon)': { 'male': 'MALE', 'female': r'FEMALE $p1', }, @@ -136,10 +157,32 @@ 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)}'); }); + }); + + 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: [ @@ -168,7 +211,7 @@ void main() { }, ), ]).toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + locale: _locale, map: { 'myEntry': { 'myList': [], @@ -190,24 +233,338 @@ void main() { (objectNode2.entries['myList'] as ListNode).genericType, 'MyType2'); }); - test('Should not include context type if values are unspecified', () { + 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', + }, + }, + } + }, + locale: _locale, + ); + + _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', + }, + }, + } + }, + locale: _locale, + ); + + _checkInterfaceResult(resultUsingConfig); + }); + }); + + group('Sanitization', () { + test('Should sanitize reserved keyword', () { final result = TranslationModelBuilder.build( - buildConfig: RawConfig.defaultConfig.copyWith(contexts: [ - ContextType( - enumName: 'GenderCon', - enumValues: null, - paths: [], - defaultParameter: 'gender', - generateEnum: true, - ), - ]).toBuildModelConfig(), - localeDebug: RawConfig.defaultBaseLocale, + buildConfig: RawConfig.defaultConfig.toBuildModelConfig(), + locale: _locale, map: { - 'a': 'b', + 'continue': 'Continue', }, ); - expect(result.contexts, []); + 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('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 + .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) { + 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'); +} diff --git a/slang/test/unit/decoder/arb_decoder_test.dart b/slang/test/unit/decoder/arb_decoder_test.dart index 0fbb08a6..3b677ad4 100644 --- a/slang/test/unit/decoder/arb_decoder_test.dart +++ b/slang/test/unit/decoder/arb_decoder_test.dart @@ -1,36 +1,36 @@ 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}'}, ); }); - test('Should decode with meta', () { + test('Should decode with description', () { expect( _decodeArb({ - "hello": "world", - "@hello": {"description": "This is a description"}, + 'hello': 'world', + '@hello': {'description': 'This is a description'}, }), { 'hello': 'world', @@ -39,11 +39,91 @@ 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 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({ - "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 +138,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)': { @@ -71,11 +151,25 @@ 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({ - "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 +184,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 +206,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/model/i18n_data_test.dart b/slang/test/unit/model/i18n_data_test.dart index ed7b758b..ac3e2f0f 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]) { @@ -17,6 +17,7 @@ I18nData _i18n(String locale, [bool base = false]) { ), contexts: [], interfaces: [], + types: {}, ); } 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 75c88884..b1670111 100644 --- a/slang/test/unit/model/node_test.dart +++ b/slang/test/unit/model/node_test.dart @@ -1,5 +1,6 @@ -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/node.dart'; +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'; import '../../util/text_node_builder.dart'; @@ -271,6 +272,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: { @@ -280,6 +289,35 @@ 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'}); + }); + + 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, () { @@ -351,6 +389,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 +461,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'}); + }); }); }); diff --git a/slang/test/unit/runner/analyze_test.dart b/slang/test/unit/runner/analyze_test.dart index ef62a6a1..62b9e003 100644 --- a/slang/test/unit/runner/analyze_test.dart +++ b/slang/test/unit/runner/analyze_test.dart @@ -1,3 +1,8 @@ +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'; import 'package:slang/src/runner/analyze.dart'; import 'package:test/test.dart'; @@ -28,5 +33,206 @@ 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'); + }); + }); + + 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 translations', () { + final result = _getUnusedTranslations({ + 'en': { + 'a': 'A', + }, + 'de': { + 'a': 'A', + 'b': 'B', + }, + }); + + 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': { + 'a': 'A', + }, + 'de': { + 'a': 'A @:b', + 'b': 'B', + }, + }); + + expect(result[I18nLocale(language: 'de')], isEmpty); + }); + }); +} + +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( + locale: I18nLocale(language: entry.key), + translations: entry.value, + ); + } + + return TranslationModelListBuilder.build( + RawConfig.defaultConfig, + map, + ); } 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/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/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']); + }); +} diff --git a/slang/test/unit/utils/path_utils_test.dart b/slang/test/unit/utils/path_utils_test.dart index 1459f3df..74e59ba7 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/model/i18n_locale.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/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'}); + }); + }); +} diff --git a/slang/test/unit/utils/regex_utils_test.dart b/slang/test/unit/utils/regex_utils_test.dart index 3d582d3c..99774365 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() { @@ -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'); @@ -185,6 +191,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/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', + ); + }); +} diff --git a/slang/test/unit/utils/secret_test.dart b/slang/test/unit/utils/secret_test.dart index d1bf853d..60792167 100644 --- a/slang/test/unit/utils/secret_test.dart +++ b/slang/test/unit/utils/secret_test.dart @@ -1,12 +1,12 @@ -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:slang/src/builder/model/obfuscation_config.dart'; import 'package:test/test.dart'; 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"); }); }); diff --git a/slang/test/unit/utils/string_extensions_test.dart b/slang/test/unit/utils/string_extensions_test.dart index 1c66ab5a..e72534c1 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/model/enums.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..d882c941 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) { @@ -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', () { 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/test/util/text_node_builder.dart b/slang/test/util/text_node_builder.dart index 93c62e11..1a18922c 100644 --- a/slang/test/util/text_node_builder.dart +++ b/slang/test/util/text_node_builder.dart @@ -1,21 +1,29 @@ -import 'package:slang/builder/model/enums.dart'; -import 'package:slang/builder/model/node.dart'; +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'; + +final _locale = I18nLocale(language: 'en'); StringTextNode textNode( String raw, StringInterpolation interpolation, { CaseStyle? paramCase, Map>? linkParamMap, + Map formatters = const {}, }) { return StringTextNode( path: '', rawPath: '', modifiers: {}, + locale: _locale, + types: formatters, raw: raw, comment: null, interpolation: interpolation, paramCase: paramCase, shouldEscape: true, + handleTypes: true, linkParamMap: linkParamMap, ); } @@ -31,10 +39,13 @@ RichTextNode richTextNode( rawPath: '', modifiers: {}, comment: null, + locale: _locale, + types: {}, raw: raw, interpolation: interpolation, paramCase: paramCase, shouldEscape: true, + handleTypes: true, linkParamMap: linkParamMap, ); } diff --git a/slang_build_runner/CHANGELOG.md b/slang_build_runner/CHANGELOG.md index 358a89ef..cf5f1b25 100644 --- a/slang_build_runner/CHANGELOG.md +++ b/slang_build_runner/CHANGELOG.md @@ -1,3 +1,32 @@ +## 4.4.0 + +- bump `slang` to `4.4.0` + +## 4.3.0 + +- bump `slang` to `4.3.0` + +## 4.2.0 + +- bump `slang` to `4.2.0` + +## 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` + +## 3.30.0 + +- Bump `slang` to `3.30.0` + +## 3.29.0 + +- Bump `slang` to `3.29.0` + ## 3.28.0 - Bump `slang` to `3.28.0` 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_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..a28fd039 100644 --- a/slang_build_runner/lib/slang_build_runner.dart +++ b/slang_build_runner/lib/slang_build_runner.dart @@ -1,17 +1,26 @@ import 'dart:async'; import 'package:build/build.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'; -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:dart_style/dart_style.dart'; import 'package:glob/glob.dart'; +// ignore: implementation_imports +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'; +// ignore: implementation_imports +import 'package:slang/src/builder/utils/path_utils.dart'; /// Static entry point for build_runner Builder i18nBuilder(BuilderOptions options) { @@ -27,7 +36,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 { @@ -66,44 +75,32 @@ 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 + + final formatter = DartFormatter( + pageWidth: config.format.width, + ); + + FileUtils.writeFile( + path: BuildResultPaths.mainPath(outputFilePath), + content: result.main.formatted(config, formatter), + ); + + 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.formatted(config, formatter), ); - 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!, - ); - } } } @@ -114,13 +111,16 @@ 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); } + + /// 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 148c1deb..4fee804e 100644 --- a/slang_build_runner/pubspec.yaml +++ b/slang_build_runner/pubspec.yaml @@ -1,14 +1,18 @@ 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: 4.4.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 + dart_style: ^2.3.0 glob: ^2.0.2 # Use a tight version to ensure that all features are available - slang: '>=3.28.0 <3.29.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 754b870d..adf8ba85 100644 --- a/slang_flutter/CHANGELOG.md +++ b/slang_flutter/CHANGELOG.md @@ -1,3 +1,31 @@ +## 4.4.0 + +- bump `slang` to `4.4.0` + +## 4.3.0 + +- bump `slang` to `4.3.0` + +## 4.2.0 + +- bump `slang` to `4.2.0` + +## 4.1.0 + +- bump `slang` to `4.1.0` + +## 4.0.0 + +- Bump `slang` to `4.0.0` + +## 3.30.0 + +- Bump `slang` to `3.30.0` + +## 3.29.0 + +- Bump `slang` to `3.29.0` + ## 3.28.0 - Bump `slang` to `3.28.0` 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_flutter/lib/slang_flutter.dart b/slang_flutter/lib/slang_flutter.dart index 87bb9f4a..c366a014 100644 --- a/slang_flutter/lib/slang_flutter.dart +++ b/slang_flutter/lib/slang_flutter.dart @@ -1,13 +1,16 @@ 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/generated.dart'; +import 'package:slang/overrides.dart'; +// ignore: implementation_imports +import 'package:slang/src/builder/model/pluralization.dart'; 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. @@ -45,6 +48,7 @@ class BaseFlutterLocaleSettings, T extends BaseTranslations> extends BaseLocaleSettings { BaseFlutterLocaleSettings({ required super.utils, + required super.lazy, }); @override @@ -59,9 +63,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. @@ -72,14 +82,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, ), @@ -89,15 +97,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() { @@ -135,17 +141,19 @@ 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]!; + this.translations = widget.settings.translationMap[localeTyped]; }); } @override Widget build(BuildContext context) { + translations ??= widget.settings.currentTranslations; return InheritedLocaleData( - translations: translations, + translations: translations!, child: widget.child, ); } 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. diff --git a/slang_flutter/pubspec.yaml b/slang_flutter/pubspec.yaml index dc61b054..cc1e5be9 100644 --- a/slang_flutter/pubspec.yaml +++ b/slang_flutter/pubspec.yaml @@ -1,18 +1,20 @@ name: slang_flutter description: Flutter support for slang. This library provides helpful Flutter API. -version: 3.28.0 +version: 4.4.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.28.0 <3.29.0' + slang: '>=4.4.0 <4.5.0' dev_dependencies: flutter_test: 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..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 @@ -1,36 +1,37 @@ /// 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, unused_element +// ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; -import 'package:slang/builder/model/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; -const AppLocale _baseLocale = AppLocale.en; +import 'strings_a_de.g.dart' deferred as _$de; +part 'strings_a_en.g.dart'; -/// Supported locales, see extension methods below. +/// Supported locales. /// /// 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: _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 +39,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 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]. - _StringsAEn get translations => LocaleSettings.instance.translationMap[this]!; + Translations get translations => + LocaleSettings.instance.getTranslations(this); } /// Method A: Simple @@ -54,7 +99,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 +116,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,31 +132,32 @@ 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 { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); +class LocaleSettings + extends BaseFlutterLocaleSettings { + LocaleSettings._() + : super( + utils: AppLocaleUtils.instance, + lazy: true, + ); 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, + 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,12 +168,37 @@ 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); + : super( + baseLocale: AppLocale.en, + locales: AppLocale.values, + ); static final instance = AppLocaleUtils._(); @@ -152,97 +216,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..1a9c475b --- /dev/null +++ b/slang_flutter/test/integration/multi_package/gen/strings_a_de.g.dart @@ -0,0 +1,56 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:slang/generated.dart'; +import 'strings_a.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 + 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 TranslationsDe { + 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..943e43f6 --- /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, unused_import + +part of 'strings_a.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 + 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..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 @@ -1,36 +1,37 @@ /// 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, unused_element +// ignore_for_file: type=lint, unused_import import 'package:flutter/widgets.dart'; -import 'package:slang/builder/model/node.dart'; +import 'package:slang/generated.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; -const AppLocale _baseLocale = AppLocale.en; +import 'strings_b_de.g.dart' deferred as _$de; +part 'strings_b_en.g.dart'; -/// Supported locales, see extension methods below. +/// Supported locales. /// /// 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: _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 +39,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 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]. - _StringsBEn get translations => LocaleSettings.instance.translationMap[this]!; + Translations get translations => + LocaleSettings.instance.getTranslations(this); } /// Method A: Simple @@ -54,7 +99,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 +116,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,31 +132,32 @@ 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 { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); +class LocaleSettings + extends BaseFlutterLocaleSettings { + LocaleSettings._() + : super( + utils: AppLocaleUtils.instance, + lazy: true, + ); 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, + 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,12 +168,37 @@ 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); + : super( + baseLocale: AppLocale.en, + locales: AppLocale.values, + ); static final instance = AppLocaleUtils._(); @@ -152,97 +216,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..13aa5ae5 --- /dev/null +++ b/slang_flutter/test/integration/multi_package/gen/strings_b_de.g.dart @@ -0,0 +1,56 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:slang/generated.dart'; +import 'strings_b.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 + 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 TranslationsDe { + 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..496a0506 --- /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, unused_import + +part of 'strings_b.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 + 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/CHANGELOG.md b/slang_gpt/CHANGELOG.md index cabbb9aa..c6cf2b8c 100644 --- a/slang_gpt/CHANGELOG.md +++ b/slang_gpt/CHANGELOG.md @@ -1,3 +1,20 @@ +## 0.11.0 + +- deps: bump slang to 4.0.0 + +## 0.10.3 + +- feat: add `gpt-4o-mini` model + +## 0.10.2 + +- feat: add `gpt-4o`, `gpt-4-turbo` models + +## 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/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 diff --git a/slang_gpt/README.md b/slang_gpt/README.md index e7fb0d3e..2233de23 100644 --- a/slang_gpt/README.md +++ b/slang_gpt/README.md @@ -71,11 +71,16 @@ 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 | +| 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 | +| `gpt-4o-mini` | Open AI | 128000 | $0.00015 | $0.0006 | + +1k tokens = 750 words (English) ## GPT context length 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/model/gpt_model.dart b/slang_gpt/lib/model/gpt_model.dart index 6c958b91..fea03848 100644 --- a/slang_gpt/lib/model/gpt_model.dart +++ b/slang_gpt/lib/model/gpt_model.dart @@ -2,27 +2,40 @@ enum GptProvider { openai, } +// ignore_for_file: constant_identifier_names 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, + costPer1kInputToken: 0.01, + costPer1kOutputToken: 0.03), + gpt4o('gpt-4o', GptProvider.openai, + 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( this.id, this.provider, { required this.defaultInputLength, - required this.costPerInputToken, - required this.costPerOutputToken, + required this.costPer1kInputToken, + required this.costPer1kOutputToken, }); /// The id of this model. @@ -41,8 +54,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/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 ef0ff8ff..8bea8560 100644 --- a/slang_gpt/lib/runner.dart +++ b/slang_gpt/lib/runner.dart @@ -2,13 +2,20 @@ 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/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/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'; +// 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/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 ba65289c..ed4a5389 100644 --- a/slang_gpt/lib/util/logger.dart +++ b/slang_gpt/lib/util/logger.dart @@ -1,9 +1,11 @@ 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/model/i18n_locale.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..409cf432 100644 --- a/slang_gpt/lib/util/maps.dart +++ b/slang_gpt/lib/util/maps.dart @@ -1,8 +1,9 @@ -import 'package:slang/builder/utils/node_utils.dart'; +// ignore: implementation_imports +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..c376cbd7 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.0 +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.25.0 + slang: '>=4.0.0 <5.0.0' dev_dependencies: lints: ^2.0.0 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';