├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── build.yaml ├── example ├── assets │ └── i18n │ │ ├── en.json │ │ └── es.json ├── build.yaml ├── lib │ ├── localization │ │ ├── keys.dart │ │ └── keys.g.dart │ └── main.dart └── pubspec.yaml ├── flutter_translate_gen.iml ├── lib ├── annotation_generator.dart ├── builder.dart ├── flutter_translate_gen.dart ├── keys_class_generator.dart └── localized_item.dart └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | .idea/ 5 | # Remove the following pattern if you wish to check in your lock file 6 | pubspec.lock 7 | 8 | # Conventional directory for build outputs 9 | build/ 10 | 11 | # Directory created by dartdoc 12 | doc/api/ 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial release 4 | 5 | ## 1.1.0 6 | 7 | - Implemented multiple case style options 8 | 9 | ## 1.2.0 10 | 11 | - Added support for pluralization 12 | - '0', '1' and 'else' are now reserved words 13 | 14 | ## 1.2.4 15 | 16 | - Fixed dependency compatibility issues 17 | 18 | ## 1.3.0 19 | 20 | - Upgraded dependencies 21 | 22 | ## 2.0.0 23 | 24 | - Upgraded dependencies 25 | 26 | ## 2.1.0 27 | 28 | - Added camel case support 29 | 30 | ## 2.2.0 31 | 32 | - Upgraded dependencies -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jesway Labs 4 | Website: https://jesway.com 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flutter_translate_gen 2 | 3 | [![pub package](https://img.shields.io/pub/v/flutter_translate_gen.svg?color=important)](https://pub.dev/packages/flutter_translate_gen) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-ff69b4.svg)](https://github.com/Jesway/Flutter-Translate/blob/master/LICENSE) 5 | [![Flutter.io](https://img.shields.io/badge/Flutter-Website-deepskyblue.svg)](https://flutter.io/) 6 | 7 | Statically-typed localization keys generator for [flutter translate](https://github.com/Jesway/Flutter-Translate) 8 | 9 | Please [check this wiki page](https://github.com/Jesway/Flutter-Translate/wiki/3.-Generating-statically-typed-localization-keys) for documentation on how to generate static localization keys. 10 | 11 | And [here you can find](https://github.com/Jesway/Flutter-Translate/tree/master/example_static_keys) a fully working example. 12 | 13 | ## Issues 14 | Please file any issues, bugs or feature request [here](https://github.com/Jesway/Flutter-Translate-Gen/issues). 15 | 16 | ## License 17 | 18 | This project is licensed under the [MIT License](https://github.com/Jesway/Flutter-Translate-Gen/blob/master/LICENSE) -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | # enable-experiment: 3 | # - non-nullable 4 | -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | flutter_translate_gen|static_keys_generator: 5 | enabled: true 6 | 7 | builders: 8 | static_keys_generator: 9 | target: ":flutter_translate_gen" 10 | import: "package:flutter_translate_gen/builder.dart" 11 | builder_factories: ["staticKeysBuilder"] 12 | build_extensions: {".dart": ["*.g.part"]} 13 | auto_apply: dependents 14 | build_to: cache 15 | applies_builders: ["source_gen|combining_builder"] 16 | -------------------------------------------------------------------------------- /example/assets/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_bar": { 3 | "title": "Welcome to the home page" 4 | }, 5 | "button": { 6 | "cancel": "Cancel", 7 | "change_language": "Change Language" 8 | }, 9 | "language": { 10 | "name": { 11 | "en": "English", 12 | "es": "Spanish", 13 | "fa": "Persian" 14 | }, 15 | "selected_message": "Currently selected language is {language}", 16 | "selection": { 17 | "message": "Please select a language from the list", 18 | "title": "Language Selection" 19 | } 20 | }, 21 | "plural": { 22 | "demo": { 23 | "0": "Por favor, comience a presionar el botón 'más'.", 24 | "1": "Has presionado el botón una vez.", 25 | "else": "Ha presionado el botón {{value}} veces." 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/assets/i18n/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_bar": { 3 | "title": "Bienvenido a la página de inicio" 4 | }, 5 | "button": { 6 | "cancel": "Cancelar", 7 | "change_language": "Cambiar idioma" 8 | }, 9 | "language": { 10 | "name": { 11 | "en": "Inglés", 12 | "es": "Español", 13 | "fa": "Persa" 14 | }, 15 | "selected_message": "El idioma seleccionado actualmente es {language}", 16 | "selection": { 17 | "message": "Por favor seleccione un idioma de la lista", 18 | "title": "Selección de idioma" 19 | } 20 | }, 21 | "plural": { 22 | "demo": { 23 | "0": "Por favor, comience a presionar el botón 'más'.", 24 | "1": "Has presionado el botón una vez.", 25 | "else": "Ha presionado el botón {{value}} veces." 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | sources: 4 | include: 5 | - $package$ 6 | - assets/i18n/**/*.json 7 | - lib/** -------------------------------------------------------------------------------- /example/lib/localization/keys.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_translate_annotations/flutter_translate_annotations.dart'; 2 | 3 | part 'keys.g.dart'; 4 | 5 | @TranslateKeysOptions(path: 'assets/i18n', caseStyle: CaseStyle.titleCase, separator: "_") 6 | class _$Keys // ignore: unused_element 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /example/lib/localization/keys.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'keys.dart'; 4 | 5 | // ************************************************************************** 6 | // Generator: FlutterTranslateGen 7 | // ************************************************************************** 8 | 9 | class Keys { 10 | static const String App_Bar_Title = 'app_bar.title'; 11 | 12 | static const String Button_Cancel = 'button.cancel'; 13 | 14 | static const String Button_Change_Language = 'button.change_language'; 15 | 16 | static const String Language_Name_En = 'language.name.en'; 17 | 18 | static const String Language_Name_Es = 'language.name.es'; 19 | 20 | static const String Language_Name_Fa = 'language.name.fa'; 21 | 22 | static const String Language_Selected_Message = 'language.selected_message'; 23 | 24 | static const String Language_Selection_Message = 'language.selection.message'; 25 | 26 | static const String Language_Selection_Title = 'language.selection.title'; 27 | 28 | static const String Plural_Demo = 'plural.demo'; 29 | } 30 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | main() { 2 | } 3 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: Example for flutter_translate_gen 3 | 4 | version: 1.0.0 5 | 6 | environment: 7 | sdk: ">=2.12.0 <3.0.0" 8 | 9 | dependencies: 10 | flutter_translate_annotations: ^2.1.0 11 | 12 | dev_dependencies: 13 | flutter_translate_gen: 14 | path: ../ 15 | build_runner: ^2.1.10 16 | -------------------------------------------------------------------------------- /flutter_translate_gen.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /lib/annotation_generator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:async/async.dart'; 3 | import 'package:analyzer/dart/element/element.dart'; 4 | import 'package:build/build.dart'; 5 | import 'package:source_gen/source_gen.dart'; 6 | 7 | abstract class AnnotationGenerator extends Generator 8 | { 9 | const AnnotationGenerator(); 10 | 11 | TypeChecker get typeChecker => TypeChecker.fromRuntime(T); 12 | 13 | @override 14 | FutureOr generate(LibraryReader library, BuildStep buildStep) async 15 | { 16 | final values = Set(); 17 | 18 | for (var annotatedElement in library.annotatedWith(typeChecker)) 19 | { 20 | final generatedValue = generateForAnnotatedElement(annotatedElement.element, annotatedElement.annotation, buildStep); 21 | 22 | await for (var value in normalizeGeneratorOutput(generatedValue)) 23 | { 24 | assert((value.length == value.trim().length)); 25 | 26 | values.add(value); 27 | } 28 | } 29 | 30 | return values.join('\n\n'); 31 | } 32 | 33 | dynamic generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep); 34 | 35 | Stream normalizeGeneratorOutput(Object? value) 36 | { 37 | if (value == null) 38 | { 39 | return const Stream.empty(); 40 | } 41 | else if (value is Future) 42 | { 43 | return StreamCompleter.fromFuture(value.then(normalizeGeneratorOutput)); 44 | } 45 | else if (value is String) 46 | { 47 | value = [value]; 48 | } 49 | 50 | if (value is Iterable) 51 | { 52 | value = Stream.fromIterable(value); 53 | } 54 | 55 | if (value is Stream) 56 | { 57 | return value.where((e) => e != null).map((e) 58 | { 59 | if (e is String) 60 | { 61 | return e.trim(); 62 | } 63 | 64 | throw _argError(e); 65 | 66 | }).where((e) => e.isNotEmpty); 67 | } 68 | 69 | throw _argError(value); 70 | } 71 | 72 | ArgumentError _argError(Object value) => ArgumentError('Must be a String or be an Iterable/Stream containing String values. Found `${Error.safeToString(value)}` (${value.runtimeType}).'); 73 | } 74 | -------------------------------------------------------------------------------- /lib/builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:build/build.dart'; 2 | import 'package:flutter_translate_gen/flutter_translate_gen.dart'; 3 | import 'package:source_gen/source_gen.dart'; 4 | 5 | Builder staticKeysBuilder(BuilderOptions options) 6 | { 7 | return SharedPartBuilder([FlutterTranslateGen()], 'static_keys_generator'); 8 | } 9 | -------------------------------------------------------------------------------- /lib/flutter_translate_gen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'package:analyzer/dart/element/element.dart'; 4 | import 'package:build/build.dart'; 5 | import 'package:code_builder/code_builder.dart'; 6 | import 'package:dart_casing/dart_casing.dart'; 7 | import 'package:dart_style/dart_style.dart'; 8 | import 'package:dart_utils/dart_utils.dart'; 9 | import 'package:flutter_translate_annotations/flutter_translate_annotations.dart'; 10 | import 'package:flutter_translate_gen/annotation_generator.dart'; 11 | import 'package:flutter_translate_gen/keys_class_generator.dart'; 12 | import 'package:flutter_translate_gen/localized_item.dart'; 13 | import 'package:glob/glob.dart'; 14 | import 'package:source_gen/source_gen.dart'; 15 | 16 | class FlutterTranslateGen extends AnnotationGenerator 17 | { 18 | static List reservedKeys = const["0", "1", "else"]; 19 | 20 | const FlutterTranslateGen(); 21 | 22 | @override 23 | Future generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) async 24 | { 25 | validateClass(element); 26 | 27 | final options = parseOptions(annotation); 28 | 29 | var className = element.name; 30 | 31 | validateClassName(className); 32 | 33 | List translations; 34 | 35 | try 36 | { 37 | translations = await getKeyMap(buildStep, options); 38 | } 39 | on FormatException catch (_) 40 | { 41 | throw InvalidGenerationSourceError("Ths JSON format is invalid."); 42 | } 43 | 44 | final file = Library((lb) => lb..body.addAll([KeysClassGenerator.generateClass(options, translations, className!)])); 45 | 46 | final DartEmitter emitter = DartEmitter(allocator: Allocator.none); 47 | 48 | return DartFormatter().format('${file.accept(emitter)}'); 49 | } 50 | 51 | TranslateKeysOptions parseOptions(ConstantReader annotation) 52 | { 53 | final caseStyle = enumFromString(CaseStyle.values, annotation.peek("caseStyle")?.revive().accessor) ?? CaseStyle.titleCase; 54 | 55 | return TranslateKeysOptions( 56 | path: annotation.peek("path")!.stringValue, 57 | caseStyle: caseStyle, 58 | separator: annotation.peek("separator")!.stringValue); 59 | } 60 | 61 | Future> getKeyMap(BuildStep step, TranslateKeysOptions options) async 62 | { 63 | var mapping = >{}; 64 | 65 | var assets = await step.findAssets(Glob(options.path, recursive: true)).toList(); 66 | 67 | for (var entity in assets) 68 | { 69 | Map jsonMap = json.decode(await step.readAsString(entity)); 70 | 71 | var translationMap = getTranslationMap(jsonMap); 72 | 73 | translationMap.forEach((key, value) => (mapping[key] ??= []).add(value)); 74 | } 75 | 76 | List translations = []; 77 | 78 | mapping.forEach((id, trans) => translations.add(LocalizedItem(id, trans, getKeyFieldName(id, options)))); 79 | 80 | return translations; 81 | } 82 | 83 | String getKeyFieldName(String key, TranslateKeysOptions options) 84 | { 85 | switch(options.caseStyle) 86 | { 87 | case CaseStyle.camelCase: return Casing.camelCase(key); 88 | case CaseStyle.titleCase: return Casing.titleCase(key,separator: options.separator); 89 | case CaseStyle.upperCase: return Casing.upperCase(key,separator: options.separator); 90 | case CaseStyle.lowerCase: return Casing.lowerCase(key,separator: options.separator); 91 | default: return throw InvalidGenerationSourceError("Invalid CaseStyle specified: ${options.caseStyle.toString()}"); 92 | } 93 | } 94 | 95 | Map getTranslationMap(Map jsonMap, {String? parentKey}) 96 | { 97 | final map = Map(); 98 | 99 | for(var entry in jsonMap.keys) 100 | { 101 | String? key; 102 | 103 | if(reservedKeys.contains(entry)) 104 | { 105 | key = parentKey; 106 | } 107 | else 108 | { 109 | key = parentKey != null ? "$parentKey.$entry" : entry; 110 | } 111 | 112 | if(key == null) continue; 113 | 114 | var value = jsonMap[entry]; 115 | 116 | if(value is String) 117 | { 118 | map.putIfAbsent(key, () => value); 119 | } 120 | else 121 | { 122 | var entries = getTranslationMap(value, parentKey: key); 123 | 124 | map.addAll(entries); 125 | } 126 | } 127 | 128 | return map; 129 | } 130 | 131 | 132 | void validateClassName(String? className) 133 | { 134 | if(className == null || className.isEmpty || !className.startsWith("_\$")) 135 | { 136 | throw InvalidGenerationSourceError("The annotated class name (currently '$className') must start with _\$. For example _\$Keys or _\$LocalizationKeys"); 137 | } 138 | } 139 | 140 | void validateClass(Element element) 141 | { 142 | if (element is! ClassElement) 143 | { 144 | throw InvalidGenerationSourceError("The annotated element is not a Class! TranslateKeyOptions should be used on Classes.", element: element); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/keys_class_generator.dart: -------------------------------------------------------------------------------- 1 | import 'package:code_builder/code_builder.dart'; 2 | import 'package:flutter_translate_annotations/flutter_translate_annotations.dart'; 3 | import 'package:flutter_translate_gen/localized_item.dart'; 4 | 5 | class KeysClassGenerator 6 | { 7 | static Reference get stringType => TypeReference((trb) => trb..symbol = "String"); 8 | 9 | static Class generateClass(TranslateKeysOptions options, List items, String className) 10 | { 11 | return Class((x) => x 12 | ..name = className.substring(2) 13 | ..fields.addAll(items 14 | .map((translation) => generateField(translation, options)) 15 | .toList()), 16 | ); 17 | } 18 | 19 | static Field generateField(LocalizedItem item, TranslateKeysOptions options) 20 | { 21 | return Field((x) => x 22 | ..name = item.fieldName 23 | ..type = stringType 24 | ..static = true 25 | ..modifier = FieldModifier.constant 26 | ..assignment = literalString(item.key).code, 27 | ); 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /lib/localized_item.dart: -------------------------------------------------------------------------------- 1 | class LocalizedItem 2 | { 3 | final String key; 4 | final List translations; 5 | final String fieldName; 6 | 7 | LocalizedItem(this.key, this.translations, this.fieldName); 8 | } 9 | 10 | List keysOf(List items) 11 | { 12 | return items.map((x) => x.key).toList(); 13 | } 14 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_translate_gen 2 | description: Statically-typed localization keys generator for flutter_translate. 3 | version: 2.2.0 4 | homepage: https://jesway.com 5 | repository: https://github.com/Jesway/Flutter-Translate-Gen 6 | 7 | environment: 8 | sdk: ">=2.12.0 <3.0.0" 9 | 10 | dependencies: 11 | analyzer: ^4.0.0 12 | async: ^2.9.0 13 | build: ^2.3.0 14 | code_builder: ^4.1.0 15 | dart_casing: ^2.0.0 16 | dart_style: ^2.2.3 17 | dart_utils: ^2.0.0 18 | flutter_translate_annotations: ^2.1.0 19 | glob: ^2.0.2 20 | source_gen: ^1.2.2 21 | 22 | dev_dependencies: 23 | build_runner: ^2.1.10 --------------------------------------------------------------------------------