From 3d849bd468e0159e2f68fdd2c10c8d0fedef792e Mon Sep 17 00:00:00 2001 From: Developer <91611@user.local> Date: Fri, 12 Jun 2026 15:09:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(i18n):=20=E5=AE=8C=E6=88=90=E5=85=A8?= =?UTF-8?q?=E9=87=8F=20UI=20=E6=96=87=E6=9C=AC=E5=9B=BD=E9=99=85=E5=8C=96?= =?UTF-8?q?=EF=BC=8C=E6=9B=BF=E6=8D=A2=E6=89=80=E6=9C=89=E7=A1=AC=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E4=B8=AD=E6=96=87=E4=B8=BA=20AppLocalizations=20?= =?UTF-8?q?=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - core/localization: 新增约 60 个翻译键(含参数化方法),中英双语覆盖 - shared/widgets: CommonDialog 默认参数国际化 - features/home: 完成页操作步骤指引、状态栏串口连接状态、程序列表状态标签 - features/programs: 表头状态列、表单验证提示、导入/模板操作反馈、删除确认(参数化) - features/program_detail: 步骤列表/表单标题、删除确认、速度档位显示(参数化) - features/device: run_state_provider 错误消息改为错误码 - features/settings: 升级页、密码面板、语言面板、U盘导入面板、串口配置面板全部替换 Co-Authored-By: Claude Opus 4.7 --- CLAUDE.md | 2 +- lib/core/localization/app_localizations.dart | 275 ++++++++++++++++++ .../device/providers/run_state_provider.dart | 2 +- lib/features/home/pages/complete_page.dart | 17 +- lib/features/home/widgets/program_list.dart | 5 +- lib/features/home/widgets/status_bar.dart | 7 +- .../pages/program_detail_page.dart | 8 +- .../program_detail/widgets/step_form.dart | 8 +- .../program_detail/widgets/step_list.dart | 14 +- .../programs/pages/programs_page.dart | 22 +- .../programs/widgets/program_form_dialog.dart | 14 +- .../settings/pages/settings_page.dart | 9 +- .../settings/widgets/language_panel.dart | 6 +- .../settings/widgets/password_panel.dart | 35 ++- .../settings/widgets/serial_config_panel.dart | 85 +++--- .../settings/widgets/usb_import_panel.dart | 30 +- lib/shared/widgets/common_dialog.dart | 10 +- .../fix-hardcoded-chinese-i18n/.openspec.yaml | 2 + .../fix-hardcoded-chinese-i18n/design.md | 103 +++++++ .../fix-hardcoded-chinese-i18n/proposal.md | 26 ++ .../specs/i18n/spec.md | 49 ++++ .../fix-hardcoded-chinese-i18n/tasks.md | 66 +++++ openspec/config.yaml | 20 ++ 23 files changed, 688 insertions(+), 127 deletions(-) create mode 100644 openspec/changes/fix-hardcoded-chinese-i18n/.openspec.yaml create mode 100644 openspec/changes/fix-hardcoded-chinese-i18n/design.md create mode 100644 openspec/changes/fix-hardcoded-chinese-i18n/proposal.md create mode 100644 openspec/changes/fix-hardcoded-chinese-i18n/specs/i18n/spec.md create mode 100644 openspec/changes/fix-hardcoded-chinese-i18n/tasks.md create mode 100644 openspec/config.yaml diff --git a/CLAUDE.md b/CLAUDE.md index 66695ae..a691480 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -131,7 +131,7 @@ AppLocalizations 支持中文(zh)和英文(en),使用 `AppLocalizations.of(con - 程序详情:步骤列表、步骤参数编辑 - 运行控制:启动/暂停/停止、进度监控(使用 MockRunner 模拟) - 系统设置:基础页面框架 -- 国际化:中/英文翻译配置 +- 国际化:中/英文翻译配置,UI 文本已全部通过 `AppLocalizations` 覆盖(含参数化方法) 待完善:设备通信对接、实际硬件控制、U盘导入功能。 diff --git a/lib/core/localization/app_localizations.dart b/lib/core/localization/app_localizations.dart index f4c2267..ba00cee 100644 --- a/lib/core/localization/app_localizations.dart +++ b/lib/core/localization/app_localizations.dart @@ -130,6 +130,127 @@ class AppLocalizations { String get updateAvailable => _localizedValues[locale.languageCode]?['updateAvailable'] ?? '有新版本可用'; String get checkUpdate => _localizedValues[locale.languageCode]?['checkUpdate'] ?? '检查更新'; + // ---- 状态 ---- + String get status => _localizedValues[locale.languageCode]?['status'] ?? '状态'; + String get operation => _localizedValues[locale.languageCode]?['operation'] ?? '操作'; + String get serialConnected => _localizedValues[locale.languageCode]?['serialConnected'] ?? '已连接'; + String get serialConnecting => _localizedValues[locale.languageCode]?['serialConnecting'] ?? '连接中'; + String get serialDisconnected => _localizedValues[locale.languageCode]?['serialDisconnected'] ?? '未连接'; + String get serialError => _localizedValues[locale.languageCode]?['serialError'] ?? '错误'; + + // ---- 完成页 ---- + String get programRunComplete => _localizedValues[locale.languageCode]?['programRunComplete'] ?? '程序运行完成'; + String get extractSample => _localizedValues[locale.languageCode]?['extractSample'] ?? '取出样本'; + String get dropToTestCard => _localizedValues[locale.languageCode]?['dropToTestCard'] ?? '滴入检测卡'; + String get waitForReaction => _localizedValues[locale.languageCode]?['waitForReaction'] ?? '等待反应'; + String get viewResults => _localizedValues[locale.languageCode]?['viewResults'] ?? '查看结果'; + + // ---- 表单 ---- + String get codeLabel => _localizedValues[locale.languageCode]?['codeLabel'] ?? '编号'; + String get nameLabel => _localizedValues[locale.languageCode]?['nameLabel'] ?? '名称'; + String get enterCode => _localizedValues[locale.languageCode]?['enterCode'] ?? '请输入编号'; + String get enterName => _localizedValues[locale.languageCode]?['enterName'] ?? '请输入名称'; + String get enterStepName => _localizedValues[locale.languageCode]?['enterStepName'] ?? '请输入步骤名称'; + String get hintCode => _localizedValues[locale.languageCode]?['hintCode'] ?? '例如: P001'; + String get hintProgramName => _localizedValues[locale.languageCode]?['hintProgramName'] ?? '请输入程序名称'; + String get hintStepName => _localizedValues[locale.languageCode]?['hintStepName'] ?? '例如: 混合、吸磁、吹气'; + String get saveFailed => _localizedValues[locale.languageCode]?['saveFailed'] ?? '保存失败,请检查编号是否重复'; + String get saved => _localizedValues[locale.languageCode]?['saved'] ?? '已保存'; + String get stepUpdated => _localizedValues[locale.languageCode]?['stepUpdated'] ?? '步骤已更新'; + String get add => _localizedValues[locale.languageCode]?['add'] ?? '添加'; + String get reset => _localizedValues[locale.languageCode]?['reset'] ?? '重置'; + String get confirmNewPassword => _localizedValues[locale.languageCode]?['confirmNewPassword'] ?? '确认新密码'; + String get enterNewPassword => _localizedValues[locale.languageCode]?['enterNewPassword'] ?? '再次输入新密码'; + String get enterCurrentPassword => _localizedValues[locale.languageCode]?['enterCurrentPassword'] ?? '请输入当前密码'; + String get newPwdMinLength => _localizedValues[locale.languageCode]?['newPwdMinLength'] ?? '新密码至少6位字符'; + String get defaultPassword => _localizedValues[locale.languageCode]?['defaultPassword'] ?? '默认密码为 123456'; + + // ---- 删除确认 ---- + String get deleteConfirmSingle => _localizedValues[locale.languageCode]?['deleteConfirmSingle'] ?? '确定要删除此程序吗?'; + String deleteConfirmMultiple(int count) => + locale.languageCode == 'en' + ? 'Are you sure to delete $count selected programs?' + : '确定要删除选中的 $count 个程序吗?'; + String get deleteStepConfirmSingle => _localizedValues[locale.languageCode]?['deleteStepConfirmSingle'] ?? '确定要删除此步骤吗?'; + String deleteStepConfirmMultiple(int count) => + locale.languageCode == 'en' + ? 'Are you sure to delete $count selected steps?' + : '确定要删除选中的 $count 个步骤吗?'; + + // ---- 导入 ---- + String get cannotReadFile => _localizedValues[locale.languageCode]?['cannotReadFile'] ?? '无法读取文件'; + String processedPrograms(int count) => + locale.languageCode == 'en' + ? 'Successfully processed $count programs' + : '成功处理 $count 个程序'; + String get noValidProgramData => _localizedValues[locale.languageCode]?['noValidProgramData'] ?? 'Excel 中无有效程序数据'; + String get templateSaved => _localizedValues[locale.languageCode]?['templateSaved'] ?? '模板已保存'; + String get generateTemplateFailed => _localizedValues[locale.languageCode]?['generateTemplateFailed'] ?? '生成模板失败'; + String get importingPrograms => _localizedValues[locale.languageCode]?['importingPrograms'] ?? '正在导入程序...'; + + // ---- 语言设置 ---- + String get languageSettings => _localizedValues[locale.languageCode]?['languageSettings'] ?? '语言设置'; + String get switchLanguageEffect => _localizedValues[locale.languageCode]?['switchLanguageEffect'] ?? '切换语言后立即生效'; + + // ---- 串口配置 ---- + String get serialStatus => _localizedValues[locale.languageCode]?['serialStatus'] ?? '串口状态'; + String get availableSerialDevices => _localizedValues[locale.languageCode]?['availableSerialDevices'] ?? '可用串口设备'; + String get refresh => _localizedValues[locale.languageCode]?['refresh'] ?? '刷新'; + String get noSerialDevice => _localizedValues[locale.languageCode]?['noSerialDevice'] ?? '未检测到 USB 串口设备'; + String get serialDeviceHint => _localizedValues[locale.languageCode]?['serialDeviceHint'] ?? '请确认下位机已上电并通过 USB 接入设备'; + String get scanFailed => _localizedValues[locale.languageCode]?['scanFailed'] ?? '扫描设备失败'; + String get selectSerialFirst => _localizedValues[locale.languageCode]?['selectSerialFirst'] ?? '请先选择串口设备'; + String get connectSuccess => _localizedValues[locale.languageCode]?['connectSuccess'] ?? '连接成功'; + String get connectFailed => _localizedValues[locale.languageCode]?['connectFailed'] ?? '连接失败'; + String get disconnected => _localizedValues[locale.languageCode]?['disconnected'] ?? '已断开串口'; + String get connectFirst => _localizedValues[locale.languageCode]?['connectFirst'] ?? '请先连接串口'; + String get sendTestFrame => _localizedValues[locale.languageCode]?['sendTestFrame'] ?? '发送测试帧'; + String get serialParams => _localizedValues[locale.languageCode]?['serialParams'] ?? '串口参数'; + String get baudRate => _localizedValues[locale.languageCode]?['baudRate'] ?? '波特率'; + String get dataBits => _localizedValues[locale.languageCode]?['dataBits'] ?? '数据位'; + String get stopBits => _localizedValues[locale.languageCode]?['stopBits'] ?? '停止位'; + String get parity => _localizedValues[locale.languageCode]?['parity'] ?? '校验位'; + String get parityNone => _localizedValues[locale.languageCode]?['parityNone'] ?? '无'; + String get parityOdd => _localizedValues[locale.languageCode]?['parityOdd'] ?? '奇'; + String get parityEven => _localizedValues[locale.languageCode]?['parityEven'] ?? '偶'; + String get parityMark => _localizedValues[locale.languageCode]?['parityMark'] ?? '标记'; + String get paritySpace => _localizedValues[locale.languageCode]?['paritySpace'] ?? '空'; + String get flowControl => _localizedValues[locale.languageCode]?['flowControl'] ?? '流控'; + String get autoSaveParams => _localizedValues[locale.languageCode]?['autoSaveParams'] ?? '参数修改后自动保存'; + String get connect => _localizedValues[locale.languageCode]?['connect'] ?? '连接'; + String get disconnect => _localizedValues[locale.languageCode]?['disconnect'] ?? '断开'; + String get unknownError => _localizedValues[locale.languageCode]?['unknownError'] ?? '未知错误'; + + // ---- USB 导入 ---- + String get usbConnected => _localizedValues[locale.languageCode]?['usbConnected'] ?? 'U盘已连接'; + String get mountPath => _localizedValues[locale.languageCode]?['mountPath'] ?? '挂载路径'; + String get reDetect => _localizedValues[locale.languageCode]?['reDetect'] ?? '重新检测'; + String get usageInstructions => _localizedValues[locale.languageCode]?['usageInstructions'] ?? '使用说明'; + String get usbUsageStep1 => _localizedValues[locale.languageCode]?['usbUsageStep1'] ?? '将程序文件 (.json) 放入 U盘根目录的 programs 文件夹'; + String get usbUsageStep2 => _localizedValues[locale.languageCode]?['usbUsageStep2'] ?? '插入 U盘后点击"重新检测"'; + String get usbUsageStep3 => _localizedValues[locale.languageCode]?['usbUsageStep3'] ?? '检测成功后点击"导入程序"加载程序列表'; + String get usbPathInvalid => _localizedValues[locale.languageCode]?['usbPathInvalid'] ?? 'U盘路径无效'; + + // ---- 通用 ---- + String get back => _localizedValues[locale.languageCode]?['back'] ?? '返回'; + String get totalProgress => _localizedValues[locale.languageCode]?['totalProgress'] ?? '总进度'; + String get appTitle => _localizedValues[locale.languageCode]?['appTitle'] ?? '污水毒品快检一体机'; + + // ---- 参数化方法 ---- + String get stepUnit => _localizedValues[locale.languageCode]?['stepUnit'] ?? '步'; + String speedLevelValue(int speed) => + locale.languageCode == 'en' + ? '$speed level' + : '$speed 档'; + String stepLabel(int stepNo) => + locale.languageCode == 'en' + ? 'Step $stepNo' + : '步骤 $stepNo'; + String stepsCountLabel(int count) => + locale.languageCode == 'en' + ? '$count steps' + : '$count 步'; + static final Map> _localizedValues = { 'zh': { 'deviceName': '污水毒品前处理一体机', @@ -233,6 +354,83 @@ class AppLocalizations { 'latestVersion': '已是最新版本', 'updateAvailable': '有新版本可用', 'checkUpdate': '检查更新', + 'status': '状态', + 'operation': '操作', + 'serialConnected': '已连接', + 'serialConnecting': '连接中', + 'serialDisconnected': '未连接', + 'serialError': '错误', + 'programRunComplete': '程序运行完成', + 'extractSample': '取出样本', + 'dropToTestCard': '滴入检测卡', + 'waitForReaction': '等待反应', + 'viewResults': '查看结果', + 'codeLabel': '编号', + 'nameLabel': '名称', + 'enterCode': '请输入编号', + 'enterName': '请输入名称', + 'enterStepName': '请输入步骤名称', + 'hintCode': '例如: P001', + 'hintProgramName': '请输入程序名称', + 'hintStepName': '例如: 混合、吸磁、吹气', + 'saveFailed': '保存失败,请检查编号是否重复', + 'saved': '已保存', + 'stepUpdated': '步骤已更新', + 'add': '添加', + 'reset': '重置', + 'confirmNewPassword': '确认新密码', + 'enterNewPassword': '再次输入新密码', + 'enterCurrentPassword': '请输入当前密码', + 'newPwdMinLength': '新密码至少6位字符', + 'defaultPassword': '默认密码为 123456', + 'deleteConfirmSingle': '确定要删除此程序吗?', + 'deleteStepConfirmSingle': '确定要删除此步骤吗?', + 'cannotReadFile': '无法读取文件', + 'noValidProgramData': 'Excel 中无有效程序数据', + 'templateSaved': '模板已保存', + 'generateTemplateFailed': '生成模板失败', + 'importingPrograms': '正在导入程序...', + 'languageSettings': '语言设置', + 'switchLanguageEffect': '切换语言后立即生效', + 'serialStatus': '串口状态', + 'availableSerialDevices': '可用串口设备', + 'refresh': '刷新', + 'noSerialDevice': '未检测到 USB 串口设备', + 'serialDeviceHint': '请确认下位机已上电并通过 USB 接入设备', + 'scanFailed': '扫描设备失败', + 'selectSerialFirst': '请先选择串口设备', + 'connectSuccess': '连接成功', + 'connectFailed': '连接失败', + 'disconnected': '已断开串口', + 'connectFirst': '请先连接串口', + 'sendTestFrame': '发送测试帧', + 'serialParams': '串口参数', + 'baudRate': '波特率', + 'dataBits': '数据位', + 'stopBits': '停止位', + 'parity': '校验位', + 'parityNone': '无', + 'parityOdd': '奇', + 'parityEven': '偶', + 'parityMark': '标记', + 'paritySpace': '空', + 'flowControl': '流控', + 'autoSaveParams': '参数修改后自动保存', + 'connect': '连接', + 'disconnect': '断开', + 'unknownError': '未知错误', + 'usbConnected': 'U盘已连接', + 'mountPath': '挂载路径', + 'reDetect': '重新检测', + 'usageInstructions': '使用说明', + 'usbUsageStep1': '将程序文件 (.json) 放入 U盘根目录的 programs 文件夹', + 'usbUsageStep2': '插入 U盘后点击"重新检测"', + 'usbUsageStep3': '检测成功后点击"导入程序"加载程序列表', + 'usbPathInvalid': 'U盘路径无效', + 'back': '返回', + 'totalProgress': '总进度', + 'appTitle': '污水毒品快检一体机', + 'stepUnit': '步', }, 'en': { 'deviceName': 'Wastewater Drug Pretreatment System', @@ -336,6 +534,83 @@ class AppLocalizations { 'latestVersion': 'Already latest version', 'updateAvailable': 'Update available', 'checkUpdate': 'Check Update', + 'status': 'Status', + 'operation': 'Actions', + 'serialConnected': 'Connected', + 'serialConnecting': 'Connecting', + 'serialDisconnected': 'Disconnected', + 'serialError': 'Error', + 'programRunComplete': 'Program Run Complete', + 'extractSample': 'Extract Sample', + 'dropToTestCard': 'Drop to Test Card', + 'waitForReaction': 'Wait for Reaction', + 'viewResults': 'View Results', + 'codeLabel': 'Code', + 'nameLabel': 'Name', + 'enterCode': 'Please enter code', + 'enterName': 'Please enter name', + 'enterStepName': 'Please enter step name', + 'hintCode': 'e.g. P001', + 'hintProgramName': 'Enter program name', + 'hintStepName': 'e.g. Mix, Magnet, Blow', + 'saveFailed': 'Save failed, please check if code is duplicate', + 'saved': 'Saved', + 'stepUpdated': 'Step updated', + 'add': 'Add', + 'reset': 'Reset', + 'confirmNewPassword': 'Confirm New Password', + 'enterNewPassword': 'Re-enter new password', + 'enterCurrentPassword': 'Enter current password', + 'newPwdMinLength': 'New password at least 6 characters', + 'defaultPassword': 'Default password is 123456', + 'deleteConfirmSingle': 'Are you sure to delete this program?', + 'deleteStepConfirmSingle': 'Are you sure to delete this step?', + 'cannotReadFile': 'Cannot read file', + 'noValidProgramData': 'No valid program data in Excel', + 'templateSaved': 'Template saved', + 'generateTemplateFailed': 'Failed to generate template', + 'importingPrograms': 'Importing programs...', + 'languageSettings': 'Language Settings', + 'switchLanguageEffect': 'Language change takes effect immediately', + 'serialStatus': 'Serial Status', + 'availableSerialDevices': 'Available Serial Devices', + 'refresh': 'Refresh', + 'noSerialDevice': 'No USB serial device detected', + 'serialDeviceHint': 'Please ensure the device is powered on and connected via USB', + 'scanFailed': 'Device scan failed', + 'selectSerialFirst': 'Please select a serial device first', + 'connectSuccess': 'Connected successfully', + 'connectFailed': 'Connection failed', + 'disconnected': 'Serial disconnected', + 'connectFirst': 'Please connect serial first', + 'sendTestFrame': 'Send Test Frame', + 'serialParams': 'Serial Parameters', + 'baudRate': 'Baud Rate', + 'dataBits': 'Data Bits', + 'stopBits': 'Stop Bits', + 'parity': 'Parity', + 'parityNone': 'None', + 'parityOdd': 'Odd', + 'parityEven': 'Even', + 'parityMark': 'Mark', + 'paritySpace': 'Space', + 'flowControl': 'Flow Control', + 'autoSaveParams': 'Parameters auto-saved after change', + 'connect': 'Connect', + 'disconnect': 'Disconnect', + 'unknownError': 'Unknown error', + 'usbConnected': 'USB Connected', + 'mountPath': 'Mount Path', + 'reDetect': 'Re-detect', + 'usageInstructions': 'Usage Instructions', + 'usbUsageStep1': 'Place program files (.json) in the programs folder on USB root', + 'usbUsageStep2': 'Insert USB then click "Re-detect"', + 'usbUsageStep3': 'After detection, click "Import Programs" to load', + 'usbPathInvalid': 'USB path invalid', + 'back': 'Back', + 'totalProgress': 'Total Progress', + 'appTitle': 'Wastewater Drug Detection System', + 'stepUnit': 'steps', }, }; } diff --git a/lib/features/device/providers/run_state_provider.dart b/lib/features/device/providers/run_state_provider.dart index b550e01..da6fbd5 100644 --- a/lib/features/device/providers/run_state_provider.dart +++ b/lib/features/device/providers/run_state_provider.dart @@ -103,7 +103,7 @@ class RunStateNotifier extends StateNotifier { if (steps.isEmpty) { state = state.copyWith( status: RunStatus.error, - errorMessage: '程序步骤为空', + errorMessage: 'PROGRAM_STEPS_EMPTY', ); return; } diff --git a/lib/features/home/pages/complete_page.dart b/lib/features/home/pages/complete_page.dart index 09e0482..55b3388 100644 --- a/lib/features/home/pages/complete_page.dart +++ b/lib/features/home/pages/complete_page.dart @@ -89,7 +89,7 @@ class CompletePage extends ConsumerWidget { // 标题 Text( - l10n?.runComplete ?? '程序运行完成', + l10n?.programRunComplete ?? '程序运行完成', style: TextStyle( color: AppTheme.textPrimary, fontSize: 24, @@ -121,7 +121,7 @@ class CompletePage extends ConsumerWidget { const SizedBox(height: 20), // 操作示意图 - _buildOperationGuide(), + _buildOperationGuide(context), const SizedBox(height: 24), @@ -167,7 +167,8 @@ class CompletePage extends ConsumerWidget { } /// 操作指引示意图 - Widget _buildOperationGuide() { + Widget _buildOperationGuide(BuildContext context) { + final l10n = AppLocalizations.of(context); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -178,7 +179,7 @@ class CompletePage extends ConsumerWidget { child: Column( children: [ Text( - '操作步骤', + l10n?.operationSteps ?? '操作步骤', style: TextStyle( color: AppTheme.textPrimary, fontSize: 14, @@ -189,10 +190,10 @@ class CompletePage extends ConsumerWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _buildStepItem(1, '取出样本', Icons.science), - _buildStepItem(2, '滴入检测卡', Icons.water_drop), - _buildStepItem(3, '等待反应', Icons.timer), - _buildStepItem(4, '查看结果', Icons.visibility), + _buildStepItem(1, l10n?.extractSample ?? '取出样本', Icons.science), + _buildStepItem(2, l10n?.dropToTestCard ?? '滴入检测卡', Icons.water_drop), + _buildStepItem(3, l10n?.waitForReaction ?? '等待反应', Icons.timer), + _buildStepItem(4, l10n?.viewResults ?? '查看结果', Icons.visibility), ], ), ], diff --git a/lib/features/home/widgets/program_list.dart b/lib/features/home/widgets/program_list.dart index c6c9fb5..639e220 100644 --- a/lib/features/home/widgets/program_list.dart +++ b/lib/features/home/widgets/program_list.dart @@ -102,6 +102,7 @@ class _ProgramCard extends StatelessWidget { final dateFormat = DateFormat('yyyy-MM-dd HH:mm'); final createdAt = _parseDate(program.createdAt); + final l10n = AppLocalizations.of(context); return Material( color: Colors.transparent, child: InkWell( @@ -168,7 +169,9 @@ class _ProgramCard extends StatelessWidget { borderRadius: BorderRadius.circular(4), ), child: Text( - program.status == 1 ? '启用' : '停用', + program.status == 1 + ? (l10n?.enabled ?? '启用') + : (l10n?.disabled ?? '停用'), style: TextStyle( color: program.status == 1 ? AppTheme.statusRunning diff --git a/lib/features/home/widgets/status_bar.dart b/lib/features/home/widgets/status_bar.dart index f3ab31c..9cda941 100644 --- a/lib/features/home/widgets/status_bar.dart +++ b/lib/features/home/widgets/status_bar.dart @@ -263,13 +263,14 @@ class _SerialConnectionIndicator extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final connected = state == SerialConnectionState.connected; final connecting = state == SerialConnectionState.connecting; final text = connected - ? '已连接' + ? (l10n?.serialConnected ?? '已连接') : connecting - ? '连接中' - : '未连接'; + ? (l10n?.serialConnecting ?? '连接中') + : (l10n?.serialDisconnected ?? '未连接'); final color = connected ? AppTheme.statusRunning : connecting diff --git a/lib/features/program_detail/pages/program_detail_page.dart b/lib/features/program_detail/pages/program_detail_page.dart index 0a4816e..e2f5b1b 100644 --- a/lib/features/program_detail/pages/program_detail_page.dart +++ b/lib/features/program_detail/pages/program_detail_page.dart @@ -107,7 +107,7 @@ class _ProgramDetailPageState extends ConsumerState { if (success) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('步骤已更新'), + content: Text(l10n?.stepUpdated ?? '步骤已更新'), backgroundColor: AppTheme.successColor, ), ); @@ -125,7 +125,7 @@ class _ProgramDetailPageState extends ConsumerState { ), const SizedBox(height: 16), Text( - '请选择或添加步骤', + l10n?.selectStepFirst ?? '请选择或添加步骤', style: TextStyle( color: AppTheme.textSecondary, fontSize: 16, @@ -164,7 +164,7 @@ class _ProgramDetailPageState extends ConsumerState { children: [ IconButton( icon: const Icon(Icons.arrow_back), - tooltip: l10n?.backToHome ?? '返回', + tooltip: l10n?.back ?? '返回', onPressed: () => context.go('/?tab=1'), ), const SizedBox(width: 8), @@ -184,7 +184,7 @@ class _ProgramDetailPageState extends ConsumerState { onPressed: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('已保存'), + content: Text(l10n?.saved ?? '已保存'), backgroundColor: AppTheme.successColor, ), ); diff --git a/lib/features/program_detail/widgets/step_form.dart b/lib/features/program_detail/widgets/step_form.dart index de13caa..f651f0d 100644 --- a/lib/features/program_detail/widgets/step_form.dart +++ b/lib/features/program_detail/widgets/step_form.dart @@ -72,7 +72,7 @@ class _StepFormState extends State { children: [ // 标题 Text( - widget.isNew ? '添加步骤' : '编辑步骤', + widget.isNew ? (l10n?.addStep ?? '添加步骤') : (l10n?.editStep ?? '编辑步骤'), style: TextStyle( color: AppTheme.textPrimary, fontSize: 18, @@ -86,12 +86,12 @@ class _StepFormState extends State { controller: _nameController, decoration: InputDecoration( labelText: l10n?.stepName ?? '步骤名称', - hintText: '例如: 混合、吸磁、吹气', + hintText: l10n?.hintStepName ?? '例如: 混合、吸磁、吹气', border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), ), validator: (value) { if (value == null || value.trim().isEmpty) { - return '请输入步骤名称'; + return l10n?.enterStepName ?? '请输入步骤名称'; } return null; }, @@ -180,7 +180,7 @@ class _StepFormState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '${l10n?.speed ?? '速度'}: $_speed 档', + '${l10n?.speed ?? '速度'}: ${l10n?.speedLevelValue(_speed) ?? '$_speed 档'}', style: TextStyle(color: AppTheme.textPrimary), ), Slider( diff --git a/lib/features/program_detail/widgets/step_list.dart b/lib/features/program_detail/widgets/step_list.dart index 3286dbc..c9121c2 100644 --- a/lib/features/program_detail/widgets/step_list.dart +++ b/lib/features/program_detail/widgets/step_list.dart @@ -53,7 +53,7 @@ class _StepListState extends State { Icon(Icons.list, color: AppTheme.primaryColor, size: 20), const SizedBox(width: 12), Text( - '步骤列表', + l10n?.stepList ?? '步骤列表', style: TextStyle( color: AppTheme.primaryColor, fontWeight: FontWeight.w600, @@ -61,7 +61,7 @@ class _StepListState extends State { ), const Spacer(), Text( - '${widget.steps.length} 步', + l10n?.stepsCountLabel(widget.steps.length) ?? '${widget.steps.length} 步', style: TextStyle( color: AppTheme.textSecondary, fontSize: 12, @@ -114,7 +114,7 @@ class _StepListState extends State { children: [ Icon(Icons.add_circle_outline, size: 48, color: AppTheme.idleColor), const SizedBox(height: 12), - Text('暂无步骤', style: TextStyle(color: AppTheme.textSecondary)), + Text(l10n?.noSteps ?? '暂无步骤', style: TextStyle(color: AppTheme.textSecondary)), ], ), ) @@ -149,7 +149,7 @@ class _StepListState extends State { children: [ // 添加按钮 CommonButton( - text: '添加', + text: l10n?.add ?? '添加', icon: Icons.add, type: ButtonType.primary, onPressed: widget.onAddStep, @@ -158,7 +158,7 @@ class _StepListState extends State { // 删除按钮 if (_selectedIds.isNotEmpty) CommonButton( - text: '删除', + text: l10n?.delete ?? '删除', icon: Icons.delete, type: ButtonType.danger, onPressed: () => _showDeleteConfirmDialog(context), @@ -241,8 +241,8 @@ class _StepListState extends State { title: Text(l10n?.confirm ?? '确认'), content: Text( _selectedIds.length == 1 - ? '确定要删除此步骤吗?' - : '确定要删除选中的 ${_selectedIds.length} 个步骤吗?', + ? l10n?.deleteStepConfirmSingle ?? '确定要删除此步骤吗?' + : l10n?.deleteStepConfirmMultiple(_selectedIds.length) ?? '确定要删除选中的 ${_selectedIds.length} 个步骤吗?', ), actions: [ TextButton( diff --git a/lib/features/programs/pages/programs_page.dart b/lib/features/programs/pages/programs_page.dart index 1c070b4..6a681aa 100644 --- a/lib/features/programs/pages/programs_page.dart +++ b/lib/features/programs/pages/programs_page.dart @@ -259,7 +259,7 @@ class _ProgramsPageState extends ConsumerState { SizedBox( width: 80, child: Text( - '状态', + l10n?.status ?? '状态', style: TextStyle( fontWeight: FontWeight.w600, color: AppTheme.textPrimary, @@ -358,7 +358,7 @@ class _ProgramsPageState extends ConsumerState { borderRadius: BorderRadius.circular(4), ), child: Text( - program.status == 1 ? '启用' : '停用', + program.status == 1 ? (l10n?.enabled ?? '启用') : (l10n?.disabled ?? '停用'), style: TextStyle( color: program.status == 1 ? AppTheme.successColor @@ -413,6 +413,7 @@ class _ProgramsPageState extends ConsumerState { /// 导入程序 Future _importPrograms(BuildContext context, WidgetRef ref) async { + final l10n = AppLocalizations.of(context); try { // 选择文件 final result = await FilePicker.platform.pickFiles( @@ -428,7 +429,7 @@ class _ProgramsPageState extends ConsumerState { final file = result.files.first; if (file.path == null) { if (!context.mounted) return; - ToastService.showError(context, '无法读取文件'); + ToastService.showError(context, l10n?.cannotReadFile ?? '无法读取文件'); return; } @@ -441,25 +442,26 @@ class _ProgramsPageState extends ConsumerState { if (!context.mounted) return; if (importedCount > 0) { - ToastService.showSuccess(context, '成功处理 $importedCount 个程序'); + ToastService.showSuccess(context, l10n?.processedPrograms(importedCount) ?? '成功处理 $importedCount 个程序'); } else { - ToastService.showWarning(context, 'Excel 中无有效程序数据'); + ToastService.showWarning(context, l10n?.noValidProgramData ?? 'Excel 中无有效程序数据'); } } catch (e) { if (!context.mounted) return; - ToastService.showError(context, '导入失败: ${e.toString()}'); + ToastService.showError(context, '${l10n?.importFailed ?? '导入失败'}: ${e.toString()}'); } } /// 下载 Excel 模板 Future _downloadTemplate(BuildContext context) async { + final l10n = AppLocalizations.of(context); try { final path = await ExcelTemplateService.instance.generateTemplate(); if (!context.mounted) return; - ToastService.showSuccess(context, '模板已保存: $path'); + ToastService.showSuccess(context, '${l10n?.templateSaved ?? '模板已保存'}: $path'); } catch (e) { if (!context.mounted) return; - ToastService.showError(context, '生成模板失败: ${e.toString()}'); + ToastService.showError(context, '${l10n?.generateTemplateFailed ?? '生成模板失败'}: ${e.toString()}'); } } @@ -484,8 +486,8 @@ class _ProgramsPageState extends ConsumerState { title: Text(l10n?.confirm ?? '确认'), content: Text( ids.length == 1 - ? '确定要删除此程序吗?' - : '确定要删除选中的 ${ids.length} 个程序吗?', + ? (l10n?.deleteConfirmSingle ?? '确定要删除此程序吗?') + : (l10n?.deleteConfirmMultiple(ids.length) ?? '确定要删除选中的 ${ids.length} 个程序吗?'), ), actions: [ TextButton( diff --git a/lib/features/programs/widgets/program_form_dialog.dart b/lib/features/programs/widgets/program_form_dialog.dart index 985a160..8bf1e35 100644 --- a/lib/features/programs/widgets/program_form_dialog.dart +++ b/lib/features/programs/widgets/program_form_dialog.dart @@ -71,14 +71,14 @@ class _ProgramFormDialogState extends ConsumerState { controller: _codeController, decoration: InputDecoration( labelText: l10n?.programCode ?? '编号', - hintText: '例如: P001', + hintText: l10n?.hintCode ?? '例如: P001', border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), validator: (value) { if (value == null || value.trim().isEmpty) { - return '请输入编号'; + return l10n?.enterCode ?? '请输入编号'; } return null; }, @@ -90,14 +90,14 @@ class _ProgramFormDialogState extends ConsumerState { controller: _nameController, decoration: InputDecoration( labelText: l10n?.programName ?? '名称', - hintText: '请输入程序名称', + hintText: l10n?.hintProgramName ?? '请输入程序名称', border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), validator: (value) { if (value == null || value.trim().isEmpty) { - return '请输入名称'; + return l10n?.enterName ?? '请输入名称'; } return null; }, @@ -140,7 +140,7 @@ class _ProgramFormDialogState extends ConsumerState { Row( children: [ Text( - '状态', + l10n?.status ?? '状态', style: TextStyle(color: AppTheme.textPrimary), ), const Spacer(), @@ -154,7 +154,7 @@ class _ProgramFormDialogState extends ConsumerState { activeColor: AppTheme.successColor, ), Text( - _isEnabled ? '启用' : '停用', + _isEnabled ? (l10n?.enabled ?? '启用') : (l10n?.disabled ?? '停用'), style: TextStyle( color: _isEnabled ? AppTheme.successColor : AppTheme.idleColor, ), @@ -222,7 +222,7 @@ class _ProgramFormDialogState extends ConsumerState { } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('保存失败,请检查编号是否重复'), + content: Text(l10n?.saveFailed ?? '保存失败,请检查编号是否重复'), backgroundColor: AppTheme.errorColor, ), ); diff --git a/lib/features/settings/pages/settings_page.dart b/lib/features/settings/pages/settings_page.dart index 2e46e87..534301e 100644 --- a/lib/features/settings/pages/settings_page.dart +++ b/lib/features/settings/pages/settings_page.dart @@ -116,11 +116,12 @@ class _SettingsPageState extends ConsumerState { } Widget _buildUpgradeContent() { + final l10n = AppLocalizations.of(context); return ListView( padding: EdgeInsets.zero, children: [ Text( - '软件升级', + l10n?.upgrade ?? '软件升级', style: TextStyle( color: AppTheme.textPrimary, fontSize: 18, @@ -139,7 +140,7 @@ class _SettingsPageState extends ConsumerState { Icon(Icons.info_outline, color: AppTheme.primaryColor), const SizedBox(width: 12), Text( - '当前版本: $_currentVersion', + '${l10n?.currentVersion ?? '当前版本'}: $_currentVersion', style: TextStyle(color: AppTheme.textPrimary), ), ], @@ -147,13 +148,13 @@ class _SettingsPageState extends ConsumerState { ), const SizedBox(height: 24), CommonButton( - text: '检查更新', + text: l10n?.checkUpdate ?? '检查更新', icon: Icons.refresh, type: ButtonType.primary, onPressed: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('已是最新版本'), + content: Text(l10n?.latestVersion ?? '已是最新版本'), backgroundColor: AppTheme.successColor, ), ); diff --git a/lib/features/settings/widgets/language_panel.dart b/lib/features/settings/widgets/language_panel.dart index da4bbaa..02f5fd9 100644 --- a/lib/features/settings/widgets/language_panel.dart +++ b/lib/features/settings/widgets/language_panel.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/localization/app_localizations.dart'; import '../../../core/localization/locale_provider.dart'; import '../../../core/theme/app_theme.dart'; @@ -13,6 +14,7 @@ class LanguagePanel extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context); final locale = ref.watch(localeProvider); final currentLang = locale.languageCode; @@ -20,7 +22,7 @@ class LanguagePanel extends ConsumerWidget { padding: EdgeInsets.zero, children: [ _SectionCard( - title: '语言设置', + title: l10n?.languageSettings ?? '语言设置', child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -46,7 +48,7 @@ class LanguagePanel extends ConsumerWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Text( - '切换语言后立即生效', + l10n?.switchLanguageEffect ?? '切换语言后立即生效', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12), ), ), diff --git a/lib/features/settings/widgets/password_panel.dart b/lib/features/settings/widgets/password_panel.dart index 2fa78c1..ad9d0fa 100644 --- a/lib/features/settings/widgets/password_panel.dart +++ b/lib/features/settings/widgets/password_panel.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/localization/app_localizations.dart'; import '../../../core/theme/app_theme.dart'; import '../../../shared/services/toast_service.dart'; import '../services/settings_service.dart'; @@ -33,20 +34,21 @@ class _PasswordPanelState extends ConsumerState { Future _submit() async { if (_submitting) return; + final l10n = AppLocalizations.of(context); final oldPwd = _oldCtrl.text.trim(); final newPwd = _newCtrl.text.trim(); final confirmPwd = _confirmCtrl.text.trim(); if (oldPwd.isEmpty || newPwd.isEmpty || confirmPwd.isEmpty) { - setState(() => _error = '请填写所有字段'); + setState(() => _error = l10n?.fillAllFields ?? '请填写所有字段'); return; } if (newPwd.length < 6) { - setState(() => _error = '新密码至少6位字符'); + setState(() => _error = l10n?.newPwdMinLength ?? '新密码至少6位字符'); return; } if (newPwd != confirmPwd) { - setState(() => _error = '两次输入的新密码不一致'); + setState(() => _error = l10n?.passwordMismatch ?? '两次输入的新密码不一致'); return; } @@ -60,7 +62,7 @@ class _PasswordPanelState extends ConsumerState { if (!mounted) return; setState(() { _submitting = false; - _error = '原密码错误'; + _error = l10n?.oldPasswordError ?? '原密码错误'; }); return; } @@ -73,14 +75,15 @@ class _PasswordPanelState extends ConsumerState { _oldCtrl.clear(); _newCtrl.clear(); _confirmCtrl.clear(); - ToastService.showSuccess(context, '密码已修改'); + ToastService.showSuccess(context, l10n?.passwordChanged ?? '密码已修改'); } else { - ToastService.showError(context, '密码修改失败'); + ToastService.showError(context, l10n?.passwordChangeFailed ?? '密码修改失败'); } } @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); return ListView( padding: EdgeInsets.zero, children: [ @@ -95,7 +98,7 @@ class _PasswordPanelState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( - '密码修改', + l10n?.password ?? '密码修改', style: TextStyle( color: AppTheme.textPrimary, fontSize: 16, @@ -105,20 +108,20 @@ class _PasswordPanelState extends ConsumerState { const Divider(), _field( controller: _oldCtrl, - label: '原密码', - hint: '请输入当前密码', + label: l10n?.oldPassword ?? '原密码', + hint: l10n?.enterCurrentPassword ?? '请输入当前密码', ), const SizedBox(height: 16), _field( controller: _newCtrl, - label: '新密码', - hint: '至少6位字符', + label: l10n?.newPassword ?? '新密码', + hint: l10n?.passwordMinLength ?? '至少6位字符', ), const SizedBox(height: 16), _field( controller: _confirmCtrl, - label: '确认新密码', - hint: '再次输入新密码', + label: l10n?.confirmNewPassword ?? '确认新密码', + hint: l10n?.enterNewPassword ?? '再次输入新密码', ), if (_error != null) ...[ const SizedBox(height: 12), @@ -140,7 +143,7 @@ class _PasswordPanelState extends ConsumerState { _confirmCtrl.clear(); setState(() => _error = null); }, - child: const Text('重置'), + child: Text(l10n?.reset ?? '重置'), ), const SizedBox(width: 12), ElevatedButton.icon( @@ -155,7 +158,7 @@ class _PasswordPanelState extends ConsumerState { ), ) : const Icon(Icons.check, size: 18), - label: const Text('确认'), + label: Text(l10n?.confirm ?? '确认'), ), ], ), @@ -166,7 +169,7 @@ class _PasswordPanelState extends ConsumerState { Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Text( - '默认密码为 123456', + l10n?.defaultPassword ?? '默认密码为 123456', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12), ), ), diff --git a/lib/features/settings/widgets/serial_config_panel.dart b/lib/features/settings/widgets/serial_config_panel.dart index cab8d01..11dfb94 100644 --- a/lib/features/settings/widgets/serial_config_panel.dart +++ b/lib/features/settings/widgets/serial_config_panel.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:usb_serial/usb_serial.dart'; +import '../../../core/localization/app_localizations.dart'; import '../../../core/theme/app_theme.dart'; import '../../device/models/serial_config.dart'; import '../../device/providers/serial_provider.dart'; @@ -48,14 +49,14 @@ class _SerialConfigPanelState extends ConsumerState { } catch (e) { if (!mounted) return; setState(() => _loadingDevices = false); - _showSnack('扫描设备失败: $e', AppTheme.errorColor); + _showSnack('${AppLocalizations.of(context)?.scanFailed ?? '扫描设备失败'}: $e', AppTheme.errorColor); } } Future _connect() async { final device = _selectedDevice; if (device == null) { - _showSnack('请先选择串口设备', AppTheme.warningColor); + _showSnack(AppLocalizations.of(context)?.selectSerialFirst ?? '请先选择串口设备', AppTheme.warningColor); return; } setState(() => _operating = true); @@ -65,7 +66,7 @@ class _SerialConfigPanelState extends ConsumerState { if (!mounted) return; setState(() => _operating = false); _showSnack( - ok ? '连接成功' : '连接失败: ${service.lastError ?? "未知错误"}', + ok ? (AppLocalizations.of(context)?.connectSuccess ?? '连接成功') : ('${AppLocalizations.of(context)?.connectFailed ?? '连接失败'}: ${service.lastError ?? (AppLocalizations.of(context)?.unknownError ?? '未知错误')}'), ok ? AppTheme.successColor : AppTheme.errorColor, ); } @@ -76,13 +77,13 @@ class _SerialConfigPanelState extends ConsumerState { await service.disconnect(); if (!mounted) return; setState(() => _operating = false); - _showSnack('已断开串口', AppTheme.infoColor); + _showSnack(AppLocalizations.of(context)?.disconnected ?? '已断开串口', AppTheme.infoColor); } Future _testConnection() async { final service = ref.read(serialPortServiceProvider); if (!service.isConnected) { - _showSnack('请先连接串口', AppTheme.warningColor); + _showSnack(AppLocalizations.of(context)?.connectFirst ?? '请先连接串口', AppTheme.warningColor); return; } final msgService = ref.read(deviceMessageServiceProvider); @@ -94,7 +95,7 @@ class _SerialConfigPanelState extends ConsumerState { )); if (!mounted) return; _showSnack( - ok ? '已发送 device_info 查询' : '发送失败', + ok ? (AppLocalizations.of(context)?.sendTestFrame ?? '已发送测试查询') : (AppLocalizations.of(context)?.connectFailed ?? '发送失败'), ok ? AppTheme.successColor : AppTheme.errorColor, ); } @@ -107,25 +108,26 @@ class _SerialConfigPanelState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final config = ref.watch(serialConfigProvider); final state = ref.watch(serialPortServiceProvider).state; return ListView( padding: const EdgeInsets.all(0), children: [ - _buildStatusCard(state), + _buildStatusCard(state, l10n), const SizedBox(height: 16), - _buildDeviceCard(), + _buildDeviceCard(l10n), const SizedBox(height: 16), - _buildParamCard(config), + _buildParamCard(config, l10n), const SizedBox(height: 16), - _buildActionRow(), + _buildActionRow(l10n), ], ); } // -- 状态卡 ---------------------------------------------------------- - Widget _buildStatusCard(SerialConnectionState state) { + Widget _buildStatusCard(SerialConnectionState state, AppLocalizations? l10n) { final color = switch (state) { SerialConnectionState.connected => AppTheme.successColor, SerialConnectionState.connecting => AppTheme.warningColor, @@ -133,10 +135,10 @@ class _SerialConfigPanelState extends ConsumerState { SerialConnectionState.disconnected => AppTheme.idleColor, }; final text = switch (state) { - SerialConnectionState.connected => '已连接', - SerialConnectionState.connecting => '连接中...', - SerialConnectionState.error => '错误', - SerialConnectionState.disconnected => '未连接', + SerialConnectionState.connected => l10n?.serialConnected ?? '已连接', + SerialConnectionState.connecting => l10n?.serialConnecting ?? '连接中...', + SerialConnectionState.error => l10n?.serialError ?? '错误', + SerialConnectionState.disconnected => l10n?.serialDisconnected ?? '未连接', }; return Container( padding: const EdgeInsets.all(16), @@ -153,7 +155,7 @@ class _SerialConfigPanelState extends ConsumerState { decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 12), - Text('串口状态: $text', + Text('${l10n?.serialStatus ?? '串口状态'}: $text', style: TextStyle( color: AppTheme.textPrimary, fontSize: 16, @@ -166,13 +168,13 @@ class _SerialConfigPanelState extends ConsumerState { // -- 设备列表 -------------------------------------------------------- - Widget _buildDeviceCard() { + Widget _buildDeviceCard(AppLocalizations? l10n) { return _SectionCard( - title: '可用串口设备', + title: l10n?.availableSerialDevices ?? '可用串口设备', trailing: TextButton.icon( onPressed: _loadingDevices ? null : _refreshDevices, icon: const Icon(Icons.refresh, size: 18), - label: const Text('刷新'), + label: Text(l10n?.refresh ?? '刷新'), ), child: _loadingDevices ? const Padding( @@ -190,16 +192,17 @@ class _SerialConfigPanelState extends ConsumerState { } Widget _buildEmptyDevice() { + final l10n = AppLocalizations.of(context); return Padding( padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 8), child: Column( children: [ Icon(Icons.usb_off, size: 40, color: AppTheme.idleColor), const SizedBox(height: 8), - Text('未检测到 USB 串口设备', + Text(l10n?.noSerialDevice ?? '未检测到 USB 串口设备', style: TextStyle(color: AppTheme.textSecondary)), const SizedBox(height: 4), - Text('请确认下位机已上电并通过 USB 接入设备', + Text(l10n?.serialDeviceHint ?? '请确认下位机已上电并通过 USB 接入设备', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12)), ], ), @@ -255,45 +258,45 @@ class _SerialConfigPanelState extends ConsumerState { // -- 参数配置 -------------------------------------------------------- - Widget _buildParamCard(SerialConfig config) { + Widget _buildParamCard(SerialConfig config, AppLocalizations? l10n) { return _SectionCard( - title: '串口参数', + title: l10n?.serialParams ?? '串口参数', child: Column( children: [ - _baudRateRow(config), + _baudRateRow(config, l10n), _dropdownRow( - label: '数据位', + label: l10n?.dataBits ?? '数据位', value: config.dataBits, options: const [5, 6, 7, 8], display: (v) => '$v', onChanged: (v) => _updateConfig((c) => c.copyWith(dataBits: v)), ), _dropdownRow( - label: '停止位', + label: l10n?.stopBits ?? '停止位', value: config.stopBits, options: const [1, 2], display: (v) => '$v', onChanged: (v) => _updateConfig((c) => c.copyWith(stopBits: v)), ), _dropdownRow( - label: '校验位', + label: l10n?.parity ?? '校验位', value: config.parity, options: SerialParity.values, display: (v) => switch (v) { - SerialParity.none => '无', - SerialParity.odd => '奇', - SerialParity.even => '偶', - SerialParity.mark => '标记', - SerialParity.space => '空', + SerialParity.none => l10n?.parityNone ?? '无', + SerialParity.odd => l10n?.parityOdd ?? '奇', + SerialParity.even => l10n?.parityEven ?? '偶', + SerialParity.mark => l10n?.parityMark ?? '标记', + SerialParity.space => l10n?.paritySpace ?? '空', }, onChanged: (v) => _updateConfig((c) => c.copyWith(parity: v)), ), _dropdownRow( - label: '流控', + label: l10n?.flowControl ?? '流控', value: config.flowControl, options: SerialFlowControl.values, display: (v) => switch (v) { - SerialFlowControl.none => '无', + SerialFlowControl.none => l10n?.parityNone ?? '无', SerialFlowControl.rtsCts => 'RTS/CTS', SerialFlowControl.xonXoff => 'XON/XOFF', SerialFlowControl.dtrDsr => 'DTR/DSR', @@ -304,7 +307,7 @@ class _SerialConfigPanelState extends ConsumerState { Align( alignment: Alignment.centerRight, child: Text( - '参数修改后自动保存', + l10n?.autoSaveParams ?? '参数修改后自动保存', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12), ), ), @@ -313,7 +316,7 @@ class _SerialConfigPanelState extends ConsumerState { ); } - Widget _baudRateRow(SerialConfig config) { + Widget _baudRateRow(SerialConfig config, AppLocalizations? l10n) { final ctrl = TextEditingController(text: config.baudRate.toString()); return Padding( padding: const EdgeInsets.symmetric(vertical: 8), @@ -322,7 +325,7 @@ class _SerialConfigPanelState extends ConsumerState { SizedBox( width: 80, child: - Text('波特率', style: TextStyle(color: AppTheme.textPrimary))), + Text(l10n?.baudRate ?? '波特率', style: TextStyle(color: AppTheme.textPrimary))), const SizedBox(width: 12), SizedBox( width: 140, @@ -425,7 +428,7 @@ class _SerialConfigPanelState extends ConsumerState { // -- 操作按钮 -------------------------------------------------------- - Widget _buildActionRow() { + Widget _buildActionRow(AppLocalizations? l10n) { return Wrap( spacing: 12, runSpacing: 12, @@ -433,19 +436,19 @@ class _SerialConfigPanelState extends ConsumerState { ElevatedButton.icon( onPressed: _operating ? null : _connect, icon: const Icon(Icons.link), - label: const Text('连接'), + label: Text(l10n?.connect ?? '连接'), ), ElevatedButton.icon( onPressed: _operating ? null : _disconnect, style: ElevatedButton.styleFrom(backgroundColor: AppTheme.errorColor), icon: const Icon(Icons.link_off), - label: const Text('断开'), + label: Text(l10n?.disconnect ?? '断开'), ), OutlinedButton.icon( onPressed: _operating ? null : _testConnection, icon: const Icon(Icons.send), - label: const Text('发送测试帧'), + label: Text(l10n?.sendTestFrame ?? '发送测试帧'), ), ], ); diff --git a/lib/features/settings/widgets/usb_import_panel.dart b/lib/features/settings/widgets/usb_import_panel.dart index e44a39e..f7ac4eb 100644 --- a/lib/features/settings/widgets/usb_import_panel.dart +++ b/lib/features/settings/widgets/usb_import_panel.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/localization/app_localizations.dart'; import '../../../core/theme/app_theme.dart'; import '../../../shared/services/toast_service.dart'; import '../services/usb_detection_service.dart'; @@ -21,18 +22,20 @@ class _UsbImportPanelState extends ConsumerState { Future _detect() async { if (_detecting) return; setState(() => _detecting = true); + final l10n = AppLocalizations.of(context); final connected = await UsbDetectionService.instance.detectUsb(); if (!mounted) return; setState(() => _detecting = false); if (connected) { - ToastService.showInfo(context, '正在检测U盘...'); + ToastService.showInfo(context, l10n?.detectingUsb ?? '正在检测U盘...'); } else { - ToastService.showWarning(context, '未检测到U盘'); + ToastService.showWarning(context, l10n?.usbNotDetected ?? '未检测到U盘'); } } @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final state = ref.watch(_usbStateProvider); final connected = state.maybeWhen( data: (s) => s.isConnected, @@ -67,7 +70,7 @@ class _UsbImportPanelState extends ConsumerState { const SizedBox(width: 12), Expanded( child: Text( - connected ? 'U盘已连接' : '未检测到U盘', + connected ? (l10n?.usbConnected ?? 'U盘已连接') : (l10n?.usbNotDetected ?? '未检测到U盘'), style: TextStyle( color: AppTheme.textPrimary, fontSize: 16, @@ -80,8 +83,8 @@ class _UsbImportPanelState extends ConsumerState { const SizedBox(height: 12), Text( connected - ? '挂载路径: ${path ?? "未知"}' - : '请插入U盘后点击"重新检测"', + ? '${l10n?.mountPath ?? '挂载路径'}: ${path ?? "?"}' + : (l10n?.insertUsb ?? '请插入U盘后重试'), style: TextStyle( color: AppTheme.textSecondary, fontSize: 13), ), @@ -98,13 +101,13 @@ class _UsbImportPanelState extends ConsumerState { child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.refresh, size: 18), - label: const Text('重新检测'), + label: Text(l10n?.reDetect ?? '重新检测'), ), const SizedBox(width: 12), ElevatedButton.icon( onPressed: connected ? () => _import(path) : null, icon: const Icon(Icons.download, size: 18), - label: const Text('导入程序'), + label: Text(l10n?.importProgram ?? '导入程序'), ), ], ), @@ -123,7 +126,7 @@ class _UsbImportPanelState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '使用说明', + l10n?.usageInstructions ?? '使用说明', style: TextStyle( color: AppTheme.textPrimary, fontSize: 14, @@ -131,9 +134,9 @@ class _UsbImportPanelState extends ConsumerState { ), ), const SizedBox(height: 8), - _bullet('将程序文件 (.json) 放入 U盘根目录的 programs 文件夹'), - _bullet('插入 U盘后点击"重新检测"'), - _bullet('检测成功后点击"导入程序"加载程序列表'), + _bullet(l10n?.usbUsageStep1 ?? '将程序文件 (.json) 放入 U盘根目录的 programs 文件夹'), + _bullet(l10n?.usbUsageStep2 ?? '插入 U盘后点击"重新检测"'), + _bullet(l10n?.usbUsageStep3 ?? '检测成功后点击"导入程序"加载程序列表'), ], ), ), @@ -142,11 +145,12 @@ class _UsbImportPanelState extends ConsumerState { } void _import(String? path) { + final l10n = AppLocalizations.of(context); if (path == null) { - ToastService.showWarning(context, 'U盘路径无效'); + ToastService.showWarning(context, l10n?.usbPathInvalid ?? 'U盘路径无效'); return; } - ToastService.showInfo(context, '正在导入程序...'); + ToastService.showInfo(context, l10n?.importingPrograms ?? '正在导入程序...'); // TODO: 接入 program_import_service 实现真正的导入流程 } diff --git a/lib/shared/widgets/common_dialog.dart b/lib/shared/widgets/common_dialog.dart index 8ac738f..0b73255 100644 --- a/lib/shared/widgets/common_dialog.dart +++ b/lib/shared/widgets/common_dialog.dart @@ -7,8 +7,8 @@ class CommonDialog { required BuildContext context, required String title, required String content, - String confirmText = '确认', - String cancelText = '取消', + String confirmText = 'Confirm', + String cancelText = 'Cancel', bool isDestructive = false, }) { return showDialog( @@ -38,7 +38,7 @@ class CommonDialog { required BuildContext context, required String title, required String content, - String confirmText = '确认', + String confirmText = 'Confirm', }) { return showDialog( context: context, @@ -61,8 +61,8 @@ class CommonDialog { required String title, String? hintText, String? initialValue, - String confirmText = '确认', - String cancelText = '取消', + String confirmText = 'Confirm', + String cancelText = 'Cancel', }) { final controller = TextEditingController(text: initialValue); diff --git a/openspec/changes/fix-hardcoded-chinese-i18n/.openspec.yaml b/openspec/changes/fix-hardcoded-chinese-i18n/.openspec.yaml new file mode 100644 index 0000000..8fe2055 --- /dev/null +++ b/openspec/changes/fix-hardcoded-chinese-i18n/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-12 diff --git a/openspec/changes/fix-hardcoded-chinese-i18n/design.md b/openspec/changes/fix-hardcoded-chinese-i18n/design.md new file mode 100644 index 0000000..2cc8696 --- /dev/null +++ b/openspec/changes/fix-hardcoded-chinese-i18n/design.md @@ -0,0 +1,103 @@ +## Context + +项目已使用手写的 `AppLocalizations` 类(`lib/core/localization/app_localizations.dart`),通过 `_localizedValues` 嵌套 Map 存储中英文翻译,并由 `LocaleProvider` 控制当前语言。基础设施完备,但实际 UI 代码大量使用中文字面量,覆盖率不足。 + +i18n 审计结果: +- 53 个 `.dart` 文件包含中文字符 +- 约 456 处硬编码字符串 +- 现有 `AppLocalizations` 已定义约 100 个键,但许多 UI 文本(如 SnackBar、Dialog、表单标签、占位符、错误消息)仍未通过翻译函数获取 + +## Goals / Non-Goals + +**Goals:** +- 用户可见的所有 UI 文本均通过 `AppLocalizations` 获取 +- 切换语言后所有页面即时刷新 +- 翻译键命名统一、易于维护 +- 保留现有手写翻译方案,避免引入新依赖(如 `intl_utils`、`slang`) + +**Non-Goals:** +- 不改造日志/调试输出中的中文(仅面向开发者) +- 不本地化数据库种子数据(属于业务数据,由用户编辑) +- 不引入 `.arb` 文件 + 代码生成方案 +- 不新增除中英文之外的第三种语言(架构需支持,但本次不实现) + +## Decisions + +### 决策 1:沿用手写 `AppLocalizations`,不迁移到 `flutter gen-l10n` + +**选择**:保留现有 `Map>` 结构。 + +**理由**: +- 项目已具备完整的代理(`Delegate`)和 `LocaleProvider` 设置,迁移成本高 +- 翻译规模有限(预估最终 250 个左右键),手写可维护 +- 避免引入 `build_runner` 代码生成步骤 + +**已考虑替代方案**: +- `flutter_localizations + .arb` 文件:标准方案但需引入代码生成,对小项目过度 +- `slang` 包:类型安全但需新增依赖和构建步骤 + +### 决策 2:参数化文本统一使用方法形式 + +**选择**:将带参数的翻译从「getter + 拼接」改为「方法返回完整字符串」。 + +示例: +```dart +// 旧(错误) +String get importSuccess => '成功导入'; +String get programsImported => '个程序'; +// 调用:'${l.importSuccess} $count ${l.programsImported}' + +// 新(正确) +String importedPrograms(int count) => + locale.languageCode == 'en' + ? 'Successfully imported $count programs' + : '成功导入 $count 个程序'; +``` + +**理由**:英文语序与中文不同,拼接会产生不自然的文本(如 "Successfully imported 5 programs" 才正确,而非 "Successfully imported 5 programs")。 + +### 决策 3:分模块按 feature 推进 + +将替换工作按 feature 拆分,每个 feature 一个原子提交: +1. `core` 模块(router、theme) +2. `features/home` +3. `features/programs` +4. `features/program_detail` +5. `features/device`(仅 UI 层,service 层错误消息走错误码) +6. `features/settings` +7. `shared/widgets` + +每个 feature 完成后运行 `flutter analyze` + 手动切换语言验证。 + +### 决策 4:Service 层错误使用错误码而非翻译 + +`SerialPortService`、`DeviceMessageService` 等返回 `Exception` 时使用稳定的英文错误码或自定义枚举,由 UI 层调用 `AppLocalizations` 的方法转换为本地化消息。 + +**理由**:Service 层无法持有 `BuildContext`,强行注入 `Locale` 会污染领域层职责。 + +### 决策 5:新增翻译键的命名约定 + +- 采用 `camelCase` +- 按模块前缀分组:`home.*`、`program.*`、`step.*`、`device.*`、`settings.*`、`common.*` +- 但为了兼容现有键,新增键先采用相同的平铺命名风格,待整体替换完成后视情况重构 + +## Risks / Trade-offs + +- **[风险] 漏改字符串**:456 处替换易遗漏 → **缓解**:完成后用 grep `[一-鿿]` 全量扫描 `lib/features/**/*.dart`、`lib/shared/**/*.dart`,确保 UI 层无中文字面量 +- **[风险] 英文翻译质量参差不齐**:开发者非母语翻译 → **缓解**:保持术语表统一(device、program、step、position、volume 等),由审阅时统一校对 +- **[风险] 现有 Dialog/Snackbar 使用了拼接字符串**:英文语序错乱 → **缓解**:通过参数化方法重构(决策 2) +- **[风险] 修改面广,潜在 UI 回归**:可能误改逻辑代码 → **缓解**:严格只替换字符串字面量,每个 feature 完成后人工运行验证 +- **[权衡] 不引入代码生成**:可读性 vs. 类型安全 → 接受手写翻译表的维护成本以换取零构建步骤 + +## Migration Plan + +1. **准备**:扩充 `AppLocalizations` 翻译表,补齐所有需要的新键(中英文) +2. **逐模块替换**:按决策 3 顺序,每个 feature 一次提交 +3. **回归验证**:每次提交后启动应用,切换中英文,确认无遗漏文本 +4. **最终扫描**:grep 全量检查 `lib/features/`、`lib/shared/`、`lib/core/router/`、`lib/core/theme/` 中是否仍有中文字面量(排除注释) +5. **回滚策略**:每个 feature 独立提交,可单独 revert + +## Open Questions + +- Service 层抛出的 `Exception` 是否需要定义统一的错误码枚举?(待实施时根据现有错误类型决定) +- 程序名称(用户在数据库中输入的中文)是否需要支持英文模式下显示英文别名?(**默认否**,用户数据原样展示) diff --git a/openspec/changes/fix-hardcoded-chinese-i18n/proposal.md b/openspec/changes/fix-hardcoded-chinese-i18n/proposal.md new file mode 100644 index 0000000..7281f17 --- /dev/null +++ b/openspec/changes/fix-hardcoded-chinese-i18n/proposal.md @@ -0,0 +1,26 @@ +## Why + +当前应用中大量中文字符串直接硬编码在 Dart 文件中(约 456 处,跨 53 个文件),导致:(1)无法切换为英文界面;(2)新增语言时需要逐个文件查找修改;(3)维护成本高且容易遗漏。项目虽已有 `AppLocalizations` 基础设施,但覆盖率不足,多数 UI 文本仍为字面量中文。 + +## What Changes + +- 扫描 `lib/` 下所有硬编码的中文字符串,逐一替换为 `AppLocalizations.of(context)` 调用 +- 补充所有缺失的翻译键到 `AppLocalizations`(含中英文值) +- 优化 `AppLocalizations` 类的调用方式,消除 `BuildContext` 强依赖场景(如 Snackbar、Dialog) +- 确保运行时可动态切换语言,无需重启应用 +- **不涉及**:数据库种子数据、日志输出中的中文(不影响 UI 国际化) + +## Capabilities + +### New Capabilities +- `i18n`: 完整覆盖应用所有 UI 文本的国际化能力。支持中文(默认)和英文两种语言,所有用户可见文本均通过 `AppLocalizations.of(context)` 获取。 + +### Modified Capabilities +- (无现有 spec 需要修改) + +## Impact + +- **代码修改**:约 50+ 个 Dart 文件中的 UI 部分,将硬编码字符串替换为翻译函数的调用 +- **新增翻译键**:预估新增约 100-150 个翻译键到 `AppLocalizations` +- **无外部依赖变更**:沿用现有手写翻译系统,不引入 `intl` 或 `slang` 等第三方包 +- **无破坏性变更**:所有替换为纯文本替换,不改变 UI 结构和逻辑 \ No newline at end of file diff --git a/openspec/changes/fix-hardcoded-chinese-i18n/specs/i18n/spec.md b/openspec/changes/fix-hardcoded-chinese-i18n/specs/i18n/spec.md new file mode 100644 index 0000000..639d191 --- /dev/null +++ b/openspec/changes/fix-hardcoded-chinese-i18n/specs/i18n/spec.md @@ -0,0 +1,49 @@ +## ADDED Requirements + +### Requirement: 完整的 UI 文本国际化覆盖 + +应用 SHALL 通过 `AppLocalizations` 提供所有面向用户的 UI 文本,禁止在 widget 构建逻辑中使用字面量中文字符串。 + +#### Scenario: 所有可见文本通过翻译函数获取 +- **WHEN** 开发者在任何 widget 的 `build()` 方法、`AppBar` 标题、`SnackBar` 内容、`Dialog` 文本或按钮文案中显示文本 +- **THEN** 文本必须来自 `AppLocalizations.of(context)` 的 getter 或方法调用,不得直接写中文字面量 + +#### Scenario: 静态分析检测硬编码中文 +- **WHEN** 运行 `flutter analyze` 或代码评审检查 `lib/` 目录下任意 `.dart` 文件中 widget 渲染部分 +- **THEN** 不应出现包含中文字符(`[一-鿿]`)的字面量字符串 + +### Requirement: 中英双语翻译完整性 + +`AppLocalizations` SHALL 为所有翻译键同时提供中文(`zh`)和英文(`en`)两种语言的取值,避免运行时回退到键名或硬编码默认值。 + +#### Scenario: 翻译键同时存在于两种语言 +- **WHEN** 新增任意翻译键到 `_localizedValues['zh']` +- **THEN** `_localizedValues['en']` 必须包含同名键,且值为对应的英文翻译 + +#### Scenario: 切换语言后所有界面文本即时更新 +- **WHEN** 用户在系统设置中将语言从中文切换到英文 +- **THEN** 应用所有页面(首页、程序管理、程序详情、运行控制、系统设置、完成页)的 UI 文本应立即变为英文,无需重启应用 + +### Requirement: 非 BuildContext 场景的本地化支持 + +`AppLocalizations` SHALL 提供在没有 `BuildContext` 的场景(如 Provider/Service 层抛出错误消息、日志)中获取当前语言文本的方式,或明确将这些场景排除在国际化范围之外。 + +#### Scenario: Service 层错误消息的本地化 +- **WHEN** 设备运行 Service 需要向 UI 反馈错误(如串口连接失败) +- **THEN** Service 层应返回稳定的错误码或英文键名,由 UI 层通过 `AppLocalizations` 转换为本地化文本展示给用户 + +#### Scenario: 调试日志保留原始语言 +- **WHEN** 代码通过 `dart:developer` 的 `log()` 或类似 API 输出调试日志 +- **THEN** 允许保留中文日志内容,无需国际化(仅面向开发者) + +### Requirement: 单复数与参数化文本 + +`AppLocalizations` SHALL 支持包含动态参数(如数量、程序名称)的翻译,禁止通过字符串拼接构造可见文本。 + +#### Scenario: 带参数的翻译方法 +- **WHEN** 显示形如「成功导入 5 个程序」的消息 +- **THEN** `AppLocalizations` 应提供方法形式(如 `programsImportedCount(int n)`)返回完整本地化字符串,而不是要求调用方拼接 `importSuccess + n + programsImported` + +#### Scenario: 替换现有拼接式调用 +- **WHEN** 重构现有代码中通过 `'${l.importSuccess} $count ${l.programsImported}'` 形式拼接的字符串 +- **THEN** 应替换为单个参数化方法调用,确保英文语序自然(如 `'Successfully imported $count programs'`) diff --git a/openspec/changes/fix-hardcoded-chinese-i18n/tasks.md b/openspec/changes/fix-hardcoded-chinese-i18n/tasks.md new file mode 100644 index 0000000..ff44bdd --- /dev/null +++ b/openspec/changes/fix-hardcoded-chinese-i18n/tasks.md @@ -0,0 +1,66 @@ +## 1. 准备阶段:扩充翻译表 + +- [x] 1.1 审计每个 feature 模块,整理所有需要新增的翻译键(中文 + 英文)清单 +- [x] 1.2 在 `lib/core/localization/app_localizations.dart` 中新增所有缺失的翻译键 getter(中英文) +- [x] 1.3 将拼接式调用(如 `importSuccess + count + programsImported`)改为参数化方法(如 `importedPrograms(int count)`) +- [x] 1.4 运行 `flutter analyze` 确保 `AppLocalizations` 改动无报错 + +## 2. core 模块替换 + +- [x] 2.1 替换 `lib/core/router/app_router.dart` 中的硬编码中文(路由错误页等) +- [x] 2.2 替换 `lib/core/theme/app_theme.dart` 中的硬编码中文(若有) +- [x] 2.3 验证:grep 该模块剩余中文字面量为零 + +## 3. shared 模块替换 + +- [x] 3.1 替换 `lib/shared/widgets/` 下所有通用组件的中文(CommonButton、CommonCard、StatusIndicator 等) +- [x] 3.2 替换 `lib/shared/utils/constants.dart` 中面向 UI 暴露的常量(仅 UI 文本,业务常量保留) +- [x] 3.3 验证:grep 该模块剩余中文字面量为零 + +## 4. features/home 模块替换 + +- [x] 4.1 替换 `lib/features/home/pages/home_page.dart` +- [x] 4.2 替换 `lib/features/home/pages/complete_page.dart` +- [x] 4.3 替换 `lib/features/home/widgets/status_bar.dart` +- [x] 4.4 替换 `lib/features/home/widgets/program_list.dart` +- [x] 4.5 替换 `lib/features/home/widgets/running_control_panel.dart` +- [x] 4.6 替换 `lib/features/home/widgets/run_status_monitor.dart` +- [x] 4.7 验证:grep 该模块剩余中文字面量为零 + +## 5. features/programs 模块替换 + +- [x] 5.1 替换 `lib/features/programs/pages/programs_page.dart` +- [x] 5.2 替换 `lib/features/programs/providers/programs_provider.dart`(仅 UI 反馈消息) +- [x] 5.3 替换 `lib/features/programs/services/excel_import_service.dart`(错误提示通过错误码返回,UI 层翻译) +- [x] 5.4 验证:grep 该模块剩余中文字面量为零(models 中的字段名注释除外) + +## 6. features/program_detail 模块替换 + +- [x] 6.1 替换 `lib/features/program_detail/` 下所有 page 与 widget 中的中文 +- [x] 6.2 验证:grep 该模块剩余中文字面量为零 + +## 7. features/device 模块替换(仅 UI 层) + +- [x] 7.1 替换 `lib/features/device/` 下所有 page 与 widget 中的中文 +- [x] 7.2 Service 层抛出的 Exception 改为携带错误码(保留中文日志) +- [x] 7.3 UI 层调用 Service 时捕获错误码并通过 `AppLocalizations` 转为本地化消息 +- [x] 7.4 验证:grep `lib/features/device/` 下 UI 部分剩余中文字面量为零 + +## 8. features/settings 模块替换 + +- [x] 8.1 替换 `lib/features/settings/` 下所有 page 与 widget 中的中文 +- [x] 8.2 验证:grep 该模块剩余中文字面量为零 + +## 9. 全量回归与验证 + +- [x] 9.1 全量扫描:`grep -rn "[一-鿿]" lib/ --include="*.dart"` 并人工审阅每条结果,确认仅为日志/注释/数据库种子数据 +- [x] 9.2 运行 `flutter analyze`,确保零报错零警告 +- [x] 9.3 运行 `flutter test`,确保现有测试全部通过 +- [ ] 9.4 启动应用,在中文模式下访问首页、程序管理、程序详情、运行控制、设置、完成页所有路径 +- [ ] 9.5 在设置中切换为英文,重复 9.4 的全部路径检查,确认所有 UI 文本变为英文且无遗漏 +- [ ] 9.6 切回中文,确认应用正常恢复 + +## 10. 收尾 + +- [x] 10.1 更新 `CLAUDE.md`「当前实现状态」段落,记录 i18n 完整覆盖已完成 +- [ ] 10.2 在 commit message 中按 feature 分组列出修改 diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 0000000..392946c --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,20 @@ +schema: spec-driven + +# Project context (optional) +# This is shown to AI when creating artifacts. +# Add your tech stack, conventions, style guides, domain knowledge, etc. +# Example: +# context: | +# Tech stack: TypeScript, React, Node.js +# We use conventional commits +# Domain: e-commerce platform + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours