Skip to content

Commit f3e4e99

Browse files
authored
feat(ui): generate labels from .arb (#9340)
1 parent 75ade1a commit f3e4e99

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+14649
-0
lines changed

Diff for: packages/firebase_ui_localizations/.gitignore

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Miscellaneous
2+
*.class
3+
*.log
4+
*.pyc
5+
*.swp
6+
.DS_Store
7+
.atom/
8+
.buildlog/
9+
.history
10+
.svn/
11+
migrate_working_dir/
12+
13+
# IntelliJ related
14+
*.iml
15+
*.ipr
16+
*.iws
17+
.idea/
18+
19+
# The .vscode folder contains launch configuration and tasks you configure in
20+
# VS Code which you may wish to be included in version control, so this line
21+
# is commented out by default.
22+
#.vscode/
23+
24+
# Flutter/Dart/Pub related
25+
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
26+
/pubspec.lock
27+
**/doc/api/
28+
.dart_tool/
29+
.packages
30+
build/

Diff for: packages/firebase_ui_localizations/.metadata

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# This file tracks properties of this Flutter project.
2+
# Used by Flutter tool to assess capabilities and perform upgrades etc.
3+
#
4+
# This file should be version controlled and should not be manually edited.
5+
6+
version:
7+
revision: f1875d570e39de09040c8f79aa13cc56baab8db1
8+
channel: stable
9+
10+
project_type: package

Diff for: packages/firebase_ui_localizations/CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
## 1.0.0-dev.0
2+
3+
- Bump "firebase_ui_localizations" to `1.0.0-dev.0`.
4+
5+
## 0.0.1
6+
7+
* TODO: Describe initial release.

Diff for: packages/firebase_ui_localizations/LICENSE

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Copyright 2017, the Chromium project authors. All rights reserved.
2+
Redistribution and use in source and binary forms, with or without
3+
modification, are permitted provided that the following conditions are
4+
met:
5+
6+
* Redistributions of source code must retain the above copyright
7+
notice, this list of conditions and the following disclaimer.
8+
* Redistributions in binary form must reproduce the above
9+
copyright notice, this list of conditions and the following
10+
disclaimer in the documentation and/or other materials provided
11+
with the distribution.
12+
* Neither the name of Google Inc. nor the names of its
13+
contributors may be used to endorse or promote products derived
14+
from this software without specific prior written permission.
15+
16+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Diff for: packages/firebase_ui_localizations/README.md

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Localization
2+
3+
Firebase UI for Flutter supports localization, so every single label can be customized.
4+
5+
## Installation
6+
7+
```yaml
8+
dependencies:
9+
flutter_localizations:
10+
sdk: flutter
11+
firebase_ui_localizations: ^1.0.0
12+
```
13+
14+
## Usage
15+
16+
If your app supports only a single language, and you want to override labels – you will need to provide a custom class that implements [`DefaultLocalizations`](https://pub.dev/documentation/firebase_ui_localizations/latest/DefaultLocalizations-class.html),
17+
for example:
18+
19+
```dart
20+
import 'package:flutter_localizations/flutter_localizations.dart';
21+
import 'package:firebase_ui_localizations/firebase_ui_localizations.dart';
22+
23+
class LabelOverrides extends DefaultLocalizations {
24+
const LabelOverrides();
25+
26+
@override
27+
String get emailInputLabel => 'Enter your email';
28+
29+
@override
30+
String get passwordInputLabel => 'Enter your password';
31+
}
32+
```
33+
34+
Once created, pass the instance of `LabelOverrides` to the `localizationsDelegates` list in your `MaterialApp`/`CupertinoApp`:
35+
36+
```dart
37+
class MyApp extends StatelessWidget {
38+
@override
39+
Widget build(BuildContext context) {
40+
return MaterialApp(
41+
localizationsDelegates: [
42+
// Creates an instance of FirebaseUILocalizationDelegate with overridden labels
43+
FirebaseUILocalizations.withDefaultOverrides(const LabelOverrides()),
44+
45+
// Delegates below take care of built-in flutter widgets
46+
GlobalMaterialLocalizations.delegate,
47+
GlobalWidgetsLocalizations.delegate,
48+
49+
// This delegate is required to provide the labels that are not overridden by LabelOverrides
50+
FirebaseUILocalizations.delegate,
51+
],
52+
// ...
53+
);
54+
}
55+
}
56+
```
57+
58+
If you need to support multiple languages – follow the [official Flutter localization guide](https://docs.flutter.dev/development/accessibility-and-localization/internationalization#an-alternative-class-for-the-apps-localized-resources)
59+
and make sure that your custom delegate extends `LocalizationsDelegate<FirebaseUILocalizations>`.
60+
61+
> Note: check out [API reference](https://pub.dev/documentation/firebase_ui_localizations/latest/FlutterFireUILocalizationLabels-class.html) to learn what labels are used by specific widgets
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include: package:flutter_lints/flutter.yaml
2+
3+
# Additional information about this file can be found at
4+
# https://dart.dev/guides/language/analysis-options

Diff for: packages/firebase_ui_localizations/bin/gen_l10n.dart

+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
import 'package:path/path.dart' as path;
4+
5+
late String cwd;
6+
late Directory outDir;
7+
8+
void main() async {
9+
cwd = Directory.current.path;
10+
final l10nSrc = Directory(path.join(cwd, 'lib', 'l10n'));
11+
outDir = Directory(path.join(cwd, 'lib', 'src'));
12+
13+
if (!outDir.existsSync()) {
14+
outDir.createSync(recursive: true);
15+
}
16+
17+
final readFutures = await l10nSrc.list().map((event) {
18+
final file = File(event.path);
19+
return file.openRead().transform(const Utf8Decoder()).toList();
20+
}).toList();
21+
22+
final sources = await Future.wait(readFutures);
23+
24+
final labelsByLocale = sources.fold<Map<String, dynamic>>({}, (acc, lines) {
25+
final fullSrc = lines.join();
26+
final arbJson = jsonDecode(fullSrc);
27+
final localeString = arbJson['@@locale'];
28+
29+
final parsed = localeString.split('_');
30+
31+
return {
32+
...acc,
33+
parsed[0]: {
34+
...(acc[parsed[0]] ?? {}),
35+
if (parsed.length > 1) parsed[1]: arbJson else 'default': arbJson,
36+
}
37+
};
38+
});
39+
40+
final genOps = labelsByLocale.entries.map((entry) {
41+
if (entry.value.length == 1) {
42+
return [
43+
generateLocalizationsClass(
44+
locale: entry.key,
45+
arb: entry.value['default'],
46+
)
47+
];
48+
}
49+
50+
return [
51+
generateLocalizationsClass(
52+
locale: entry.key,
53+
arb: entry.value['default'],
54+
),
55+
...entry.value.entries
56+
.where((element) => element.key != 'default')
57+
.map((e) {
58+
return generateLocalizationsClass(
59+
locale: entry.key,
60+
countryCode: e.key,
61+
arb: e.value,
62+
);
63+
}).toList(),
64+
];
65+
}).expand((element) => element);
66+
67+
await Future.wait([...genOps.cast<Future>()]);
68+
await generateLanguagesList(labelsByLocale);
69+
Process.runSync('dart', ['format', outDir.path]);
70+
}
71+
72+
bool isLabelEntry(MapEntry<String, dynamic> entry) {
73+
return !entry.key.startsWith('@');
74+
}
75+
76+
String dartFilename(String locale, [String? countryCode]) {
77+
return '$locale'
78+
'${countryCode != null ? '_${countryCode.toLowerCase()}' : ''}'
79+
'.dart';
80+
}
81+
82+
String dartClassName(String locale, [String? countryCode]) {
83+
return '${locale.capitalize()}'
84+
'${countryCode?.capitalize() ?? ''}Localizations';
85+
}
86+
87+
Future<void> generateLocalizationsClass({
88+
required String locale,
89+
required Map<String, dynamic> arb,
90+
String? countryCode,
91+
}) async {
92+
final filename = dartFilename(locale, countryCode);
93+
final outFile = File(path.join(outDir.path, 'lang', filename));
94+
95+
if (!outFile.existsSync()) {
96+
outFile.createSync(recursive: true);
97+
}
98+
99+
final out = outFile.openWrite();
100+
101+
out.writeln("import '../default_localizations.dart';");
102+
103+
final labels = arb.entries.where(isLabelEntry).map((e) {
104+
final meta = arb['@${e.key}'] ?? {};
105+
106+
return Label(
107+
key: e.key,
108+
translation: e.value,
109+
description: meta['description'],
110+
);
111+
}).toList();
112+
113+
out.writeln();
114+
115+
final className = dartClassName(locale, countryCode);
116+
117+
out.writeln('class $className extends FirebaseUILocalizationLabels {');
118+
out.writeln(' const $className();');
119+
120+
for (var label in labels) {
121+
final escapedTranslation = jsonEncode(label.translation);
122+
123+
out.writeln();
124+
out.writeln(' @override');
125+
out.writeln(' String get ${label.key} => $escapedTranslation;');
126+
}
127+
128+
out.writeln('}');
129+
130+
await out.flush();
131+
await out.close();
132+
}
133+
134+
Future<void> generateLanguagesList(Map<String, dynamic> arb) async {
135+
final outFile = File(path.join(outDir.path, 'all_languages.dart'));
136+
137+
if (!outFile.existsSync()) {
138+
outFile.createSync(recursive: true);
139+
}
140+
141+
final out = outFile.openWrite();
142+
out.writeln('import "./default_localizations.dart";');
143+
out.writeln();
144+
145+
for (var entry in arb.entries) {
146+
final locale = entry.key;
147+
final countryCodes = entry.value.keys.where((e) => e != 'default').toList();
148+
149+
final filename = dartFilename(locale);
150+
out.writeln("import 'lang/$filename';");
151+
152+
for (var countryCode in countryCodes) {
153+
out.writeln("import 'lang/${dartFilename(locale, countryCode)}';");
154+
}
155+
}
156+
157+
out.writeln();
158+
out.writeln('final localizations = <String, FirebaseUILocalizationLabels>{');
159+
160+
for (var entry in arb.entries) {
161+
final locale = entry.key;
162+
final countryCodes = entry.value.keys.where((e) => e != 'default').toList();
163+
164+
out.writeln(" '$locale': const ${dartClassName(locale)}(),");
165+
166+
for (var countryCode in countryCodes) {
167+
final key = '${locale}_${countryCode.toLowerCase()}';
168+
out.writeln(" '$key': const ${dartClassName(locale, countryCode)}(),");
169+
}
170+
}
171+
172+
out.writeln('};');
173+
174+
await out.flush();
175+
await out.close();
176+
}
177+
178+
extension StringExtension on String {
179+
String capitalize() {
180+
return "${this[0].toUpperCase()}${substring(1)}";
181+
}
182+
}
183+
184+
class Label {
185+
final String key;
186+
final String translation;
187+
final String? description;
188+
189+
Label({
190+
required this.key,
191+
required this.translation,
192+
this.description,
193+
});
194+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export 'src/l10n.dart'
2+
show FirebaseUILocalizations, FirebaseUILocalizationDelegate;
3+
4+
export 'src/default_localizations.dart'
5+
show DefaultLocalizations, FirebaseUILocalizationLabels;

0 commit comments

Comments
 (0)