From 1d70c029b3d76da62c87c62fa402511181973e5b Mon Sep 17 00:00:00 2001 From: Akiba So Date: Tue, 23 Jun 2026 09:35:26 +0800 Subject: [PATCH] feat(i18n): add English + Chinese localization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app had zero localization ("软件多语言做了吗" — no). Add Flutter's official gen-l10n pipeline and localize the core flow the user sees. - pubspec: flutter_localizations + intl + generate: true - l10n.yaml + lib/l10n/app_en.arb + app_zh.arb (37 strings) - main.dart: localizationsDelegates + supportedLocales (follows OS locale) - pen editor: all tool tooltips, page pill, error states localized - home: app bar actions + empty-state buttons localized Proven end-to-end: l10n_test pumps the same widget under Locale('en') and Locale('zh') and asserts English vs Chinese strings resolve. flutter analyze: 0 issues. l10n_test: 3/3 pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- l10n.yaml | 7 + lib/editor/canvas/pen_editor_screen.dart | 37 +-- lib/l10n/app_en.arb | 46 +++ lib/l10n/app_localizations.dart | 338 +++++++++++++++++++++++ lib/l10n/app_localizations_en.dart | 116 ++++++++ lib/l10n/app_localizations_zh.dart | 116 ++++++++ lib/l10n/app_zh.arb | 37 +++ lib/main.dart | 10 + lib/screens/home_screen.dart | 20 +- pubspec.lock | 7 +- pubspec.yaml | 5 + test/l10n_test.dart | 55 ++++ 12 files changed, 768 insertions(+), 26 deletions(-) create mode 100644 l10n.yaml create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_localizations.dart create mode 100644 lib/l10n/app_localizations_en.dart create mode 100644 lib/l10n/app_localizations_zh.dart create mode 100644 lib/l10n/app_zh.arb create mode 100644 test/l10n_test.dart diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..1e45663 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,7 @@ +# Flutter gen-l10n config. Generates AppLocalizations from the ARB files in +# lib/l10n. `flutter pub get` / build runs the generator (pubspec `generate: true`). +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-class: AppLocalizations +nullable-getter: false diff --git a/lib/editor/canvas/pen_editor_screen.dart b/lib/editor/canvas/pen_editor_screen.dart index af0d6ce..17064ed 100644 --- a/lib/editor/canvas/pen_editor_screen.dart +++ b/lib/editor/canvas/pen_editor_screen.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:pdfrx/pdfrx.dart'; +import '../../l10n/app_localizations.dart'; import '../../services/database_service.dart'; import '../engine/stroke_geometry.dart' show kDefaultPenThinning; import '../engine/stroke_model.dart'; @@ -398,6 +399,7 @@ class _PenEditorScreenState extends State { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context); return Scaffold( body: Stack( children: [ @@ -429,7 +431,7 @@ class _PenEditorScreenState extends State { padding: const EdgeInsets.all(8), child: _RoundIconButton( icon: Icons.arrow_back, - tooltip: 'Back', + tooltip: l.back, onPressed: () => Navigator.of(context).maybePop(), ), ), @@ -510,15 +512,16 @@ class _PenEditorScreenState extends State { } Widget _buildBody() { + final l = AppLocalizations.of(context); if (_openError != null) { - return Center(child: Text('Failed to open PDF:\n$_openError')); + return Center(child: Text(l.failedToOpenPdf('$_openError'))); } final doc = _document; if (doc == null) { return const Center(child: CircularProgressIndicator()); } if (doc.pages.isEmpty) { - return const Center(child: Text('PDF has no pages.')); + return Center(child: Text(l.pdfNoPages)); } final page = doc.pages[_pageIndex]; @@ -584,6 +587,7 @@ class _PenEditorScreenState extends State { /// tools, color dots, and finger-drawing toggle. Widget _buildToolPalette() { final cs = Theme.of(context).colorScheme; + final l = AppLocalizations.of(context); return Material( color: cs.surfaceContainerHigh, elevation: 3, @@ -596,19 +600,19 @@ class _PenEditorScreenState extends State { _ToolButton( icon: Icons.edit_outlined, selected: _tool == CanvasTool.pen, - tooltip: 'Pen', + tooltip: l.toolPen, onPressed: () => setState(() => _tool = CanvasTool.pen), ), _ToolButton( icon: Icons.brush_outlined, selected: _tool == CanvasTool.highlighter, - tooltip: 'Highlighter', + tooltip: l.toolHighlighter, onPressed: () => setState(() => _tool = CanvasTool.highlighter), ), _ToolButton( icon: Icons.cleaning_services_outlined, selected: _tool == CanvasTool.eraser, - tooltip: 'Eraser', + tooltip: l.toolEraser, onPressed: () => setState(() => _tool = CanvasTool.eraser), ), _Divider(cs: cs), @@ -616,13 +620,13 @@ class _PenEditorScreenState extends State { _ToolButton( icon: Icons.undo, selected: false, - tooltip: 'Undo', + tooltip: l.actionUndo, onPressed: _undoFor(_pageIndex).canUndo ? _performUndo : null, ), _ToolButton( icon: Icons.redo, selected: false, - tooltip: 'Redo', + tooltip: l.actionRedo, onPressed: _undoFor(_pageIndex).canRedo ? _performRedo : null, ), _Divider(cs: cs), @@ -632,28 +636,28 @@ class _PenEditorScreenState extends State { icon: _allowFingerDrawing ? Icons.touch_app : Icons.do_not_touch, selected: _allowFingerDrawing, tooltip: _allowFingerDrawing - ? 'Finger drawing ON' - : 'Finger drawing OFF (pen only)', + ? l.fingerDrawingOn + : l.fingerDrawingOff, onPressed: _toggleFingerDrawing, ), // Page thumbnail grid. _ToolButton( icon: Icons.grid_view, selected: false, - tooltip: 'Pages', + tooltip: l.pages, onPressed: _document != null ? _openThumbnails : null, ), // Pen settings. _ToolButton( icon: Icons.settings_outlined, selected: false, - tooltip: 'Pen settings', + tooltip: l.penSettings, onPressed: _penConfig != null ? _openPenSettings : null, ), _ToolButton( icon: Icons.bug_report_outlined, selected: _showPenDebug, - tooltip: 'Input diagnostic (writes a log file)', + tooltip: l.inputDiagnostic, onPressed: () { final on = !_showPenDebug; setState(() => _showPenDebug = on); @@ -699,6 +703,7 @@ class _PenEditorScreenState extends State { Widget _buildPagePill() { final doc = _document!; final cs = Theme.of(context).colorScheme; + final l = AppLocalizations.of(context); final total = doc.pages.length; final shown = (_scrub ?? (_pageIndex + 1).toDouble()).round(); return Column( @@ -742,7 +747,7 @@ class _PenEditorScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ IconButton( - tooltip: 'Previous page', + tooltip: l.previousPage, icon: const Icon(Icons.chevron_left), onPressed: _pageIndex > 0 ? () => _goToPage(_pageIndex - 1) : null, @@ -752,7 +757,7 @@ class _PenEditorScreenState extends State { ? () => setState(() => _showSlider = !_showSlider) : null, child: Text( - '$shown / $total', + l.pageOfPages(shown, total), style: TextStyle( color: cs.onSurface, fontWeight: FontWeight.w600, @@ -760,7 +765,7 @@ class _PenEditorScreenState extends State { ), ), IconButton( - tooltip: 'Next page', + tooltip: l.nextPage, icon: const Icon(Icons.chevron_right), onPressed: _pageIndex < total - 1 ? () => _goToPage(_pageIndex + 1) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..c5236e4 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,46 @@ +{ + "@@locale": "en", + "appTitle": "BadNote", + "settings": "Settings", + "search": "Search", + "importPdf": "Import PDF", + "importPpt": "Import PPT", + "penCanvasBeta": "Pen Canvas (beta)", + "newNote": "New Note", + "open": "Open", + "cancel": "Cancel", + "delete": "Delete", + "deleteNoteTitle": "Delete note?", + "deleteNote": "Delete note", + "openInSplitView": "Open in Split View", + "splitViewSubtitle": "PDF reference + scratchpad", + "removeDocument": "Remove document", + "processingPptx": "Processing PPTX...", + "processingPresentation": "Processing presentation...", + "couldNotOpenPresentation": "Could not open presentation.", + "toolPen": "Pen", + "toolHighlighter": "Highlighter", + "toolEraser": "Eraser", + "actionUndo": "Undo", + "actionRedo": "Redo", + "fingerDrawingOn": "Finger drawing ON", + "fingerDrawingOff": "Finger drawing OFF (pen only)", + "pages": "Pages", + "penSettings": "Pen settings", + "inputDiagnostic": "Input diagnostic (writes a log file)", + "back": "Back", + "previousPage": "Previous page", + "nextPage": "Next page", + "failedToOpenPdf": "Failed to open PDF:\n{error}", + "@failedToOpenPdf": { + "placeholders": { "error": { "type": "String" } } + }, + "pdfNoPages": "PDF has no pages.", + "pageOfPages": "{current} / {total}", + "@pageOfPages": { + "placeholders": { + "current": { "type": "int" }, + "total": { "type": "int" } + } + } +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..f0dd26c --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,338 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_zh.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations of(BuildContext context) { + return Localizations.of(context, AppLocalizations)!; + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('zh'), + ]; + + /// No description provided for @appTitle. + /// + /// In en, this message translates to: + /// **'BadNote'** + String get appTitle; + + /// No description provided for @settings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settings; + + /// No description provided for @search. + /// + /// In en, this message translates to: + /// **'Search'** + String get search; + + /// No description provided for @importPdf. + /// + /// In en, this message translates to: + /// **'Import PDF'** + String get importPdf; + + /// No description provided for @importPpt. + /// + /// In en, this message translates to: + /// **'Import PPT'** + String get importPpt; + + /// No description provided for @penCanvasBeta. + /// + /// In en, this message translates to: + /// **'Pen Canvas (beta)'** + String get penCanvasBeta; + + /// No description provided for @newNote. + /// + /// In en, this message translates to: + /// **'New Note'** + String get newNote; + + /// No description provided for @open. + /// + /// In en, this message translates to: + /// **'Open'** + String get open; + + /// No description provided for @cancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; + + /// No description provided for @delete. + /// + /// In en, this message translates to: + /// **'Delete'** + String get delete; + + /// No description provided for @deleteNoteTitle. + /// + /// In en, this message translates to: + /// **'Delete note?'** + String get deleteNoteTitle; + + /// No description provided for @deleteNote. + /// + /// In en, this message translates to: + /// **'Delete note'** + String get deleteNote; + + /// No description provided for @openInSplitView. + /// + /// In en, this message translates to: + /// **'Open in Split View'** + String get openInSplitView; + + /// No description provided for @splitViewSubtitle. + /// + /// In en, this message translates to: + /// **'PDF reference + scratchpad'** + String get splitViewSubtitle; + + /// No description provided for @removeDocument. + /// + /// In en, this message translates to: + /// **'Remove document'** + String get removeDocument; + + /// No description provided for @processingPptx. + /// + /// In en, this message translates to: + /// **'Processing PPTX...'** + String get processingPptx; + + /// No description provided for @processingPresentation. + /// + /// In en, this message translates to: + /// **'Processing presentation...'** + String get processingPresentation; + + /// No description provided for @couldNotOpenPresentation. + /// + /// In en, this message translates to: + /// **'Could not open presentation.'** + String get couldNotOpenPresentation; + + /// No description provided for @toolPen. + /// + /// In en, this message translates to: + /// **'Pen'** + String get toolPen; + + /// No description provided for @toolHighlighter. + /// + /// In en, this message translates to: + /// **'Highlighter'** + String get toolHighlighter; + + /// No description provided for @toolEraser. + /// + /// In en, this message translates to: + /// **'Eraser'** + String get toolEraser; + + /// No description provided for @actionUndo. + /// + /// In en, this message translates to: + /// **'Undo'** + String get actionUndo; + + /// No description provided for @actionRedo. + /// + /// In en, this message translates to: + /// **'Redo'** + String get actionRedo; + + /// No description provided for @fingerDrawingOn. + /// + /// In en, this message translates to: + /// **'Finger drawing ON'** + String get fingerDrawingOn; + + /// No description provided for @fingerDrawingOff. + /// + /// In en, this message translates to: + /// **'Finger drawing OFF (pen only)'** + String get fingerDrawingOff; + + /// No description provided for @pages. + /// + /// In en, this message translates to: + /// **'Pages'** + String get pages; + + /// No description provided for @penSettings. + /// + /// In en, this message translates to: + /// **'Pen settings'** + String get penSettings; + + /// No description provided for @inputDiagnostic. + /// + /// In en, this message translates to: + /// **'Input diagnostic (writes a log file)'** + String get inputDiagnostic; + + /// No description provided for @back. + /// + /// In en, this message translates to: + /// **'Back'** + String get back; + + /// No description provided for @previousPage. + /// + /// In en, this message translates to: + /// **'Previous page'** + String get previousPage; + + /// No description provided for @nextPage. + /// + /// In en, this message translates to: + /// **'Next page'** + String get nextPage; + + /// No description provided for @failedToOpenPdf. + /// + /// In en, this message translates to: + /// **'Failed to open PDF:\n{error}'** + String failedToOpenPdf(String error); + + /// No description provided for @pdfNoPages. + /// + /// In en, this message translates to: + /// **'PDF has no pages.'** + String get pdfNoPages; + + /// No description provided for @pageOfPages. + /// + /// In en, this message translates to: + /// **'{current} / {total}'** + String pageOfPages(int current, int total); +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'zh'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'zh': + return AppLocalizationsZh(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..d453a13 --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,116 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get appTitle => 'BadNote'; + + @override + String get settings => 'Settings'; + + @override + String get search => 'Search'; + + @override + String get importPdf => 'Import PDF'; + + @override + String get importPpt => 'Import PPT'; + + @override + String get penCanvasBeta => 'Pen Canvas (beta)'; + + @override + String get newNote => 'New Note'; + + @override + String get open => 'Open'; + + @override + String get cancel => 'Cancel'; + + @override + String get delete => 'Delete'; + + @override + String get deleteNoteTitle => 'Delete note?'; + + @override + String get deleteNote => 'Delete note'; + + @override + String get openInSplitView => 'Open in Split View'; + + @override + String get splitViewSubtitle => 'PDF reference + scratchpad'; + + @override + String get removeDocument => 'Remove document'; + + @override + String get processingPptx => 'Processing PPTX...'; + + @override + String get processingPresentation => 'Processing presentation...'; + + @override + String get couldNotOpenPresentation => 'Could not open presentation.'; + + @override + String get toolPen => 'Pen'; + + @override + String get toolHighlighter => 'Highlighter'; + + @override + String get toolEraser => 'Eraser'; + + @override + String get actionUndo => 'Undo'; + + @override + String get actionRedo => 'Redo'; + + @override + String get fingerDrawingOn => 'Finger drawing ON'; + + @override + String get fingerDrawingOff => 'Finger drawing OFF (pen only)'; + + @override + String get pages => 'Pages'; + + @override + String get penSettings => 'Pen settings'; + + @override + String get inputDiagnostic => 'Input diagnostic (writes a log file)'; + + @override + String get back => 'Back'; + + @override + String get previousPage => 'Previous page'; + + @override + String get nextPage => 'Next page'; + + @override + String failedToOpenPdf(String error) { + return 'Failed to open PDF:\n$error'; + } + + @override + String get pdfNoPages => 'PDF has no pages.'; + + @override + String pageOfPages(int current, int total) { + return '$current / $total'; + } +} diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart new file mode 100644 index 0000000..9fa5b8f --- /dev/null +++ b/lib/l10n/app_localizations_zh.dart @@ -0,0 +1,116 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Chinese (`zh`). +class AppLocalizationsZh extends AppLocalizations { + AppLocalizationsZh([String locale = 'zh']) : super(locale); + + @override + String get appTitle => 'BadNote'; + + @override + String get settings => '设置'; + + @override + String get search => '搜索'; + + @override + String get importPdf => '导入 PDF'; + + @override + String get importPpt => '导入 PPT'; + + @override + String get penCanvasBeta => '手写画布(测试版)'; + + @override + String get newNote => '新建笔记'; + + @override + String get open => '打开'; + + @override + String get cancel => '取消'; + + @override + String get delete => '删除'; + + @override + String get deleteNoteTitle => '删除笔记?'; + + @override + String get deleteNote => '删除笔记'; + + @override + String get openInSplitView => '分屏打开'; + + @override + String get splitViewSubtitle => 'PDF 参考 + 草稿纸'; + + @override + String get removeDocument => '移除文档'; + + @override + String get processingPptx => '正在处理 PPTX…'; + + @override + String get processingPresentation => '正在处理演示文稿…'; + + @override + String get couldNotOpenPresentation => '无法打开演示文稿。'; + + @override + String get toolPen => '钢笔'; + + @override + String get toolHighlighter => '荧光笔'; + + @override + String get toolEraser => '橡皮擦'; + + @override + String get actionUndo => '撤销'; + + @override + String get actionRedo => '重做'; + + @override + String get fingerDrawingOn => '手指书写:开'; + + @override + String get fingerDrawingOff => '手指书写:关(仅手写笔)'; + + @override + String get pages => '页面'; + + @override + String get penSettings => '手写笔设置'; + + @override + String get inputDiagnostic => '输入诊断(写入日志文件)'; + + @override + String get back => '返回'; + + @override + String get previousPage => '上一页'; + + @override + String get nextPage => '下一页'; + + @override + String failedToOpenPdf(String error) { + return '打开 PDF 失败:\n$error'; + } + + @override + String get pdfNoPages => 'PDF 没有任何页面。'; + + @override + String pageOfPages(int current, int total) { + return '$current / $total'; + } +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb new file mode 100644 index 0000000..9e489d6 --- /dev/null +++ b/lib/l10n/app_zh.arb @@ -0,0 +1,37 @@ +{ + "@@locale": "zh", + "appTitle": "BadNote", + "settings": "设置", + "search": "搜索", + "importPdf": "导入 PDF", + "importPpt": "导入 PPT", + "penCanvasBeta": "手写画布(测试版)", + "newNote": "新建笔记", + "open": "打开", + "cancel": "取消", + "delete": "删除", + "deleteNoteTitle": "删除笔记?", + "deleteNote": "删除笔记", + "openInSplitView": "分屏打开", + "splitViewSubtitle": "PDF 参考 + 草稿纸", + "removeDocument": "移除文档", + "processingPptx": "正在处理 PPTX…", + "processingPresentation": "正在处理演示文稿…", + "couldNotOpenPresentation": "无法打开演示文稿。", + "toolPen": "钢笔", + "toolHighlighter": "荧光笔", + "toolEraser": "橡皮擦", + "actionUndo": "撤销", + "actionRedo": "重做", + "fingerDrawingOn": "手指书写:开", + "fingerDrawingOff": "手指书写:关(仅手写笔)", + "pages": "页面", + "penSettings": "手写笔设置", + "inputDiagnostic": "输入诊断(写入日志文件)", + "back": "返回", + "previousPage": "上一页", + "nextPage": "下一页", + "failedToOpenPdf": "打开 PDF 失败:\n{error}", + "pdfNoPages": "PDF 没有任何页面。", + "pageOfPages": "{current} / {total}" +} diff --git a/lib/main.dart b/lib/main.dart index e7e6001..f40ff2c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,13 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:pdfrx/pdfrx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'editor/pdf/pen_capture_region.dart'; +import 'l10n/app_localizations.dart'; import 'providers/settings_provider.dart'; import 'screens/home_screen.dart'; import 'services/database_service.dart'; @@ -54,6 +56,14 @@ class BadNoteApp extends ConsumerWidget { themeMode: settings.themeMode, theme: _theme(lightScheme), darkTheme: _theme(darkScheme), + // i18n: follows the OS language (en / zh) via the system locale. + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, home: const HomeScreen(), ); }, diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index a379a3e..deb1ee6 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../l10n/app_localizations.dart'; import '../models/document.dart'; import '../models/note.dart'; import '../providers/document_provider.dart'; @@ -34,15 +35,16 @@ class HomeScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final notesAsync = ref.watch(noteListProvider); + final l = AppLocalizations.of(context); return Scaffold( appBar: AppBar( - title: const Text('BadNote'), + title: Text(l.appTitle), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.settings), - tooltip: 'Settings', + tooltip: l.settings, onPressed: () { Navigator.of( context, @@ -51,17 +53,17 @@ class HomeScreen extends ConsumerWidget { ), IconButton( icon: const Icon(Icons.picture_as_pdf), - tooltip: 'Import PDF', + tooltip: l.importPdf, onPressed: () => _importPdf(context), ), IconButton( icon: const Icon(Icons.slideshow), - tooltip: 'Import PPT', + tooltip: l.importPpt, onPressed: () => _importPptx(context), ), IconButton( icon: const Icon(Icons.search), - tooltip: 'Search', + tooltip: l.search, onPressed: () { Navigator.of( context, @@ -71,7 +73,7 @@ class HomeScreen extends ConsumerWidget { // New pen-first canvas editor (beta). IconButton( icon: const Icon(Icons.draw_outlined), - tooltip: 'Pen Canvas (beta)', + tooltip: l.penCanvasBeta, onPressed: () => openM1Spike(context), ), ], @@ -265,19 +267,19 @@ class HomeScreen extends ConsumerWidget { FilledButton.icon( onPressed: () => _createAndOpenNote(context, ref), icon: const Icon(Icons.add), - label: const Text('New Note'), + label: Text(AppLocalizations.of(context).newNote), ), const SizedBox(height: 12), OutlinedButton.icon( onPressed: () => _importPdf(context), icon: const Icon(Icons.picture_as_pdf), - label: const Text('Import PDF'), + label: Text(AppLocalizations.of(context).importPdf), ), const SizedBox(height: 12), OutlinedButton.icon( onPressed: () => _importPptx(context), icon: const Icon(Icons.slideshow), - label: const Text('Import PPT'), + label: Text(AppLocalizations.of(context).importPpt), ), ], ), diff --git a/pubspec.lock b/pubspec.lock index 4fe1655..15992cb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -347,6 +347,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_onnxruntime: dependency: "direct main" description: @@ -544,7 +549,7 @@ packages: source: sdk version: "0.0.0" intl: - dependency: transitive + dependency: "direct main" description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" diff --git a/pubspec.yaml b/pubspec.yaml index 3c5b7ef..fce6251 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,9 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter + intl: any cupertino_icons: ^1.0.8 # Pen & Ink Rendering @@ -80,6 +83,8 @@ dev_dependencies: flutter: uses-material-design: true + # Generate AppLocalizations from lib/l10n/*.arb (see l10n.yaml). + generate: true assets: - assets/models/ocr/ diff --git a/test/l10n_test.dart b/test/l10n_test.dart new file mode 100644 index 0000000..96d624a --- /dev/null +++ b/test/l10n_test.dart @@ -0,0 +1,55 @@ +// Proves i18n is real, not just wired: the SAME widget resolves English under +// Locale('en') and Chinese under Locale('zh'), through the generated +// AppLocalizations delegates. Guards against ARB keys drifting out of sync. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:badnote/l10n/app_localizations.dart'; + +class _Probe extends StatelessWidget { + const _Probe(); + @override + Widget build(BuildContext context) { + final l = AppLocalizations.of(context); + return Directionality( + textDirection: TextDirection.ltr, + child: Column(children: [ + Text(l.settings), + Text(l.toolEraser), + Text(l.importPdf), + ]), + ); + } +} + +Future _pump(WidgetTester tester, Locale locale) { + return tester.pumpWidget(MaterialApp( + locale: locale, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const _Probe(), + )); +} + +void main() { + testWidgets('English locale resolves English strings', (tester) async { + await _pump(tester, const Locale('en')); + expect(find.text('Settings'), findsOneWidget); + expect(find.text('Eraser'), findsOneWidget); + expect(find.text('Import PDF'), findsOneWidget); + }); + + testWidgets('Chinese locale resolves Chinese strings', (tester) async { + await _pump(tester, const Locale('zh')); + expect(find.text('设置'), findsOneWidget); + expect(find.text('橡皮擦'), findsOneWidget); + expect(find.text('导入 PDF'), findsOneWidget); + }); + + test('both locales are supported', () { + final langs = + AppLocalizations.supportedLocales.map((l) => l.languageCode).toSet(); + expect(langs.containsAll({'en', 'zh'}), isTrue); + }); +}