1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:intl/locale.dart';
6
7import '../base/common.dart';
8import '../base/file_system.dart';
9import '../base/logger.dart';
10import '../convert.dart';
11import 'localizations_utils.dart';
12import 'message_parser.dart';
13
14// The set of date formats that can be automatically localized.
15//
16// The localizations generation tool makes use of the intl library's
17// DateFormat class to properly format dates based on the locale, the
18// desired format, as well as the passed in [DateTime]. For example, using
19// DateFormat.yMMMMd("en_US").format(DateTime.utc(1996, 7, 10)) results
20// in the string "July 10, 1996".
21//
22// Since the tool generates code that uses DateFormat's constructor and its
23// add_* methods, it is necessary to verify that the constructor/method exists,
24// or the tool will generate code that may cause a compile-time error.
25//
26// See also:
27//
28// *
29// *
30// *
31const Set<String> validDateFormats = <String>{
32 'd',
33 'E',
34 'EEEE',
35 'LLL',
36 'LLLL',
37 'M',
38 'Md',
39 'MEd',
40 'MMM',
41 'MMMd',
42 'MMMEd',
43 'MMMM',
44 'MMMMd',
45 'MMMMEEEEd',
46 'QQQ',
47 'QQQQ',
48 'y',
49 'yM',
50 'yMd',
51 'yMEd',
52 'yMMM',
53 'yMMMd',
54 'yMMMEd',
55 'yMMMM',
56 'yMMMMd',
57 'yMMMMEEEEd',
58 'yQQQ',
59 'yQQQQ',
60 'H',
61 'Hm',
62 'Hms',
63 'j',
64 'jm',
65 'jms',
66 'jmv',
67 'jmz',
68 'jv',
69 'jz',
70 'm',
71 'ms',
72 's',
73};
74
75const String _dateFormatPartsDelimiter = '+';
76
77// The set of number formats that can be automatically localized.
78//
79// The localizations generation tool makes use of the intl library's
80// NumberFormat class to properly format numbers based on the locale and
81// the desired format. For example, using
82// NumberFormat.compactLong("en_US").format(1200000) results
83// in the string "1.2 million".
84//
85// Since the tool generates code that uses NumberFormat's constructor, it is
86// necessary to verify that the constructor exists, or the
87// tool will generate code that may cause a compile-time error.
88//
89// See also:
90//
91// *
92// *
93const Set<String> _validNumberFormats = <String>{
94 'compact',
95 'compactCurrency',
96 'compactSimpleCurrency',
97 'compactLong',
98 'currency',
99 'decimalPattern',
100 'decimalPatternDigits',
101 'decimalPercentPattern',
102 'percentPattern',
103 'scientificPattern',
104 'simpleCurrency',
105};
106
107// The names of the NumberFormat factory constructors which have named
108// parameters rather than positional parameters.
109//
110// This helps the tool correctly generate number formatting code correctly.
111//
112// Example of code that uses named parameters:
113// final NumberFormat format = NumberFormat.compact(
114// locale: localeName,
115// );
116//
117// Example of code that uses positional parameters:
118// final NumberFormat format = NumberFormat.scientificPattern(localeName);
119const Set<String> _numberFormatsWithNamedParameters = <String>{
120 'compact',
121 'compactCurrency',
122 'compactSimpleCurrency',
123 'compactLong',
124 'currency',
125 'decimalPatternDigits',
126 'decimalPercentPattern',
127 'simpleCurrency',
128};
129
130class L10nException implements Exception {
131 L10nException(this.message);
132
133 final String message;
134
135 @override
136 String toString() => message;
137}
138
139class L10nParserException extends L10nException {
140 L10nParserException(
141 this.error,
142 this.fileName,
143 this.messageId,
144 this.messageString,
145 this.charNumber,
146 ) : super('''
147[$fileName:$messageId] $error
148 $messageString
149 ${List<String>.filled(charNumber, ' ').join()}^''');
150
151 final String error;
152 final String fileName;
153 final String messageId;
154 final String messageString;
155 // Position of character within the "messageString" where the error is.
156 final int charNumber;
157}
158
159class L10nMissingPlaceholderException extends L10nParserException {
160 L10nMissingPlaceholderException(
161 super.error,
162 super.fileName,
163 super.messageId,
164 super.messageString,
165 super.charNumber,
166 this.placeholderName,
167 );
168
169 final String placeholderName;
170}
171
172// One optional named parameter to be used by a NumberFormat.
173//
174// Some of the NumberFormat factory constructors have optional named parameters.
175// For example NumberFormat.compactCurrency has a decimalDigits parameter that
176// specifies the number of decimal places to use when formatting.
177//
178// Optional parameters for NumberFormat placeholders are specified as a
179// JSON map value for optionalParameters in a resource's "@" ARB file entry:
180//
181// "@myResourceId": {
182// "placeholders": {
183// "myNumberPlaceholder": {
184// "type": "double",
185// "format": "compactCurrency",
186// "optionalParameters": {
187// "decimalDigits": 2
188// }
189// }
190// }
191// }
192class OptionalParameter {
193 const OptionalParameter(this.name, this.value);
194
195 final String name;
196 final Object value;
197}
198
199// One message parameter: one placeholder from an @foo entry in the template ARB file.
200//
201// Placeholders are specified as a JSON map with one entry for each placeholder.
202// One placeholder must be specified for each message "{parameter}".
203// Each placeholder entry is also a JSON map. If the map is empty, the placeholder
204// is assumed to be an Object value whose toString() value will be displayed.
205// For example:
206//
207// "greeting": "{hello} {world}",
208// "@greeting": {
209// "description": "A message with a two parameters",
210// "placeholders": {
211// "hello": {},
212// "world": {}
213// }
214// }
215//
216// Each placeholder can optionally specify a valid Dart type. If the type
217// is NumberFormat or DateFormat then a format which matches one of the
218// type's factory constructors can also be specified. In this example the
219// date placeholder is to be formatted with DateFormat.yMMMMd:
220//
221// "helloWorldOn": "Hello World on {date}",
222// "@helloWorldOn": {
223// "description": "A message with a date parameter",
224// "placeholders": {
225// "date": {
226// "type": "DateTime",
227// "format": "yMMMMd"
228// }
229// }
230// }
231//
232class Placeholder {
233 Placeholder(this.resourceId, this.name, Map<String, Object?> attributes)
234 : example = _stringAttribute(resourceId, name, attributes, 'example'),
235 type = _stringAttribute(resourceId, name, attributes, 'type'),
236 format = _stringAttribute(resourceId, name, attributes, 'format'),
237 optionalParameters = _optionalParameters(resourceId, name, attributes),
238 isCustomDateFormat = _boolAttribute(resourceId, name, attributes, 'isCustomDateFormat');
239
240 final String resourceId;
241 final String name;
242 final String? example;
243 final String? format;
244 final List<OptionalParameter> optionalParameters;
245 final bool? isCustomDateFormat;
246 // The following will be initialized after all messages are parsed in the Message constructor.
247 String? type;
248 bool isPlural = false;
249 bool isSelect = false;
250 bool isDateTime = false;
251 bool requiresDateFormatting = false;
252
253 bool get requiresFormatting => requiresDateFormatting || requiresNumFormatting;
254 bool get requiresNumFormatting =>
255 <String>['int', 'num', 'double'].contains(type) && format != null;
256 bool get hasValidNumberFormat => _validNumberFormats.contains(format);
257 bool get hasNumberFormatWithParameters => _numberFormatsWithNamedParameters.contains(format);
258 // 'format' can contain a number of date time formats separated by `dateFormatPartsDelimiter`.
259 List<String> get dateFormatParts => format?.split(_dateFormatPartsDelimiter) ?? <String>[];
260 bool get hasValidDateFormat => dateFormatParts.every(validDateFormats.contains);
261
262 static String? _stringAttribute(
263 String resourceId,
264 String name,
265 Map<String, Object?> attributes,
266 String attributeName,
267 ) {
268 final Object? value = attributes[attributeName];
269 if (value == null) {
270 return null;
271 }
272 if (value is! String || value.isEmpty) {
273 throw L10nException(
274 'The "$attributeName" value of the "$name" placeholder in message $resourceId '
275 'must be a non-empty string.',
276 );
277 }
278 return value;
279 }
280
281 static bool? _boolAttribute(
282 String resourceId,
283 String name,
284 Map<String, Object?> attributes,
285 String attributeName,
286 ) {
287 final Object? value = attributes[attributeName];
288 if (value == null) {
289 return null;
290 }
291 if (value is bool) {
292 return value;
293 }
294 if (value != 'true' && value != 'false') {
295 throw L10nException(
296 'The "$attributeName" value of the "$name" placeholder in message $resourceId '
297 'must be a boolean value.',
298 );
299 }
300 return value == 'true';
301 }
302
303 static List<OptionalParameter> _optionalParameters(
304 String resourceId,
305 String name,
306 Map<String, Object?> attributes,
307 ) {
308 final Object? value = attributes['optionalParameters'];
309 if (value == null) {
310 return <OptionalParameter>[];
311 }
312 if (value is! Map<String, Object?>) {
313 throw L10nException(
314 'The "optionalParameters" value of the "$name" placeholder in message '
315 '$resourceId is not a properly formatted Map. Ensure that it is a map '
316 'with keys that are strings.',
317 );
318 }
319 final Map<String, Object?> optionalParameterMap = value;
320 return optionalParameterMap.keys.map<OptionalParameter>((String parameterName) {
321 return OptionalParameter(parameterName, optionalParameterMap[parameterName]!);
322 }).toList();
323 }
324}
325
326// All translations for a given message specified by a resource id.
327//
328// The template ARB file must contain an entry called @myResourceId for each
329// message named myResourceId. The @ entry describes message parameters
330// called "placeholders" and can include an optional description.
331// Here's a simple example message with no parameters:
332//
333// "helloWorld": "Hello World",
334// "@helloWorld": {
335// "description": "The conventional newborn programmer greeting"
336// }
337//
338// The value of this Message is "Hello World". The Message's value is the
339// localized string to be shown for the template ARB file's locale.
340// The docs for the Placeholder explain how placeholder entries are defined.
341class Message {
342 Message(
343 AppResourceBundle templateBundle,
344 AppResourceBundleCollection allBundles,
345 this.resourceId,
346 bool isResourceAttributeRequired, {
347 this.useRelaxedSyntax = false,
348 this.useEscaping = false,
349 this.logger,
350 }) : assert(resourceId.isNotEmpty),
351 value = _value(templateBundle.resources, resourceId),
352 description = _description(
353 templateBundle.resources,
354 resourceId,
355 isResourceAttributeRequired,
356 ),
357 templatePlaceholders = _placeholders(
358 templateBundle.resources,
359 resourceId,
360 isResourceAttributeRequired,
361 ),
362 localePlaceholders = <LocaleInfo, Map<String, Placeholder>>{},
363 messages = <LocaleInfo, String?>{},
364 parsedMessages = <LocaleInfo, Node?>{} {
365 // Filenames for error handling.
366 final Map<LocaleInfo, String> filenames = <LocaleInfo, String>{};
367 // Collect all translations from allBundles and parse them.
368 for (final AppResourceBundle bundle in allBundles.bundles) {
369 filenames[bundle.locale] = bundle.file.basename;
370 final String? translation = bundle.translationFor(resourceId);
371 messages[bundle.locale] = translation;
372
373 localePlaceholders[bundle.locale] =
374 templateBundle.locale == bundle.locale
375 ? templatePlaceholders
376 : _placeholders(bundle.resources, resourceId, false);
377
378 List<String>? validPlaceholders;
379 if (useRelaxedSyntax) {
380 validPlaceholders =
381 templatePlaceholders.entries.map((MapEntry<String, Placeholder> e) => e.key).toList();
382 }
383 try {
384 parsedMessages[bundle.locale] =
385 translation == null
386 ? null
387 : Parser(
388 resourceId,
389 bundle.file.basename,
390 translation,
391 useEscaping: useEscaping,
392 placeholders: validPlaceholders,
393 logger: logger,
394 ).parse();
395 } on L10nParserException catch (error) {
396 logger?.printError(error.toString());
397 // Treat it as an untranslated message in case we can't parse.
398 parsedMessages[bundle.locale] = null;
399 hadErrors = true;
400 }
401 }
402 // Infer the placeholders
403 _inferPlaceholders();
404 }
405
406 final String resourceId;
407 final String value;
408 final String? description;
409 late final Map<LocaleInfo, String?> messages;
410 final Map<LocaleInfo, Node?> parsedMessages;
411 final Map<LocaleInfo, Map<String, Placeholder>> localePlaceholders;
412 final Map<String, Placeholder> templatePlaceholders;
413 final bool useEscaping;
414 final bool useRelaxedSyntax;
415 final Logger? logger;
416 bool hadErrors = false;
417
418 Iterable<Placeholder> getPlaceholders(LocaleInfo locale) {
419 final Map<String, Placeholder>? placeholders = localePlaceholders[locale];
420 if (placeholders == null) {
421 return templatePlaceholders.values;
422 }
423 return templatePlaceholders.values.map(
424 (Placeholder templatePlaceholder) =>
425 placeholders[templatePlaceholder.name] ?? templatePlaceholder,
426 );
427 }
428
429 static String _value(Map<String, Object?> bundle, String resourceId) {
430 final Object? value = bundle[resourceId];
431 if (value == null) {
432 throw L10nException('A value for resource "$resourceId" was not found.');
433 }
434 if (value is! String) {
435 throw L10nException('The value of "$resourceId" is not a string.');
436 }
437 return value;
438 }
439
440 static Map<String, Object?>? _attributes(
441 Map<String, Object?> bundle,
442 String resourceId,
443 bool isResourceAttributeRequired,
444 ) {
445 final Object? attributes = bundle['@$resourceId'];
446 if (isResourceAttributeRequired) {
447 if (attributes == null) {
448 throw L10nException(
449 'Resource attribute "@$resourceId" was not found. Please '
450 'ensure that each resource has a corresponding @resource.',
451 );
452 }
453 }
454
455 if (attributes != null && attributes is! Map<String, Object?>) {
456 throw L10nException(
457 'The resource attribute "@$resourceId" is not a properly formatted Map. '
458 'Ensure that it is a map with keys that are strings.',
459 );
460 }
461
462 return attributes as Map<String, Object?>?;
463 }
464
465 static String? _description(
466 Map<String, Object?> bundle,
467 String resourceId,
468 bool isResourceAttributeRequired,
469 ) {
470 final Map<String, Object?>? resourceAttributes = _attributes(
471 bundle,
472 resourceId,
473 isResourceAttributeRequired,
474 );
475 if (resourceAttributes == null) {
476 return null;
477 }
478
479 final Object? value = resourceAttributes['description'];
480 if (value == null) {
481 return null;
482 }
483 if (value is! String) {
484 throw L10nException('The description for "@$resourceId" is not a properly formatted String.');
485 }
486 return value;
487 }
488
489 static Map<String, Placeholder> _placeholders(
490 Map<String, Object?> bundle,
491 String resourceId,
492 bool isResourceAttributeRequired,
493 ) {
494 final Map<String, Object?>? resourceAttributes = _attributes(
495 bundle,
496 resourceId,
497 isResourceAttributeRequired,
498 );
499 if (resourceAttributes == null) {
500 return <String, Placeholder>{};
501 }
502 final Object? allPlaceholdersMap = resourceAttributes['placeholders'];
503 if (allPlaceholdersMap == null) {
504 return <String, Placeholder>{};
505 }
506 if (allPlaceholdersMap is! Map<String, Object?>) {
507 throw L10nException(
508 'The "placeholders" attribute for message "$resourceId", is not '
509 'properly formatted. Ensure that it is a map with string valued keys.',
510 );
511 }
512 return Map<String, Placeholder>.fromEntries(
513 allPlaceholdersMap.keys.map((String placeholderName) {
514 final Object? value = allPlaceholdersMap[placeholderName];
515 if (value is! Map<String, Object?>) {
516 throw L10nException(
517 'The value of the "$placeholderName" placeholder attribute for message '
518 '"$resourceId", is not properly formatted. Ensure that it is a map '
519 'with string valued keys.',
520 );
521 }
522 return MapEntry<String, Placeholder>(
523 placeholderName,
524 Placeholder(resourceId, placeholderName, value),
525 );
526 }),
527 );
528 }
529
530 // Using parsed translations, attempt to infer types of placeholders used by plurals and selects.
531 // For undeclared placeholders, create a new placeholder.
532 void _inferPlaceholders() {
533 // We keep the undeclared placeholders separate so that we can sort them alphabetically afterwards.
534 final Map<String, Placeholder> undeclaredPlaceholders = <String, Placeholder>{};
535 // Helper for getting placeholder by name.
536 for (final LocaleInfo locale in parsedMessages.keys) {
537 Placeholder? getPlaceholder(String name) =>
538 localePlaceholders[locale]?[name] ??
539 templatePlaceholders[name] ??
540 undeclaredPlaceholders[name];
541 if (parsedMessages[locale] == null) {
542 continue;
543 }
544 final List<Node> traversalStack = <Node>[parsedMessages[locale]!];
545 while (traversalStack.isNotEmpty) {
546 final Node node = traversalStack.removeLast();
547 if (<ST>[
548 ST.placeholderExpr,
549 ST.pluralExpr,
550 ST.selectExpr,
551 ST.argumentExpr,
552 ].contains(node.type)) {
553 final String identifier = node.children[1].value!;
554 Placeholder? placeholder = getPlaceholder(identifier);
555 if (placeholder == null) {
556 placeholder = Placeholder(resourceId, identifier, <String, Object?>{});
557 undeclaredPlaceholders[identifier] = placeholder;
558 }
559 if (node.type == ST.pluralExpr) {
560 placeholder.isPlural = true;
561 } else if (node.type == ST.selectExpr) {
562 placeholder.isSelect = true;
563 } else if (node.type == ST.argumentExpr) {
564 placeholder.isDateTime = true;
565 } else {
566 // Here the node type must be ST.placeholderExpr.
567 // A DateTime placeholder must require date formatting.
568 if (placeholder.type == 'DateTime') {
569 placeholder.requiresDateFormatting = true;
570 }
571 }
572 }
573 traversalStack.addAll(node.children);
574 }
575 }
576 templatePlaceholders.addEntries(
577 undeclaredPlaceholders.entries.toList()..sort(
578 (MapEntry<String, Placeholder> p1, MapEntry<String, Placeholder> p2) =>
579 p1.key.compareTo(p2.key),
580 ),
581 );
582
583 bool atMostOneOf(bool x, bool y, bool z) {
584 return x && !y && !z || !x && y && !z || !x && !y && z || !x && !y && !z;
585 }
586
587 for (final Placeholder placeholder in templatePlaceholders.values.followedBy(
588 localePlaceholders.values.expand((Map<String, Placeholder> e) => e.values),
589 )) {
590 if (!atMostOneOf(placeholder.isPlural, placeholder.isDateTime, placeholder.isSelect)) {
591 throw L10nException('Placeholder is used as plural/select/datetime in certain languages.');
592 } else if (placeholder.isPlural) {
593 if (placeholder.type == null) {
594 placeholder.type = 'num';
595 } else if (!<String>['num', 'int'].contains(placeholder.type)) {
596 throw L10nException("Placeholders used in plurals must be of type 'num' or 'int'");
597 }
598 } else if (placeholder.isSelect) {
599 if (placeholder.type == null) {
600 placeholder.type = 'String';
601 } else if (placeholder.type != 'String') {
602 throw L10nException("Placeholders used in selects must be of type 'String'");
603 }
604 } else if (placeholder.isDateTime) {
605 if (placeholder.type == null) {
606 placeholder.type = 'DateTime';
607 } else if (placeholder.type != 'DateTime') {
608 throw L10nException(
609 "Placeholders used in datetime expressions much be of type 'DateTime'",
610 );
611 }
612 }
613 placeholder.type ??= 'Object';
614 }
615 }
616}
617
618/// Represents the contents of one ARB file.
619class AppResourceBundle {
620 /// Assuming that the caller has verified that the file exists and is readable.
621 factory AppResourceBundle(File file) {
622 final Map<String, Object?> resources;
623 try {
624 final String content = file.readAsStringSync().trim();
625 if (content.isEmpty) {
626 resources = <String, Object?>{};
627 } else {
628 resources = json.decode(content) as Map<String, Object?>;
629 }
630 } on FormatException catch (e) {
631 throw L10nException(
632 'The arb file ${file.path} has the following formatting issue: \n'
633 '$e',
634 );
635 }
636
637 String? localeString = resources['@@locale'] as String?;
638
639 // Look for the first instance of an ISO 639-1 language code, matching exactly.
640 final String fileName = file.fileSystem.path.basenameWithoutExtension(file.path);
641
642 for (int index = 0; index < fileName.length; index += 1) {
643 // If an underscore was found, check if locale string follows.
644 if (fileName[index] == '_') {
645 // If Locale.tryParse fails, it returns null.
646 final Locale? parserResult = Locale.tryParse(fileName.substring(index + 1));
647 // If the parserResult is not an actual locale identifier, end the loop.
648 if (parserResult != null && _iso639Languages.contains(parserResult.languageCode)) {
649 // The parsed result uses dashes ('-'), but we want underscores ('_').
650 final String parserLocaleString = parserResult.toString().replaceAll('-', '_');
651
652 if (localeString == null) {
653 // If @@locale was not defined, use the filename locale suffix.
654 localeString = parserLocaleString;
655 } else {
656 // If the localeString was defined in @@locale and in the filename, verify to
657 // see if the parsed locale matches, throw an error if it does not. This
658 // prevents developers from confusing issues when both @@locale and
659 // "_{locale}" is specified in the filename.
660 if (localeString != parserLocaleString) {
661 throw L10nException(
662 'The locale specified in @@locale and the arb filename do not match. \n'
663 'Please make sure that they match, since this prevents any confusion \n'
664 'with which locale to use. Otherwise, specify the locale in either the \n'
665 'filename or the @@locale key only.\n'
666 'Current @@locale value: $localeString\n'
667 'Current filename extension: $parserLocaleString',
668 );
669 }
670 }
671 break;
672 }
673 }
674 }
675
676 if (localeString == null) {
677 throw L10nException(
678 "The following .arb file's locale could not be determined: \n"
679 '${file.path} \n'
680 "Make sure that the locale is specified in the file's '@@locale' "
681 'property or as part of the filename (e.g. file_en.arb)',
682 );
683 }
684
685 final Iterable<String> ids = resources.keys.where((String key) => !key.startsWith('@'));
686 return AppResourceBundle._(file, LocaleInfo.fromString(localeString), resources, ids);
687 }
688
689 const AppResourceBundle._(this.file, this.locale, this.resources, this.resourceIds);
690
691 final File file;
692 final LocaleInfo locale;
693
694 /// JSON representation of the contents of the ARB file.
695 final Map<String, Object?> resources;
696 final Iterable<String> resourceIds;
697
698 String? translationFor(String resourceId) {
699 final Object? result = resources[resourceId];
700 if (result is! String?) {
701 throwToolExit(
702 'Localized message for key "$resourceId" in "${file.path}" '
703 'is not a string.',
704 );
705 }
706 return result;
707 }
708
709 @override
710 String toString() {
711 return 'AppResourceBundle($locale, ${file.path})';
712 }
713}
714
715// Represents all of the ARB files in [directory] as [AppResourceBundle]s.
716class AppResourceBundleCollection {
717 factory AppResourceBundleCollection(Directory directory) {
718 // Assuming that the caller has verified that the directory is readable.
719
720 final RegExp filenameRE = RegExp(r'(\w+)\.arb$');
721 final Map<LocaleInfo, AppResourceBundle> localeToBundle = <LocaleInfo, AppResourceBundle>{};
722 final Map<String, List<LocaleInfo>> languageToLocales = <String, List<LocaleInfo>>{};
723 // We require the list of files to be sorted so that
724 // "languageToLocales[bundle.locale.languageCode]" is not null
725 // by the time we handle locales with country codes.
726 final List<File> files =
727 directory
728 .listSync()
729 .whereType<File>()
730 .where((File e) => filenameRE.hasMatch(e.path))
731 .toList()
732 ..sort(sortFilesByPath);
733 for (final File file in files) {
734 final AppResourceBundle bundle = AppResourceBundle(file);
735 if (localeToBundle[bundle.locale] != null) {
736 throw L10nException(
737 "Multiple arb files with the same '${bundle.locale}' locale detected. \n"
738 'Ensure that there is exactly one arb file for each locale.',
739 );
740 }
741 localeToBundle[bundle.locale] = bundle;
742 languageToLocales[bundle.locale.languageCode] ??= <LocaleInfo>[];
743 languageToLocales[bundle.locale.languageCode]!.add(bundle.locale);
744 }
745
746 languageToLocales.forEach((String language, List<LocaleInfo> listOfCorrespondingLocales) {
747 final List<String> localeStrings =
748 listOfCorrespondingLocales.map((LocaleInfo locale) {
749 return locale.toString();
750 }).toList();
751 if (!localeStrings.contains(language)) {
752 throw L10nException(
753 'Arb file for a fallback, $language, does not exist, even though \n'
754 'the following locale(s) exist: $listOfCorrespondingLocales. \n'
755 'When locales specify a script code or country code, a \n'
756 'base locale (without the script code or country code) should \n'
757 'exist as the fallback. Please create a {fileName}_$language.arb \n'
758 'file.',
759 );
760 }
761 });
762
763 return AppResourceBundleCollection._(directory, localeToBundle, languageToLocales);
764 }
765
766 const AppResourceBundleCollection._(
767 this._directory,
768 this._localeToBundle,
769 this._languageToLocales,
770 );
771
772 final Directory _directory;
773 final Map<LocaleInfo, AppResourceBundle> _localeToBundle;
774 final Map<String, List<LocaleInfo>> _languageToLocales;
775
776 Iterable<LocaleInfo> get locales => _localeToBundle.keys;
777 Iterable<AppResourceBundle> get bundles => _localeToBundle.values;
778 AppResourceBundle? bundleFor(LocaleInfo locale) => _localeToBundle[locale];
779
780 Iterable<String> get languages => _languageToLocales.keys;
781 Iterable<LocaleInfo> localesForLanguage(String language) =>
782 _languageToLocales[language] ?? <LocaleInfo>[];
783
784 @override
785 String toString() {
786 return 'AppResourceBundleCollection(${_directory.path}, ${locales.length} locales)';
787 }
788}
789
790// A set containing all the ISO630-1 languages. This list was pulled from https://datahub.io/core/language-codes.
791final Set<String> _iso639Languages = <String>{
792 'aa',
793 'ab',
794 'ae',
795 'af',
796 'ak',
797 'am',
798 'an',
799 'ar',
800 'as',
801 'av',
802 'ay',
803 'az',
804 'ba',
805 'be',
806 'bg',
807 'bh',
808 'bi',
809 'bm',
810 'bn',
811 'bo',
812 'br',
813 'bs',
814 'ca',
815 'ce',
816 'ch',
817 'co',
818 'cr',
819 'cs',
820 'cu',
821 'cv',
822 'cy',
823 'da',
824 'de',
825 'dv',
826 'dz',
827 'ee',
828 'el',
829 'en',
830 'eo',
831 'es',
832 'et',
833 'eu',
834 'fa',
835 'ff',
836 'fi',
837 'fil',
838 'fj',
839 'fo',
840 'fr',
841 'fy',
842 'ga',
843 'gd',
844 'gl',
845 'gn',
846 'gsw',
847 'gu',
848 'gv',
849 'ha',
850 'he',
851 'hi',
852 'ho',
853 'hr',
854 'ht',
855 'hu',
856 'hy',
857 'hz',
858 'ia',
859 'id',
860 'ie',
861 'ig',
862 'ii',
863 'ik',
864 'io',
865 'is',
866 'it',
867 'iu',
868 'ja',
869 'jv',
870 'ka',
871 'kg',
872 'ki',
873 'kj',
874 'kk',
875 'kl',
876 'km',
877 'kn',
878 'ko',
879 'kr',
880 'ks',
881 'ku',
882 'kv',
883 'kw',
884 'ky',
885 'la',
886 'lb',
887 'lg',
888 'li',
889 'ln',
890 'lo',
891 'lt',
892 'lu',
893 'lv',
894 'mg',
895 'mh',
896 'mi',
897 'mk',
898 'ml',
899 'mn',
900 'mr',
901 'ms',
902 'mt',
903 'my',
904 'na',
905 'nb',
906 'nd',
907 'ne',
908 'ng',
909 'nl',
910 'nn',
911 'no',
912 'nr',
913 'nv',
914 'ny',
915 'oc',
916 'oj',
917 'om',
918 'or',
919 'os',
920 'pa',
921 'pi',
922 'pl',
923 'ps',
924 'pt',
925 'qu',
926 'rm',
927 'rn',
928 'ro',
929 'ru',
930 'rw',
931 'sa',
932 'sc',
933 'sd',
934 'se',
935 'sg',
936 'si',
937 'sk',
938 'sl',
939 'sm',
940 'sn',
941 'so',
942 'sq',
943 'sr',
944 'ss',
945 'st',
946 'su',
947 'sv',
948 'sw',
949 'ta',
950 'te',
951 'tg',
952 'th',
953 'ti',
954 'tk',
955 'tl',
956 'tn',
957 'to',
958 'tr',
959 'ts',
960 'tt',
961 'tw',
962 'ty',
963 'ug',
964 'uk',
965 'ur',
966 'uz',
967 've',
968 'vi',
969 'vo',
970 'wa',
971 'wo',
972 'xh',
973 'yi',
974 'yo',
975 'za',
976 'zh',
977 'zu',
978};
979

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com