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

This commit is contained in:
Ian Hickson 2021-11-30 17:24:05 -08:00 committed by GitHub
parent bd77118ecb
commit eb00598bec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 123 additions and 68 deletions

View file

@ -248,6 +248,7 @@ abstract class _ErrorDiagnostic extends DiagnosticsProperty<List<Object>> {
/// 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<String>? 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

View file

@ -111,11 +111,11 @@ typedef LocaleResolutionCallback = Locale? Function(Locale? locale, Iterable<Loc
/// To summarize, the main matching priority is:
///
/// 1. [Locale.languageCode], [Locale.scriptCode], and [Locale.countryCode]
/// 1. [Locale.languageCode] and [Locale.scriptCode] only
/// 1. [Locale.languageCode] and [Locale.countryCode] only
/// 1. [Locale.languageCode] only (with caveats, see above)
/// 1. [Locale.countryCode] only when all [preferredLocales] fail to match
/// 1. Returns the first element of [supportedLocales] as a fallback
/// 2. [Locale.languageCode] and [Locale.scriptCode] only
/// 3. [Locale.languageCode] and [Locale.countryCode] only
/// 4. [Locale.languageCode] only (with caveats, see above)
/// 5. [Locale.countryCode] only when all [preferredLocales] fail to match
/// 6. Returns the first element of [supportedLocales] as a fallback
///
/// 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`
@ -885,7 +885,7 @@ class WidgetsApp extends StatefulWidget {
/// `[const Locale('en', 'US')]`.
///
/// 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]
/// 2. [Locale.languageCode] and [Locale.scriptCode] only
@ -899,7 +899,7 @@ class WidgetsApp extends StatefulWidget {
///
/// The default locale resolution algorithm can be overridden by providing a
/// 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
/// [Unicode TR35](https://unicode.org/reports/tr35/#LanguageMatching)) that
/// takes distances between languages into account.
@ -1493,35 +1493,37 @@ class _WidgetsAppState extends State<WidgetsApp> 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(), <String>['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;

View file

@ -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 {
late BuildContext capturedContext;
await tester.pumpWidget(

View file

@ -37,7 +37,7 @@ void main() {
],
localizationsDelegates: <LocalizationsDelegate<dynamic>>[
_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 <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
],
localizationsDelegates: GlobalMaterialLocalizations.delegates,
supportedLocales: const <Locale>[
Locale('es', 'ES'),
Locale('zh'),

View file

@ -35,9 +35,7 @@ void main() {
await tester.pumpWidget(MaterialApp(
supportedLocales: <Locale>[locale],
locale: locale,
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
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: locale,
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
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: locale,
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
],
localizationsDelegates: GlobalMaterialLocalizations.delegates,
home: Builder(builder: (BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
completer.complete(<DateType, String>{
@ -184,12 +178,9 @@ void main() {
await tester.pumpWidget(MaterialApp(
locale: const Locale('en', 'US'),
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
],
localizationsDelegates: GlobalMaterialLocalizations.delegates,
home: Builder(builder: (BuildContext context) {
dateFormat = DateFormat('EEE, d MMM yyyy HH:mm:ss', 'en_US');
return Container();
}),
));

View file

@ -175,9 +175,10 @@ void main() {
await tester.pumpWidget(
buildFrame(
delegates: <FooMaterialLocalizationsDelegate>[
delegates: <LocalizationsDelegate<dynamic>>[
const FooMaterialLocalizationsDelegate(supportedLanguage: 'fr', backButtonTooltip: 'FR'),
const FooMaterialLocalizationsDelegate(supportedLanguage: 'de', backButtonTooltip: 'DE'),
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const <Locale>[
Locale('en'),
@ -211,8 +212,9 @@ void main() {
buildFrame(
// Accept whatever locale we're given
localeResolutionCallback: (Locale? locale, Iterable<Locale> supportedLocales) => locale,
delegates: <FooMaterialLocalizationsDelegate>[
delegates: <LocalizationsDelegate<dynamic>>[
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: <FooMaterialLocalizationsDelegate>[
delegates: <LocalizationsDelegate<dynamic>>[
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<Text>(find.byKey(textKey)).data, 'yi_IL');
// Indonesian was in (ISO-639) is id (ISO-639-1)

View file

@ -22,9 +22,7 @@ void main() {
return const Text('Next');
},
},
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
],
localizationsDelegates: GlobalMaterialLocalizations.delegates,
supportedLocales: const <Locale>[
Locale('en', 'US'),
Locale('es', 'ES'),
@ -108,9 +106,7 @@ void main() {
return const Text('Next');
},
},
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
],
localizationsDelegates: GlobalMaterialLocalizations.delegates,
supportedLocales: const <Locale>[
Locale('en', 'US'),
Locale('es', 'ES'),