feat(i18n): add English + Chinese localization
All checks were successful
CI / Windows build (push) Successful in 12m34s

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-23 09:35:26 +08:00
parent f48e43f13d
commit 1d70c029b3
12 changed files with 768 additions and 26 deletions

46
lib/l10n/app_en.arb Normal file
View File

@@ -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" }
}
}
}

View File

@@ -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, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects 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<AppLocalizations>(context, AppLocalizations)!;
}
static const LocalizationsDelegate<AppLocalizations> 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<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
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<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['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.',
);
}

View File

@@ -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';
}
}

View File

@@ -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';
}
}

37
lib/l10n/app_zh.arb Normal file
View File

@@ -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}"
}