修复:Material 内置组件中文化——挂 zh-CN 三件套 delegate(M3.5-01)
用户 Linux 桌面实测反馈「日期选择器全英文」。根因是 MaterialApp 从一迭代
建起就没配 localizationsDelegates/supportedLocales/locale,pubspec 也没有
flutter_localizations;Flutter 在缺 delegate 时静默回退内置的
DefaultMaterialLocalizations(只有英文),于是业务自绘文案全中文、Material
内置组件全英文,同一弹窗里中英混排:
Select date → 选择日期
OK / Cancel → 确定 / 取消
Enter Date → 输入日期
Switch to input → 切换到输入模式
Invalid format. / Out of range. → 格式无效。/ 超出范围。
mm/dd/yyyy → yyyy/mm/dd(zh 顺序年在前)
September 2026 → 2026年9月
处置:
- pubspec 加 flutter_localizations(sdk)与 intl(日期符号底座,显式直接
依赖以锁定与 SDK 一致的版本)。
- 新增 lib/app/app_localization.dart 把 Global{Material,Cupertino,Widgets}
三件套 delegate、supportedLocales、locale 收在一处常量:widget 测试若只
pumpWidget(MaterialApp(home: ...)) 而不挂 delegate,测到的「中文」是假的,
测试直接引用同一份常量,生产与测试不会各配一套而漂移。
- 单语言 zh-CN,不列 en:避免设备语言为英文时回退英文,再造混排。
- pets 域三份 widget 测试的 MaterialApp 同步挂上 delegate,
tap(find.text('OK')) 相应改 '确定'——让测试与生产一致,而不是在英文兜底下测。
新增 test/app/app_localization_test.dart 守住这一行不被回删:断言根
MaterialApp 实际挂上三件套 + locale zh-CN,并从运行期 MaterialLocalizations
取回中文文案。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:patbond_flutter/analytics/analytics_route_observer.dart';
|
||||
import 'package:patbond_flutter/analytics/analytics_service.dart';
|
||||
import 'package:patbond_flutter/analytics/page_view_tracker.dart';
|
||||
import 'package:patbond_flutter/analytics/session_tracker.dart';
|
||||
import 'package:patbond_flutter/app/app_localization.dart';
|
||||
import 'package:patbond_flutter/core/network/api_client.dart';
|
||||
import 'package:patbond_flutter/core/network/token_refresher.dart';
|
||||
import 'package:patbond_flutter/core/theme/app_theme.dart';
|
||||
@@ -268,6 +269,11 @@ class _AppState extends State<App> {
|
||||
title: 'Patbond',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: buildAppTheme(),
|
||||
// M3.5-01:Material 内置组件中文化(此前未配 delegate,日期选择器等
|
||||
// 全部回退英文兜底)。单语言 zh-CN,不随设备语言回退英文。
|
||||
localizationsDelegates: appLocalizationsDelegates,
|
||||
supportedLocales: appSupportedLocales,
|
||||
locale: appLocale,
|
||||
navigatorObservers: [_routeObserver],
|
||||
home: ListenableBuilder(
|
||||
listenable: sessionManager,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/// 应用本地化配置(M3.5-01)。
|
||||
///
|
||||
/// 根因备忘:M3 之前 [MaterialApp] 未配任何 `localizationsDelegates`,
|
||||
/// Flutter 回退到内置的 `DefaultMaterialLocalizations`(仅英文),导致
|
||||
/// 一切 Material 内置组件(日期选择器标题「Select date」、确定/取消
|
||||
/// 「OK」/「Cancel」、模式切换 tooltip「Switch to input」、格式报错
|
||||
/// 「Invalid format.」)全英文,而业务自绘文案全中文——同一弹窗内中英混排。
|
||||
/// 挂上三件套后分别为「选择日期 / 确定 / 取消 / 切换到输入模式 / 格式无效。」。
|
||||
///
|
||||
/// 这里把三件套 delegate 与 locale 收在一处,供 `App` 与 widget 测试共用:
|
||||
/// 测试若只 `pumpWidget(MaterialApp(home: ...))` 而不带 delegate,看到的仍是
|
||||
/// 英文兜底,与真机不一致;需要断言中文渲染的测试请一并挂上本文件的常量。
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
|
||||
/// Material / Cupertino / Widgets 三件套本地化 delegate。
|
||||
const List<LocalizationsDelegate<Object>> appLocalizationsDelegates =
|
||||
<LocalizationsDelegate<Object>>[
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// 简体中文(中国大陆)。M3.5 仅单语言,暂不做语言切换。
|
||||
const Locale appLocale = Locale('zh', 'CN');
|
||||
|
||||
/// 支持语言列表。仅 zh-CN——避免设备语言为英文时回退到英文,
|
||||
/// 造成同一界面内业务中文 + 组件英文的混排。
|
||||
const List<Locale> appSupportedLocales = <Locale>[appLocale];
|
||||
@@ -235,6 +235,11 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -399,6 +404,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -43,6 +43,13 @@ dependencies:
|
||||
image_picker: ^1.2.0
|
||||
# T3-13 媒体上传:原生编解码压缩(长边重采样 + JPEG 质量 + EXIF 方向矫正)。
|
||||
flutter_image_compress: ^2.4.0
|
||||
# M3.5-01 中文本地化:Material/Cupertino/Widgets 内置组件文案(日期选择器
|
||||
# 标题、确定/取消、输入模式提示与格式错误)由 Global*Localizations 提供。
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
# flutter_localizations 的日期符号/数字格式底座;显式直接依赖以锁定与 SDK
|
||||
# 一致的版本(避免间接依赖升级悄悄改变格式化行为)。
|
||||
intl: ^0.20.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:patbond_flutter/app/app.dart';
|
||||
import 'package:patbond_flutter/app/app_localization.dart';
|
||||
import 'package:patbond_flutter/features/auth/session_manager.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../helpers/auth_test_helpers.dart';
|
||||
import '../helpers/community_test_helpers.dart';
|
||||
import '../helpers/pet_test_helpers.dart';
|
||||
|
||||
/// M3.5-01:根 [MaterialApp] 实际挂上 zh-CN 本地化。
|
||||
///
|
||||
/// 用户桌面实测反馈「日期选择器全英文」的根因就在这里——delegate 一行没配,
|
||||
/// Material 静默回退英文兜底。本测试守住这一行不被回删。
|
||||
void main() {
|
||||
testWidgets('MaterialApp 挂三件套 delegate + 单语言 zh-CN', (tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final session = SessionManager(store: InMemoryTokenStore())
|
||||
..markAuthenticated();
|
||||
|
||||
await tester.pumpWidget(
|
||||
App(
|
||||
sessionManager: session,
|
||||
authRepository: FakeAuthRepository(),
|
||||
petsRepository: FakePetsRepository(),
|
||||
communityRepository: FakeCommunityRepository()
|
||||
..onFeed = (_, _) async => feedPage(const []),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final app = tester.widget<MaterialApp>(find.byType(MaterialApp));
|
||||
expect(app.localizationsDelegates, appLocalizationsDelegates);
|
||||
expect(app.supportedLocales, appSupportedLocales);
|
||||
expect(app.locale, const Locale('zh', 'CN'));
|
||||
|
||||
// 运行期实际解析出的 Material 文案为中文(不是 Default* 英文兜底)。
|
||||
final context = tester.element(find.byType(NavigationBar));
|
||||
final l10n = MaterialLocalizations.of(context);
|
||||
expect(l10n.okButtonLabel, '确定');
|
||||
expect(l10n.cancelButtonLabel, '取消');
|
||||
expect(l10n.datePickerHelpText, '选择日期');
|
||||
expect(l10n.dateInputLabel, '输入日期');
|
||||
expect(l10n.inputDateModeButtonLabel, '切换到输入模式');
|
||||
expect(l10n.invalidDateFormatLabel, '格式无效。');
|
||||
expect(l10n.formatMonthYear(DateTime(2026, 9, 10)), '2026年9月');
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:patbond_flutter/core/network/api_exception.dart';
|
||||
import 'package:patbond_flutter/app/app_localization.dart';
|
||||
import 'package:patbond_flutter/core/theme/app_theme.dart';
|
||||
import 'package:patbond_flutter/core/widgets/empty_state_illustration.dart';
|
||||
import 'package:patbond_flutter/features/pets/care_reminder_form_page.dart';
|
||||
@@ -39,6 +40,11 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: buildAppTheme(),
|
||||
// M3.5-01:与生产一致挂 zh-CN delegate,日期选择器等内置组件文案
|
||||
// 才是用户真机看到的中文。
|
||||
localizationsDelegates: appLocalizationsDelegates,
|
||||
supportedLocales: appSupportedLocales,
|
||||
locale: appLocale,
|
||||
home: const Scaffold(body: Text('详情基底')),
|
||||
),
|
||||
);
|
||||
@@ -206,7 +212,7 @@ void main() {
|
||||
);
|
||||
await tester.tap(find.text('到期日期'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('OK'));
|
||||
await tester.tap(find.text('确定'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('保存提醒'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:patbond_flutter/core/network/api_exception.dart';
|
||||
import 'package:patbond_flutter/app/app_localization.dart';
|
||||
import 'package:patbond_flutter/core/theme/app_theme.dart';
|
||||
import 'package:patbond_flutter/features/pets/health_record_analytics.dart';
|
||||
import 'package:patbond_flutter/features/pets/pet_exceptions.dart';
|
||||
@@ -36,6 +37,11 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: buildAppTheme(),
|
||||
// M3.5-01:与生产一致挂 zh-CN delegate,日期选择器等内置组件文案
|
||||
// 才是用户真机看到的中文。
|
||||
localizationsDelegates: appLocalizationsDelegates,
|
||||
supportedLocales: appSupportedLocales,
|
||||
locale: appLocale,
|
||||
home: const Scaffold(body: Text('列表基底')),
|
||||
),
|
||||
);
|
||||
@@ -65,7 +71,7 @@ void main() {
|
||||
Future<void> pickDate(WidgetTester tester, String tileLabel) async {
|
||||
await tester.tap(find.text(tileLabel));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('OK'));
|
||||
await tester.tap(find.text('确定'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:patbond_flutter/core/network/api_exception.dart';
|
||||
import 'package:patbond_flutter/app/app_localization.dart';
|
||||
import 'package:patbond_flutter/core/theme/app_theme.dart';
|
||||
import 'package:patbond_flutter/core/widgets/empty_state_illustration.dart';
|
||||
import 'package:patbond_flutter/features/pets/health_record_analytics.dart';
|
||||
@@ -39,6 +40,11 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: buildAppTheme(),
|
||||
// M3.5-01:与生产一致挂 zh-CN delegate,日期选择器等内置组件文案
|
||||
// 才是用户真机看到的中文。
|
||||
localizationsDelegates: appLocalizationsDelegates,
|
||||
supportedLocales: appSupportedLocales,
|
||||
locale: appLocale,
|
||||
home: const Scaffold(body: Text('详情基底')),
|
||||
),
|
||||
);
|
||||
@@ -166,7 +172,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('计划接种日期'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('OK'));
|
||||
await tester.tap(find.text('确定'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('保存登记'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
Reference in New Issue
Block a user