From 06acc1a7b9f394d36f599a93b59e5192107cbcc9 Mon Sep 17 00:00:00 2001 From: Andreigr0 Date: Mon, 29 Mar 2021 13:19:32 +0300 Subject: [PATCH] migrate to null safety --- README.md | 19 +- bin/download.dart | 33 +- example/pubspec.yaml | 7 +- lib/arbify_download.dart | 3 - lib/src/api/arbify_api.dart | 51 +-- lib/src/api/export_info.dart | 2 +- lib/src/arb_parser/arb_file.dart | 8 +- lib/src/arb_parser/arb_message.dart | 22 +- lib/src/arb_parser/arb_parser.dart | 40 +-- lib/src/arbify_cli.dart | 319 ++++-------------- lib/src/config/config.dart | 22 -- lib/src/config/pubspec_config.dart | 12 +- lib/src/generator/intl_translation.dart | 143 -------- lib/src/generator/l10n_dart_generator.dart | 127 ------- lib/src/icu_parser/icu_messages.dart | 259 -------------- lib/src/icu_parser/icu_parser.dart | 120 ------- .../language_identifier_parser/locale.dart | 6 +- .../locale_parser.dart | 15 +- lib/src/output_file_utils.dart | 24 +- lib/src/print_instructions.dart | 52 +++ pubspec.yaml | 27 +- test/arbify_api_test.dart | 22 +- test/arbify_api_test.mocks.dart | 38 +++ 23 files changed, 332 insertions(+), 1039 deletions(-) delete mode 100644 lib/src/config/config.dart delete mode 100644 lib/src/generator/intl_translation.dart delete mode 100644 lib/src/generator/l10n_dart_generator.dart delete mode 100644 lib/src/icu_parser/icu_messages.dart delete mode 100644 lib/src/icu_parser/icu_parser.dart create mode 100644 lib/src/print_instructions.dart create mode 100644 test/arbify_api_test.mocks.dart diff --git a/README.md b/README.md index 4998f04..fda127d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![pub package][pub-package-badge]][pub-package] [![Flutter workflow][flutter-workflow-badge]][flutter-workflow] -A package providing support for internationalizing Flutter applications using [intl] package with [Arbify]. +A wrapper of [intl_utils](https://pub.dev/packages/intl_utils). Provides your translations server instead of `localizely` server ## Usage @@ -23,9 +23,20 @@ Use `flutter pub run arbify:download` to run a command-line utility that will gu ```yaml arbify: - url: https://arb.company.com - project_id: 17 - outpur_dir: lib/l10n # default, can be ommited + url: https://arb.company.com + project_id: 17 + outpur_dir: lib/l10n # default, can be omitted + ``` + + Additional configs from [intl_utils](https://pub.dev/packages/intl_utils): + ```yaml + flutter_intl: + enabled: false # Required. If true IDE plugin will watch changes of files and generate it by itself + class_name: S # Optional. Sets the name for the generated localization class. Default: S + main_locale: en # Optional. Sets the main locale used for generating localization files. Provided value should consist of language code and optional script and country codes separated with underscore (e.g. 'en', 'en_GB', 'zh_Hans', 'zh_Hans_CN'). Default: en + arb_dir: lib/l10n # Optional. Sets the directory of your ARB resource files. Provided value should be a valid path on your system. Default: lib/l10n + output_dir: lib/generated # Optional. Sets the directory of generated localization files. Provided value should be a valid path on your system. Default: lib/generated + use_deferred_loading: false # Optional. Must be set to true to generate localization code that is loaded with deferred loading. Default: false ``` 2. Adding your secret (obtained at https://arb.company.com/account/secrets/create) to `.secret.arbify` file. diff --git a/bin/download.dart b/bin/download.dart index 180dffa..f5b17cf 100644 --- a/bin/download.dart +++ b/bin/download.dart @@ -1,5 +1,36 @@ import 'package:arbify/arbify_download.dart'; +import 'package:args/args.dart'; +import 'package:universal_io/io.dart'; + +Future main(List arguments) async { + final _argParser = ArgParser() + ..addFlag( + 'help', + abbr: 'h', + negatable: false, + help: 'Shows this help message.', + ) + ..addFlag( + 'interactive', + abbr: 'i', + defaultsTo: true, + help: 'Whether the command-line utility can ask you interactively.', + ) + ..addOption( + 'secret', + abbr: 's', + valueHelp: 'secret', + help: 'Secret to be used for authenticating to the Arbify API.\n' + 'Overrides the secret from the .secret.arbify file.', + ); + + final ArgResults args = _argParser.parse(arguments); + + if (args['help'] as bool) { + print('Arbify download command-line utility.\n'); + print(_argParser.usage); + exit(0); + } -Future main(List args) async { await ArbifyCli().run(args); } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 254c238..e1f1bca 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -6,19 +6,20 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter - intl: ^0.16.1 + intl: ^0.17.0 dev_dependencies: + arbify: + path: ../ flutter_test: sdk: flutter - arbify: ^0.0.6 flutter: uses-material-design: true diff --git a/lib/arbify_download.dart b/lib/arbify_download.dart index 0a58e15..93ea8db 100644 --- a/lib/arbify_download.dart +++ b/lib/arbify_download.dart @@ -4,9 +4,6 @@ export 'src/api/arbify_api.dart'; export 'src/arb_parser/arb_file.dart'; export 'src/arb_parser/arb_parser.dart'; export 'src/arbify_cli.dart'; -export 'src/config/config.dart'; export 'src/config/pubspec_config.dart'; export 'src/config/secret.dart'; -export 'src/generator/intl_translation.dart'; -export 'src/generator/l10n_dart_generator.dart'; export 'src/output_file_utils.dart'; diff --git a/lib/src/api/arbify_api.dart b/lib/src/api/arbify_api.dart index 6f0b23a..ae1f0c8 100644 --- a/lib/src/api/arbify_api.dart +++ b/lib/src/api/arbify_api.dart @@ -1,40 +1,41 @@ -import 'package:meta/meta.dart'; import 'package:dio/dio.dart'; import 'export_info.dart'; class ArbifyApi { - static const _apiPrefix = '/api/v1'; + late final Dio _client; - final Dio _client; + ArbifyApi({required Uri apiUrl, required String secret, Dio? client}) { + _client = client ?? Dio(); - ArbifyApi({@required Uri apiUrl, @required String secret, Dio client}) - : _client = client ?? Dio() { - _client.options = _client.options.merge( - baseUrl: apiUrl.toString() + _apiPrefix, - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json; charset=utf-8', - 'Authorization': 'Bearer $secret', - }, - ); + final options = _client.options; + options.baseUrl = '$apiUrl/api/v1'; + options.headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + 'Authorization': 'Bearer $secret', + }; } /// Fetches available exports with their last modification date from /// a project with a given [projectId]. - Future> fetchAvailableExports(int projectId) async { - return _client.get('/projects/$projectId/arb').then((response) { - return (response.data as Map) - .entries - .map((entry) => - ExportInfo(entry.key, DateTime.parse(entry.value as String))) - .toList(); - }); + Future> fetchAvailableExportsForProj(int projectId) async { + final response = await _client.get('/projects/$projectId/arb'); + + return (response.data as Map).entries.map((entry) { + return ExportInfo( + languageCode: entry.key, + lastModified: DateTime.parse(entry.value as String), + ); + }).toList(); } - Future fetchExport(int projectId, String languageCode) async { - return _client - .get('/projects/$projectId/arb/$languageCode') - .then((response) => response.data as String); + Future fetchExport({ + required int projectId, + required String languageCode, + }) async { + final path = '/projects/$projectId/arb/$languageCode'; + final response = await _client.get(path); + return response.data as String; } } diff --git a/lib/src/api/export_info.dart b/lib/src/api/export_info.dart index 273e81c..4e741a6 100644 --- a/lib/src/api/export_info.dart +++ b/lib/src/api/export_info.dart @@ -2,7 +2,7 @@ class ExportInfo { final String languageCode; final DateTime lastModified; - ExportInfo(this.languageCode, this.lastModified); + ExportInfo({required this.languageCode, required this.lastModified}); @override String toString() => diff --git a/lib/src/arb_parser/arb_file.dart b/lib/src/arb_parser/arb_file.dart index 5407494..bf6a4bf 100644 --- a/lib/src/arb_parser/arb_file.dart +++ b/lib/src/arb_parser/arb_file.dart @@ -3,18 +3,18 @@ import 'arb_message.dart'; class ArbFile { /// [locale] is the locale for which messages/resources are stored /// in this file. - final String locale; + final String? locale; /// [context] describes (in text) the context in which all these /// resources apply. - final String context; + final String? context; /// [lastModified] is the last modified time of this ARB file/data. - final DateTime lastModified; + final DateTime? lastModified; /// [author] is the author of these messages. In the case of localized /// ARB files it can contain the names/details of the translator. - final String author; + final String? author; /// [customAttributes] is a map of customized attributes that are /// the attributes prefixed with "x-". diff --git a/lib/src/arb_parser/arb_message.dart b/lib/src/arb_parser/arb_message.dart index 834a48f..e10d070 100644 --- a/lib/src/arb_parser/arb_message.dart +++ b/lib/src/arb_parser/arb_message.dart @@ -1,5 +1,3 @@ -import 'package:meta/meta.dart'; - // https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification class ArbMessage { /// [id] is the resource id is the identifier for the resource in a given @@ -23,7 +21,7 @@ class ArbMessage { /// [type] describes the type of resource. Possible values are "text", /// "image", "css". Program should not rely on this attribute in run time. /// It is mainly for the localization tools. - final String type; + final String? type; /// [context] describes (in text) the context in which this resource applies. /// Context is organized in hierarchy, and level separated by ":". @@ -34,12 +32,12 @@ class ArbMessage { /// Example: /// /// "context":"homePage:Print dialog box" - final String context; + final String? context; /// [description] is a short paragraph describing the resource and how it is /// being used by the app, and message that need to be passed to /// localization process and translators. - final String description; + final String? description; /// [placeholders] is a map from placeholder id to placeholder properties, /// including description and example. Placeholder can be specified using @@ -100,23 +98,23 @@ class ArbMessage { /// } /// } /// }, - final Map> placeholders; + final Map>? placeholders; /// [screenshot] is a URL to the image location or base-64 encoded image /// data. - final String screenshot; + final String? screenshot; /// [video] is a URL to a video of the app/resource/widget in action. - final String video; + final String? video; /// [sourceText] is the source of the text from where this message is /// translated from. This is used to track source arb change and determine /// if this message need to be updated. - final String sourceText; + final String? sourceText; /// [customAttributes] is a map of customized attributes that are /// the attributes prefixed with "x-". - final Map customAttributes; + final Map? customAttributes; /// Resource values ([value]) in an ARB file is always in the form of /// a string. Most of those strings represent translatable text. Some strings @@ -127,7 +125,8 @@ class ArbMessage { final String value; ArbMessage({ - @required this.id, + required this.id, + required this.value, this.type, this.context, this.description, @@ -136,6 +135,5 @@ class ArbMessage { this.video, this.sourceText, this.customAttributes, - @required this.value, }); } diff --git a/lib/src/arb_parser/arb_parser.dart b/lib/src/arb_parser/arb_parser.dart index 6b5d0ad..9913deb 100644 --- a/lib/src/arb_parser/arb_parser.dart +++ b/lib/src/arb_parser/arb_parser.dart @@ -4,7 +4,7 @@ import 'arb_file.dart'; import 'arb_message.dart'; class ArbParser { - ArbFile parseString(String content) { + ArbFile parseArbFile(String content) { final json = jsonDecode(content) as Map; final messages = []; @@ -13,7 +13,7 @@ class ArbParser { return; } - final attributes = json['@$key'] as Map; + final attributes = json['@$key'] as Map?; final message = parseMessage(key, value as String, attributes); messages.add(message); @@ -21,10 +21,10 @@ class ArbParser { final file = ArbFile( messages: messages, - locale: json['@@locale'] as String, - context: json['@@context'] as String, - lastModified: DateTime.tryParse(json['@@last_modified'] as String), - author: json['@@author'] as String, + locale: json['@@locale'] as String?, + context: json['@@context'] as String?, + lastModified: DateTime.tryParse(json['@@last_modified'] as String? ?? ''), + author: json['@@author'] as String?, ); return file; @@ -33,7 +33,7 @@ class ArbParser { ArbMessage parseMessage( String id, String value, - Map attributes, + Map? attributes, ) { final attrs = attributes ?? {}; @@ -41,26 +41,28 @@ class ArbParser { final message = ArbMessage( id: id, value: value, - type: attrs['type'] as String, - context: attrs['context'] as String, - description: attrs['description'] as String, - placeholders: attrs['placeholders'] as Map>, - screenshot: attrs['screenshot'] as String, - video: attrs['video'] as String, - sourceText: attrs['source_text'] as String, + type: attrs['type'] as String?, + context: attrs['context'] as String?, + description: attrs['description'] as String?, + placeholders: attrs['placeholders'] as Map>?, + screenshot: attrs['screenshot'] as String?, + video: attrs['video'] as String?, + sourceText: attrs['source_text'] as String?, customAttributes: customAttributes, ); return message; } - Map parseCustomAttributes(Map attributes) { + Map? parseCustomAttributes(Map attributes) { final entries = attributes.entries .where((attribute) => attribute.key.startsWith('x-')) - .map((attribute) => MapEntry( - attribute.key.substring(2), - attribute.value, - )); + .map((attribute) { + return MapEntry( + attribute.key.substring(2), + attribute.value, + ); + }); return Map.fromEntries(entries); } diff --git a/lib/src/arbify_cli.dart b/lib/src/arbify_cli.dart index 2873e7b..b7fbf56 100644 --- a/lib/src/arbify_cli.dart +++ b/lib/src/arbify_cli.dart @@ -1,294 +1,115 @@ -import 'package:path/path.dart' as path; import 'package:arbify/src/api/arbify_api.dart'; -import 'package:arbify/src/arb_parser/arb_file.dart'; import 'package:arbify/src/arb_parser/arb_parser.dart'; -import 'package:arbify/src/config/config.dart'; import 'package:arbify/src/config/pubspec_config.dart'; import 'package:arbify/src/config/secret.dart'; -import 'package:arbify/src/generator/intl_translation.dart'; -import 'package:arbify/src/generator/l10n_dart_generator.dart'; import 'package:arbify/src/output_file_utils.dart'; +import 'package:arbify/src/print_instructions.dart'; import 'package:args/args.dart'; import 'package:dio/dio.dart'; +import 'package:intl_utils/intl_utils.dart'; import 'package:universal_io/io.dart'; class ArbifyCli { - final _argParser = ArgParser() - ..addFlag( - 'help', - abbr: 'h', - negatable: false, - help: 'Shows this help message.', - ) - ..addFlag( - 'interactive', - abbr: 'i', - defaultsTo: true, - help: 'Whether the command-line utility can ask you interactively.', - ) - ..addOption( - 'secret', - abbr: 's', - valueHelp: 'secret', - help: 'Secret to be used for authenticating to the Arbify API.\n' - 'Overrides the secret from the .secret.arbify file.', - ); - + late final OutputFileUtils _fileUtils; final _arbFilesPattern = RegExp(r'intl_(.*)\.arb'); - OutputFileUtils _fileUtils; - - Future run(List args) async { - final results = _argParser.parse(args); - - if (results['help'] as bool) { - _printHelp(); - exit(0); - } - - final config = _getConfig( - secretOverride: results['secret'] as String, - interactive: results['interactive'] as bool, - ); - _fileUtils = OutputFileUtils(config.outputDir); - _ensureDirectories(); - - try { - await runDownload(config); - } on DioError catch (e) { - if (e.type == DioErrorType.RESPONSE) { - if (e.response.statusCode == 403) { - _printApiForbidden(config.projectId); - } else if (e.response.statusCode == 404) { - _printApiNotFound(config.projectId); - } else { - print('API exception\n'); - print(e.toString()); - } - } else { - print('Exception while communicating with the Arbify ' - 'at ${config.apiUrl.toString()}\n'); - print(e.toString()); - } - - exit(3); - } - } - - void _printHelp() { - print('Arbify download command-line utility.\n'); - print(_argParser.usage); - } - - Config _getConfig({String secretOverride, bool interactive}) { - final pubspec = _getPubspecConfig(); - final apiSecret = _getApiSecret( - arbifyUrl: pubspec.url, - interactive: interactive, - overrideSecret: secretOverride, - ); - - return Config( - apiUrl: pubspec.url, - projectId: pubspec.projectId, - outputDir: pubspec.outputDir ?? 'lib/l10n', - apiSecret: apiSecret, - ); - } - - PubspecConfig _getPubspecConfig() { + Future run(ArgResults args) async { final pubspec = PubspecConfig.fromPubspec(); if (pubspec.url == null || pubspec.projectId == null) { - _printPubspecInstructions(); + PrintInstructions.pubspec(); exit(1); } + final interactive = args['interactive'] as bool; + final Uri apiUrl = pubspec.url!; + final int projectId = pubspec.projectId!; + final String outputDir = pubspec.outputDir ?? 'lib/l10n'; - return pubspec; - } - - void _printPubspecInstructions() { - print(""" -You don't have all the required configuration options. You can -copy the template below and place it at the end of your pubspec. - -arbify: - url: https://arb.example.org - project_id: 12 - output_dir: lib/l10n # This is the default value."""); - } - - String _getApiSecret({ - Uri arbifyUrl, - bool interactive, - String overrideSecret, - }) { final secret = Secret(); - if (overrideSecret != null) { - return overrideSecret; - } - - if (secret.exists()) { - return secret.value(); - } + final String apiSecret; + final overrideSecret = args['secret'] as String?; - if (!interactive) { - _printNoInteractiveSecretInstructions(arbifyUrl); + if (overrideSecret != null) { + apiSecret = overrideSecret; + } else if (secret.exists()) { + apiSecret = secret.value(); + } else if (!interactive) { + PrintInstructions.noInteractiveSecret(apiUrl); exit(2); + } else { + apiSecret = PrintInstructions.promptInteractiveSecret(apiUrl); + secret.create(apiSecret); + secret.ensureGitIgnored(); } - final apiSecret = _promptInteractiveSecretInstructions(arbifyUrl); - secret.create(apiSecret); - secret.ensureGitIgnored(); - - return apiSecret; - } - - void _printNoInteractiveSecretInstructions(Uri arbifyUrl) { - final createSecretUrl = arbifyUrl.replace(path: '/account/secrets/create'); - print(""" -We couldn't find an Arbify secret. Please create a secret using -the URL below, paste it to .secret.arbify file in your project -directory and try again. Don't commit this file to your -version control software. - -$createSecretUrl -"""); - } - - String _promptInteractiveSecretInstructions(Uri arbifyUrl) { - final createSecretUrl = arbifyUrl.replace(path: '/account/secrets/create'); - stdout.write(""" -We couldn't find an Arbify secret. Please create a secret using -the URL below, paste it here and press Enter. + _fileUtils = OutputFileUtils(outputDir: outputDir); -$createSecretUrl - -Secret: """); - return stdin.readLineSync(); - } - - void _ensureDirectories() { if (!_fileUtils.dirExists()) { stdout.write("Output directory doesn't exist. Creating... "); _fileUtils.createDir(); stdout.write('done.\n'); } - } - - Future runDownload(Config config) async { - await _fetchExports(config); - _saveLocalizationDartFileOrExit(); - _runIntlTranslationGenerateFromArb(config); - } - Future _fetchExports(Config config) async { - final api = ArbifyApi(apiUrl: config.apiUrl, secret: config.apiSecret); - final arbParser = ArbParser(); - - final localArbFiles = _fileUtils.fetch(_arbFilesPattern); + try { + final api = ArbifyApi(apiUrl: apiUrl, secret: apiSecret); + final availableExports = + await api.fetchAvailableExportsForProj(projectId); - final availableExports = await api.fetchAvailableExports(config.projectId); - final availableLocalFiles = Map.fromEntries( - localArbFiles.map((contents) { - final arb = arbParser.parseString(contents); + final arbParser = ArbParser(); + final localArbFiles = _fileUtils.fetch(_arbFilesPattern); + final availableLocalFiles = Map.fromEntries( + localArbFiles.map((contents) { + final arb = arbParser.parseArbFile(contents); - return MapEntry(arb.locale, arb.lastModified); - }), - ); + return MapEntry(arb.locale, arb.lastModified); + }), + ); - for (final availableExport in availableExports) { - stdout.write(availableExport.languageCode.padRight(20)); + for (final availableExport in availableExports) { + stdout.write(availableExport.languageCode.padRight(20)); - final localFileLastModified = - availableLocalFiles[availableExport.languageCode]; + final DateTime? localFileLastModified = + availableLocalFiles[availableExport.languageCode]; - // If there is no local file for a given export or if it's older - // than the available export, download it. - if (localFileLastModified == null || - localFileLastModified.isBefore(availableExport.lastModified)) { - stdout.write('Downloading... '); + /// If there is no local file for a given export or if it's older + /// than the available export, download it. + if (localFileLastModified == null || + localFileLastModified.isBefore(availableExport.lastModified)) { + stdout.write('Downloading... '); - final remoteArb = await api.fetchExport( - config.projectId, - availableExport.languageCode, - ); + final remoteArb = await api.fetchExport( + projectId: projectId, + languageCode: availableExport.languageCode, + ); - _fileUtils.put('intl_${availableExport.languageCode}.arb', remoteArb); + _fileUtils.put('intl_${availableExport.languageCode}.arb', remoteArb); - stdout.write('done.\n'); - } else { - stdout.write('Up-to-date\n'); + stdout.write('done.\n'); + } else { + stdout.write('Up-to-date\n'); + } } - } - } - - void _printApiForbidden(int projectId) { - print(''' -API returned response with a 403 Forbidden status. Make sure you -have access to the project with a project id $projectId and that -you correctly setup the secret. Check .secret.arbify file again.'''); - } - - void _printApiNotFound(int projectId) { - print(''' -API returned response with a 404 Not Found status. Make sure you -put right project id in the pubspec.yaml file. The current -project id is $projectId.'''); - } - - void _saveLocalizationDartFileOrExit() { - const templateOrder = ['en', 'en-US', 'en-GB']; - - stdout.write('Generating l10n.dart file... '); - - final localFiles = _fileUtils.fetch(_arbFilesPattern); - - final arbParser = ArbParser(); - final locales = []; - ArbFile template; - for (final file in localFiles) { - final arb = arbParser.parseString(file); - locales.add(arb.locale); - - // Use file with highest priority as a template - // or the first one as a fallback. - template ??= arb; - - final fileIndexInOrder = templateOrder.indexOf(arb.locale); - var templateIndexInOrder = templateOrder.indexOf(template.locale); - // If the template's language isn't in order list, make its index big - // so it doesn't prevent from an actual template to override it. - if (templateIndexInOrder == -1) templateIndexInOrder = 10000; - - if (fileIndexInOrder != -1 && fileIndexInOrder < templateIndexInOrder) { - template = arb; + stdout.write('Generating messages dart files... '); + await Generator().generateAsync(); + stdout.write('done\n'); + } on DioError catch (e) { + if (e.type == DioErrorType.response) { + if (e.response?.statusCode == 403) { + PrintInstructions.apiForbidden(projectId); + } else if (e.response?.statusCode == 404) { + PrintInstructions.apiNotFound(projectId); + } else { + print('API exception\n'); + print(e.toString()); + } + } else { + print('Exception while communicating with the Arbify ' + 'at ${apiUrl.toString()}\n'); + print(e.toString()); } - } - if (template == null) { - print("fail\nCouldn't find intl_en.arb to use :("); - exit(4); + exit(3); } - - const generator = L10nDartGenerator(); - final l10nDartContents = generator.generate(template, locales); - - _fileUtils.put('l10n.dart', l10nDartContents); - - stdout.write('done\n'); - } - - void _runIntlTranslationGenerateFromArb(Config config) { - stdout.write('Generating messages dart files... '); - - IntlTranslation().generateFromArb( - config.outputDir, - [path.join(config.outputDir, 'l10n.dart')], - _fileUtils.list(RegExp('intl_(.*).arb')), - ); - - stdout.write('done\n'); } } diff --git a/lib/src/config/config.dart b/lib/src/config/config.dart deleted file mode 100644 index d7f8d9a..0000000 --- a/lib/src/config/config.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:meta/meta.dart'; - -class Config { - /// The url of an Arbify instance. - final Uri apiUrl; - - /// The id of a project. - final int projectId; - - /// The secret to use for authentication. - final String apiSecret; - - /// The directory to output results to. - final String outputDir; - - const Config({ - @required this.apiUrl, - @required this.projectId, - @required this.apiSecret, - @required this.outputDir, - }); -} diff --git a/lib/src/config/pubspec_config.dart b/lib/src/config/pubspec_config.dart index ec91d4b..b3d683a 100644 --- a/lib/src/config/pubspec_config.dart +++ b/lib/src/config/pubspec_config.dart @@ -3,9 +3,9 @@ import 'package:universal_io/io.dart'; import 'package:yaml/yaml.dart' as yaml; class PubspecConfig { - final Uri url; - final int projectId; - final String outputDir; + final Uri? url; + final int? projectId; + final String? outputDir; const PubspecConfig._({this.url, this.projectId, this.outputDir}); @@ -15,9 +15,9 @@ class PubspecConfig { final pubspec = yaml.loadYaml(utf8.decode(pubspecBytes))['arbify'] ?? {}; return PubspecConfig._( - url: Uri.tryParse(pubspec['url'] as String), - projectId: pubspec['project_id'] as int, - outputDir: pubspec['output_dir'] as String, + url: Uri.tryParse(pubspec['url'] as String? ?? ''), + projectId: pubspec['project_id'] as int?, + outputDir: pubspec['output_dir'] as String?, ); } diff --git a/lib/src/generator/intl_translation.dart b/lib/src/generator/intl_translation.dart deleted file mode 100644 index 0d6aed4..0000000 --- a/lib/src/generator/intl_translation.dart +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2020 The Localizely Authors. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without modification, -// are permitted provided that the following conditions are met: - -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials provided -// with the distribution. -// * Neither the name of Localizely Inc. nor the names of its -// contributors may be used to endorse or promote products derived -// from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// -// Modified by Albert Wolszon. - -// ignore_for_file: implementation_imports -import 'dart:convert'; - -import 'package:intl_translation/extract_messages.dart'; -import 'package:intl_translation/generate_localized.dart'; -import 'package:intl_translation/src/icu_parser.dart'; -import 'package:intl_translation/src/intl_message.dart'; -import 'package:path/path.dart' as path; -import 'package:universal_io/io.dart'; - -class IntlTranslation { - final pluralAndGenderParser = IcuParser().message; - final plainParser = IcuParser().nonIcuMessage; - final JsonCodec jsonDecoder = const JsonCodec(); - - final MessageExtraction extraction = MessageExtraction(); - final MessageGeneration generation = MessageGeneration(); - final Map> messages = - {}; // Track of all processed messages, keyed by message name - - IntlTranslation() { - extraction.suppressWarnings = true; - generation.useDeferredLoading = false; - generation.generatedFilePrefix = ''; - } - - void generateFromArb( - String outputDir, List dartFiles, List arbFiles) { - final allMessages = - dartFiles.map((file) => extraction.parseFile(File(file))); - for (final messageMap in allMessages) { - messageMap.forEach( - (key, value) => messages.putIfAbsent(key, () => []).add(value)); - } - - final messagesByLocale = >{}; - // Note: To group messages by locale, we eagerly read all data, which might cause a memory issue for large projects - for (final arbFile in arbFiles) { - _loadData(arbFile, messagesByLocale); - } - messagesByLocale.forEach((locale, data) { - _generateLocaleFile(locale, data, outputDir); - }); - - final mainImportFile = File(path.join( - outputDir, '${generation.generatedFilePrefix}messages_all.dart')); - mainImportFile.writeAsStringSync(generation.generateMainImportFile()); - } - - void _loadData(String filename, Map> messagesByLocale) { - final file = File(filename); - final src = file.readAsStringSync(); - final data = jsonDecoder.decode(src) as Map; - String locale = (data['@@locale'] ?? data['_locale']) as String; - if (locale == null) { - // Get the locale from the end of the file name. This assumes that the file - // name doesn't contain any underscores except to begin the language tag - // and to separate language from country. Otherwise we can't tell if - // my_file_fr.arb is locale "fr" or "file_fr". - final name = path.basenameWithoutExtension(file.path); - locale = name.split('_').skip(1).join('_'); - // info( - // "No @@locale or _locale field found in $name, assuming '$locale' based on the file name."); - } - messagesByLocale.putIfAbsent(locale, () => []).add(data); - generation.allLocales.add(locale); - } - - void _generateLocaleFile( - String locale, - List localeData, - String targetDir, - ) { - final translations = []; - for (final jsonTranslations in localeData) { - jsonTranslations.forEach((id, messageData) { - final message = _recreateIntlObjects(id as String, messageData); - if (message != null) { - translations.add(message); - } - }); - } - generation.generateIndividualMessageFile(locale, translations, targetDir); - } - - /// Regenerate the original IntlMessage objects from the given [data]. For - /// things that are messages, we expect [id] not to start with "@" and - /// [data] to be a String. For metadata we expect [id] to start with "@" - /// and [data] to be a Map or null. For metadata we return null. - BasicTranslatedMessage _recreateIntlObjects(String id, data) { - if (id.startsWith('@')) return null; - if (data == null) return null; - var parsed = pluralAndGenderParser.parse(data as String).value; - if (parsed is LiteralString && parsed.string.isEmpty) { - parsed = plainParser.parse(data as String).value; - } - return BasicTranslatedMessage(id, parsed as Message, messages); - } -} - -/// A TranslatedMessage that just uses the name as the id and knows how to look up its original messages in our [messages]. -class BasicTranslatedMessage extends TranslatedMessage { - Map> messages; - - BasicTranslatedMessage(String name, Message translated, this.messages) - : super(name, translated); - - @override - List get originalMessages => (super.originalMessages == null) - ? _findOriginals() - : super.originalMessages; - - // We know that our [id] is the name of the message, which is used as the key in [messages]. - List _findOriginals() => originalMessages = messages[id]; -} diff --git a/lib/src/generator/l10n_dart_generator.dart b/lib/src/generator/l10n_dart_generator.dart deleted file mode 100644 index 5883842..0000000 --- a/lib/src/generator/l10n_dart_generator.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:arbify/src/arb_parser/arb_file.dart'; -import 'package:arbify/src/icu_parser/icu_parser.dart'; -import 'package:arbify/src/language_identifier_parser/locale.dart'; -import 'package:arbify/src/language_identifier_parser/locale_parser.dart'; - -class L10nDartGenerator { - const L10nDartGenerator(); - - String generate(ArbFile template, List locales) { - final messagesBuilder = StringBuffer(); - - for (final message in template.messages) { - final parsedMessage = IcuParser().parse(message.value); - final messageCode = parsedMessage.toCode(); - final arguments = parsedMessage.arguments; - - String signature; - if (arguments.all.isEmpty) { - signature = 'String get ${message.id}'; - } else { - final args = arguments.all.entries - .map((arg) => '${arg.value} ${arg.key}') - .join(', '); - - signature = 'String ${message.id}($args)'; - } - - messagesBuilder.write(""" - - - $signature => Intl.message( - '$messageCode', - name: '${message.id}',"""); - - if (message.description != null && message.description.isNotEmpty) { - final description = message.description.replaceAll("'", r"\'"); - - messagesBuilder.write(""" - - desc: '$description',"""); - } - - if (arguments.all.isNotEmpty) { - final args = arguments.all.keys.join(', '); - - messagesBuilder.write(''' - - args: [$args],'''); - } - - messagesBuilder.write('\n );'); - } - - final messages = messagesBuilder.toString(); - - final parsedLocales = locales - .map((locale) => LanguageIdentifierParser().parse(locale)) - .toList(); - final supportedLocales = _generateSupportedLocalesArray(parsedLocales); - final localeItems = - parsedLocales.map((locale) => "\n '${locale.language}',").join(); - - return """ -// File generated with arbify_flutter. -// DO NOT MODIFY BY HAND. -// ignore_for_file: lines_longer_than_80_chars, non_constant_identifier_names -// ignore_for_file: unnecessary_brace_in_string_interps - -import 'package:flutter/widgets.dart'; -import 'package:intl/intl.dart'; -import 'messages_all.dart'; - -class S { - final String localeName; - - const S(this.localeName); - - static const delegate = ArbifyLocalizationsDelegate(); - - static Future load(Locale locale) { - final localeName = Intl.canonicalizedLocale(locale.toString()); - - return initializeMessages(localeName).then((_) { - Intl.defaultLocale = localeName; - return S(localeName); - }); - } - - static S of(BuildContext context) => Localizations.of(context, S);$messages -} - -class ArbifyLocalizationsDelegate extends LocalizationsDelegate { - const ArbifyLocalizationsDelegate(); - - List get supportedLocales => [ -$supportedLocales ]; - - @override - bool isSupported(Locale locale) => [$localeItems - ].contains(locale.languageCode); - - @override - Future load(Locale locale) => S.load(locale); - - @override - bool shouldReload(ArbifyLocalizationsDelegate old) => false; -} -"""; - } - - String _generateSupportedLocalesArray(List locales) { - final supportedLocales = StringBuffer(); - - for (final locale in locales) { - final languageCode = "languageCode: '${locale.language}'"; - final scriptCode = - locale.script == null ? '' : ", scriptCode: '${locale.script}'"; - final countryCode = - locale.script == null ? '' : ", countryCode: '${locale.region}'"; - - supportedLocales.writeln( - ' Locale.fromSubtags($languageCode$scriptCode$countryCode),'); - } - - return supportedLocales.toString(); - } -} diff --git a/lib/src/icu_parser/icu_messages.dart b/lib/src/icu_parser/icu_messages.dart deleted file mode 100644 index 2fd5312..0000000 --- a/lib/src/icu_parser/icu_messages.dart +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright 2013, the Dart project authors. All rights reserved. -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials provided -// with the distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived -// from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// -// Heavily modified by Albert Wolszon. - -import 'package:meta/meta.dart'; - -class ArgumentsList { - final Map _arguments = {}; - - void add(String name, [String type = 'String']) { - // Add (overrite) only when the argument isn't yet on the list - // or if it's of type String. - if (!_arguments.containsKey(name) || _arguments[name] == 'String') { - _arguments[name] = type; - } - } - - void addAll(ArgumentsList list) { - list.all.forEach((key, value) => add(key, value)); - } - - Map get all => Map.unmodifiable(_arguments); -} - -abstract class Message { - final arguments = ArgumentsList(); - - Message parent; - - @mustCallSuper - Message(this.parent); - - factory Message.from(Object value, Message parent) { - if (value is String) { - return LiteralString(value, parent); - } else if (value is int) { - return VariableSubstitution(value.toString(), parent)..passArgumentsUp(); - } else if (value is List) { - if (value.length == 1) { - return Message.from(value[0], parent)..passArgumentsUp(); - } - - final message = CompositeMessage([], parent); - message.pieces.addAll( - value - .map((value) => Message.from(value, message)..passArgumentsUp()) - .toList(), - ); - - return message..passArgumentsUp(); - } - - return (value as Message) - ..parent = parent - ..passArgumentsUp(); - } - - String toCode(); - - void passArgumentsUp() { - if (parent != null) { - parent.arguments.addAll(arguments); - } - } - - String escapeString(String value) { - const escapes = { - r'\': r'\\', - '"': r'\"', - '\b': r'\b', - '\f': r'\f', - '\n': r'\n', - '\r': r'\r', - '\t': r'\t', - '\v': r'\v', - "'": r"\'", - r'$': r'\$', - }; - - return value.splitMapJoin( - '', - onNonMatch: (value) => escapes[value] ?? value, - ); - } -} - -class CompositeMessage extends Message { - List pieces; - - CompositeMessage(this.pieces, Message parent) : super(parent); - - @override - String toCode() => pieces.map((piece) => piece.toCode()).join(); -} - -class LiteralString extends Message { - String string; - - LiteralString(this.string, Message parent) : super(parent); - - @override - String toCode() => escapeString(string); -} - -class VariableSubstitution extends Message { - String variableName; - - VariableSubstitution(this.variableName, Message parent) : super(parent) { - arguments.add(variableName); - } - - @override - String toCode() => '\${$variableName}'; -} - -abstract class IcuMessage extends Message { - String icuName; - - String variableName; - - Map clauses; - - IcuMessage( - this.icuName, - this.variableName, - this.clauses, - Message parent, { - String variableType = 'String', - }) : super(parent) { - arguments.add(variableName, variableType); - } - - @override - String toCode() { - final buffer = StringBuffer(); - buffer.write('\${Intl.$icuName('); - buffer.write(variableName); - clauses.forEach((key, value) { - buffer.write(", $key: '${value.toCode()}'"); - }); - - if (arguments.all.isNotEmpty) { - final args = arguments.all.keys.join(', '); - - buffer.write(', args: [$args]'); - } - - buffer.write(')}'); - - return buffer.toString(); - } -} - -class Gender extends IcuMessage { - Gender(String variableName, Map clauses, Message parent) - : super('gender', variableName, clauses, parent); - - factory Gender.from( - String variableName, - List genderClauses, - Message parent, - ) { - final gender = Gender(variableName, {}, parent); - gender.clauses.addEntries(genderClauses.map( - (clause) => - MapEntry(clause[0] as String, Message.from(clause[1], gender)), - )); - - return gender; - } -} - -class Plural extends IcuMessage { - Plural( - String variableName, - Map pluralClauses, - Message parent, - ) : super('plural', variableName, pluralClauses, parent, variableType: 'num'); - - factory Plural.from( - String variableName, - List pluralClauses, - Message parent, - ) { - final plural = Plural(variableName, {}, parent); - plural.clauses.addEntries(pluralClauses.map( - (clause) => - MapEntry(clause[0] as String, Message.from(clause[1], plural)), - )); - - return plural; - } -} - -class Select extends IcuMessage { - Select( - String variableName, Map selectClauses, Message parent) - : super('select', variableName, selectClauses, parent); - - factory Select.from( - String variableName, - List selectClauses, - Message parent, - ) { - final select = Select(variableName, {}, parent); - select.clauses.addEntries(selectClauses.map( - (clause) => - MapEntry(clause[0] as String, Message.from(clause[1], select)), - )); - - return select; - } - - @override - String toCode() { - final buffer = StringBuffer(); - buffer.write('\${Intl.select('); - buffer.write('$variableName, cases: {'); - clauses.forEach((key, value) { - buffer.write("'$key': '${value.toCode()}',"); - }); - buffer.write('}'); - - if (arguments.all.isNotEmpty) { - final args = arguments.all.keys.join(', '); - - buffer.write(', args: [$args]'); - } - - buffer.write(')}'); - - return buffer.toString(); - } -} diff --git a/lib/src/icu_parser/icu_parser.dart b/lib/src/icu_parser/icu_parser.dart deleted file mode 100644 index e4783f3..0000000 --- a/lib/src/icu_parser/icu_parser.dart +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2013, the Dart project authors. All rights reserved. -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials provided -// with the distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived -// from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// -// Modified by Albert Wolszon. - -import 'package:petitparser/petitparser.dart'; -import 'icu_messages.dart'; - -/// This defines a grammar for ICU MessageFormat syntax. Usage is -/// IcuParser.message.parse().value; -/// The "parse" method will return a Success or Failure object which responds -/// to "value". -class IcuParser { - Parser get openCurly => char('{'); - - Parser get closeCurly => char('}'); - Parser get quotedCurly => (string("'{'") | string("'}'")).map((x) => x[1]); - - Parser get icuEscapedText => quotedCurly | twoSingleQuotes; - Parser get curly => openCurly | closeCurly; - Parser get notAllowedInIcuText => curly | char('<'); - Parser get icuText => notAllowedInIcuText.neg(); - Parser get notAllowedInNormalText => char('{'); - Parser get normalText => notAllowedInNormalText.neg(); - Parser get messageText => - (icuEscapedText | icuText).plus().map((x) => x.join()); - Parser get nonIcuMessageText => normalText.plus().map((x) => x.join()); - Parser get twoSingleQuotes => string("''").map((x) => "'"); - Parser get number => digit().plus().flatten().trim().map(int.parse); - Parser get id => (letter() & (word() | char('_')).star()).flatten().trim(); - Parser get comma => char(',').trim(); - - /// Given a list of possible keywords, return a rule that accepts any of them. - /// e.g., given ["male", "female", "other"], accept any of them. - Parser asKeywords(List list) => - list.map(string).cast().reduce((a, b) => a | b).flatten().trim(); - - Parser get pluralKeyword => asKeywords( - ['=0', '=1', '=2', 'zero', 'one', 'two', 'few', 'many', 'other']); - Parser get genderKeyword => asKeywords(['female', 'male', 'other']); - - SettableParser interiorText = undefined(); - - Parser get preface => (openCurly & id & comma).map((values) => values[1]); - - Parser get pluralLiteral => string('plural'); - Parser get pluralClause => - (pluralKeyword & openCurly & interiorText & closeCurly) - .trim() - .map((result) => [result[0], result[2]]); - Parser get plural => - preface & pluralLiteral & comma & pluralClause.plus() & closeCurly; - Parser get intlPlural => plural.map( - (values) => Plural.from(values.first as String, values[3] as List, null)); - - Parser get genderLiteral => string('gender'); - Parser get genderClause => - (genderKeyword & openCurly & interiorText & closeCurly) - .trim() - .map((result) => [result[0], result[2]]); - Parser get gender => - preface & genderLiteral & comma & genderClause.plus() & closeCurly; - Parser get intlGender => gender.map( - (values) => Gender.from(values.first as String, values[3] as List, null)); - - Parser get selectLiteral => string('select'); - Parser get selectClause => - (id & openCurly & interiorText & closeCurly).map((x) => [x.first, x[2]]); - Parser get generalSelect => - preface & selectLiteral & comma & selectClause.plus() & closeCurly; - Parser get intlSelect => generalSelect.map( - (values) => Select.from(values.first as String, values[3] as List, null)); - - Parser get pluralOrGenderOrSelect => intlPlural | intlGender | intlSelect; - - Parser get contents => pluralOrGenderOrSelect | parameter | messageText; - Parser get simpleText => (nonIcuMessageText | parameter | openCurly).plus(); - Parser get empty => epsilon().map((_) => ''); - - Parser get parameter => (openCurly & id & closeCurly) - .map((param) => VariableSubstitution(param[1] as String, null)); - - Message parse(String text) { - // final result = (pluralOrGenderOrSelect | simpleText | empty) - final result = - interiorText.map((value) => Message.from(value, null)).parse(text); - - return result.value; - } - - IcuParser() { - // There is a cycle here, so we need the explicit set to avoid - // infinite recursion. - interiorText.set(contents.plus() | empty); - } -} diff --git a/lib/src/language_identifier_parser/locale.dart b/lib/src/language_identifier_parser/locale.dart index 163bc46..8702b6b 100644 --- a/lib/src/language_identifier_parser/locale.dart +++ b/lib/src/language_identifier_parser/locale.dart @@ -1,7 +1,7 @@ class Locale { - final String language; - final String script; - final String region; + final String? language; + final String? script; + final String? region; Locale({this.language, this.script, this.region}); } diff --git a/lib/src/language_identifier_parser/locale_parser.dart b/lib/src/language_identifier_parser/locale_parser.dart index 48971ff..7c288a1 100644 --- a/lib/src/language_identifier_parser/locale_parser.dart +++ b/lib/src/language_identifier_parser/locale_parser.dart @@ -4,25 +4,30 @@ import 'package:petitparser/petitparser.dart'; class LanguageIdentifierParser { Parser get alphanum => letter() | digit(); + Parser get sep => char('_') | char('-'); Parser get language => (letter().repeat(2, 3) | letter().repeat(5, 8)).flatten(); Parser get script => letter().times(4).flatten(); + Parser get region => (letter().times(2) | digit().times(3)).flatten(); Parser get scriptPart => (sep & script).map((value) => value[1]).optional(); + Parser get regionPart => (sep & region).map((value) => value[1]).optional(); Parser get id => language & scriptPart & regionPart; Locale parse(String text) => id - .map((value) => Locale( - language: value[0] as String, - script: value[1] as String, - region: value[2] as String, - )) + .map((value) { + return Locale( + language: value[0] as String?, + script: value[1] as String?, + region: value[2] as String?, + ); + }) .parse(text) .value; } diff --git a/lib/src/output_file_utils.dart b/lib/src/output_file_utils.dart index 46b4b22..fc66842 100644 --- a/lib/src/output_file_utils.dart +++ b/lib/src/output_file_utils.dart @@ -4,29 +4,33 @@ import 'package:universal_io/io.dart'; class OutputFileUtils { final String outputDir; - const OutputFileUtils(this.outputDir); + const OutputFileUtils({required this.outputDir}); + + Directory _dir() => Directory(outputDir); bool dirExists() => _dir().existsSync(); void createDir() => _dir().createSync(recursive: true); - List fetch([Pattern pattern]) { - var files = _dir().listSync().whereType(); + List fetch([Pattern? pattern]) { + Iterable files = _dir().listSync().whereType(); if (pattern != null) { - files = files.where( - (file) => pattern.allMatches(path.basename(file.path)).isNotEmpty); + files = files.where((file) { + return pattern.allMatches(path.basename(file.path)).isNotEmpty; + }); } return files.map((file) => file.readAsStringSync()).toList(); } - List list([Pattern pattern]) { - var files = _dir().listSync().whereType(); + List list([Pattern? pattern]) { + Iterable files = _dir().listSync().whereType(); if (pattern != null) { - files = files.where( - (file) => pattern.allMatches(path.basename(file.path)).isNotEmpty); + files = files.where((file) { + return pattern.allMatches(path.basename(file.path)).isNotEmpty; + }); } return files.map((file) => file.path).toList(); @@ -35,6 +39,4 @@ class OutputFileUtils { void put(String filename, String contents) { File(path.join(outputDir, filename)).writeAsStringSync(contents); } - - Directory _dir() => Directory(outputDir); } diff --git a/lib/src/print_instructions.dart b/lib/src/print_instructions.dart new file mode 100644 index 0000000..b195c58 --- /dev/null +++ b/lib/src/print_instructions.dart @@ -0,0 +1,52 @@ +import 'package:universal_io/io.dart'; + +abstract class PrintInstructions { + static void pubspec() { + print(""" +You don't have all the required configuration options. You can +copy the template below and place it at the end of your pubspec. + +arbify: + url: https://arb.example.org + project_id: 12 + output_dir: lib/l10n # This is the default value."""); + } + + static void apiForbidden(int projectId) { + print(''' +API returned response with a 403 Forbidden status. Make sure you +have access to the project with a project id $projectId and that +you correctly setup the secret. Check .secret.arbify file again.'''); + } + + static void apiNotFound(int projectId) { + print(''' +API returned response with a 404 Not Found status. Make sure you +put right project id in the pubspec.yaml file. The current +project id is $projectId.'''); + } + + static void noInteractiveSecret(Uri arbifyUrl) { + final createSecretUrl = arbifyUrl.replace(path: '/account/secrets/create'); + print(""" +We couldn't find an Arbify secret. Please create a secret using +the URL below, paste it to .secret.arbify file in your project +directory and try again. Don't commit this file to your +version control software. + +$createSecretUrl +"""); + } + + static String promptInteractiveSecret(Uri arbifyUrl) { + final createSecretUrl = arbifyUrl.replace(path: '/account/secrets/create'); + stdout.write(""" +We couldn't find an Arbify secret. Please create a secret using +the URL below, paste it here and press Enter. + +$createSecretUrl + +Secret: """); + return stdin.readLineSync()!; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 4e98def..0ccccdc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,25 +2,26 @@ name: arbify description: > A package providing support for internationalizing Flutter applications using intl package with Arbify. -version: 0.0.11 +version: 0.1.0 homepage: https://github.com/Arbify/arbify_flutter environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: + args: ^2.0.0 + dio: ^4.0.0 flutter: sdk: flutter - args: ^1.6.0 - dio: ^3.0.9 - intl_translation: ^0.17.9 - meta: ^1.1.8 - path: ^1.6.4 - petitparser: '>=2.4.0 <4.0.0' - universal_io: ^1.0.1 - yaml: ^2.2.1 + intl: ^0.17.0 + intl_utils: ^2.1.0 + path: ^1.8.0 + petitparser: ^4.0.2 + universal_io: ^2.0.3 + yaml: ^3.1.0 dev_dependencies: - mockito: ^4.1.1 - test: ^1.14.7 - lint: ^1.2.0 + build_runner: ^1.12.2 + lint: ^1.5.3 + mockito: ^5.0.3 + test: ^1.16.8 diff --git a/test/arbify_api_test.dart b/test/arbify_api_test.dart index ae462e1..0e41dd0 100644 --- a/test/arbify_api_test.dart +++ b/test/arbify_api_test.dart @@ -3,20 +3,21 @@ import 'dart:convert'; import 'package:arbify/src/api/arbify_api.dart'; import 'package:arbify/src/api/export_info.dart'; import 'package:dio/dio.dart'; +import 'package:mockito/annotations.dart'; import 'package:test/test.dart'; import 'package:mockito/mockito.dart'; +import 'arbify_api_test.mocks.dart'; -class DioAdapterMock extends Mock implements HttpClientAdapter {} - +@GenerateMocks([HttpClientAdapter]) void main() { final dio = Dio(); - DioAdapterMock adapterMock; - ArbifyApi api; + late MockHttpClientAdapter adapterMock; + late ArbifyApi api; setUp(() { - adapterMock = DioAdapterMock(); + adapterMock = MockHttpClientAdapter(); dio.httpClientAdapter = adapterMock; api = ArbifyApi( - apiUrl: Uri.parse('http://test'), + apiUrl: Uri.parse('https://test'), secret: 'secret', client: dio, ); @@ -29,8 +30,11 @@ void main() { when(adapterMock.fetch(any, any, any)) .thenAnswer((_) async => mockResponse); - final exports = await api.fetchAvailableExports(2); - final response = ExportInfo('en', DateTime.utc(2020, 6, 7, 18, 13, 57)); + final exports = await api.fetchAvailableExportsForProj(2); + final response = ExportInfo( + languageCode: 'en', + lastModified: DateTime.utc(2020, 6, 7, 18, 13, 57), + ); expect(exports, isList); expect(exports, isNotEmpty); @@ -47,7 +51,7 @@ void main() { when(adapterMock.fetch(any, any, any)) .thenAnswer((_) async => mockResponse); - final export = await api.fetchExport(2, 'en'); + final export = await api.fetchExport(languageCode: 'en', projectId: 2); expect(export, equals(data)); }); }); diff --git a/test/arbify_api_test.mocks.dart b/test/arbify_api_test.mocks.dart new file mode 100644 index 0000000..9246b0b --- /dev/null +++ b/test/arbify_api_test.mocks.dart @@ -0,0 +1,38 @@ +// Mocks generated by Mockito 5.0.3 from annotations +// in arbify/test/arbify_api_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i3; +import 'dart:typed_data' as _i5; + +import 'package:dio/src/adapter.dart' as _i2; +import 'package:dio/src/options.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: comment_references +// ignore_for_file: unnecessary_parenthesis + +class _FakeResponseBody extends _i1.Fake implements _i2.ResponseBody {} + +/// A class which mocks [HttpClientAdapter]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHttpClientAdapter extends _i1.Mock implements _i2.HttpClientAdapter { + MockHttpClientAdapter() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.ResponseBody> fetch( + _i4.RequestOptions? options, + _i3.Stream<_i5.Uint8List>? requestStream, + _i3.Future? cancelFuture) => + (super.noSuchMethod( + Invocation.method(#fetch, [options, requestStream, cancelFuture]), + returnValue: Future.value(_FakeResponseBody())) + as _i3.Future<_i2.ResponseBody>); + @override + void close({bool? force = false}) => + super.noSuchMethod(Invocation.method(#close, [], {#force: force}), + returnValueForMissingStub: null); +}