diff --git a/.gitattributes b/.gitattributes index 3c11f2a9..ef440fe2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ pkgs/intl4x/lib/src/bindings/* linguist-generated=true +pkgs/intl4x/**/*.g.dart linguist-generated=true diff --git a/.github/workflows/messages.yml b/.github/workflows/messages.yml index f217bd44..171d1630 100644 --- a/.github/workflows/messages.yml +++ b/.github/workflows/messages.yml @@ -21,28 +21,34 @@ jobs: defaults: run: working-directory: pkgs/messages - strategy: - matrix: - sdk: [stable, dev] # {pkgs.versions} - dependencies: [path, published] - include: - - sdk: stable - run-tests: true steps: - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 - - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + + - uses: flutter-actions/setup-flutter@61f93c6f11e0234fa9ec4df59362c9699b979660 with: - sdk: ${{matrix.sdk}} + channel: beta + + - run: dart pub get - run: dart pub get + working-directory: pkgs/messages/example - - run: (cd example_json; dart pub get) + - run: flutter pub get + working-directory: pkgs/messages/examples_flutter/my_application + + - run: flutter pub get + working-directory: pkgs/messages/examples_flutter/my_shopping_cart - run: dart analyze --fatal-infos - run: dart format --output=none --set-exit-if-changed . - if: ${{matrix.run-tests}} - run: dart test - if: ${{matrix.run-tests}} + + - name: Regenerate and run example + working-directory: pkgs/messages/example + run: | + dart run messages + git diff --exit-code + dart run diff --git a/.github/workflows/messages_builder.yml b/.github/workflows/messages_builder.yml index 0f07d85a..9c3de7c5 100644 --- a/.github/workflows/messages_builder.yml +++ b/.github/workflows/messages_builder.yml @@ -23,9 +23,9 @@ jobs: working-directory: pkgs/messages_builder strategy: matrix: - sdk: [stable, dev] # {pkgs.versions} + sdk: [dev] include: - - sdk: stable + - sdk: dev run-tests: true steps: - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 diff --git a/.github/workflows/messages_serializer.yml b/.github/workflows/messages_serializer.yml index 236d5aaa..646d6145 100644 --- a/.github/workflows/messages_serializer.yml +++ b/.github/workflows/messages_serializer.yml @@ -23,9 +23,9 @@ jobs: working-directory: pkgs/messages_serializer strategy: matrix: - sdk: [stable, dev] # {pkgs.versions} + sdk: [dev] include: - - sdk: stable + - sdk: dev run-tests: true steps: - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 diff --git a/.github/workflows/messages_shrinker.yml b/.github/workflows/messages_shrinker.yml index be414273..a6573c4d 100644 --- a/.github/workflows/messages_shrinker.yml +++ b/.github/workflows/messages_shrinker.yml @@ -23,9 +23,9 @@ jobs: working-directory: pkgs/messages_shrinker strategy: matrix: - sdk: [stable, dev] # {pkgs.versions} + sdk: [dev] include: - - sdk: stable + - sdk: dev run-tests: true steps: - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 diff --git a/pkgs/messages/CHANGELOG.md b/pkgs/messages/CHANGELOG.md index 69036625..6996952a 100644 --- a/pkgs/messages/CHANGELOG.md +++ b/pkgs/messages/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.0-wip + +- Adapt to output data files to assets. + ## 0.2.0 - Remove `IntlObject` interface. diff --git a/pkgs/messages/README.md b/pkgs/messages/README.md index 60688da0..b39d0481 100644 --- a/pkgs/messages/README.md +++ b/pkgs/messages/README.md @@ -34,10 +34,9 @@ The `builder` to generate the named methods and data files from the input `arb` The logic for serializing `arb` message files into data files. ## Example -Add `package:messages` and `package:messages_builder` to your dependencies: +Add `package:messages` to your dependencies: ```bash dart pub add messages -dart pub add dev:messages_builder ``` Given translation message files in two languages: @@ -68,7 +67,7 @@ This translated file was created by a translator given the reference `en.arb`. ``` you can then run -`dart run build_runner build -d` +`dart run messages` This will generate both code to call your messages, as well as data files which will be shipped with your application. You can then use these generated files by importing the generated files: @@ -105,4 +104,4 @@ package_options: // multi // line // header -``` \ No newline at end of file +``` diff --git a/pkgs/messages/bin/messages.dart b/pkgs/messages/bin/messages.dart new file mode 100644 index 00000000..da1c937d --- /dev/null +++ b/pkgs/messages/bin/messages.dart @@ -0,0 +1,31 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +Future main(List args) async { + final runBuilder = await Process.run('dart', ['run', 'messages_builder']); + stdout.write(runBuilder.stdout as String); + + final runBuilderStdErr = runBuilder.stderr as String; + final messagesBuilderNotInDeps = + runBuilderStdErr.contains('Could not find package `messages_builder`'); + if (messagesBuilderNotInDeps) { + print('Adding `package:messages_builder` to dev dependencies...'); + final addBuilder = await runDart(['pub', 'add', 'dev:messages_builder']); + if (addBuilder.exitCode == 0) { + print('Re-running message generation'); + await runDart(['run', 'messages_builder']); + } + } else { + stderr.write(runBuilderStdErr); + } +} + +Future runDart(List arguments) async { + final processResult = await Process.run('dart', arguments); + stdout.write(processResult.stdout as String); + stderr.write(processResult.stderr as String); + return processResult; +} diff --git a/pkgs/messages/example_json/.gitignore b/pkgs/messages/example/.gitignore similarity index 89% rename from pkgs/messages/example_json/.gitignore rename to pkgs/messages/example/.gitignore index 3c8a1572..61e95f41 100644 --- a/pkgs/messages/example_json/.gitignore +++ b/pkgs/messages/example/.gitignore @@ -4,3 +4,4 @@ # Conventional directory for build output. build/ +bin/example/ diff --git a/pkgs/messages/example_json/README.md b/pkgs/messages/example/README.md similarity index 100% rename from pkgs/messages/example_json/README.md rename to pkgs/messages/example/README.md diff --git a/pkgs/messages/example_json/analysis_options.yaml b/pkgs/messages/example/analysis_options.yaml similarity index 100% rename from pkgs/messages/example_json/analysis_options.yaml rename to pkgs/messages/example/analysis_options.yaml diff --git a/pkgs/messages/example_json/lib/testarb.arb b/pkgs/messages/example/assets/l10n/testarb.arb similarity index 99% rename from pkgs/messages/example_json/lib/testarb.arb rename to pkgs/messages/example/assets/l10n/testarb.arb index 6516f400..3b16f132 100644 --- a/pkgs/messages/example_json/lib/testarb.arb +++ b/pkgs/messages/example/assets/l10n/testarb.arb @@ -36,4 +36,4 @@ } }, "helloAndWelcome2": "Welcome {firstName} von {lastName}!" -} \ No newline at end of file +} diff --git a/pkgs/messages/example_json/lib/testarb_de.arb b/pkgs/messages/example/assets/l10n/testarb_de.arb similarity index 99% rename from pkgs/messages/example_json/lib/testarb_de.arb rename to pkgs/messages/example/assets/l10n/testarb_de.arb index 65b07899..1c4c30ba 100644 --- a/pkgs/messages/example_json/lib/testarb_de.arb +++ b/pkgs/messages/example/assets/l10n/testarb_de.arb @@ -5,4 +5,4 @@ "helloAndWelcome2": "Willkommen {firstName} von {lastName} 2", "newMessages": "testde {newMessages, plural, =0 {No new messages} =1 {One new message} two{Two new Messages} other {test {newMessages} new messages}}", "newMessages2": "testdse is just a simple message" -} \ No newline at end of file +} diff --git a/pkgs/messages/example_json/lib/testarbctx2.arb b/pkgs/messages/example/assets/l10n/testarbctx2.arb similarity index 99% rename from pkgs/messages/example_json/lib/testarbctx2.arb rename to pkgs/messages/example/assets/l10n/testarbctx2.arb index 41fb05bb..25f81957 100644 --- a/pkgs/messages/example_json/lib/testarbctx2.arb +++ b/pkgs/messages/example/assets/l10n/testarbctx2.arb @@ -43,4 +43,4 @@ } } } -} \ No newline at end of file +} diff --git a/pkgs/messages/example_json/lib/testarbctx2_fr.arb b/pkgs/messages/example/assets/l10n/testarbctx2_fr.arb similarity index 95% rename from pkgs/messages/example_json/lib/testarbctx2_fr.arb rename to pkgs/messages/example/assets/l10n/testarbctx2_fr.arb index cbd6af7d..ec1c77ed 100644 --- a/pkgs/messages/example_json/lib/testarbctx2_fr.arb +++ b/pkgs/messages/example/assets/l10n/testarbctx2_fr.arb @@ -1,8 +1,9 @@ { "@@context": "AboutPage", + "@@locale": "fr", "helloAndWelcome": "Welcome {firstName} von {lastName} <", "otherMsg": "other", "aboutMessage": "Sur {websitename}", "newMessages": "test {newMessages, plural, =0 {No new messages} =1 {One new message} two{Two new Messages} other {test {newMessages} new messages}}", "newMessages2": "test {gender, select,male {No new messages} female {One new message} other{Two new Messages} other {test {gender} new messages of type {newVar}}}" -} \ No newline at end of file +} diff --git a/pkgs/messages/example/assets/testarb.arb.json b/pkgs/messages/example/assets/testarb.arb.json new file mode 100644 index 00000000..9a44323d --- /dev/null +++ b/pkgs/messages/example/assets/testarb.arb.json @@ -0,0 +1 @@ +[0,"en","dr9Md951",1,null,["helloAndWelcome","Welcome von !",[8,0],[13,1]],["helloAndWelcome2","Welcome von !",[8,0],[13,1]],[6,"newMessages","test ",[3,0,["test new messages",[5,0]],[0,"No new messages",1,"One new message","w2","Two new Messages"]]],[6,"newMessages2","test ",[4,0,"Two new Messages",{"male":"No new messages","female":"One new message"}]]] \ No newline at end of file diff --git a/pkgs/messages/example/assets/testarb_de.arb.json b/pkgs/messages/example/assets/testarb_de.arb.json new file mode 100644 index 00000000..70e189e6 --- /dev/null +++ b/pkgs/messages/example/assets/testarb_de.arb.json @@ -0,0 +1 @@ +[0,"de","hbDN1MhX",1,null,["helloAndWelcome","Willkommen von ",[11,0],[16,1]],["helloAndWelcome2","Willkommen von 2",[11,0],[16,1]],[6,"newMessages","testde ",[3,0,["test new messages",[5,0]],[0,"No new messages",1,"One new message","w2","Two new Messages"]]],["newMessages2","testdse is just a simple message"]] \ No newline at end of file diff --git a/pkgs/messages/example/assets/testarbctx2.arb.json b/pkgs/messages/example/assets/testarbctx2.arb.json new file mode 100644 index 00000000..5bd68e20 --- /dev/null +++ b/pkgs/messages/example/assets/testarbctx2.arb.json @@ -0,0 +1 @@ +[0,"en","QrwRSsOy",1,null,["aboutMessage","About ",[6,0]],["helloAndWelcome","Welcome von <",[8,0],[13,1]],[6,"newMessages","test ",[3,0,["test new messages",[5,0]],[0,"No new messages",1,"One new message","w2","Two new Messages"]]],[6,"newMessages2","test ",[4,0,"Two new Messages",{"male":"No new messages","female":"One new message"}]],["otherMsg","other"]] \ No newline at end of file diff --git a/pkgs/messages/example/assets/testarbctx2_fr.arb.json b/pkgs/messages/example/assets/testarbctx2_fr.arb.json new file mode 100644 index 00000000..d3895436 --- /dev/null +++ b/pkgs/messages/example/assets/testarbctx2_fr.arb.json @@ -0,0 +1 @@ +[0,"fr","390XWry3",1,null,["aboutMessage","Sur ",[4,0]],["helloAndWelcome","Welcome von <",[8,0],[13,1]],[6,"newMessages","test ",[3,0,["test new messages",[5,0]],[0,"No new messages",1,"One new message","w2","Two new Messages"]]],[6,"newMessages2","test ",[4,0,"Two new Messages",{"male":"No new messages","female":"One new message"}]],["otherMsg","other"]] \ No newline at end of file diff --git a/pkgs/messages/example_json/bin/example.dart b/pkgs/messages/example/bin/example.dart similarity index 89% rename from pkgs/messages/example_json/bin/example.dart rename to pkgs/messages/example/bin/example.dart index f53bce4c..793f5d9a 100644 --- a/pkgs/messages/example_json/bin/example.dart +++ b/pkgs/messages/example/bin/example.dart @@ -6,11 +6,12 @@ import 'dart:io'; -import 'package:example_json/testarbctx2.g.dart'; +import 'package:example/messages.g.dart'; Future main(List arguments) async { - final messages = - AboutPageMessages((String id) async => File(id).readAsString()); + final messages = AboutPageMessages( + (id) => File(id.split('/').skip(2).join('/')).readAsString(), + ); // final index = AboutPageMessagesEnum.aboutMessage; await messages.loadLocale('en'); diff --git a/pkgs/messages/example/lib/messages.g.dart b/pkgs/messages/example/lib/messages.g.dart new file mode 100644 index 00000000..fc227262 --- /dev/null +++ b/pkgs/messages/example/lib/messages.g.dart @@ -0,0 +1,172 @@ +// Generated by package:messages_builder. + +// ignore_for_file: non_constant_identifier_names + +import 'package:intl/intl.dart'; +import 'package:messages/messages_json.dart'; + +Message _pluralSelector( + num howMany, + String locale, { + required Message other, + Message? few, + Message? many, + Map? numberCases, + Map? wordCases, +}) { + return Intl.pluralLogic( + howMany, + few: few, + many: many, + zero: numberCases?[0] ?? wordCases?[0], + one: numberCases?[1] ?? wordCases?[1], + two: numberCases?[2] ?? wordCases?[2], + other: other, + locale: locale, + ); +} + +class AboutPageMessages { + AboutPageMessages(this._assetLoader); + + final Future Function(String id) _assetLoader; + + String _currentLocale = 'en'; + + final Map _messages = {}; + + static const _dataFiles = { + 'en': ('packages/example/assets/testarbctx2.arb.json', 'QrwRSsOy'), + 'fr': ('packages/example/assets/testarbctx2_fr.arb.json', '390XWry3') + }; + + String get currentLocale => _currentLocale; + + MessageList get _currentMessages => _messages[currentLocale]!; + + String getById( + String id, [ + List args = const [], + ]) { + return _currentMessages.generateStringAtId(id, args); + } + + static Iterable get knownLocales => _dataFiles.keys; + + Future loadLocale(String locale) async { + if (!_messages.containsKey(locale)) { + final info = _dataFiles[locale]; + final dataFile = info?.$1; + if (dataFile == null) { + throw ArgumentError('Locale $locale is not in $knownLocales'); + } + final data = await _assetLoader(dataFile); + final messageList = MessageListJson.fromString(data, _pluralSelector); + if (messageList.preamble.hash != info?.$2) { + throw ArgumentError(''' + Messages file for locale $locale has different hash "${messageList.preamble.hash}" than generated code "${info?.$2}".'''); + } + _messages[locale] = messageList; + } + _currentLocale = locale; + } + + Future loadAllLocales() async { + for (final locale in knownLocales) { + await loadLocale(locale); + } + } + + String aboutMessage(String websitename) => + _currentMessages.generateStringAtIndex(0, [websitename]); + + String helloAndWelcome( + String firstName, + int lastName, + ) => + _currentMessages.generateStringAtIndex(1, [firstName, lastName]); + + String newMessages(int newMessages) => + _currentMessages.generateStringAtIndex(2, [newMessages]); + + String newMessages2( + String gender, + int newVar, + ) => + _currentMessages.generateStringAtIndex(3, [gender, newVar]); + + String get otherMsg => _currentMessages.generateStringAtIndex(4, []); +} + +class HomePageMessages { + HomePageMessages(this._assetLoader); + + final Future Function(String id) _assetLoader; + + String _currentLocale = 'en'; + + final Map _messages = {}; + + static const _dataFiles = { + 'de': ('packages/example/assets/testarb_de.arb.json', 'hbDN1MhX'), + 'en': ('packages/example/assets/testarb.arb.json', 'dr9Md951') + }; + + String get currentLocale => _currentLocale; + + MessageList get _currentMessages => _messages[currentLocale]!; + + String getById( + String id, [ + List args = const [], + ]) { + return _currentMessages.generateStringAtId(id, args); + } + + static Iterable get knownLocales => _dataFiles.keys; + + Future loadLocale(String locale) async { + if (!_messages.containsKey(locale)) { + final info = _dataFiles[locale]; + final dataFile = info?.$1; + if (dataFile == null) { + throw ArgumentError('Locale $locale is not in $knownLocales'); + } + final data = await _assetLoader(dataFile); + final messageList = MessageListJson.fromString(data, _pluralSelector); + if (messageList.preamble.hash != info?.$2) { + throw ArgumentError(''' + Messages file for locale $locale has different hash "${messageList.preamble.hash}" than generated code "${info?.$2}".'''); + } + _messages[locale] = messageList; + } + _currentLocale = locale; + } + + Future loadAllLocales() async { + for (final locale in knownLocales) { + await loadLocale(locale); + } + } + + String helloAndWelcome( + String firstName, + String lastName, + ) => + _currentMessages.generateStringAtIndex(0, [firstName, lastName]); + + String helloAndWelcome2( + String firstName, + String lastName, + ) => + _currentMessages.generateStringAtIndex(1, [firstName, lastName]); + + String newMessages(int newMessages) => + _currentMessages.generateStringAtIndex(2, [newMessages]); + + String newMessages2( + String gender, + int newVar, + ) => + _currentMessages.generateStringAtIndex(3, [gender, newVar]); +} diff --git a/pkgs/messages/example/pubspec.yaml b/pkgs/messages/example/pubspec.yaml new file mode 100644 index 00000000..6887fd91 --- /dev/null +++ b/pkgs/messages/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: example +description: An example on how to use `package:messages`. +publish_to: none + +environment: + sdk: ^3.0.0 + +dependencies: + intl: ^0.19.0 + messages: + path: ../../messages + +dev_dependencies: + dart_flutter_team_lints: ^3.1.0 + lints: ^4.0.0 + messages_builder: + path: ../../messages_builder + path: ^1.8.3 + test: ^1.16.0 + +package_options: + messages_builder: + generate_methods: true + generate_find_by_id: true + generate_find_by: integer + plural_selector: intl + arb_input_folder: assets/l10n/ + message_output_folder: assets/ + generated_code_file: lib/messages.g.dart diff --git a/pkgs/messages/example_json/lib/testarb.g.dart b/pkgs/messages/example_json/lib/testarb.g.dart deleted file mode 100644 index f35050f6..00000000 --- a/pkgs/messages/example_json/lib/testarb.g.dart +++ /dev/null @@ -1,90 +0,0 @@ -// Generated by package:messages_builder. - -import 'package:intl4x/intl4x.dart'; -import 'package:messages/messages_json.dart'; - -class HomePageMessages { - HomePageMessages(this._fileLoader); - - final Future Function(String id) _fileLoader; - - String _currentLocale = 'en'; - - final Map _messages = {}; - - static const _dataFiles = { - 'de': ('lib/testarb_de.json', 'hbDN1MhX'), - 'en': ('lib/testarb.json', 'dr9Md951') - }; - - String get currentLocale => _currentLocale; - - MessageList get _currentMessages => _messages[currentLocale]!; - - static Iterable get knownLocales => _dataFiles.keys; - - Future loadLocale(String locale) async { - if (!_messages.containsKey(locale)) { - final info = _dataFiles[locale]; - final carb = info?.$1; - if (carb == null) { - throw ArgumentError('Locale $locale is not in $knownLocales'); - } - final data = await _fileLoader(carb); - final messageList = MessageListJson.fromString(data, pluralSelector); - if (messageList.preamble.hash != info?.$2) { - throw ArgumentError(''' - Messages file for locale $locale has different hash "${messageList.preamble.hash}" than generated code "${info?.$2}".'''); - } - _messages[locale] = messageList; - } - _currentLocale = locale; - } - - void loadAllLocales() { - for (final locale in knownLocales) { - loadLocale(locale); - } - } - - Message pluralSelector( - num howMany, { - required Message other, - Message? few, - Message? many, - Map? numberCases, - Map? wordCases, - }) { - Message getCase(int i) => numberCases?[i] ?? wordCases?[i] ?? other; - return switch ( - Intl(locale: Locale.parse(currentLocale)).plural().select(howMany)) { - PluralCategory.zero => getCase(0), - PluralCategory.one => getCase(1), - PluralCategory.two => getCase(2), - PluralCategory.few => few ?? other, - PluralCategory.many => many ?? other, - PluralCategory.other => other, - }; - } - - String helloAndWelcome( - String firstName, - String lastName, - ) => - _currentMessages.generateStringAtIndex(0, [firstName, lastName]); - - String helloAndWelcome2( - String firstName, - String lastName, - ) => - _currentMessages.generateStringAtIndex(1, [firstName, lastName]); - - String newMessages(int newMessages) => - _currentMessages.generateStringAtIndex(2, [newMessages]); - - String newMessages2( - String gender, - int newVar, - ) => - _currentMessages.generateStringAtIndex(3, [gender, newVar]); -} diff --git a/pkgs/messages/example_json/lib/testarb.json b/pkgs/messages/example_json/lib/testarb.json deleted file mode 100644 index d46d15af..00000000 --- a/pkgs/messages/example_json/lib/testarb.json +++ /dev/null @@ -1 +0,0 @@ -[0,"en","dr9Md951",0,null,["Welcome von !",[8,0],[13,1]],["Welcome von !",[8,0],[13,1]],[6,"test ",[3,0,["test new messages",[5,0]],[0,"No new messages",1,"One new message","w2","Two new Messages"]]],[6,"test ",[4,0,"Two new Messages",{"male":"No new messages","female":"One new message"}]]] \ No newline at end of file diff --git a/pkgs/messages/example_json/lib/testarb_de.json b/pkgs/messages/example_json/lib/testarb_de.json deleted file mode 100644 index 8480d165..00000000 --- a/pkgs/messages/example_json/lib/testarb_de.json +++ /dev/null @@ -1 +0,0 @@ -[0,"de","hbDN1MhX",0,null,["Willkommen von ",[11,0],[16,1]],["Willkommen von 2",[11,0],[16,1]],[6,"testde ",[3,0,["test new messages",[5,0]],[0,"No new messages",1,"One new message","w2","Two new Messages"]]],"testdse is just a simple message"] \ No newline at end of file diff --git a/pkgs/messages/example_json/lib/testarbctx2.g.dart b/pkgs/messages/example_json/lib/testarbctx2.g.dart deleted file mode 100644 index 55aa20f5..00000000 --- a/pkgs/messages/example_json/lib/testarbctx2.g.dart +++ /dev/null @@ -1,89 +0,0 @@ -// Generated by package:messages_builder. - -import 'package:intl4x/intl4x.dart'; -import 'package:messages/messages_json.dart'; - -class AboutPageMessages { - AboutPageMessages(this._fileLoader); - - final Future Function(String id) _fileLoader; - - String _currentLocale = 'en'; - - final Map _messages = {}; - - static const _dataFiles = { - 'fr': ('lib/testarbctx2_fr.json', 'EyPjEJJU'), - 'en': ('lib/testarbctx2.json', 'QrwRSsOy') - }; - - String get currentLocale => _currentLocale; - - MessageList get _currentMessages => _messages[currentLocale]!; - - static Iterable get knownLocales => _dataFiles.keys; - - Future loadLocale(String locale) async { - if (!_messages.containsKey(locale)) { - final info = _dataFiles[locale]; - final carb = info?.$1; - if (carb == null) { - throw ArgumentError('Locale $locale is not in $knownLocales'); - } - final data = await _fileLoader(carb); - final messageList = MessageListJson.fromString(data, pluralSelector); - if (messageList.preamble.hash != info?.$2) { - throw ArgumentError(''' - Messages file for locale $locale has different hash "${messageList.preamble.hash}" than generated code "${info?.$2}".'''); - } - _messages[locale] = messageList; - } - _currentLocale = locale; - } - - void loadAllLocales() { - for (final locale in knownLocales) { - loadLocale(locale); - } - } - - Message pluralSelector( - num howMany, { - required Message other, - Message? few, - Message? many, - Map? numberCases, - Map? wordCases, - }) { - Message getCase(int i) => numberCases?[i] ?? wordCases?[i] ?? other; - return switch ( - Intl(locale: Locale.parse(currentLocale)).plural().select(howMany)) { - PluralCategory.zero => getCase(0), - PluralCategory.one => getCase(1), - PluralCategory.two => getCase(2), - PluralCategory.few => few ?? other, - PluralCategory.many => many ?? other, - PluralCategory.other => other, - }; - } - - String aboutMessage(String websitename) => - _currentMessages.generateStringAtIndex(0, [websitename]); - - String helloAndWelcome( - String firstName, - int lastName, - ) => - _currentMessages.generateStringAtIndex(1, [firstName, lastName]); - - String newMessages(int newMessages) => - _currentMessages.generateStringAtIndex(2, [newMessages]); - - String newMessages2( - String gender, - int newVar, - ) => - _currentMessages.generateStringAtIndex(3, [gender, newVar]); - - String get otherMsg => _currentMessages.generateStringAtIndex(4, []); -} diff --git a/pkgs/messages/example_json/lib/testarbctx2.json b/pkgs/messages/example_json/lib/testarbctx2.json deleted file mode 100644 index a56c79c5..00000000 --- a/pkgs/messages/example_json/lib/testarbctx2.json +++ /dev/null @@ -1 +0,0 @@ -[0,"en","QrwRSsOy",0,null,["About ",[6,0]],["Welcome von <",[8,0],[13,1]],[6,"test ",[3,0,["test new messages",[5,0]],[0,"No new messages",1,"One new message","w2","Two new Messages"]]],[6,"test ",[4,0,"Two new Messages",{"male":"No new messages","female":"One new message"}]],"other"] \ No newline at end of file diff --git a/pkgs/messages/example_json/lib/testarbctx2_fr.json b/pkgs/messages/example_json/lib/testarbctx2_fr.json deleted file mode 100644 index 61b6c08f..00000000 --- a/pkgs/messages/example_json/lib/testarbctx2_fr.json +++ /dev/null @@ -1 +0,0 @@ -[0,"fr","EyPjEJJU",0,null,["Sur ",[4,0]],["Welcome von <",[8,0],[13,1]],[6,"test ",[3,0,["test new messages",[5,0]],[0,"No new messages",1,"One new message","w2","Two new Messages"]]],[6,"test ",[4,0,"Two new Messages",{"male":"No new messages","female":"One new message"}]],"other"] \ No newline at end of file diff --git a/pkgs/messages/example_json/pubspec.yaml b/pkgs/messages/example_json/pubspec.yaml deleted file mode 100644 index 00f0fac2..00000000 --- a/pkgs/messages/example_json/pubspec.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: example_json -description: An example on how to use `package:messages`. -publish_to: none - -environment: - sdk: ^3.0.0 - -dependencies: - intl4x: ^0.7.0 - messages: ^0.2.0 - -dev_dependencies: - build_daemon: ^4.0.0 - build_runner: ^2.3.3 - build_web_compilers: ^4.0.0 - dart_flutter_team_lints: ^3.0.0 - messages_builder: ^0.2.0 - path: ^1.8.3 - test: ^1.16.0 - -package_options: - messages_builder: - generateMethods: true - generateFindById: false - generateFindBy: integer - pluralSelector: intl4x diff --git a/pkgs/messages/example_json/pubspec_overrides.yaml b/pkgs/messages/example_json/pubspec_overrides.yaml deleted file mode 100644 index 22dff701..00000000 --- a/pkgs/messages/example_json/pubspec_overrides.yaml +++ /dev/null @@ -1,9 +0,0 @@ -dependency_overrides: - messages: - path: ../ - messages_serializer: - path: ../../messages_serializer - messages_builder: - path: ../../messages_builder - intl4x: - path: ../../intl4x diff --git a/pkgs/messages/example_json/tools/daemon.dart b/pkgs/messages/example_json/tools/daemon.dart deleted file mode 100644 index 24526e5b..00000000 --- a/pkgs/messages/example_json/tools/daemon.dart +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'dart:io'; - -import 'package:build_daemon/client.dart'; -import 'package:build_daemon/data/build_target.dart'; -import 'package:path/path.dart' as p; - -void main(List args) async { - BuildDaemonClient client; - final workingDirectory = p.normalize(Directory.current.path); - - try { - // First we connect to the daemon. This will start one if one is not - // currently running. - client = await BuildDaemonClient.connect( - workingDirectory, - [ - 'dart', - 'run', - 'build_runner', - 'daemon', - '--delete-conflicting-outputs', - ], - logHandler: print); - } catch (e) { - if (e is VersionSkew) { - print('Version skew. Please disconnect all other clients ' - 'before trying to start a new one.'); - } else if (e is OptionsSkew) { - print('Options skew. Please disconnect all other clients ' - 'before trying to start a new one.'); - } else { - print('Unexpected error: $e'); - } - - exit(1); - } - print('Connected to Dart Build Daemon'); - - // Next we register a build target (directory) to build. - // Note this will not cause a build to occur unless there are relevant file - // changes. - client.registerBuildTarget(DefaultBuildTarget((b) => b - ..target = 'lib' - ..blackListPatterns.replace([RegExp(r'.*(? print('BUILD STATUS: $status')); - - await client.finished; -} diff --git a/pkgs/messages/examples_flutter/my_application/.gitignore b/pkgs/messages/examples_flutter/my_application/.gitignore new file mode 100644 index 00000000..79c113f9 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/pkgs/messages/examples_flutter/my_application/.metadata b/pkgs/messages/examples_flutter/my_application/.metadata new file mode 100644 index 00000000..63438dcf --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "4ca51a1de59772d6adc6f8fbcc627c79d975a564" + channel: "master" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 4ca51a1de59772d6adc6f8fbcc627c79d975a564 + base_revision: 4ca51a1de59772d6adc6f8fbcc627c79d975a564 + - platform: android + create_revision: 4ca51a1de59772d6adc6f8fbcc627c79d975a564 + base_revision: 4ca51a1de59772d6adc6f8fbcc627c79d975a564 + - platform: ios + create_revision: 4ca51a1de59772d6adc6f8fbcc627c79d975a564 + base_revision: 4ca51a1de59772d6adc6f8fbcc627c79d975a564 + - platform: linux + create_revision: 4ca51a1de59772d6adc6f8fbcc627c79d975a564 + base_revision: 4ca51a1de59772d6adc6f8fbcc627c79d975a564 + - platform: macos + create_revision: 4ca51a1de59772d6adc6f8fbcc627c79d975a564 + base_revision: 4ca51a1de59772d6adc6f8fbcc627c79d975a564 + - platform: web + create_revision: 4ca51a1de59772d6adc6f8fbcc627c79d975a564 + base_revision: 4ca51a1de59772d6adc6f8fbcc627c79d975a564 + - platform: windows + create_revision: 4ca51a1de59772d6adc6f8fbcc627c79d975a564 + base_revision: 4ca51a1de59772d6adc6f8fbcc627c79d975a564 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/pkgs/messages/examples_flutter/my_application/README.md b/pkgs/messages/examples_flutter/my_application/README.md new file mode 100644 index 00000000..e26f1167 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/README.md @@ -0,0 +1,3 @@ +# my_application + +A new Flutter project. diff --git a/pkgs/messages/examples_flutter/my_application/analysis_options.yaml b/pkgs/messages/examples_flutter/my_application/analysis_options.yaml new file mode 100644 index 00000000..f9b30346 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/pkgs/messages/examples_flutter/my_application/assets/l10n/messages.arb b/pkgs/messages/examples_flutter/my_application/assets/l10n/messages.arb new file mode 100644 index 00000000..3ad19b4c --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/assets/l10n/messages.arb @@ -0,0 +1,14 @@ +{ + "@@locale": "en_US", + "@@context": "my_app", + "current_sale_name": "{arg, select, other{No sale} winter{Winter sale} summer{Summer sale}}", + "@current_sale_name": { + "description": "current_sale_name", + "placeholders": { + "arg": { + "type":"String", + "example":"1" + } + } + } +} \ No newline at end of file diff --git a/pkgs/messages/examples_flutter/my_application/assets/messages.arb.json b/pkgs/messages/examples_flutter/my_application/assets/messages.arb.json new file mode 100644 index 00000000..f270825d --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/assets/messages.arb.json @@ -0,0 +1 @@ +[0,"en_US","IT21w/eV",0,null,[4,0,"No sale",{"winter":"Winter sale","summer":"Summer sale"}]] \ No newline at end of file diff --git a/pkgs/messages/examples_flutter/my_application/lib/main.dart b/pkgs/messages/examples_flutter/my_application/lib/main.dart new file mode 100644 index 00000000..8c049d09 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/lib/main.dart @@ -0,0 +1,75 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:my_application/src/messages.g.dart'; +import 'package:my_shopping_cart/my_shopping_cart.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatefulWidget { + const MainApp({super.key}); + + @override + State createState() => _MainAppState(); +} + +class _MainAppState extends State { + int counter = 0; + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: FutureBuilder( + future: initMyMessages(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const LinearProgressIndicator(); + } + final myAppMessages = snapshot.data!; + return FutureBuilder( + future: initShoppingCart(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const CircularProgressIndicator(); + } + final myShoppingCart = snapshot.data!; + + return Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () => setState(() => counter++)), + body: Center( + child: Text( + 'Currently: ${sale(myAppMessages)}. ${myShoppingCart.itemsInCart(counter)}!'), + ), + ); + }); + }), + ); + } + + Future initShoppingCart() async { + var myShoppingCart = MyShoppingCart(); + await myShoppingCart.loadLocales(); + return myShoppingCart; + } + + Future initMyMessages() async { + var myAppMessages = MyAppMessages((id) => + rootBundle.loadString(id.substring('packages/my_application/'.length))); + await myAppMessages.loadLocale('en_US'); + return myAppMessages; + } + + String sale(MyAppMessages myAppMessages) { + return myAppMessages.current_sale_name( + DateTime.now().month < 4 || DateTime.now().month > 10 + ? 'winter' + : 'summer', + ); + } +} diff --git a/pkgs/messages/examples_flutter/my_application/lib/src/empty_de.dart b/pkgs/messages/examples_flutter/my_application/lib/src/empty_de.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/lib/src/empty_de.dart @@ -0,0 +1 @@ + diff --git a/pkgs/messages/examples_flutter/my_application/lib/src/empty_en.dart b/pkgs/messages/examples_flutter/my_application/lib/src/empty_en.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/lib/src/empty_en.dart @@ -0,0 +1 @@ + diff --git a/pkgs/messages/examples_flutter/my_application/lib/src/messages.g.dart b/pkgs/messages/examples_flutter/my_application/lib/src/messages.g.dart new file mode 100644 index 00000000..798b3dbc --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/lib/src/messages.g.dart @@ -0,0 +1,74 @@ +// Generated by package:messages_builder. + +// ignore_for_file: non_constant_identifier_names + +import 'package:intl/intl.dart'; +import 'package:messages/messages_json.dart'; + +Message _pluralSelector( + num howMany, + String locale, { + required Message other, + Message? few, + Message? many, + Map? numberCases, + Map? wordCases, +}) { + return Intl.pluralLogic( + howMany, + few: few, + many: many, + zero: numberCases?[0] ?? wordCases?[0], + one: numberCases?[1] ?? wordCases?[1], + two: numberCases?[2] ?? wordCases?[2], + other: other, + locale: locale, + ); +} + +class MyAppMessages { + MyAppMessages(this._assetLoader); + + final Future Function(String id) _assetLoader; + + String _currentLocale = 'en_US'; + + final Map _messages = {}; + + static const _dataFiles = { + 'en_US': ('packages/my_application/assets/messages.arb.json', 'IT21w/eV') + }; + + String get currentLocale => _currentLocale; + + MessageList get _currentMessages => _messages[currentLocale]!; + + static Iterable get knownLocales => _dataFiles.keys; + + Future loadLocale(String locale) async { + if (!_messages.containsKey(locale)) { + final info = _dataFiles[locale]; + final dataFile = info?.$1; + if (dataFile == null) { + throw ArgumentError('Locale $locale is not in $knownLocales'); + } + final data = await _assetLoader(dataFile); + final messageList = MessageListJson.fromString(data, _pluralSelector); + if (messageList.preamble.hash != info?.$2) { + throw ArgumentError(''' + Messages file for locale $locale has different hash "${messageList.preamble.hash}" than generated code "${info?.$2}".'''); + } + _messages[locale] = messageList; + } + _currentLocale = locale; + } + + Future loadAllLocales() async { + for (final locale in knownLocales) { + await loadLocale(locale); + } + } + + String current_sale_name(String arg) => + _currentMessages.generateStringAtIndex(0, [arg]); +} diff --git a/pkgs/messages/examples_flutter/my_application/linux/.gitignore b/pkgs/messages/examples_flutter/my_application/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/pkgs/messages/examples_flutter/my_application/linux/CMakeLists.txt b/pkgs/messages/examples_flutter/my_application/linux/CMakeLists.txt new file mode 100644 index 00000000..bb0bb66d --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/linux/CMakeLists.txt @@ -0,0 +1,145 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "my_application") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.my_application") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/pkgs/messages/examples_flutter/my_application/linux/flutter/CMakeLists.txt b/pkgs/messages/examples_flutter/my_application/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/pkgs/messages/examples_flutter/my_application/linux/flutter/generated_plugin_registrant.cc b/pkgs/messages/examples_flutter/my_application/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..e71a16d2 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/pkgs/messages/examples_flutter/my_application/linux/flutter/generated_plugin_registrant.h b/pkgs/messages/examples_flutter/my_application/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/pkgs/messages/examples_flutter/my_application/linux/flutter/generated_plugins.cmake b/pkgs/messages/examples_flutter/my_application/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..2e1de87a --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/pkgs/messages/examples_flutter/my_application/linux/main.cc b/pkgs/messages/examples_flutter/my_application/linux/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/pkgs/messages/examples_flutter/my_application/linux/my_application.cc b/pkgs/messages/examples_flutter/my_application/linux/my_application.cc new file mode 100644 index 00000000..80d9c762 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/linux/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "my_application"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "my_application"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/pkgs/messages/examples_flutter/my_application/linux/my_application.h b/pkgs/messages/examples_flutter/my_application/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/pkgs/messages/examples_flutter/my_application/pubspec.yaml b/pkgs/messages/examples_flutter/my_application/pubspec.yaml new file mode 100644 index 00000000..24af6ad7 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_application/pubspec.yaml @@ -0,0 +1,29 @@ +name: my_application +description: "A new Flutter project." +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: ^3.6.0-270.0.dev + +dependencies: + flutter: + sdk: flutter + intl: ^0.19.0 + messages: + path: ../../../messages + my_shopping_cart: + path: ../my_shopping_cart + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + messages_builder: + path: ../../../messages_builder + +flutter: + uses-material-design: true + + assets: + - assets/messages.arb.json diff --git a/pkgs/messages/examples_flutter/my_shopping_cart/.gitignore b/pkgs/messages/examples_flutter/my_shopping_cart/.gitignore new file mode 100644 index 00000000..eb6c05cd --- /dev/null +++ b/pkgs/messages/examples_flutter/my_shopping_cart/.gitignore @@ -0,0 +1,31 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +build/ diff --git a/pkgs/messages/examples_flutter/my_shopping_cart/.metadata b/pkgs/messages/examples_flutter/my_shopping_cart/.metadata new file mode 100644 index 00000000..60a99bd6 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_shopping_cart/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "4ca51a1de59772d6adc6f8fbcc627c79d975a564" + channel: "master" + +project_type: package diff --git a/pkgs/messages/examples_flutter/my_shopping_cart/CHANGELOG.md b/pkgs/messages/examples_flutter/my_shopping_cart/CHANGELOG.md new file mode 100644 index 00000000..41cc7d81 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_shopping_cart/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/pkgs/messages/examples_flutter/my_shopping_cart/LICENSE b/pkgs/messages/examples_flutter/my_shopping_cart/LICENSE new file mode 100644 index 00000000..ba75c69f --- /dev/null +++ b/pkgs/messages/examples_flutter/my_shopping_cart/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/pkgs/messages/examples_flutter/my_shopping_cart/README.md b/pkgs/messages/examples_flutter/my_shopping_cart/README.md new file mode 100644 index 00000000..4a260d8d --- /dev/null +++ b/pkgs/messages/examples_flutter/my_shopping_cart/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/pkgs/messages/examples_flutter/my_shopping_cart/analysis_options.yaml b/pkgs/messages/examples_flutter/my_shopping_cart/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/pkgs/messages/examples_flutter/my_shopping_cart/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/pkgs/messages/examples_flutter/my_shopping_cart/assets/l10n/messages.arb b/pkgs/messages/examples_flutter/my_shopping_cart/assets/l10n/messages.arb new file mode 100644 index 00000000..796b6095 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_shopping_cart/assets/l10n/messages.arb @@ -0,0 +1,14 @@ +{ + "@@locale": "en_US", + "@@context": "shopping_cart", + "itemsInCart": "{count, plural, other{{count} items in cart} zero{No items in cart} one{1 item in cart}}", + "@itemsInCart": { + "description": "itemsInCart", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + } +} \ No newline at end of file diff --git a/pkgs/messages/examples_flutter/my_shopping_cart/assets/messages.arb.json b/pkgs/messages/examples_flutter/my_shopping_cart/assets/messages.arb.json new file mode 100644 index 00000000..05e3e2c5 --- /dev/null +++ b/pkgs/messages/examples_flutter/my_shopping_cart/assets/messages.arb.json @@ -0,0 +1 @@ +[0,"en_US","Nj229ee7",0,null,[3,0,[" items in cart",[0,0]],["w0","No items in cart","w1","1 item in cart"]]] \ No newline at end of file diff --git a/pkgs/messages/examples_flutter/my_shopping_cart/lib/my_shopping_cart.dart b/pkgs/messages/examples_flutter/my_shopping_cart/lib/my_shopping_cart.dart new file mode 100644 index 00000000..d30ff2fe --- /dev/null +++ b/pkgs/messages/examples_flutter/my_shopping_cart/lib/my_shopping_cart.dart @@ -0,0 +1,15 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +library my_shopping_cart; + +import 'package:flutter/services.dart'; + +import 'src/messages.g.dart'; + +class MyShoppingCart { + final _messages = ShoppingCartMessages(rootBundle.loadString); + Future loadLocales() async => await _messages.loadAllLocales(); + + String itemsInCart(int number) => _messages.itemsInCart(number); +} diff --git a/pkgs/messages/examples_flutter/my_shopping_cart/lib/src/messages.g.dart b/pkgs/messages/examples_flutter/my_shopping_cart/lib/src/messages.g.dart new file mode 100644 index 00000000..c6c3c26b --- /dev/null +++ b/pkgs/messages/examples_flutter/my_shopping_cart/lib/src/messages.g.dart @@ -0,0 +1,74 @@ +// Generated by package:messages_builder. + +// ignore_for_file: non_constant_identifier_names + +import 'package:intl/intl.dart'; +import 'package:messages/messages_json.dart'; + +Message _pluralSelector( + num howMany, + String locale, { + required Message other, + Message? few, + Message? many, + Map? numberCases, + Map? wordCases, +}) { + return Intl.pluralLogic( + howMany, + few: few, + many: many, + zero: numberCases?[0] ?? wordCases?[0], + one: numberCases?[1] ?? wordCases?[1], + two: numberCases?[2] ?? wordCases?[2], + other: other, + locale: locale, + ); +} + +class ShoppingCartMessages { + ShoppingCartMessages(this._assetLoader); + + final Future Function(String id) _assetLoader; + + String _currentLocale = 'en_US'; + + final Map _messages = {}; + + static const _dataFiles = { + 'en_US': ('packages/my_shopping_cart/assets/messages.arb.json', 'Nj229ee7') + }; + + String get currentLocale => _currentLocale; + + MessageList get _currentMessages => _messages[currentLocale]!; + + static Iterable get knownLocales => _dataFiles.keys; + + Future loadLocale(String locale) async { + if (!_messages.containsKey(locale)) { + final info = _dataFiles[locale]; + final dataFile = info?.$1; + if (dataFile == null) { + throw ArgumentError('Locale $locale is not in $knownLocales'); + } + final data = await _assetLoader(dataFile); + final messageList = MessageListJson.fromString(data, _pluralSelector); + if (messageList.preamble.hash != info?.$2) { + throw ArgumentError(''' + Messages file for locale $locale has different hash "${messageList.preamble.hash}" than generated code "${info?.$2}".'''); + } + _messages[locale] = messageList; + } + _currentLocale = locale; + } + + Future loadAllLocales() async { + for (final locale in knownLocales) { + await loadLocale(locale); + } + } + + String itemsInCart(int count) => + _currentMessages.generateStringAtIndex(0, [count]); +} diff --git a/pkgs/messages/examples_flutter/my_shopping_cart/pubspec.yaml b/pkgs/messages/examples_flutter/my_shopping_cart/pubspec.yaml new file mode 100644 index 00000000..09a0a53f --- /dev/null +++ b/pkgs/messages/examples_flutter/my_shopping_cart/pubspec.yaml @@ -0,0 +1,33 @@ +name: my_shopping_cart +description: "A new Flutter package project." +version: 0.0.1 + +publish_to: none + +environment: + sdk: ^3.6.0-270.0.dev + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + intl: ^0.19.0 + messages: + path: ../../../messages + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + messages_builder: + path: ../../../messages_builder + +package_options: + messages_builder: + arb_input_folder: assets/l10n/ + message_output_folder: assets/ + generated_code_file: lib/src/messages.g.dart + +flutter: + assets: + - assets/messages.arb.json diff --git a/pkgs/messages/lib/src/message.dart b/pkgs/messages/lib/src/message.dart index 54adec19..7890bea2 100644 --- a/pkgs/messages/lib/src/message.dart +++ b/pkgs/messages/lib/src/message.dart @@ -110,6 +110,7 @@ final class PluralMessage extends Message { }) { return pluralSelector( allArgs[argIndex] as num, + locale!, numberCases: numberCases, wordCases: wordCases, few: few, diff --git a/pkgs/messages/lib/src/message_list_json.dart b/pkgs/messages/lib/src/message_list_json.dart index bb0eb1c4..998ad831 100644 --- a/pkgs/messages/lib/src/message_list_json.dart +++ b/pkgs/messages/lib/src/message_list_json.dart @@ -64,11 +64,12 @@ class MessageListJson extends MessageList { String generateStringAtId(String id, List args) => messages .where((element) => element.id == id) .first - .generateString(args, pluralSelector: _selector); + .generateString(args, locale: preamble.locale, pluralSelector: _selector); @override String generateStringAtIndex(int index, List args) => - messages[getIndex(index)].generateString(args, pluralSelector: _selector); + messages[getIndex(index)].generateString(args, + locale: preamble.locale, pluralSelector: _selector); int getIndex(int index) => messageIndices?[index] ?? index; } diff --git a/pkgs/messages/lib/src/plural_selector.dart b/pkgs/messages/lib/src/plural_selector.dart index 5d6899bc..6676ae64 100644 --- a/pkgs/messages/lib/src/plural_selector.dart +++ b/pkgs/messages/lib/src/plural_selector.dart @@ -5,7 +5,8 @@ import 'message.dart'; typedef PluralSelector = Message Function( - num howMany, { + num howMany, + String locale, { Map? numberCases, Map? wordCases, Message? few, diff --git a/pkgs/messages/pubspec.yaml b/pkgs/messages/pubspec.yaml index 0f753dbe..9c7c23bf 100644 --- a/pkgs/messages/pubspec.yaml +++ b/pkgs/messages/pubspec.yaml @@ -1,15 +1,18 @@ name: messages description: A lightweight modular library for localization (l10n) functionality. -version: 0.2.0 +version: 0.3.0-wip repository: https://github.com/dart-lang/i18n/tree/main/pkgs/messages +publish_to: none + environment: sdk: ^3.0.0 dependencies: collection: ^1.17.1 - intl: ^0.18.0 + path: ^1.9.0 dev_dependencies: dart_flutter_team_lints: ^3.0.0 + intl: ^0.19.0 test: ^1.16.0 diff --git a/pkgs/messages/test/messagelist_json_test.dart b/pkgs/messages/test/messagelist_json_test.dart index ab291453..d5d8023e 100644 --- a/pkgs/messages/test/messagelist_json_test.dart +++ b/pkgs/messages/test/messagelist_json_test.dart @@ -7,13 +7,13 @@ import 'package:messages/messages_json.dart'; import 'package:test/test.dart'; Message intlPluralSelector( - num howMany, { + num howMany, + String locale, { Map? numberCases, Map? wordCases, Message? few, Message? many, required Message other, - String? locale, }) { return old_intl.Intl.pluralLogic( howMany, diff --git a/pkgs/messages_builder/CHANGELOG.md b/pkgs/messages_builder/CHANGELOG.md index 902d845a..836ac579 100644 --- a/pkgs/messages_builder/CHANGELOG.md +++ b/pkgs/messages_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.0-wip + +- Adapt to output data files to assets. + ## 0.2.1 - Bump dependencies. diff --git a/pkgs/messages_builder/bin/messages_builder.dart b/pkgs/messages_builder/bin/messages_builder.dart new file mode 100644 index 00000000..b9a08917 --- /dev/null +++ b/pkgs/messages_builder/bin/messages_builder.dart @@ -0,0 +1,40 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:messages_builder/builder.dart'; +import 'package:messages_builder/generation_options.dart'; +import 'package:messages_builder/message_data_builder.dart'; + +/// Regenerates both data files and code for retrieving the messages from the +/// data files. +/// +/// Executed by running `dart run messages_builder` in a project with a +/// dependency on `package:messages`. +Future main(List args) async { + final generationOptions = await _generationOptions(); + final inputFolder = Directory.fromUri( + Directory.current.uri.resolve('assets/').resolve('l10n/')); + final outputFolder = + Directory.fromUri(Directory.current.uri.resolve('assets/')); + + final mapping = await MessageDataFileBuilder( + inputFolder: inputFolder, + outputFolder: outputFolder, + generationOptions: generationOptions, + ).run(); + + await MessageCallingCodeGenerator( + mapping: mapping, + options: generationOptions, + ).build(); +} + +Future _generationOptions() async { + final pubspecUri = Directory.current.uri.resolve('pubspec.yaml'); + final file = File.fromUri(pubspecUri); + final pubspecContents = await file.readAsString(); + return await GenerationOptions.fromPubspec(pubspecContents); +} diff --git a/pkgs/messages_builder/build.yaml b/pkgs/messages_builder/build.yaml index 38ac91d8..13de16e9 100644 --- a/pkgs/messages_builder/build.yaml +++ b/pkgs/messages_builder/build.yaml @@ -3,6 +3,9 @@ builders: copyBuilder: import: "package:messages_builder/builder.dart" builder_factories: ["messagesBuilder"] - build_extensions: {'.arb': ['.g.dart', '.json'], '^pubspec.yaml': []} + build_extensions: + { + "l10n.messages": ["messages.g.dart"], + } build_to: source - auto_apply: root_package \ No newline at end of file + auto_apply: root_package diff --git a/pkgs/messages_builder/lib/arb_parser.dart b/pkgs/messages_builder/lib/arb_parser.dart index 0646efc3..0d8aba1e 100644 --- a/pkgs/messages_builder/lib/arb_parser.dart +++ b/pkgs/messages_builder/lib/arb_parser.dart @@ -4,7 +4,6 @@ import 'dart:convert'; -import 'package:build/build.dart'; import 'package:crypto/crypto.dart'; import 'message_parser/message_parser.dart'; @@ -14,11 +13,7 @@ class ArbParser { final bool addName; ArbParser([this.addName = false]); - MessagesWithMetadata parseMessageFile( - Map arb, - AssetId assetId, [ - String inferredLocale = 'en_US', - ]) { + MessagesWithMetadata parseMessageFile(Map arb) { final locale = arb['@@locale'] as String?; final context = arb['@@context'] as String?; final referencePath = arb['@@x-reference'] as String?; @@ -30,12 +25,11 @@ class ArbParser { final messages = messagesWithKeys.map((e) => e.$2).toList(); return MessagesWithMetadata( messages, - locale ?? inferredLocale, + locale, context, referencePath, getHash(arb), arb.keys.any((key) => key.startsWith('@') && !key.startsWith('@@')), - assetId, ); } diff --git a/pkgs/messages_builder/lib/builder.dart b/pkgs/messages_builder/lib/builder.dart index f18435a3..d6c26374 100644 --- a/pkgs/messages_builder/lib/builder.dart +++ b/pkgs/messages_builder/lib/builder.dart @@ -4,71 +4,61 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; +import 'dart:io'; -import 'package:build/build.dart'; +import 'package:code_builder/code_builder.dart'; import 'package:collection/collection.dart'; -import 'package:glob/glob.dart'; -import 'package:messages_serializer/messages_serializer.dart'; import 'package:path/path.dart' as path; import 'arb_parser.dart'; import 'code_generation/code.dart'; +import 'code_generation/library_generation.dart'; import 'generation_options.dart'; import 'message_with_metadata.dart'; -Builder messagesBuilder(BuilderOptions options) => - MessagesBuilder(options.config); - -class MessagesBuilder implements Builder { - final Map config; - - MessagesBuilder(this.config); - - @override - Map> get buildExtensions => { - '.arb': ['.g.dart', '.json'], - '^pubspec.yaml': [], - }; - - @override - Future build(BuildStep buildStep) async { - final generationOptions = await GenerationOptions.fromPubspec(buildStep); - await BuildStepGenerator(buildStep, generationOptions).build(); - } -} - -class BuildStepGenerator { - final BuildStep buildStep; - AssetId get inputId => buildStep.inputId; +class MessageCallingCodeGenerator { final GenerationOptions options; + final Map mapping; - BuildStepGenerator(this.buildStep, this.options); - - Serializer get serializer => getSerializer(options); + MessageCallingCodeGenerator({ + required this.options, + required this.mapping, + }); Future build() async { final allMessageFiles = await getParsedMessageFiles(); - final inputMessageFile = allMessageFiles - .singleWhere((messageFile) => messageFile.assetId == inputId); - final parentFile = getParentFile(allMessageFiles, inputMessageFile); - - final reducedMessageFile = reduce(parentFile, inputMessageFile); - - await writeDataFile(reducedMessageFile); - if (parentFile.assetId == inputId) { - await writeDartLibrary(allMessageFiles, reducedMessageFile); + final libraries = []; + for (final input in allMessageFiles) { + final parentFile = getParentFile(allMessageFiles, input); + final scrubbedMessageFile = scrub( + input.message, + parentFile.message.messages.map((e) => e.name).toList(), + ); + if (parentFile.path == input.path) { + final library = await writeDartLibrary( + allMessageFiles, + scrubbedMessageFile, + ); + libraries.add(library); + } + } + if (libraries.isNotEmpty) { + final code = CodeGenerator( + options: options, + libraries: libraries, + ).generate(); + + await options.generatedCodeFile.create(recursive: true); + await options.generatedCodeFile.writeAsString(code); } } /// Only keep the messages which are in the parent file, as only those will /// get a generated method to embed them in code. - MessagesWithMetadata reduce( - MessagesWithMetadata parentFile, + MessagesWithMetadata scrub( MessagesWithMetadata inputMessageFile, + List messageNames, ) { - final messageNames = parentFile.messages.map((e) => e.name).toList(); - final messages = inputMessageFile.messages .where((message) => messageNames.contains(message.name)) .toList() @@ -80,68 +70,51 @@ class BuildStepGenerator { /// Generates the Dart library which extracts the messages from their file /// format and makes the available to the user in a way specified through the /// `GenerationOptions`. - Future writeDartLibrary( - List assetList, + Future writeDartLibrary( + List assetList, MessagesWithMetadata messageList, ) async { - final resourcesInContext = - assetList.where((resource) => resource.context == messageList.context); - - final localeToResourceInfo = - Map.fromEntries(resourcesInContext.map((resource) => MapEntry( - resource.locale, - ( - path: resource.assetId.changeExtension('.json').path, - hasch: resource.hash, - ), - ))); - - printIncludeFilesNotification(messageList.context, localeToResourceInfo); - final libraryCode = CodeGenerator( + final resourcesInContext = assetList + .where((resource) => resource.message.context == messageList.context); + + final localeToResource = resourcesInContext + .map((resource) => ( + locale: resource.message.locale ?? 'en_US', + id: 'packages/${options.packageName}/${resource.path}', + hasch: resource.message.hash, + )) + .sortedBy((resource) => resource.locale); + printIncludeFilesNotification(messageList.context, localeToResource); + return LibraryGeneration( options, - messageList, - localeToResourceInfo, + messageList.context, + messageList.locale!, + messageList.messages, + localeToResource, ).generate(); - - await buildStep.writeAsString( - inputId.changeExtension('.g.dart'), - libraryCode, - ); } - Serializer getSerializer(GenerationOptions generationOptions) { - return JsonSerializer(generationOptions.findById); - } + Future> getParsedMessageFiles() async => + Future.wait(mapping.entries + .map((p) async => ParsedMessageFile( + path: path.relative(p.value, from: Directory.current.path), + message: + await parseMessageFile(await getArbfile(p.key), options), + )) + .toList()); - Future> getParsedMessageFiles() async => - buildStep.findAssets(Glob('**.arb')).asyncMap(parseMessageFile).toList(); - - Future parseMessageFile(AssetId assetId) async { - final arbFile = await buildStep.readAsString(assetId); - final decoded = jsonDecode(arbFile) as Map; - final arb = Map.castFrom(decoded); - final inferredLocale = path - .basenameWithoutExtension(assetId.path) - .split('_') - .skip(1) - .join('_'); - final messageList = ArbParser(options.findById).parseMessageFile( - arb, - assetId, - inferredLocale, - ); - return messageList; - } + Future getArbfile(String path) async => + await File(path).readAsString(); /// Either get the referenced parent file, or try to infer which it might be. - static MessagesWithMetadata getParentFile( - List arbResources, - MessagesWithMetadata arb, + static ParsedMessageFile getParentFile( + List arbFiles, + ParsedMessageFile currentFile, ) { /// If the reference file is explicitly named, return that. - if (arb.referencePath != null) { - final reference = arbResources - .where((element) => element.assetId.path == arb.referencePath) + if (currentFile.message.referencePath != null) { + final reference = arbFiles + .where((element) => element.path == currentFile.message.referencePath) .firstOrNull; if (reference != null) { return reference; @@ -149,60 +122,53 @@ class BuildStepGenerator { } /// If the current file is a reference for others, return the current file. - final references = arbResources - .where((resource) => resource.referencePath == arb.assetId.path); - if (references.contains(arb)) { - return arb; + final references = arbFiles.where( + (resource) => resource.message.referencePath == currentFile.path); + if (references.contains(currentFile)) { + return currentFile; } /// Try to infer by looking at which files contain metadata, which is a sign /// they might be the references for others in the same context. final contextLeads = - arbResources.groupListsBy((resource) => resource.context); - final contextWithMetadata = contextLeads[arb.context]! - .firstWhereOrNull((element) => element.hasMetadata); + arbFiles.groupListsBy((resource) => resource.message.context); + final contextWithMetadata = contextLeads[currentFile.message.context]! + .firstWhereOrNull((element) => element.message.hasMetadata); if (contextWithMetadata != null) { return contextWithMetadata; } - return arb; - } - - /// This writes the file containing the messages, which can be either a binary - /// `.carb` file or a JSON file, depending on the serializer. - /// - /// This message data file must be shipped with the application, it is - /// unpacked at runtime so that the messages can be read from it. - /// - /// Returns the list of indices of the messages which are visible to the user. - Future writeDataFile(MessagesWithMetadata messages) async { - final serialization = serializer.serialize( - messages.hash, - messages.locale, - messages.messages.map((e) => e.message).toList(), - ); - final carbFile = messages.assetId.changeExtension('.json'); - final data = serialization.data; - if (data is Uint8List) { - await buildStep.writeAsBytes(carbFile, data); - } else if (data is String) { - await buildStep.writeAsString(carbFile, data); - } + return currentFile; } /// Display a notification to the user to include the newly generated files /// in their assets. void printIncludeFilesNotification( String? context, - Map localeToResource, + Iterable<({String hasch, String id, String locale})> localeToResource, ) { var contextMessage = 'The'; if (context != null) { contextMessage = 'For the messages in $context, the'; } - final fileList = - localeToResource.entries.map((e) => '\t${e.value.path}').join('\n'); + final fileList = localeToResource.map((e) => '\t${e.id}').join('\n'); print( '''$contextMessage following files need to be declared in your assets:\n$fileList'''); } } + +Future parseMessageFile( + String arbFile, + GenerationOptions options, +) async { + final decoded = jsonDecode(arbFile) as Map; + final arb = Map.castFrom(decoded); + return ArbParser(options.findById).parseMessageFile(arb); +} + +class ParsedMessageFile { + final String path; + final MessagesWithMetadata message; + + ParsedMessageFile({required this.path, required this.message}); +} diff --git a/pkgs/messages_builder/lib/code_generation/class_generation.dart b/pkgs/messages_builder/lib/code_generation/class_generation.dart index 4b63f5cc..5468e9e6 100644 --- a/pkgs/messages_builder/lib/code_generation/class_generation.dart +++ b/pkgs/messages_builder/lib/code_generation/class_generation.dart @@ -8,7 +8,7 @@ import '../generation_options.dart'; import '../message_with_metadata.dart'; import 'generation.dart'; -class ClassGeneration extends Generation { +class ClassGeneration { final GenerationOptions options; final List messages; final String? context; @@ -26,9 +26,9 @@ class ClassGeneration extends Generation { this.methods, ); - String getClassName(String? context) => '${context ?? ''}Messages'; + String getClassName(String? context) => + _toCamelCase('${context ?? ''}Messages'); - @override List generate() { final classes = [ Class( @@ -53,3 +53,8 @@ class ClassGeneration extends Generation { return classes; } } + +String _toCamelCase(String input) => input + .split('_') + .map((e) => e.substring(0, 1).toUpperCase() + e.substring(1)) + .join(); diff --git a/pkgs/messages_builder/lib/code_generation/code.dart b/pkgs/messages_builder/lib/code_generation/code.dart index 439d3d54..f8d80025 100644 --- a/pkgs/messages_builder/lib/code_generation/code.dart +++ b/pkgs/messages_builder/lib/code_generation/code.dart @@ -6,37 +6,131 @@ import 'package:code_builder/code_builder.dart'; import 'package:dart_style/dart_style.dart'; import '../generation_options.dart'; -import '../message_with_metadata.dart'; -import 'library_generation.dart'; +import 'import_generation.dart'; class CodeGenerator { final GenerationOptions options; - final String? context; - final String locale; - final List messages; - final Map localeToResourceInfo; + final List libraries; - CodeGenerator( - this.options, - MessagesWithMetadata messageListWithMetadata, - this.localeToResourceInfo, - ) : context = messageListWithMetadata.context, - locale = messageListWithMetadata.locale, - messages = messageListWithMetadata.messages; + CodeGenerator({required this.options, required this.libraries}); String generate() { - final libs = LibraryGeneration( - options, - context, - locale, - messages, - localeToResourceInfo, - ).generate(); + final imports = ImportGeneration(options).generate(); + final lib = libraries.fold( + Library( + (p0) => p0 + ..comments.add(options.header) + ..directives.addAll(imports) + ..body.addAll([ + if (options.pluralSelector != PluralSelectorType.custom) + pluralSelector(), + ]), + ), (value, element) { + return Library( + (p0) => p0 + ..ignoreForFile.add('non_constant_identifier_names') + ..comments.addAll({...value.comments, ...element.comments}) + ..directives.addAll({...value.directives, ...element.directives}) + ..body.addAll([ + ...value.body, + ...element.body, + ]), + ); + }); + final emitter = DartEmitter(orderDirectives: true); + final source = '${lib.accept(emitter)}'; + final code = DartFormatter().format(source); + return code; + } - assert(libs.isNotEmpty); + // Message Function(num, + // String locale, + // {Message? few, + // String? locale, + // Message? many, + // Map? numberCases, + // required Message other, + // Map? wordCases}) intl; + Method pluralSelector() => Method( + (mb) => mb + ..name = '_pluralSelector' + ..returns = const Reference('Message') + ..requiredParameters.addAll([ + Parameter( + (pb) => pb + ..name = 'howMany' + ..type = const Reference('num') + ..named = false, + ), + Parameter( + (pb) => pb + ..name = 'locale' + ..type = const Reference('String') + ..named = false, + ), + ]) + ..optionalParameters.addAll([ + Parameter( + (pb) => pb + ..name = 'other' + ..type = const Reference('Message') + ..required = true + ..named = true, + ), + Parameter( + (pb) => pb + ..name = 'few' + ..type = const Reference('Message?') + ..named = true, + ), + Parameter( + (pb) => pb + ..name = 'many' + ..type = const Reference('Message?') + ..named = true, + ), + Parameter( + (pb) => pb + ..name = 'numberCases' + ..type = const Reference('Map?') + ..named = true, + ), + Parameter( + (pb) => pb + ..name = 'wordCases' + ..type = const Reference('Map?') + ..named = true, + ), + ]) + ..body = pluralSelectorBody(), + ); - final emitter = DartEmitter(orderDirectives: true); - final source = '${libs.first.accept(emitter)}'; - return DartFormatter().format(source); + Code pluralSelectorBody() { + return switch (options.pluralSelector) { + PluralSelectorType.intl => const Code(''' +return Intl.pluralLogic( + howMany, + few: few, + many: many, + zero: numberCases?[0] ?? wordCases?[0], + one: numberCases?[1] ?? wordCases?[1], + two: numberCases?[2] ?? wordCases?[2], + other: other, + locale: locale, + ); + '''), + PluralSelectorType.intl4x => const Code(''' + Message getCase(int i) => numberCases?[i] ?? wordCases?[i] ?? other; + return switch (Intl(locale: Locale.parse(locale)).plural().select(howMany)) { + PluralCategory.zero => getCase(0), + PluralCategory.one => getCase(1), + PluralCategory.two => getCase(2), + PluralCategory.few => few ?? other, + PluralCategory.many => many ?? other, + PluralCategory.other => other, + }; + '''), + PluralSelectorType.custom => throw ArgumentError(), + }; } } diff --git a/pkgs/messages_builder/lib/code_generation/constructor_generation.dart b/pkgs/messages_builder/lib/code_generation/constructor_generation.dart index 936ef667..1358b64b 100644 --- a/pkgs/messages_builder/lib/code_generation/constructor_generation.dart +++ b/pkgs/messages_builder/lib/code_generation/constructor_generation.dart @@ -5,20 +5,18 @@ import 'package:code_builder/code_builder.dart'; import '../generation_options.dart'; -import 'generation.dart'; -class ConstructorGeneration extends Generation { +class ConstructorGeneration { final GenerationOptions options; ConstructorGeneration(this.options); - @override List generate() { final nativeConstructor = Constructor((cb) => cb ..requiredParameters.addAll([ Parameter( (pb) => pb - ..name = '_fileLoader' + ..name = '_assetLoader' ..toThis = true, ), if (options.pluralSelector == PluralSelectorType.custom) diff --git a/pkgs/messages_builder/lib/code_generation/field_generation.dart b/pkgs/messages_builder/lib/code_generation/field_generation.dart index a6d28f67..edc96468 100644 --- a/pkgs/messages_builder/lib/code_generation/field_generation.dart +++ b/pkgs/messages_builder/lib/code_generation/field_generation.dart @@ -5,11 +5,11 @@ import 'package:code_builder/code_builder.dart'; import '../generation_options.dart'; -import 'generation.dart'; -class FieldGeneration extends Generation { +class FieldGeneration { final GenerationOptions options; - final Map localeToResourceInfo; + final Iterable<({String hasch, String id, String locale})> + localeToResourceInfo; final String locale; FieldGeneration( @@ -18,13 +18,12 @@ class FieldGeneration extends Generation { this.locale, ); - @override List generate() { final loadingStrategy = Field( (fb) { final returnType = const Reference('Future').symbol; fb - ..name = '_fileLoader' + ..name = '_assetLoader' ..modifier = FieldModifier.final$ ..type = Reference('$returnType Function(String id)'); }, @@ -44,8 +43,8 @@ class FieldGeneration extends Generation { ); final dataFiles = Field( (fb) { - final paths = localeToResourceInfo.entries - .map((e) => "'${e.key}' : ('${e.value.path}', '${e.value.hasch}')") + final paths = localeToResourceInfo + .map((e) => "'${e.locale}' : ('${e.id}', '${e.hasch}')") .join(','); fb ..name = '_dataFiles' diff --git a/pkgs/messages_builder/lib/code_generation/generation.dart b/pkgs/messages_builder/lib/code_generation/generation.dart index e6bdcef7..1f8dcbce 100644 --- a/pkgs/messages_builder/lib/code_generation/generation.dart +++ b/pkgs/messages_builder/lib/code_generation/generation.dart @@ -2,10 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -abstract class Generation { - List generate(); -} - String enumName(String? context) => '${context ?? ''}MessagesEnum'; String getDataFileName(String e) => e.split('.').first; diff --git a/pkgs/messages_builder/lib/code_generation/import_generation.dart b/pkgs/messages_builder/lib/code_generation/import_generation.dart index 79ee4e52..1ab73f87 100644 --- a/pkgs/messages_builder/lib/code_generation/import_generation.dart +++ b/pkgs/messages_builder/lib/code_generation/import_generation.dart @@ -5,14 +5,12 @@ import 'package:code_builder/code_builder.dart'; import '../generation_options.dart'; -import 'generation.dart'; -class ImportGeneration extends Generation { +class ImportGeneration { final GenerationOptions options; ImportGeneration(this.options); - @override List generate() { final serializationImports = switch (options.deserialization) { DeserializationType.web => [ @@ -26,6 +24,9 @@ class ImportGeneration extends Generation { ], PluralSelectorType.custom => [], }; - return [...serializationImports, ...pluralImports]; + return [ + ...serializationImports, + ...pluralImports, + ]; } } diff --git a/pkgs/messages_builder/lib/code_generation/library_generation.dart b/pkgs/messages_builder/lib/code_generation/library_generation.dart index 4af39339..10725044 100644 --- a/pkgs/messages_builder/lib/code_generation/library_generation.dart +++ b/pkgs/messages_builder/lib/code_generation/library_generation.dart @@ -9,16 +9,15 @@ import '../message_with_metadata.dart'; import 'class_generation.dart'; import 'constructor_generation.dart'; import 'field_generation.dart'; -import 'generation.dart'; -import 'import_generation.dart'; import 'method_generation.dart'; -class LibraryGeneration extends Generation { +class LibraryGeneration { final GenerationOptions options; final String? context; final String locale; final List messages; - final Map localeToResourceInfo; + final Iterable<({String hasch, String id, String locale})> + localeToResourceInfo; LibraryGeneration( this.options, @@ -28,9 +27,7 @@ class LibraryGeneration extends Generation { this.localeToResourceInfo, ); - @override - List generate() { - final imports = ImportGeneration(options).generate(); + Library generate() { final constructors = ConstructorGeneration(options).generate(); final fields = FieldGeneration( @@ -54,11 +51,6 @@ class LibraryGeneration extends Generation { methods, ).generate(); - return [ - Library((b) => b - ..comments.add(options.header) - ..directives.addAll(imports) - ..body.addAll(classes)) - ]; + return Library((b) => b..body.addAll(classes)); } } diff --git a/pkgs/messages_builder/lib/code_generation/method_generation.dart b/pkgs/messages_builder/lib/code_generation/method_generation.dart index c2438f71..c21abe90 100644 --- a/pkgs/messages_builder/lib/code_generation/method_generation.dart +++ b/pkgs/messages_builder/lib/code_generation/method_generation.dart @@ -8,7 +8,7 @@ import '../generation_options.dart'; import '../message_with_metadata.dart'; import 'generation.dart'; -class MethodGeneration extends Generation { +class MethodGeneration { final GenerationOptions options; final String? context; final List messages; @@ -47,7 +47,6 @@ class MethodGeneration extends Generation { ); } - @override List generate() { Iterable messageCalls; if (options.messageCalls) { @@ -62,8 +61,8 @@ class MethodGeneration extends Generation { (mb) { final loading = switch (options.deserialization) { DeserializationType.web => ''' - final data = await _fileLoader(carb); - final messageList = MessageListJson.fromString(data, pluralSelector);''', + final data = await _assetLoader(dataFile); + final messageList = MessageListJson.fromString(data, _pluralSelector);''', }; mb ..name = 'loadLocale' @@ -76,8 +75,8 @@ class MethodGeneration extends Generation { ..body = Code(''' if (!_messages.containsKey(locale)) { final info = _dataFiles[locale]; - final carb = info?.\$1; - if (carb == null) { + final dataFile = info?.\$1; + if (dataFile == null) { throw ArgumentError('Locale \$locale is not in \$knownLocales'); } $loading @@ -96,10 +95,11 @@ class MethodGeneration extends Generation { (mb) { mb ..name = 'loadAllLocales' - ..returns = const Reference('void') + ..returns = const Reference('Future') + ..modifier = MethodModifier.async ..body = const Code(''' for (final locale in knownLocales) { - loadLocale(locale); + await loadLocale(locale); } '''); }, @@ -166,60 +166,6 @@ class MethodGeneration extends Generation { const Code('_currentMessages.generateStringAtIndex(val.index, args)') ..lambda = true ..returns = const Reference('String')); - // Message Function(num, - // {Message? few, - // String? locale, - // Message? many, - // Map? numberCases, - // required Message other, - // Map? wordCases}) intl; - Method pluralSelector() => Method( - (mb) => mb - ..name = 'pluralSelector' - ..returns = const Reference('Message') - ..requiredParameters.addAll([ - Parameter( - (pb) => pb - ..name = 'howMany' - ..type = const Reference('num') - ..named = false, - ), - ]) - ..optionalParameters.addAll([ - Parameter( - (pb) => pb - ..name = 'other' - ..type = const Reference('Message') - ..required = true - ..named = true, - ), - Parameter( - (pb) => pb - ..name = 'few' - ..type = const Reference('Message?') - ..named = true, - ), - Parameter( - (pb) => pb - ..name = 'many' - ..type = const Reference('Message?') - ..named = true, - ), - Parameter( - (pb) => pb - ..name = 'numberCases' - ..type = const Reference('Map?') - ..named = true, - ), - Parameter( - (pb) => pb - ..name = 'wordCases' - ..type = const Reference('Map?') - ..named = true, - ), - ]) - ..body = pluralSelectorBody(), - ); return [ getCurrentLocale, @@ -229,37 +175,7 @@ class MethodGeneration extends Generation { getKnownLocales, loadLocale, loadAllLocales, - if (options.pluralSelector != PluralSelectorType.custom) pluralSelector(), ...messageCalls, ]; } - - Code pluralSelectorBody() { - return switch (options.pluralSelector) { - PluralSelectorType.intl => const Code(''' -return Intl.pluralLogic( - howMany, - few: few, - many: many, - zero: numberCases?[0] ?? wordCases?[0], - one: numberCases?[1] ?? wordCases?[1], - two: numberCases?[2] ?? wordCases?[2], - other: other, - locale: currentLocale, - ); - '''), - PluralSelectorType.intl4x => const Code(''' -Message getCase(int i) => numberCases?[i] ?? wordCases?[i] ?? other; - return switch (Intl(locale: Locale.parse(currentLocale)).plural().select(howMany)) { - PluralCategory.zero => getCase(0), - PluralCategory.one => getCase(1), - PluralCategory.two => getCase(2), - PluralCategory.few => few ?? other, - PluralCategory.many => many ?? other, - PluralCategory.other => other, - }; - '''), - PluralSelectorType.custom => throw ArgumentError(), - }; - } } diff --git a/pkgs/messages_builder/lib/generation_options.dart b/pkgs/messages_builder/lib/generation_options.dart index 6573b3fe..54b484c4 100644 --- a/pkgs/messages_builder/lib/generation_options.dart +++ b/pkgs/messages_builder/lib/generation_options.dart @@ -2,8 +2,9 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'package:build/build.dart'; -import 'package:glob/glob.dart'; +import 'dart:io'; + +import 'package:collection/collection.dart'; import 'package:yaml/yaml.dart'; /// Options for the message data file and code generation. @@ -42,6 +43,36 @@ class GenerationOptions { /// The origin of the algorithm for determining which plural case to use. final PluralSelectorType pluralSelector; + /// Where the arb files are located. + final Directory arbFolder; + + /// Where to write the message data files to. + final Directory messageFolder; + + final File generatedCodeFile; + + final String packageName; + + static const _generateMethodsKey = 'generate_methods'; + static const _generateFindByIdKey = 'generate_find_by_id'; + static const _generateFindByKey = 'generate_find_by'; + static const _headerKey = 'header'; + static const _pluralSelectorKey = 'plural_selector'; + static const _arbInputFolderKey = 'arb_input_folder'; + static const _messageOutputFolderKey = 'message_output_folder'; + static const _generatedCodeFileKey = 'generated_code_file'; + + static List get validKeys => [ + _generateMethodsKey, + _generateFindByIdKey, + _generateFindByKey, + _pluralSelectorKey, + _headerKey, + _arbInputFolderKey, + _messageOutputFolderKey, + _generatedCodeFileKey, + ]; + GenerationOptions({ required this.serialization, required this.deserialization, @@ -50,29 +81,44 @@ class GenerationOptions { required this.indexType, required this.header, required this.pluralSelector, + required this.packageName, + required this.arbFolder, + required this.messageFolder, + required this.generatedCodeFile, }); - static Future fromPubspec(BuildStep buildStep) async { - final pubspecId = await buildStep.findAssets(Glob('pubspec.yaml')).first; - final pubspecData = await buildStep.readAsString(pubspecId); + static Future fromPubspec(String pubspecData) async { final pubspec = loadYaml(pubspecData) as YamlMap; final packageOptions = pubspec['package_options'] as YamlMap?; final messagesOptions = packageOptions?['messages_builder'] as YamlMap?; + final illegalKey = messagesOptions?.keys + .firstWhereOrNull((key) => !validKeys.contains(key)); + if (illegalKey != null) { + throw ArgumentError( + 'The message options contain the illegal key $illegalKey'); + } final generationOptions = GenerationOptions( - serialization: SerializationType.json, - deserialization: DeserializationType.web, - messageCalls: (messagesOptions?['generateMethods'] as bool?) ?? true, - findById: (messagesOptions?['generateFindById'] as bool?) ?? false, - indexType: _indexType(messagesOptions), - header: messagesOptions?['header'] as String? ?? - 'Generated by package:messages_builder.', - pluralSelector: _pluralSelector(messagesOptions), - ); + serialization: SerializationType.json, + deserialization: DeserializationType.web, + messageCalls: (messagesOptions?[_generateMethodsKey] as bool?) ?? true, + findById: (messagesOptions?[_generateFindByIdKey] as bool?) ?? false, + indexType: _indexType(messagesOptions), + header: messagesOptions?[_headerKey] as String? ?? + 'Generated by package:messages_builder.', + pluralSelector: _pluralSelector(messagesOptions), + packageName: pubspec['name'] as String, + arbFolder: Directory( + messagesOptions?[_arbInputFolderKey] as String? ?? 'assets/l10n/'), + messageFolder: Directory( + messagesOptions?[_messageOutputFolderKey] as String? ?? 'assets/'), + generatedCodeFile: File( + messagesOptions?[_generatedCodeFileKey] as String? ?? + 'lib/src/messages.g.dart')); return generationOptions; } static IndexType _indexType(YamlMap? messagesOptions) { - final generateFindString = messagesOptions?['generateFindBy'] as String?; + final generateFindString = messagesOptions?[_generateFindByKey] as String?; return generateFindString != null ? IndexType.values .where((type) => type.name == generateFindString) @@ -81,7 +127,8 @@ class GenerationOptions { } static PluralSelectorType _pluralSelector(YamlMap? messagesOptions) { - final pluralSelectorString = messagesOptions?['pluralSelector'] as String?; + final pluralSelectorString = + messagesOptions?[_pluralSelectorKey] as String?; return pluralSelectorString != null ? PluralSelectorType.values .where((type) => type.name == pluralSelectorString) diff --git a/pkgs/messages_builder/lib/message_data_builder.dart b/pkgs/messages_builder/lib/message_data_builder.dart new file mode 100644 index 00000000..9ad21ff4 --- /dev/null +++ b/pkgs/messages_builder/lib/message_data_builder.dart @@ -0,0 +1,79 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:messages_serializer/messages_serializer.dart'; +import 'package:path/path.dart' as p; + +import 'builder.dart'; +import 'generation_options.dart'; +import 'message_with_metadata.dart'; + +class MessageDataFileBuilder { + final Directory inputFolder; + final Directory outputFolder; + final GenerationOptions generationOptions; + + MessageDataFileBuilder({ + required this.inputFolder, + required this.outputFolder, + required this.generationOptions, + }); + + Future> run() async { + print('Starting to add arb files from $inputFolder to $outputFolder'); + final arbFiles = await inputFolder + .list() + .where((file) => file is File) + .map((file) => file.path) + .where((path) => p.extension(path) == '.arb') + .toList(); + + final mapping = {}; + if (arbFiles.isEmpty) { + print('No `.arb` files found in $inputFolder.'); + return mapping; + } + + for (final arbFilePath in arbFiles) { + print('Generating $arbFilePath, bundle this in your assets.'); + final arbFileUri = Uri.file(arbFilePath); + final arbFileContents = await File.fromUri(arbFileUri).readAsString(); + final messageBundle = await parseMessageFile( + arbFileContents, + generationOptions, + ); + + final serializer = JsonSerializer(generationOptions.findById); + + final data = _arbToData(messageBundle, arbFilePath, serializer); + + final assetName = p.setExtension( + p.basename(arbFilePath), + '.arb${serializer.extension}', + ); + + final outputDataPath = outputFolder.uri.resolve(assetName); + final dataFile = File.fromUri(outputDataPath); + await dataFile.create(); + await dataFile.writeAsString(data); + mapping[arbFilePath] = outputDataPath.path; + } + return mapping; + } + + String _arbToData( + MessagesWithMetadata messageBundle, + String arbFilePath, + Serializer serializer, + ) => + serializer + .serialize( + messageBundle.hash, + messageBundle.locale ?? 'en_US', + messageBundle.messages.map((e) => e.message).toList(), + ) + .data; +} diff --git a/pkgs/messages_builder/lib/message_parser/plural_parser.dart b/pkgs/messages_builder/lib/message_parser/plural_parser.dart index 77ec6543..a2e4a0a0 100644 --- a/pkgs/messages_builder/lib/message_parser/plural_parser.dart +++ b/pkgs/messages_builder/lib/message_parser/plural_parser.dart @@ -7,7 +7,7 @@ import 'package:messages/messages.dart'; import 'icu_message_parser.dart'; import 'message_parser.dart'; -Map numbers = {'one': 1, 'two': 2}; +Map numbers = {'zero': 0, 'one': 1, 'two': 2}; class PluralParser { MapEntry? getPluralCaseFrom( diff --git a/pkgs/messages_builder/lib/message_with_metadata.dart b/pkgs/messages_builder/lib/message_with_metadata.dart index 7547b75d..fe4a0acb 100644 --- a/pkgs/messages_builder/lib/message_with_metadata.dart +++ b/pkgs/messages_builder/lib/message_with_metadata.dart @@ -2,7 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'package:build/build.dart'; import 'package:messages/messages.dart'; class MessageWithMetadata { @@ -19,12 +18,11 @@ class MessageWithMetadata { class MessagesWithMetadata { final List messages; - final String locale; + final String? locale; final String? context; final String? referencePath; final String hash; final bool hasMetadata; - final AssetId assetId; MessagesWithMetadata( this.messages, @@ -33,7 +31,6 @@ class MessagesWithMetadata { this.referencePath, this.hash, this.hasMetadata, - this.assetId, ); MessagesWithMetadata copyWith({ @@ -43,7 +40,6 @@ class MessagesWithMetadata { String? referencePath, String? hash, bool? hasMetadata, - AssetId? assetId, }) { return MessagesWithMetadata( messages ?? this.messages, @@ -52,7 +48,6 @@ class MessagesWithMetadata { referencePath ?? this.referencePath, hash ?? this.hash, hasMetadata ?? this.hasMetadata, - assetId ?? this.assetId, ); } } diff --git a/pkgs/messages_builder/pubspec.yaml b/pkgs/messages_builder/pubspec.yaml index 586d4f18..2d364a5e 100644 --- a/pkgs/messages_builder/pubspec.yaml +++ b/pkgs/messages_builder/pubspec.yaml @@ -1,26 +1,27 @@ name: messages_builder description: Build the messages for consumption by package:messages -version: 0.2.1 +version: 0.3.0-wip repository: https://github.com/dart-lang/i18n/pkgs/messages_builder environment: sdk: ^3.0.0 +publish_to: none + dependencies: - build: ^2.3.1 code_builder: ^4.3.0 collection: ^1.18.0 crypto: ^3.0.3 dart_style: ^2.2.4 - glob: ^2.1.1 - intl: ^0.18.0 - messages: ^0.2.0 - messages_serializer: ^0.2.0 + intl: ^0.19.0 + logging: ^1.2.0 + messages: + path: ../messages + messages_serializer: + path: ../messages_serializer path: ^1.8.2 yaml: ^3.1.1 dev_dependencies: - build_runner: ^2.0.0 - build_web_compilers: ^3.2.7 dart_flutter_team_lints: ^3.0.0 test: ^1.16.0 diff --git a/pkgs/messages_builder/test/web_deserializer_native_test.dart b/pkgs/messages_builder/test/web_deserializer_native_test.dart index e93fa824..6f5b9898 100644 --- a/pkgs/messages_builder/test/web_deserializer_native_test.dart +++ b/pkgs/messages_builder/test/web_deserializer_native_test.dart @@ -4,7 +4,6 @@ import 'dart:convert'; -import 'package:build/src/asset/id.dart'; import 'package:intl/intl.dart' as old_intl; import 'package:messages/messages_json.dart'; import 'package:messages_builder/arb_parser.dart'; @@ -15,13 +14,13 @@ import 'package:test/test.dart'; import 'testarb.arb.dart'; Message intlPluralSelector( - num howMany, { + num howMany, + String locale, { Map? numberCases, Map? wordCases, Message? few, Message? many, required Message other, - String? locale, }) { return old_intl.Intl.pluralLogic( howMany, @@ -36,7 +35,6 @@ Message intlPluralSelector( } void main() { - final uniqueKey = AssetId('package', 'path'); test('generateMessageFile from Object json', () { final message = StringMessage('Hello World'); final message1 = MessageWithMetadata(message, [], 'helloWorld'); @@ -54,7 +52,7 @@ void main() { '@@locale': 'en', 'helloWorld': 'Hello World' }; - final parsed = ArbParser().parseMessageFile(arb, uniqueKey); + final parsed = ArbParser().parseMessageFile(arb); final buffer = JsonSerializer() .serialize('', '', parsed.messages.map((e) => e.message).toList()) .data; @@ -67,7 +65,7 @@ void main() { '@@locale': 'en', 'helloWorld': 'Hello {name}' }; - final parsed = ArbParser().parseMessageFile(arb, uniqueKey); + final parsed = ArbParser().parseMessageFile(arb); final buffer = JsonSerializer() .serialize('', '', parsed.messages.map((e) => e.message).toList()) .data; @@ -84,7 +82,7 @@ void main() { '@@locale': 'en', 'helloWorld': '{greeting}{space}{name}' }; - final parsed = ArbParser().parseMessageFile(arb, uniqueKey); + final parsed = ArbParser().parseMessageFile(arb); final buffer = JsonSerializer() .serialize('', '', parsed.messages.map((e) => e.message).toList()) .data; @@ -103,7 +101,7 @@ void main() { test('generateMessageFile from complex arb JSON', () { final arb = jsonDecode(arbFile) as Map; - final parsed = ArbParser().parseMessageFile(arb, uniqueKey); + final parsed = ArbParser().parseMessageFile(arb); final buffer = JsonSerializer() .serialize('', '', parsed.messages.map((e) => e.message).toList()) .data; diff --git a/pkgs/messages_serializer/CHANGELOG.md b/pkgs/messages_serializer/CHANGELOG.md index 97ebdb97..527f0866 100644 --- a/pkgs/messages_serializer/CHANGELOG.md +++ b/pkgs/messages_serializer/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.0-wip + +- Adapt to output data files to assets. + ## 0.2.1 - Bump dependencies. diff --git a/pkgs/messages_serializer/lib/src/serializer.dart b/pkgs/messages_serializer/lib/src/serializer.dart index ba45321f..7e4d42b2 100644 --- a/pkgs/messages_serializer/lib/src/serializer.dart +++ b/pkgs/messages_serializer/lib/src/serializer.dart @@ -15,6 +15,8 @@ abstract class Serializer { Serializer(this.writeIds); + String get extension; + Serialization serialize( String hash, String locale, diff --git a/pkgs/messages_serializer/lib/src/serializer_json.dart b/pkgs/messages_serializer/lib/src/serializer_json.dart index a1bb1a5a..ed8b1c32 100644 --- a/pkgs/messages_serializer/lib/src/serializer_json.dart +++ b/pkgs/messages_serializer/lib/src/serializer_json.dart @@ -179,4 +179,7 @@ class JsonSerializer extends Serializer { result.add(m); return result.length - 1; } + + @override + String get extension => '.json'; } diff --git a/pkgs/messages_serializer/pubspec.yaml b/pkgs/messages_serializer/pubspec.yaml index d6eef7c9..df3641ee 100644 --- a/pkgs/messages_serializer/pubspec.yaml +++ b/pkgs/messages_serializer/pubspec.yaml @@ -1,16 +1,18 @@ name: messages_serializer description: Serialization of messages for package:messages. -version: 0.2.1 +version: 0.3.0-wip repository: https://github.com/dart-lang/i18n/tree/main/pkgs/messages_serializer environment: sdk: ^3.0.0 -# Add regular dependencies here. +publish_to: none + dependencies: - messages: ^0.2.0 + intl: ^0.19.0 + messages: + path: ../messages dev_dependencies: dart_flutter_team_lints: ^3.0.0 - intl: ^0.18.1 test: ^1.21.0 diff --git a/pkgs/messages_serializer/test/messages_serializer_test.dart b/pkgs/messages_serializer/test/messages_serializer_test.dart index b9ebee90..c184e91b 100644 --- a/pkgs/messages_serializer/test/messages_serializer_test.dart +++ b/pkgs/messages_serializer/test/messages_serializer_test.dart @@ -10,13 +10,13 @@ import 'package:messages_serializer/messages_serializer.dart'; import 'package:test/test.dart'; Message intlPluralSelector( - num howMany, { + num howMany, + String locale, { Map? numberCases, Map? wordCases, Message? few, Message? many, required Message other, - String? locale, }) { return old_intl.Intl.pluralLogic( howMany, diff --git a/pkgs/messages_shrinker/CHANGELOG.md b/pkgs/messages_shrinker/CHANGELOG.md index 7188fc88..d02216c3 100644 --- a/pkgs/messages_shrinker/CHANGELOG.md +++ b/pkgs/messages_shrinker/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.2.2-wip - Add license header. +- Fixes related to API change in package:messages_builder. ## 0.2.1 diff --git a/pkgs/messages_shrinker/lib/messages_shrinker.dart b/pkgs/messages_shrinker/lib/messages_shrinker.dart index 2099b3e6..dea8b38e 100644 --- a/pkgs/messages_shrinker/lib/messages_shrinker.dart +++ b/pkgs/messages_shrinker/lib/messages_shrinker.dart @@ -33,7 +33,7 @@ class MessageShrinker { String shrinkJson(String buffer, List messagesToKeep) { final sizeBefore = buffer.length; final json = JsonDeserializer(buffer).deserialize( - (howMany, {few, locale, many, numberCases, required other, wordCases}) { + (howMany, locale, {few, many, numberCases, required other, wordCases}) { throw StateError('As the deserialized MessageList is not used, but ' 'just immediately reserialized, this selector will not be called.'); }, diff --git a/pkgs/messages_shrinker/pubspec.yaml b/pkgs/messages_shrinker/pubspec.yaml index fbd30360..336e86a3 100644 --- a/pkgs/messages_shrinker/pubspec.yaml +++ b/pkgs/messages_shrinker/pubspec.yaml @@ -10,12 +10,11 @@ environment: dependencies: analyzer: ^6.2.0 args: ^2.4.2 - build: ^2.4.1 messages: ^0.2.0 messages_builder: ^0.2.0 messages_serializer: ^0.2.0 dev_dependencies: dart_flutter_team_lints: ^3.0.0 - intl: ^0.18.1 + intl: ^0.19.0 test: ^1.21.0 diff --git a/pkgs/messages_shrinker/test/message_shrinker_test.dart b/pkgs/messages_shrinker/test/message_shrinker_test.dart index 5c8f76f9..dae14f45 100644 --- a/pkgs/messages_shrinker/test/message_shrinker_test.dart +++ b/pkgs/messages_shrinker/test/message_shrinker_test.dart @@ -8,7 +8,6 @@ library; import 'dart:convert'; import 'dart:io'; -import 'package:build/build.dart'; import 'package:intl/intl.dart' as old_intl; import 'package:messages/messages_json.dart'; import 'package:messages_builder/arb_parser.dart'; @@ -17,13 +16,13 @@ import 'package:messages_shrinker/messages_shrinker.dart'; import 'package:test/test.dart'; Message intlPluralSelector( - num howMany, { + num howMany, + String locale, { Map? numberCases, Map? wordCases, Message? few, Message? many, required Message other, - String? locale, }) { return old_intl.Intl.pluralLogic( howMany, @@ -49,14 +48,15 @@ void main() { }); String getMessage(int i, List args) => JsonDeserializer(dataFileContents) - .deserialize(intl) + .deserialize(intlPluralSelector) .generateStringAtIndex(i, args); test('Shrink a json', () { final messageIndex = 1; final output = MessageShrinker().shrinkJson(dataFileContents, [messageIndex]); - final deserialize = JsonDeserializer(output).deserialize(intl); + final deserialize = + JsonDeserializer(output).deserialize(intlPluralSelector); final args = [2]; final generateStringAtIndex = deserialize.generateStringAtIndex(1, args); expect(generateStringAtIndex, getMessage(messageIndex, args)); @@ -86,8 +86,7 @@ String readArbFileToDataFile() { final path = 'test/testarb.arb'; final arbFile = File(path).readAsStringSync(); final arb = jsonDecode(arbFile) as Map; - final parsed = - ArbParser().parseMessageFile(arb, AssetId('messsages_shrinker', path)); + final parsed = ArbParser().parseMessageFile(arb); return JsonSerializer() .serialize('', '', parsed.messages.map((e) => e.message).toList()) .data;