├── .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 | [](https://pub.dev/packages/flutter_translate_gen)
4 | [](https://github.com/Jesway/Flutter-Translate/blob/master/LICENSE)
5 | [](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
--------------------------------------------------------------------------------