diff --git a/packages/flutter/lib/src/foundation/assertions.dart b/packages/flutter/lib/src/foundation/assertions.dart index 827188c2a63..84a503aa8f7 100644 --- a/packages/flutter/lib/src/foundation/assertions.dart +++ b/packages/flutter/lib/src/foundation/assertions.dart @@ -248,6 +248,7 @@ abstract class _ErrorDiagnostic extends DiagnosticsProperty> { /// problem that was detected. /// * [ErrorHint], which provides specific, non-obvious advice that may be /// applicable. +/// * [ErrorSpacer], which renders as a blank line. /// * [FlutterError], which is the most common place to use an /// [ErrorDescription]. class ErrorDescription extends _ErrorDiagnostic { @@ -323,6 +324,7 @@ class ErrorSummary extends _ErrorDiagnostic { /// * [ErrorDescription], which provides an explanation of the problem and its /// cause, any information that may help track down the problem, background /// information, etc. +/// * [ErrorSpacer], which renders as a blank line. /// * [FlutterError], which is the most common place to use an [ErrorHint]. class ErrorHint extends _ErrorDiagnostic { /// A lint enforces that this constructor can only be called with a string @@ -516,14 +518,52 @@ class FlutterErrorDetails with Diagnosticable { /// This won't be called if [stack] is null. final IterableFilter? stackFilter; - /// A callback which, when called with a [StringBuffer] will write to that buffer - /// information that could help with debugging the problem. + /// A callback which will provide information that could help with debugging + /// the problem. /// - /// Information collector callbacks can be expensive, so the generated information - /// should be cached, rather than the callback being called multiple times. + /// Information collector callbacks can be expensive, so the generated + /// 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 - /// not end with a newline. + /// The callback is expected to return an iterable of [DiagnosticsNode] objects, + /// 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; /// Whether this error should be ignored by the default error reporting diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index d00e6f0734b..868e1138e2e 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -111,11 +111,11 @@ typedef LocaleResolutionCallback = Locale? Function(Locale? locale, Iterable with WidgetsBindingObserver { if (unsupportedTypes.isEmpty) return true; - // Currently the Cupertino library only provides english localizations. - // Remove this when https://github.com/flutter/flutter/issues/23847 - // is fixed. - if (listEquals(unsupportedTypes.map((Type type) => type.toString()).toList(), ['CupertinoLocalizations'])) - 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) { - // Currently the Cupertino library only provides english localizations. - // Remove this when https://github.com/flutter/flutter/issues/23847 - // is fixed. - if (unsupportedType.toString() == 'CupertinoLocalizations') - continue; - message.writeln( - '> A $unsupportedType delegate that supports the $appLocale locale was not found.', - ); - } - message.writeln( - 'See https://flutter.dev/tutorials/internationalization/ for more\n' - "information about configuring an app's locale, supportedLocales,\n" - 'and localizationsDelegates parameters.', - ); - message.writeln('\u2550' * 8); - debugPrint(message.toString()); + FlutterError.reportError(FlutterErrorDetails( + exception: "Warning: This application's locale, $appLocale, is not supported by all of its localization delegates.", + library: 'widgets', + informationCollector: () sync* { + for (final Type unsupportedType in unsupportedTypes) { + yield ErrorDescription( + '• A $unsupportedType delegate that supports the $appLocale locale was not found.', + ); + } + yield ErrorSpacer(); + if (unsupportedTypes.length == 1 && unsupportedTypes.single.toString() == 'CupertinoLocalizations') { + // 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.', + ); + }, + )); return true; }()); return true; diff --git a/packages/flutter/test/widgets/app_test.dart b/packages/flutter/test/widgets/app_test.dart index dd81d71a461..d9a4469770b 100644 --- a/packages/flutter/test/widgets/app_test.dart +++ b/packages/flutter/test/widgets/app_test.dart @@ -461,6 +461,30 @@ void main() { ); }); + testWidgets("WidgetsApp reports an exception if the selected locale isn't supported", (WidgetTester tester) async { + late final List? localesArg; + late final Iterable supportedLocalesArg; + await tester.pumpWidget( + MaterialApp( // This uses a MaterialApp because it introduces some actual localizations. + localeListResolutionCallback: (List? locales, Iterable 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('en', 'US'), Locale('zh', 'CN')]); + } + expect(supportedLocalesArg, const [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 { late BuildContext capturedContext; await tester.pumpWidget( diff --git a/packages/flutter_localizations/test/basics_test.dart b/packages/flutter_localizations/test/basics_test.dart index 69914700bde..58376a6c1ee 100644 --- a/packages/flutter_localizations/test/basics_test.dart +++ b/packages/flutter_localizations/test/basics_test.dart @@ -37,7 +37,7 @@ void main() { ], localizationsDelegates: >[ _DummyLocalizationsDelegate(), - GlobalMaterialLocalizations.delegate, + ...GlobalMaterialLocalizations.delegates, ], home: PageView(), ) @@ -52,9 +52,7 @@ void main() { // Regression test for https://github.com/flutter/flutter/pull/16782 await tester.pumpWidget( MaterialApp( - localizationsDelegates: const >[ - GlobalMaterialLocalizations.delegate, - ], + localizationsDelegates: GlobalMaterialLocalizations.delegates, supportedLocales: const [ Locale('es', 'ES'), Locale('zh'), diff --git a/packages/flutter_localizations/test/material/date_time_test.dart b/packages/flutter_localizations/test/material/date_time_test.dart index 0c5c9dc2e8f..9db3e8c1838 100644 --- a/packages/flutter_localizations/test/material/date_time_test.dart +++ b/packages/flutter_localizations/test/material/date_time_test.dart @@ -35,9 +35,7 @@ void main() { await tester.pumpWidget(MaterialApp( supportedLocales: [locale], locale: locale, - localizationsDelegates: const >[ - GlobalMaterialLocalizations.delegate, - ], + localizationsDelegates: GlobalMaterialLocalizations.delegates, home: Builder(builder: (BuildContext context) { completer.complete(MaterialLocalizations.of(context).formatHour(timeOfDay)); return Container(); @@ -82,9 +80,7 @@ void main() { await tester.pumpWidget(MaterialApp( supportedLocales: [locale], locale: locale, - localizationsDelegates: const >[ - GlobalMaterialLocalizations.delegate, - ], + localizationsDelegates: GlobalMaterialLocalizations.delegates, home: Builder(builder: (BuildContext context) { completer.complete(MaterialLocalizations.of(context).formatTimeOfDay(timeOfDay)); return Container(); @@ -126,9 +122,7 @@ void main() { await tester.pumpWidget(MaterialApp( supportedLocales: [locale], locale: locale, - localizationsDelegates: const >[ - GlobalMaterialLocalizations.delegate, - ], + localizationsDelegates: GlobalMaterialLocalizations.delegates, home: Builder(builder: (BuildContext context) { final MaterialLocalizations localizations = MaterialLocalizations.of(context); completer.complete({ @@ -184,12 +178,9 @@ void main() { await tester.pumpWidget(MaterialApp( locale: const Locale('en', 'US'), - localizationsDelegates: const >[ - GlobalMaterialLocalizations.delegate, - ], + localizationsDelegates: GlobalMaterialLocalizations.delegates, home: Builder(builder: (BuildContext context) { dateFormat = DateFormat('EEE, d MMM yyyy HH:mm:ss', 'en_US'); - return Container(); }), )); diff --git a/packages/flutter_localizations/test/override_test.dart b/packages/flutter_localizations/test/override_test.dart index f977bb111ba..511b6a15b30 100644 --- a/packages/flutter_localizations/test/override_test.dart +++ b/packages/flutter_localizations/test/override_test.dart @@ -175,9 +175,10 @@ void main() { await tester.pumpWidget( buildFrame( - delegates: [ + delegates: >[ const FooMaterialLocalizationsDelegate(supportedLanguage: 'fr', backButtonTooltip: 'FR'), const FooMaterialLocalizationsDelegate(supportedLanguage: 'de', backButtonTooltip: 'DE'), + GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [ Locale('en'), @@ -211,8 +212,9 @@ void main() { buildFrame( // Accept whatever locale we're given localeResolutionCallback: (Locale? locale, Iterable supportedLocales) => locale, - delegates: [ + delegates: >[ const FooMaterialLocalizationsDelegate(supportedLanguage: 'allLanguages'), + GlobalCupertinoLocalizations.delegate, ], buildContent: (BuildContext context) { // Should always be 'foo', no matter what the locale is @@ -240,8 +242,9 @@ void main() { await tester.pumpWidget( buildFrame( - delegates: [ + delegates: >[ const FooMaterialLocalizationsDelegate(), + GlobalCupertinoLocalizations.delegate, ], // supportedLocales not specified, so all locales resolve to 'en' buildContent: (BuildContext context) { @@ -297,6 +300,7 @@ void main() { // Yiddish was ji (ISO-639) is yi (ISO-639-1) await tester.binding.setLocale('ji', 'IL'); 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(find.byKey(textKey)).data, 'yi_IL'); // Indonesian was in (ISO-639) is id (ISO-639-1) diff --git a/packages/flutter_localizations/test/text_test.dart b/packages/flutter_localizations/test/text_test.dart index 4ddc9d43c21..b0501f048ba 100644 --- a/packages/flutter_localizations/test/text_test.dart +++ b/packages/flutter_localizations/test/text_test.dart @@ -22,9 +22,7 @@ void main() { return const Text('Next'); }, }, - localizationsDelegates: const >[ - GlobalMaterialLocalizations.delegate, - ], + localizationsDelegates: GlobalMaterialLocalizations.delegates, supportedLocales: const [ Locale('en', 'US'), Locale('es', 'ES'), @@ -108,9 +106,7 @@ void main() { return const Text('Next'); }, }, - localizationsDelegates: const >[ - GlobalMaterialLocalizations.delegate, - ], + localizationsDelegates: GlobalMaterialLocalizations.delegates, supportedLocales: const [ Locale('en', 'US'), Locale('es', 'ES'),