Unverified Commit eb00598b authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Use `FlutterError.reportError` instead of `debugPrint` for l10n warning (#93076)

parent bd77118e
...@@ -248,6 +248,7 @@ abstract class _ErrorDiagnostic extends DiagnosticsProperty<List<Object>> { ...@@ -248,6 +248,7 @@ abstract class _ErrorDiagnostic extends DiagnosticsProperty<List<Object>> {
/// problem that was detected. /// problem that was detected.
/// * [ErrorHint], which provides specific, non-obvious advice that may be /// * [ErrorHint], which provides specific, non-obvious advice that may be
/// applicable. /// applicable.
/// * [ErrorSpacer], which renders as a blank line.
/// * [FlutterError], which is the most common place to use an /// * [FlutterError], which is the most common place to use an
/// [ErrorDescription]. /// [ErrorDescription].
class ErrorDescription extends _ErrorDiagnostic { class ErrorDescription extends _ErrorDiagnostic {
...@@ -323,6 +324,7 @@ class ErrorSummary extends _ErrorDiagnostic { ...@@ -323,6 +324,7 @@ class ErrorSummary extends _ErrorDiagnostic {
/// * [ErrorDescription], which provides an explanation of the problem and its /// * [ErrorDescription], which provides an explanation of the problem and its
/// cause, any information that may help track down the problem, background /// cause, any information that may help track down the problem, background
/// information, etc. /// information, etc.
/// * [ErrorSpacer], which renders as a blank line.
/// * [FlutterError], which is the most common place to use an [ErrorHint]. /// * [FlutterError], which is the most common place to use an [ErrorHint].
class ErrorHint extends _ErrorDiagnostic { class ErrorHint extends _ErrorDiagnostic {
/// A lint enforces that this constructor can only be called with a string /// A lint enforces that this constructor can only be called with a string
...@@ -516,14 +518,52 @@ class FlutterErrorDetails with Diagnosticable { ...@@ -516,14 +518,52 @@ class FlutterErrorDetails with Diagnosticable {
/// This won't be called if [stack] is null. /// This won't be called if [stack] is null.
final IterableFilter<String>? stackFilter; final IterableFilter<String>? stackFilter;
/// A callback which, when called with a [StringBuffer] will write to that buffer /// A callback which will provide information that could help with debugging
/// information that could help with debugging the problem. /// the problem.
/// ///
/// Information collector callbacks can be expensive, so the generated information /// Information collector callbacks can be expensive, so the generated
/// should be cached, rather than the callback being called multiple times. /// information should be cached by the caller, rather than the callback being
/// called multiple times.
/// ///
/// The text written to the information argument may contain newlines but should /// The callback is expected to return an iterable of [DiagnosticsNode] objects,
/// not end with a newline. /// typically implemented using `sync*` and `yield`.
///
/// {@tool snippet}
/// In this example, the information collector returns two pieces of information,
/// one broadly-applicable statement regarding how the error happened, and one
/// giving a specific piece of information that may be useful in some cases but
/// may also be irrelevant most of the time (an argument to the method).
///
/// ```dart
/// void climbElevator(int pid) {
/// try {
/// // ...
/// } catch (error, stack) {
/// FlutterError.reportError(FlutterErrorDetails(
/// exception: error,
/// stack: stack,
/// informationCollector: () sync* {
/// yield ErrorDescription('This happened while climbing the space elevator.');
/// yield ErrorHint('The process ID is: $pid');
/// },
/// ));
/// }
/// }
/// ```
/// {@end-tool}
///
/// The following classes may be of particular use:
///
/// * [ErrorDescription], for information that is broadly applicable to the
/// situation being described.
/// * [ErrorHint], for specific information that may not always be applicable
/// but can be helpful in certain situations.
/// * [DiagnosticsStackTrace], for reporting stack traces.
/// * [ErrorSpacer], for adding spaces (a blank line) between other items.
///
/// For objects that implement [Diagnosticable] one may consider providing
/// additional information by yielding the output of the object's
/// [Diagnosticable.toDiagnosticsNode] method.
final InformationCollector? informationCollector; final InformationCollector? informationCollector;
/// Whether this error should be ignored by the default error reporting /// Whether this error should be ignored by the default error reporting
......
...@@ -111,11 +111,11 @@ typedef LocaleResolutionCallback = Locale? Function(Locale? locale, Iterable<Loc ...@@ -111,11 +111,11 @@ typedef LocaleResolutionCallback = Locale? Function(Locale? locale, Iterable<Loc
/// To summarize, the main matching priority is: /// To summarize, the main matching priority is:
/// ///
/// 1. [Locale.languageCode], [Locale.scriptCode], and [Locale.countryCode] /// 1. [Locale.languageCode], [Locale.scriptCode], and [Locale.countryCode]
/// 1. [Locale.languageCode] and [Locale.scriptCode] only /// 2. [Locale.languageCode] and [Locale.scriptCode] only
/// 1. [Locale.languageCode] and [Locale.countryCode] only /// 3. [Locale.languageCode] and [Locale.countryCode] only
/// 1. [Locale.languageCode] only (with caveats, see above) /// 4. [Locale.languageCode] only (with caveats, see above)
/// 1. [Locale.countryCode] only when all [preferredLocales] fail to match /// 5. [Locale.countryCode] only when all [preferredLocales] fail to match
/// 1. Returns the first element of [supportedLocales] as a fallback /// 6. Returns the first element of [supportedLocales] as a fallback
/// ///
/// This algorithm does not take language distance (how similar languages are to each other) /// This algorithm does not take language distance (how similar languages are to each other)
/// into account, and will not handle edge cases such as resolving `de` to `fr` rather than `zh` /// into account, and will not handle edge cases such as resolving `de` to `fr` rather than `zh`
...@@ -885,7 +885,7 @@ class WidgetsApp extends StatefulWidget { ...@@ -885,7 +885,7 @@ class WidgetsApp extends StatefulWidget {
/// `[const Locale('en', 'US')]`. /// `[const Locale('en', 'US')]`.
/// ///
/// The order of the list matters. The default locale resolution algorithm, /// The order of the list matters. The default locale resolution algorithm,
/// `basicLocaleListResolution`, attempts to match by the following priority: /// [basicLocaleListResolution], attempts to match by the following priority:
/// ///
/// 1. [Locale.languageCode], [Locale.scriptCode], and [Locale.countryCode] /// 1. [Locale.languageCode], [Locale.scriptCode], and [Locale.countryCode]
/// 2. [Locale.languageCode] and [Locale.scriptCode] only /// 2. [Locale.languageCode] and [Locale.scriptCode] only
...@@ -899,7 +899,7 @@ class WidgetsApp extends StatefulWidget { ...@@ -899,7 +899,7 @@ class WidgetsApp extends StatefulWidget {
/// ///
/// The default locale resolution algorithm can be overridden by providing a /// The default locale resolution algorithm can be overridden by providing a
/// value for [localeListResolutionCallback]. The provided /// value for [localeListResolutionCallback]. The provided
/// `basicLocaleListResolution` is optimized for speed and does not implement /// [basicLocaleListResolution] is optimized for speed and does not implement
/// a full algorithm (such as the one defined in /// a full algorithm (such as the one defined in
/// [Unicode TR35](https://unicode.org/reports/tr35/#LanguageMatching)) that /// [Unicode TR35](https://unicode.org/reports/tr35/#LanguageMatching)) that
/// takes distances between languages into account. /// takes distances between languages into account.
...@@ -1493,35 +1493,37 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1493,35 +1493,37 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
if (unsupportedTypes.isEmpty) if (unsupportedTypes.isEmpty)
return true; return true;
// Currently the Cupertino library only provides english localizations. FlutterError.reportError(FlutterErrorDetails(
// Remove this when https://github.com/flutter/flutter/issues/23847 exception: "Warning: This application's locale, $appLocale, is not supported by all of its localization delegates.",
// is fixed. library: 'widgets',
if (listEquals(unsupportedTypes.map((Type type) => type.toString()).toList(), <String>['CupertinoLocalizations'])) informationCollector: () sync* {
return true;
final StringBuffer message = StringBuffer();
message.writeln('\u2550' * 8);
message.writeln(
"Warning: This application's locale, $appLocale, is not supported by all of its\n"
'localization delegates.',
);
for (final Type unsupportedType in unsupportedTypes) { for (final Type unsupportedType in unsupportedTypes) {
// Currently the Cupertino library only provides english localizations. yield ErrorDescription(
// Remove this when https://github.com/flutter/flutter/issues/23847 '• A $unsupportedType delegate that supports the $appLocale locale was not found.',
// is fixed.
if (unsupportedType.toString() == 'CupertinoLocalizations')
continue;
message.writeln(
'> A $unsupportedType delegate that supports the $appLocale locale was not found.',
); );
} }
message.writeln( yield ErrorSpacer();
'See https://flutter.dev/tutorials/internationalization/ for more\n' if (unsupportedTypes.length == 1 && unsupportedTypes.single.toString() == 'CupertinoLocalizations') {
"information about configuring an app's locale, supportedLocales,\n" // We previously explicitly avoided checking for this class so it's not uncommon for applications
// to have omitted importing the required delegate.
yield ErrorHint(
'If the application is built using GlobalMaterialLocalizations.delegate, consider using '
'GlobalMaterialLocalizations.delegates (plural) instead, as that will automatically declare '
'the appropriate Cupertino localizations.'
);
yield ErrorSpacer();
}
yield ErrorHint(
'The declared supported locales for this app are: ${widget.supportedLocales.join(", ")}'
);
yield ErrorSpacer();
yield ErrorDescription(
'See https://flutter.dev/tutorials/internationalization/ for more '
"information about configuring an app's locale, supportedLocales, "
'and localizationsDelegates parameters.', 'and localizationsDelegates parameters.',
); );
message.writeln('\u2550' * 8); },
debugPrint(message.toString()); ));
return true; return true;
}()); }());
return true; return true;
......
...@@ -461,6 +461,30 @@ void main() { ...@@ -461,6 +461,30 @@ void main() {
); );
}); });
testWidgets("WidgetsApp reports an exception if the selected locale isn't supported", (WidgetTester tester) async {
late final List<Locale>? localesArg;
late final Iterable<Locale> supportedLocalesArg;
await tester.pumpWidget(
MaterialApp( // This uses a MaterialApp because it introduces some actual localizations.
localeListResolutionCallback: (List<Locale>? locales, Iterable<Locale> supportedLocales) {
localesArg = locales;
supportedLocalesArg = supportedLocales;
return const Locale('C_UTF-8');
},
builder: (BuildContext context, Widget? child) => const Placeholder(),
color: const Color(0xFF000000),
),
);
if (!kIsWeb) {
// On web, `flutter test` does not guarantee a particular locale, but
// when using `flutter_tester`, we guarantee that it's en-US, zh-CN.
// https://github.com/flutter/flutter/issues/93290
expect(localesArg, const <Locale>[Locale('en', 'US'), Locale('zh', 'CN')]);
}
expect(supportedLocalesArg, const <Locale>[Locale('en', 'US')]);
expect(tester.takeException(), "Warning: This application's locale, C_UTF-8, is not supported by all of its localization delegates.");
});
testWidgets('WidgetsApp creates a MediaQuery if `useInheritedMediaQuery` is set to false', (WidgetTester tester) async { testWidgets('WidgetsApp creates a MediaQuery if `useInheritedMediaQuery` is set to false', (WidgetTester tester) async {
late BuildContext capturedContext; late BuildContext capturedContext;
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -37,7 +37,7 @@ void main() { ...@@ -37,7 +37,7 @@ void main() {
], ],
localizationsDelegates: <LocalizationsDelegate<dynamic>>[ localizationsDelegates: <LocalizationsDelegate<dynamic>>[
_DummyLocalizationsDelegate(), _DummyLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate, ...GlobalMaterialLocalizations.delegates,
], ],
home: PageView(), home: PageView(),
) )
...@@ -52,9 +52,7 @@ void main() { ...@@ -52,9 +52,7 @@ void main() {
// Regression test for https://github.com/flutter/flutter/pull/16782 // Regression test for https://github.com/flutter/flutter/pull/16782
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[ localizationsDelegates: GlobalMaterialLocalizations.delegates,
GlobalMaterialLocalizations.delegate,
],
supportedLocales: const <Locale>[ supportedLocales: const <Locale>[
Locale('es', 'ES'), Locale('es', 'ES'),
Locale('zh'), Locale('zh'),
......
...@@ -35,9 +35,7 @@ void main() { ...@@ -35,9 +35,7 @@ void main() {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
supportedLocales: <Locale>[locale], supportedLocales: <Locale>[locale],
locale: locale, locale: locale,
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[ localizationsDelegates: GlobalMaterialLocalizations.delegates,
GlobalMaterialLocalizations.delegate,
],
home: Builder(builder: (BuildContext context) { home: Builder(builder: (BuildContext context) {
completer.complete(MaterialLocalizations.of(context).formatHour(timeOfDay)); completer.complete(MaterialLocalizations.of(context).formatHour(timeOfDay));
return Container(); return Container();
...@@ -82,9 +80,7 @@ void main() { ...@@ -82,9 +80,7 @@ void main() {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
supportedLocales: <Locale>[locale], supportedLocales: <Locale>[locale],
locale: locale, locale: locale,
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[ localizationsDelegates: GlobalMaterialLocalizations.delegates,
GlobalMaterialLocalizations.delegate,
],
home: Builder(builder: (BuildContext context) { home: Builder(builder: (BuildContext context) {
completer.complete(MaterialLocalizations.of(context).formatTimeOfDay(timeOfDay)); completer.complete(MaterialLocalizations.of(context).formatTimeOfDay(timeOfDay));
return Container(); return Container();
...@@ -126,9 +122,7 @@ void main() { ...@@ -126,9 +122,7 @@ void main() {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
supportedLocales: <Locale>[locale], supportedLocales: <Locale>[locale],
locale: locale, locale: locale,
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[ localizationsDelegates: GlobalMaterialLocalizations.delegates,
GlobalMaterialLocalizations.delegate,
],
home: Builder(builder: (BuildContext context) { home: Builder(builder: (BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context); final MaterialLocalizations localizations = MaterialLocalizations.of(context);
completer.complete(<DateType, String>{ completer.complete(<DateType, String>{
...@@ -184,12 +178,9 @@ void main() { ...@@ -184,12 +178,9 @@ void main() {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
locale: const Locale('en', 'US'), locale: const Locale('en', 'US'),
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[ localizationsDelegates: GlobalMaterialLocalizations.delegates,
GlobalMaterialLocalizations.delegate,
],
home: Builder(builder: (BuildContext context) { home: Builder(builder: (BuildContext context) {
dateFormat = DateFormat('EEE, d MMM yyyy HH:mm:ss', 'en_US'); dateFormat = DateFormat('EEE, d MMM yyyy HH:mm:ss', 'en_US');
return Container(); return Container();
}), }),
)); ));
......
...@@ -175,9 +175,10 @@ void main() { ...@@ -175,9 +175,10 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
buildFrame( buildFrame(
delegates: <FooMaterialLocalizationsDelegate>[ delegates: <LocalizationsDelegate<dynamic>>[
const FooMaterialLocalizationsDelegate(supportedLanguage: 'fr', backButtonTooltip: 'FR'), const FooMaterialLocalizationsDelegate(supportedLanguage: 'fr', backButtonTooltip: 'FR'),
const FooMaterialLocalizationsDelegate(supportedLanguage: 'de', backButtonTooltip: 'DE'), const FooMaterialLocalizationsDelegate(supportedLanguage: 'de', backButtonTooltip: 'DE'),
GlobalCupertinoLocalizations.delegate,
], ],
supportedLocales: const <Locale>[ supportedLocales: const <Locale>[
Locale('en'), Locale('en'),
...@@ -211,8 +212,9 @@ void main() { ...@@ -211,8 +212,9 @@ void main() {
buildFrame( buildFrame(
// Accept whatever locale we're given // Accept whatever locale we're given
localeResolutionCallback: (Locale? locale, Iterable<Locale> supportedLocales) => locale, localeResolutionCallback: (Locale? locale, Iterable<Locale> supportedLocales) => locale,
delegates: <FooMaterialLocalizationsDelegate>[ delegates: <LocalizationsDelegate<dynamic>>[
const FooMaterialLocalizationsDelegate(supportedLanguage: 'allLanguages'), const FooMaterialLocalizationsDelegate(supportedLanguage: 'allLanguages'),
GlobalCupertinoLocalizations.delegate,
], ],
buildContent: (BuildContext context) { buildContent: (BuildContext context) {
// Should always be 'foo', no matter what the locale is // Should always be 'foo', no matter what the locale is
...@@ -240,8 +242,9 @@ void main() { ...@@ -240,8 +242,9 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
buildFrame( buildFrame(
delegates: <FooMaterialLocalizationsDelegate>[ delegates: <LocalizationsDelegate<dynamic>>[
const FooMaterialLocalizationsDelegate(), const FooMaterialLocalizationsDelegate(),
GlobalCupertinoLocalizations.delegate,
], ],
// supportedLocales not specified, so all locales resolve to 'en' // supportedLocales not specified, so all locales resolve to 'en'
buildContent: (BuildContext context) { buildContent: (BuildContext context) {
...@@ -297,6 +300,7 @@ void main() { ...@@ -297,6 +300,7 @@ void main() {
// Yiddish was ji (ISO-639) is yi (ISO-639-1) // Yiddish was ji (ISO-639) is yi (ISO-639-1)
await tester.binding.setLocale('ji', 'IL'); await tester.binding.setLocale('ji', 'IL');
await tester.pump(); await tester.pump();
expect(tester.takeException(), "Warning: This application's locale, yi_IL, is not supported by all of its localization delegates.");
expect(tester.widget<Text>(find.byKey(textKey)).data, 'yi_IL'); expect(tester.widget<Text>(find.byKey(textKey)).data, 'yi_IL');
// Indonesian was in (ISO-639) is id (ISO-639-1) // Indonesian was in (ISO-639) is id (ISO-639-1)
......
...@@ -22,9 +22,7 @@ void main() { ...@@ -22,9 +22,7 @@ void main() {
return const Text('Next'); return const Text('Next');
}, },
}, },
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[ localizationsDelegates: GlobalMaterialLocalizations.delegates,
GlobalMaterialLocalizations.delegate,
],
supportedLocales: const <Locale>[ supportedLocales: const <Locale>[
Locale('en', 'US'), Locale('en', 'US'),
Locale('es', 'ES'), Locale('es', 'ES'),
...@@ -108,9 +106,7 @@ void main() { ...@@ -108,9 +106,7 @@ void main() {
return const Text('Next'); return const Text('Next');
}, },
}, },
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[ localizationsDelegates: GlobalMaterialLocalizations.delegates,
GlobalMaterialLocalizations.delegate,
],
supportedLocales: const <Locale>[ supportedLocales: const <Locale>[
Locale('en', 'US'), Locale('en', 'US'),
Locale('es', 'ES'), Locale('es', 'ES'),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment