55import 'package:intl/locale.dart' ;
66
77import '../base/file_system.dart' ;
8+ import '../base/logger.dart' ;
89import '../convert.dart' ;
910import 'localizations_utils.dart' ;
1011import 'message_parser.dart' ;
@@ -138,17 +139,31 @@ class L10nParserException extends L10nException {
138139 this .messageString,
139140 this .charNumber
140141 ): super ('''
141- $error
142- [$ fileName :$ messageId ] $messageString
143- ${List <String >.filled (4 + fileName . length + messageId . length + charNumber , ' ' ).join ()}^''' );
142+ [$ fileName :$ messageId ] $error
143+ $messageString
144+ ${List <String >.filled (charNumber , ' ' ).join ()}^''' );
144145
145146 final String error;
146147 final String fileName;
147148 final String messageId;
148149 final String messageString;
150+ // Position of character within the "messageString" where the error is.
149151 final int charNumber;
150152}
151153
154+ class L10nMissingPlaceholderException extends L10nParserException {
155+ L10nMissingPlaceholderException (
156+ super .error,
157+ super .fileName,
158+ super .messageId,
159+ super .messageString,
160+ super .charNumber,
161+ this .placeholderName,
162+ );
163+
164+ final String placeholderName;
165+ }
166+
152167// One optional named parameter to be used by a NumberFormat.
153168//
154169// Some of the NumberFormat factory constructors have optional named parameters.
@@ -319,7 +334,10 @@ class Message {
319334 AppResourceBundleCollection allBundles,
320335 this .resourceId,
321336 bool isResourceAttributeRequired,
322- { this .useEscaping = false }
337+ {
338+ this .useEscaping = false ,
339+ this .logger,
340+ }
323341 ) : assert (templateBundle != null ),
324342 assert (allBundles != null ),
325343 assert (resourceId != null && resourceId.isNotEmpty),
@@ -335,64 +353,16 @@ class Message {
335353 filenames[bundle.locale] = bundle.file.basename;
336354 final String ? translation = bundle.translationFor (resourceId);
337355 messages[bundle.locale] = translation;
338- parsedMessages[bundle.locale] = translation == null ? null : Parser (resourceId, bundle.file.basename, translation, useEscaping: useEscaping).parse ();
339- }
340- // Using parsed translations, attempt to infer types of placeholders used by plurals and selects.
341- for (final LocaleInfo locale in parsedMessages.keys) {
342- if (parsedMessages[locale] == null ) {
343- continue ;
344- }
345- final List <Node > traversalStack = < Node > [parsedMessages[locale]! ];
346- while (traversalStack.isNotEmpty) {
347- final Node node = traversalStack.removeLast ();
348- if (node.type == ST .pluralExpr) {
349- final Placeholder ? placeholder = placeholders[node.children[1 ].value! ];
350- if (placeholder == null ) {
351- throw L10nParserException (
352- 'Make sure that the specified plural placeholder is defined in your arb file.' ,
353- filenames[locale]! ,
354- resourceId,
355- messages[locale]! ,
356- node.children[1 ].positionInMessage
357- );
358- }
359- placeholders[node.children[1 ].value! ]! .isPlural = true ;
360- }
361- if (node.type == ST .selectExpr) {
362- final Placeholder ? placeholder = placeholders[node.children[1 ].value! ];
363- if (placeholder == null ) {
364- throw L10nParserException (
365- 'Make sure that the specified select placeholder is defined in your arb file.' ,
366- filenames[locale]! ,
367- resourceId,
368- messages[locale]! ,
369- node.children[1 ].positionInMessage
370- );
371- }
372- placeholders[node.children[1 ].value! ]! .isSelect = true ;
373- }
374- traversalStack.addAll (node.children);
375- }
376- }
377- for (final Placeholder placeholder in placeholders.values) {
378- if (placeholder.isPlural && placeholder.isSelect) {
379- throw L10nException ('Placeholder is used as both a plural and select in certain languages.' );
380- } else if (placeholder.isPlural) {
381- if (placeholder.type == null ) {
382- placeholder.type = 'num' ;
383- }
384- else if (! < String > ['num' , 'int' ].contains (placeholder.type)) {
385- throw L10nException ("Placeholders used in plurals must be of type 'num' or 'int'" );
386- }
387- } else if (placeholder.isSelect) {
388- if (placeholder.type == null ) {
389- placeholder.type = 'String' ;
390- } else if (placeholder.type != 'String' ) {
391- throw L10nException ("Placeholders used in selects must be of type 'String'" );
392- }
393- }
394- placeholder.type ?? = 'Object' ;
356+ parsedMessages[bundle.locale] = translation == null ? null : Parser (
357+ resourceId,
358+ bundle.file.basename,
359+ translation,
360+ useEscaping: useEscaping,
361+ logger: logger
362+ ).parse ();
395363 }
364+ // Infer the placeholders
365+ _inferPlaceholders (filenames);
396366 }
397367
398368 final String resourceId;
@@ -402,6 +372,7 @@ class Message {
402372 final Map <LocaleInfo , Node ?> parsedMessages;
403373 final Map <String , Placeholder > placeholders;
404374 final bool useEscaping;
375+ final Logger ? logger;
405376
406377 bool get placeholdersRequireFormatting => placeholders.values.any ((Placeholder p) => p.requiresFormatting);
407378
@@ -496,6 +467,63 @@ class Message {
496467 }),
497468 );
498469 }
470+
471+ // Using parsed translations, attempt to infer types of placeholders used by plurals and selects.
472+ // For undeclared placeholders, create a new placeholder.
473+ void _inferPlaceholders (Map <LocaleInfo , String > filenames) {
474+ // We keep the undeclared placeholders separate so that we can sort them alphabetically afterwards.
475+ final Map <String , Placeholder > undeclaredPlaceholders = < String , Placeholder > {};
476+ // Helper for getting placeholder by name.
477+ Placeholder ? getPlaceholder (String name) => placeholders[name] ?? undeclaredPlaceholders[name];
478+ for (final LocaleInfo locale in parsedMessages.keys) {
479+ if (parsedMessages[locale] == null ) {
480+ continue ;
481+ }
482+ final List <Node > traversalStack = < Node > [parsedMessages[locale]! ];
483+ while (traversalStack.isNotEmpty) {
484+ final Node node = traversalStack.removeLast ();
485+ if (< ST > [ST .placeholderExpr, ST .pluralExpr, ST .selectExpr].contains (node.type)) {
486+ final String identifier = node.children[1 ].value! ;
487+ Placeholder ? placeholder = getPlaceholder (identifier);
488+ if (placeholder == null ) {
489+ placeholder = Placeholder (resourceId, identifier, < String , Object ? > {});
490+ undeclaredPlaceholders[identifier] = placeholder;
491+ }
492+ if (node.type == ST .pluralExpr) {
493+ placeholder.isPlural = true ;
494+ } else if (node.type == ST .selectExpr) {
495+ placeholder.isSelect = true ;
496+ }
497+ }
498+ traversalStack.addAll (node.children);
499+ }
500+ }
501+ placeholders.addEntries (
502+ undeclaredPlaceholders.entries
503+ .toList ()
504+ ..sort ((MapEntry <String , Placeholder > p1, MapEntry <String , Placeholder > p2) => p1.key.compareTo (p2.key))
505+ );
506+
507+ for (final Placeholder placeholder in placeholders.values) {
508+ if (placeholder.isPlural && placeholder.isSelect) {
509+ throw L10nException ('Placeholder is used as both a plural and select in certain languages.' );
510+ } else if (placeholder.isPlural) {
511+ if (placeholder.type == null ) {
512+ placeholder.type = 'num' ;
513+ }
514+ else if (! < String > ['num' , 'int' ].contains (placeholder.type)) {
515+ throw L10nException ("Placeholders used in plurals must be of type 'num' or 'int'" );
516+ }
517+ } else if (placeholder.isSelect) {
518+ if (placeholder.type == null ) {
519+ placeholder.type = 'String' ;
520+ } else if (placeholder.type != 'String' ) {
521+ throw L10nException ("Placeholders used in selects must be of type 'String'" );
522+ }
523+ }
524+ placeholder.type ?? = 'Object' ;
525+ }
526+ }
499527}
500528
501529// Represents the contents of one ARB file.
@@ -834,50 +862,3 @@ final Set<String> _iso639Languages = <String>{
834862 'zh' ,
835863 'zu' ,
836864};
837-
838- // Used in LocalizationsGenerator._generateMethod.generateHelperMethod.
839- class HelperMethod {
840- HelperMethod (this .dependentPlaceholders, {this .helper, this .placeholder, this .string }):
841- assert ((() {
842- // At least one of helper, placeholder, string must be nonnull.
843- final bool a = helper == null ;
844- final bool b = placeholder == null ;
845- final bool c = string == null ;
846- return (! a && b && c) || (a && ! b && c) || (a && b && ! c);
847- })());
848-
849- Set <Placeholder > dependentPlaceholders;
850- String ? helper;
851- Placeholder ? placeholder;
852- String ? string;
853-
854- String get helperOrPlaceholder {
855- if (helper != null ) {
856- return '$helper ($methodArguments )' ;
857- } else if (string != null ) {
858- return '$string ' ;
859- } else {
860- if (placeholder! .requiresFormatting) {
861- return '${placeholder !.name }String' ;
862- } else {
863- return placeholder! .name;
864- }
865- }
866- }
867-
868- String get methodParameters {
869- assert (helper != null );
870- return dependentPlaceholders.map ((Placeholder placeholder) =>
871- (placeholder.requiresFormatting)
872- ? 'String ${placeholder .name }String'
873- : '${placeholder .type } ${placeholder .name }' ).join (', ' );
874- }
875-
876- String get methodArguments {
877- assert (helper != null );
878- return dependentPlaceholders.map ((Placeholder placeholder) =>
879- (placeholder.requiresFormatting)
880- ? '${placeholder .name }String'
881- : placeholder.name).join (', ' );
882- }
883- }
0 commit comments