diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..6b99e45 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,19 @@ +name: Publish to pub.dev + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+*' # tag pattern on pub.dev: 'v' +# Publish using custom workflow +jobs: + publish: + permissions: + id-token: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dart-lang/setup-dart@v1 + - name: Install dependencies + run: dart pub get + - name: Publish + run: dart pub publish --force diff --git a/CHANGELOG.md b/CHANGELOG.md index effe43c..ca6a49f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ -## 1.0.0 +## 2.0.0 +- Rewrite package to follow new `mineral: ^3.1.0` version of Core. + +## 1.1.0 +- Write readme. +## 1.0.0 - Initial version. diff --git a/README.md b/README.md index 369ec86..2a7145a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,74 @@ # 🌐 I18n -The I18n module allows you to translate your textual content into multiple languages from yaml files while injecting your variables. +The i18n module has been designed exclusively for the Mineral framework, it allows you to translate your textual content through yaml files. + +## Register the module + +After installing the module, please register it within `./src/main.dart` following the scheme below : +```dart +Future main () async { + final i18n = I18n([Lang.fr, Lang.enGB]); + + Kernel kernel = Kernel() + ..intents.defined(all: true) + ..plugins.use([i18n]); + + await kernel.init(); +} +``` + +## Translate your textual content +As a first step, please create a `lang` folder containing `{lang}.yaml` translation files. + +We consider the following software structure : +``` +lang/ + foo/ + fr.yaml + en.yaml +``` +The files will contain the following keys : +```yaml +# lang/foo/fr.yaml +bar: bar en français ! +``` +```yaml +# lang/foo/en.yaml +bar: bar in english ! +``` + +Then we can use the `t()` function to translate our key path. + +```dart +import 'package:mineral_i18n/mineral_i18n.dart'; + +class Foo extends MineralEvent with Translation { + Future handle (Ready event) async { + final String sentence = t(Lang.fr, 'foo.bar'); + print(sentence); // bar en français ! + + final String sentence = t(Lang.en_GB, 'foo.bar'); + print(sentence); // bar in english ! + } +} +``` + +## Injecting variables +The i18n module integrates the possibility of using variables thanks to special characters which will be replaced by the associated variable. + +We consider the file `lang/foo/en.yaml` as containing the following key set : +```yaml +bar: {framework} is my favourite framework ! +``` + +Our string is now waiting for a variable named xx which we will give it when we call the `t()` function. +```dart +import 'package:mineral_i18n/mineral_i18n.dart'; + +class Foo extends MineralEvent with Translation { + Future handle (Ready event) async { + final String sentence = t(Lang.en_GB, 'foo.bar', { 'framework': 'Mineral' }); + print(sentence); // Mineral is my favourite framework ! + } +} +``` diff --git a/lib/mineral_i18n.dart b/lib/mineral_i18n.dart index 2235815..f4f68da 100644 --- a/lib/mineral_i18n.dart +++ b/lib/mineral_i18n.dart @@ -1,4 +1,5 @@ library i18n; export 'src/i18n.dart'; -export 'src/lang.dart'; +export 'src/contracts/i18n_contract.dart'; +export 'src/mixins/translation.dart'; \ No newline at end of file diff --git a/lib/src/contracts/i18n_contract.dart b/lib/src/contracts/i18n_contract.dart new file mode 100644 index 0000000..8ce1782 --- /dev/null +++ b/lib/src/contracts/i18n_contract.dart @@ -0,0 +1,6 @@ +import 'dart:io'; + +abstract class I18nContract { + List get languages; + Directory get langPath; +} diff --git a/lib/src/i18n.dart b/lib/src/i18n.dart index f7a09d4..e8d1102 100644 --- a/lib/src/i18n.dart +++ b/lib/src/i18n.dart @@ -1,49 +1,50 @@ import 'dart:io'; -import 'package:mineral_i18n/src/lang.dart'; -import 'package:mineral_i18n/src/translation.dart'; -import 'package:mineral_ioc/ioc.dart'; +import 'package:mineral_contract/mineral_contract.dart'; +import 'package:mineral_i18n/src/contracts/i18n_contract.dart'; +import 'package:mineral_i18n/src/managers/translation_manager.dart'; import 'package:path/path.dart'; import 'package:yaml/yaml.dart'; -class I18n { - final String label = 'I18n'; - static String get namespace => 'Mineral/Plugins/I18n'; - late final Directory root; +class I18n extends MineralPackageContract implements I18nContract { + TranslationManager translationManager = TranslationManager(); - Translation translation = Translation(); - final List _languages; + final List _languages; final String folder; - I18n(this._languages, { this.folder = 'lang' }); + I18n(this._languages, { this.folder = 'lang' }): super('I18n', 'Official package'); /// ## Languages allowed /// ```dart /// final List allowedLanguages = i18n.languages; /// ``` - List get languages => _languages; + @override + List get languages => _languages; + /// ## Languages root directory /// ```dart - /// final Directory folder = i18n.langDirectory; + /// final Directory folder = i18n.langPath; /// ``` - Directory get langDirectory => Directory(join(root.path, folder)); - - /// Insert languages into i18n instance - void registerLanguages() { - for (final Lang lang in _languages) { - translation.cache.putIfAbsent(lang.normalize, () => {}); - } - } + @override + Directory get langPath => Directory(join(root.path, folder)); /// Initialize i18n package + @override Future init () async { - if (!await langDirectory.exists()) { + if (!await langPath.exists()) { throw Exception('Missing $folder folder'); } registerLanguages(); - _walk(langDirectory); + _walk(langPath); + } + + /// Insert languages into i18n instance + void registerLanguages() { + for (final lang in _languages) { + translationManager.cache.putIfAbsent(lang, () => {}); + } } /// Recursively browses folders to extract translations @@ -53,7 +54,7 @@ class I18n { for (final item in items) { if (item is Directory) { - translation.cache.putIfAbsent(location, () => {}); + translationManager.cache.putIfAbsent(location, () => {}); _walk(item); } @@ -61,40 +62,14 @@ class I18n { final filename = item.path.split(separator).last.split('.').first; final content = loadYaml(item.readAsStringSync()); - if (translation.cache[filename] is Map) { - if (item.parent.path == langDirectory.path) { - translation.cache[location] = content; + if (translationManager.cache[filename] is Map) { + if (item.parent.path == langPath.path) { + translationManager.cache[location] = content; } else { - translation.cache[filename].putIfAbsent(location, () => content); + translationManager.cache[filename].putIfAbsent(location, () => content); } } } } } } - -/// Translates the sentence defined by the key set into the requested language. -/// Replacement parameters can be injected. -/// ```dart -/// final String sentence = t(Lang.enGB, 'foo.bar'); -/// print(sentence) 👈 'Hello {user}' -/// -/// final String sentence = t(Lang.enGB, 'foo.bar', { 'user': 'Freeze' }); -/// print(sentence) 👈 'Hello Freeze' -/// ``` -String t (Lang lang, String key, { Map? replacers }) { - final I18n i18n = ioc.singleton(I18n.namespace); - dynamic target = i18n.translation.cache[lang.normalize]; - - for (final element in key.split('.')) { - target = target[element]; - } - - if (replacers != null) { - for (final replacer in replacers.entries) { - target = target.toString().replaceAll('{${replacer.key}}', replacer.value); - } - } - - return target; -} diff --git a/lib/src/lang.dart b/lib/src/lang.dart deleted file mode 100644 index 1834162..0000000 --- a/lib/src/lang.dart +++ /dev/null @@ -1,43 +0,0 @@ -enum Lang { - da('da', null), - de('de', null), - enGB('en-GB', 'en'), - enUS('en-US', 'en'), - esES('es-ES', 'es'), - fr('fr', null), - hr('hr', null), - it('it', null), - lt('lt', null), - hu('hu', null), - nl('nl', null), - no('no', null), - pl('pl', null), - ptBR('pt-BR', 'pt'), - ro('ro', null), - fi('fi', null), - svSE('sv-SE', 'sv'), - vi('vi', null), - tr('tr', null), - cs('cs', null), - el('el', null), - bg('bg', null), - ru('ru', null), - uk('uk', null), - hi('hi', null), - th('th', null), - zhCN('zh-CN', 'zh'), - ja('ja', null), - zhTW('zh-TW', null), - ko('ko', null); - - final String _locale; - final String? _normalize; - - const Lang(this._locale, this._normalize); - - String get locale => _locale; - String get normalize => _normalize ?? _locale; - - @override - String toString() => locale; -} diff --git a/lib/src/translation.dart b/lib/src/managers/translation_manager.dart similarity index 71% rename from lib/src/translation.dart rename to lib/src/managers/translation_manager.dart index 6f0259d..6517312 100644 --- a/lib/src/translation.dart +++ b/lib/src/managers/translation_manager.dart @@ -1,13 +1,11 @@ -import 'package:mineral_i18n/src/lang.dart'; - -class Translation { +class TranslationManager { final Map _cache = {}; Map get cache => _cache; - void addTranslations (Lang lang, Map translations) { + void addTranslations (final String lang, Map translations) { for (final translation in translations.entries) { - dynamic location = _cache[lang.normalize]; + dynamic location = _cache[lang] ?? {}; List keys = translation.key.split('.'); for (final key in keys) { diff --git a/lib/src/mixins/translation.dart b/lib/src/mixins/translation.dart new file mode 100644 index 0000000..14f64d8 --- /dev/null +++ b/lib/src/mixins/translation.dart @@ -0,0 +1,28 @@ +import 'package:mineral_i18n/mineral_i18n.dart'; +import 'package:mineral_ioc/ioc.dart'; + +mixin Translation { + /// Translates the sentence defined by the key set into the requested language. + /// Replacement parameters can be injected. + /// ```dart + /// final String sentence = t('en', 'foo.bar'); + /// print(sentence); 👈 'Hello {user}' + /// + /// final String sentence = t('en', 'foo.bar', replacers { 'user': 'Freeze' }); + /// print(sentence); 👈 'Hello Freeze' + /// ``` + String t (String lang, String key, { Map? replacers }) { + dynamic target = ioc.use().translationManager.cache[lang]; + for (final element in key.split('.')) { + target = target[element]; + } + + if (replacers != null) { + for (final replacer in replacers.entries) { + target = target.toString().replaceAll('{${replacer.key}}', replacer.value); + } + } + + return target; + } +} diff --git a/pubspec.lock b/pubspec.lock index ae62471..fea476b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -35,28 +35,28 @@ packages: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.16.0" + version: "1.17.0" convert: dependency: transitive description: name: convert url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.0" coverage: dependency: transitive description: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.6.0" + version: "1.6.1" crypto: dependency: transitive description: @@ -84,7 +84,14 @@ packages: name: glob url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.5" http_multi_server: dependency: transitive description: @@ -98,7 +105,7 @@ packages: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.0.2" io: dependency: transitive description: @@ -112,21 +119,21 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.4" + version: "0.6.5" lints: dependency: "direct dev" description: name: lints url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" logging: dependency: transitive description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.1.0" matcher: dependency: transitive description: @@ -147,14 +154,49 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" + mineral: + dependency: "direct main" + description: + name: mineral + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + mineral_cli: + dependency: transitive + description: + name: mineral_cli + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + mineral_console: + dependency: transitive + description: + name: mineral_console + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + mineral_contract: + dependency: "direct main" + description: + name: mineral_contract + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + mineral_environment: + dependency: transitive + description: + name: mineral_environment + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" mineral_ioc: dependency: "direct main" description: name: mineral_ioc url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "2.0.0" node_preamble: dependency: transitive description: @@ -175,7 +217,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.8.3" pool: dependency: transitive description: @@ -189,14 +231,21 @@ packages: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.3" + recase: + dependency: transitive + description: + name: recase + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.3.2" + version: "1.4.0" shelf_packages_handler: dependency: transitive description: @@ -217,14 +266,14 @@ packages: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" source_map_stack_trace: dependency: transitive description: name: source_map_stack_trace url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" source_maps: dependency: transitive description: @@ -252,7 +301,7 @@ packages: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: @@ -288,6 +337,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.16" + tint: + dependency: transitive + description: + name: tint + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" typed_data: dependency: transitive description: @@ -308,7 +364,7 @@ packages: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" web_socket_channel: dependency: transitive description: @@ -331,4 +387,4 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.17.1 <3.0.0" + dart: ">=2.18.1 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8952930..62f5ef9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: mineral_i18n description: I18n allows you to translate your text content into multiple languages -version: 1.0.0 +version: 2.0.0 repository: https://github.com/mineral-dart/i18n # homepage: https://www.example.com @@ -14,9 +14,11 @@ platforms: windows: dependencies: - mineral_ioc: ^1.0.0 + mineral_ioc: ^2.0.0 + mineral: ^3.1.0 path: ^1.8.2 yaml: ^3.1.1 + mineral_contract: ^1.1.0 dev_dependencies: lints: ^2.0.0 diff --git a/test/mineral_i18n_test.dart b/test/mineral_i18n_test.dart index 3d94f96..fda63ce 100644 --- a/test/mineral_i18n_test.dart +++ b/test/mineral_i18n_test.dart @@ -2,32 +2,38 @@ import 'package:mineral_i18n/mineral_i18n.dart'; import 'package:mineral_ioc/ioc.dart'; import 'package:test/test.dart'; +class Foo with Translation { + Function get translator => t; +} + void main() { + final foo = Foo(); final String targetTranslation = 'foo.bar'; - final i18n = I18n([Lang.fr, Lang.enGB]) + final i18n = I18n(['fr', 'en']) ..registerLanguages(); - i18n.translation - ..addTranslations(Lang.fr, { targetTranslation: 'Salut {user}' }) - ..addTranslations(Lang.enGB, { targetTranslation: 'Hello {user}' }); + i18n.translationManager + ..addTranslations('fr', { targetTranslation: 'Salut {user}' }) + ..addTranslations('en', { targetTranslation: 'Hello {user}' }); test('can register i18n into mineral ioc', () { - ioc.bind(namespace: I18n.namespace, service: i18n); - expect(ioc.singleton(I18n.namespace), equals(i18n)); + ioc.bind((ioc) => I18n(['fr', 'en'])); + + expect(ioc.use(), equals(i18n)); }); test('registered lang is two', () { expect(i18n.languages.length, equals(2)); - expect(i18n.languages.first, equals(Lang.fr)); + expect(i18n.languages.first, equals('fr')); }); test('can translate sentence without variables', () { - expect(t(Lang.fr, targetTranslation), equals('Salut {user}')); - expect(t(Lang.enGB, targetTranslation), equals('Hello {user}')); + expect(foo.translator('fr', targetTranslation), equals('Salut {user}')); + expect(foo.translator('en', targetTranslation), equals('Hello {user}')); }); test('can translate sentence with variables', () { - expect(t(Lang.fr, targetTranslation, replacers: { 'user': 'Freeze' }), equals('Salut Freeze')); - expect(t(Lang.enGB, targetTranslation, replacers: { 'user': 'Freeze' }), equals('Hello Freeze')); + expect(foo.translator('fr', targetTranslation, replacers: { 'user': 'Freeze' }), equals('Salut Freeze')); + expect(foo.translator('en', targetTranslation, replacers: { 'user': 'Freeze' }), equals('Hello Freeze')); }); }