mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Use FlutterError.reportError
instead of debugPrint
for l10n warning (#93076)
This commit is contained in:
parent
bd77118ecb
commit
eb00598bec
@ -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 {
|
|||||||
/// * [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 {
|
|||||||
/// 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
|
|||||||
/// 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 {
|
|||||||
/// `[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 {
|
|||||||
///
|
///
|
||||||
/// 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 {
|
|||||||
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() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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() {
|
|||||||
],
|
],
|
||||||
localizationsDelegates: <LocalizationsDelegate<dynamic>>[
|
localizationsDelegates: <LocalizationsDelegate<dynamic>>[
|
||||||
_DummyLocalizationsDelegate(),
|
_DummyLocalizationsDelegate(),
|
||||||
GlobalMaterialLocalizations.delegate,
|
...GlobalMaterialLocalizations.delegates,
|
||||||
],
|
],
|
||||||
home: PageView(),
|
home: PageView(),
|
||||||
)
|
)
|
||||||
@ -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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
// 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() {
|
|||||||
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() {
|
|||||||
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'),
|
||||||
|
Loading…
Reference in New Issue
Block a user