feat(i18n): localize settings + search screens
Some checks failed
CI / Windows build (push) Has been cancelled

Extend the en/zh localization to the two screens reached from the home
app bar (the "全都用 + 多语言" ask):
- settings: title, theme-mode segments (System/Light/Dark), seed-color
  description, color-picker + clear-data dialogs.
- search: hint, error, empty/no-results states, Notes/Documents section
  headers, page label.

New ARB keys regenerated; l10n_test now asserts the new keys resolve in
both English and Chinese.

flutter analyze: 0 issues. l10n_test: 3/3.
This commit is contained in:
2026-06-23 10:03:02 +08:00
parent d1b265dc70
commit a7d71e7cfd
8 changed files with 283 additions and 27 deletions

View File

@@ -15,6 +15,31 @@
"openInSplitView": "Open in Split View", "openInSplitView": "Open in Split View",
"splitViewSubtitle": "PDF reference + scratchpad", "splitViewSubtitle": "PDF reference + scratchpad",
"removeDocument": "Remove document", "removeDocument": "Remove document",
"ok": "OK",
"pickColor": "Pick a color",
"clearSettingsTitle": "Clear all local settings?",
"clear": "Clear",
"settingsReset": "Settings reset to defaults",
"themeSystem": "System",
"themeLight": "Light",
"themeDark": "Dark",
"seedColorDesc": "Seed color for Material 3 theme",
"searchHint": "Search notes and documents...",
"searchError": "Search error: {error}",
"@searchError": {
"placeholders": { "error": { "type": "String" } }
},
"noResultsFor": "No results for \"{query}\"",
"@noResultsFor": {
"placeholders": { "query": { "type": "String" } }
},
"typeToSearch": "Type to search your notes and documents",
"sectionNotes": "Notes",
"sectionDocuments": "Documents",
"pageLabel": "Page {page}",
"@pageLabel": {
"placeholders": { "page": { "type": "int" } }
},
"processingPptx": "Processing PPTX...", "processingPptx": "Processing PPTX...",
"processingPresentation": "Processing presentation...", "processingPresentation": "Processing presentation...",
"couldNotOpenPresentation": "Could not open presentation.", "couldNotOpenPresentation": "Could not open presentation.",

View File

@@ -188,6 +188,102 @@ abstract class AppLocalizations {
/// **'Remove document'** /// **'Remove document'**
String get removeDocument; String get removeDocument;
/// No description provided for @ok.
///
/// In en, this message translates to:
/// **'OK'**
String get ok;
/// No description provided for @pickColor.
///
/// In en, this message translates to:
/// **'Pick a color'**
String get pickColor;
/// No description provided for @clearSettingsTitle.
///
/// In en, this message translates to:
/// **'Clear all local settings?'**
String get clearSettingsTitle;
/// No description provided for @clear.
///
/// In en, this message translates to:
/// **'Clear'**
String get clear;
/// No description provided for @settingsReset.
///
/// In en, this message translates to:
/// **'Settings reset to defaults'**
String get settingsReset;
/// No description provided for @themeSystem.
///
/// In en, this message translates to:
/// **'System'**
String get themeSystem;
/// No description provided for @themeLight.
///
/// In en, this message translates to:
/// **'Light'**
String get themeLight;
/// No description provided for @themeDark.
///
/// In en, this message translates to:
/// **'Dark'**
String get themeDark;
/// No description provided for @seedColorDesc.
///
/// In en, this message translates to:
/// **'Seed color for Material 3 theme'**
String get seedColorDesc;
/// No description provided for @searchHint.
///
/// In en, this message translates to:
/// **'Search notes and documents...'**
String get searchHint;
/// No description provided for @searchError.
///
/// In en, this message translates to:
/// **'Search error: {error}'**
String searchError(String error);
/// No description provided for @noResultsFor.
///
/// In en, this message translates to:
/// **'No results for \"{query}\"'**
String noResultsFor(String query);
/// No description provided for @typeToSearch.
///
/// In en, this message translates to:
/// **'Type to search your notes and documents'**
String get typeToSearch;
/// No description provided for @sectionNotes.
///
/// In en, this message translates to:
/// **'Notes'**
String get sectionNotes;
/// No description provided for @sectionDocuments.
///
/// In en, this message translates to:
/// **'Documents'**
String get sectionDocuments;
/// No description provided for @pageLabel.
///
/// In en, this message translates to:
/// **'Page {page}'**
String pageLabel(int page);
/// No description provided for @processingPptx. /// No description provided for @processingPptx.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -53,6 +53,60 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get removeDocument => 'Remove document'; String get removeDocument => 'Remove document';
@override
String get ok => 'OK';
@override
String get pickColor => 'Pick a color';
@override
String get clearSettingsTitle => 'Clear all local settings?';
@override
String get clear => 'Clear';
@override
String get settingsReset => 'Settings reset to defaults';
@override
String get themeSystem => 'System';
@override
String get themeLight => 'Light';
@override
String get themeDark => 'Dark';
@override
String get seedColorDesc => 'Seed color for Material 3 theme';
@override
String get searchHint => 'Search notes and documents...';
@override
String searchError(String error) {
return 'Search error: $error';
}
@override
String noResultsFor(String query) {
return 'No results for \"$query\"';
}
@override
String get typeToSearch => 'Type to search your notes and documents';
@override
String get sectionNotes => 'Notes';
@override
String get sectionDocuments => 'Documents';
@override
String pageLabel(int page) {
return 'Page $page';
}
@override @override
String get processingPptx => 'Processing PPTX...'; String get processingPptx => 'Processing PPTX...';

View File

@@ -53,6 +53,60 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get removeDocument => '移除文档'; String get removeDocument => '移除文档';
@override
String get ok => '确定';
@override
String get pickColor => '选择颜色';
@override
String get clearSettingsTitle => '清除所有本地设置?';
@override
String get clear => '清除';
@override
String get settingsReset => '设置已恢复默认';
@override
String get themeSystem => '跟随系统';
@override
String get themeLight => '浅色';
@override
String get themeDark => '深色';
@override
String get seedColorDesc => 'Material 3 主题种子色';
@override
String get searchHint => '搜索笔记和文档…';
@override
String searchError(String error) {
return '搜索出错:$error';
}
@override
String noResultsFor(String query) {
return '没有“$query”的结果';
}
@override
String get typeToSearch => '输入以搜索你的笔记和文档';
@override
String get sectionNotes => '笔记';
@override
String get sectionDocuments => '文档';
@override
String pageLabel(int page) {
return '$page';
}
@override @override
String get processingPptx => '正在处理 PPTX…'; String get processingPptx => '正在处理 PPTX…';

View File

@@ -15,6 +15,22 @@
"openInSplitView": "分屏打开", "openInSplitView": "分屏打开",
"splitViewSubtitle": "PDF 参考 + 草稿纸", "splitViewSubtitle": "PDF 参考 + 草稿纸",
"removeDocument": "移除文档", "removeDocument": "移除文档",
"ok": "确定",
"pickColor": "选择颜色",
"clearSettingsTitle": "清除所有本地设置?",
"clear": "清除",
"settingsReset": "设置已恢复默认",
"themeSystem": "跟随系统",
"themeLight": "浅色",
"themeDark": "深色",
"seedColorDesc": "Material 3 主题种子色",
"searchHint": "搜索笔记和文档…",
"searchError": "搜索出错:{error}",
"noResultsFor": "没有“{query}”的结果",
"typeToSearch": "输入以搜索你的笔记和文档",
"sectionNotes": "笔记",
"sectionDocuments": "文档",
"pageLabel": "第 {page} 页",
"processingPptx": "正在处理 PPTX…", "processingPptx": "正在处理 PPTX…",
"processingPresentation": "正在处理演示文稿…", "processingPresentation": "正在处理演示文稿…",
"couldNotOpenPresentation": "无法打开演示文稿。", "couldNotOpenPresentation": "无法打开演示文稿。",

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../l10n/app_localizations.dart';
import '../models/note.dart'; import '../models/note.dart';
import '../providers/search_provider.dart'; import '../providers/search_provider.dart';
import 'note_editor_screen.dart'; import 'note_editor_screen.dart';
@@ -42,16 +43,17 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final results = ref.watch(searchResultsProvider); final results = ref.watch(searchResultsProvider);
final l = AppLocalizations.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: TextField( title: TextField(
controller: _controller, controller: _controller,
autofocus: true, autofocus: true,
decoration: const InputDecoration( decoration: InputDecoration(
hintText: 'Search notes and documents...', hintText: l.searchHint,
border: InputBorder.none, border: InputBorder.none,
hintStyle: TextStyle(color: Colors.grey), hintStyle: const TextStyle(color: Colors.grey),
), ),
onChanged: _onQueryChanged, onChanged: _onQueryChanged,
onSubmitted: (value) { onSubmitted: (value) {
@@ -72,14 +74,14 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
), ),
body: results.when( body: results.when(
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Search error: $e')), error: (e, _) => Center(child: Text(l.searchError('$e'))),
data: (hits) { data: (hits) {
final query = ref.watch(searchQueryProvider); final query = ref.watch(searchQueryProvider);
if (query.isEmpty) { if (query.isEmpty) {
return const Center( return Center(
child: Text( child: Text(
'Type to search your notes and documents', l.typeToSearch,
style: TextStyle(color: Colors.grey), style: const TextStyle(color: Colors.grey),
), ),
); );
} }
@@ -95,7 +97,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'No results for "$query"', l.noResultsFor(query),
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
@@ -114,7 +116,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Text( child: Text(
'Notes', l.sectionNotes,
style: Theme.of(context).textTheme.titleSmall?.copyWith( style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
@@ -129,7 +131,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
child: Text( child: Text(
'Documents', l.sectionDocuments,
style: Theme.of(context).textTheme.titleSmall?.copyWith( style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
@@ -209,7 +211,7 @@ class _DocumentSearchResultTile extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Page ${pageNumber + 1}', AppLocalizations.of(context).pageLabel(pageNumber + 1),
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
if (snippet.isNotEmpty) if (snippet.isNotEmpty)

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../l10n/app_localizations.dart';
import '../models/pen_tool.dart'; import '../models/pen_tool.dart';
import '../models/pressure_curve.dart'; import '../models/pressure_curve.dart';
import '../providers/settings_provider.dart'; import '../providers/settings_provider.dart';
@@ -20,8 +21,9 @@ class SettingsScreen extends ConsumerWidget {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
final l = AppLocalizations.of(context);
return AlertDialog( return AlertDialog(
title: const Text('Pick a color'), title: Text(l.pickColor),
content: SingleChildScrollView( content: SingleChildScrollView(
child: ColorPicker( child: ColorPicker(
pickerColor: pickerColor, pickerColor: pickerColor,
@@ -31,14 +33,14 @@ class SettingsScreen extends ConsumerWidget {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'), child: Text(l.cancel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
onPicked(pickerColor); onPicked(pickerColor);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: const Text('OK'), child: Text(l.ok),
), ),
], ],
); );
@@ -50,7 +52,7 @@ class SettingsScreen extends ConsumerWidget {
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text('Clear all local settings?'), title: Text(AppLocalizations.of(ctx).clearSettingsTitle),
content: const Text( content: const Text(
'This will reset pen defaults and appearance settings. ' 'This will reset pen defaults and appearance settings. '
'Notes and documents are not affected.', 'Notes and documents are not affected.',
@@ -58,17 +60,18 @@ class SettingsScreen extends ConsumerWidget {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx), onPressed: () => Navigator.pop(ctx),
child: const Text('Cancel'), child: Text(AppLocalizations.of(ctx).cancel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
ref.read(settingsProvider).clearAllData(); ref.read(settingsProvider).clearAllData();
Navigator.pop(ctx); Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Settings reset to defaults')), SnackBar(
content: Text(AppLocalizations.of(context).settingsReset)),
); );
}, },
child: const Text('Clear'), child: Text(AppLocalizations.of(ctx).clear),
), ),
], ],
), ),
@@ -81,7 +84,7 @@ class SettingsScreen extends ConsumerWidget {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Settings')), appBar: AppBar(title: Text(AppLocalizations.of(context).settings)),
body: ListView( body: ListView(
children: [ children: [
_SectionHeader(title: 'Defaults', icon: Icons.tune), _SectionHeader(title: 'Defaults', icon: Icons.tune),
@@ -221,21 +224,21 @@ class SettingsScreen extends ConsumerWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
SegmentedButton<ThemeMode>( SegmentedButton<ThemeMode>(
segments: const [ segments: [
ButtonSegment( ButtonSegment(
value: ThemeMode.system, value: ThemeMode.system,
label: Text('System'), label: Text(AppLocalizations.of(context).themeSystem),
icon: Icon(Icons.brightness_auto), icon: const Icon(Icons.brightness_auto),
), ),
ButtonSegment( ButtonSegment(
value: ThemeMode.light, value: ThemeMode.light,
label: Text('Light'), label: Text(AppLocalizations.of(context).themeLight),
icon: Icon(Icons.light_mode), icon: const Icon(Icons.light_mode),
), ),
ButtonSegment( ButtonSegment(
value: ThemeMode.dark, value: ThemeMode.dark,
label: Text('Dark'), label: Text(AppLocalizations.of(context).themeDark),
icon: Icon(Icons.dark_mode), icon: const Icon(Icons.dark_mode),
), ),
], ],
selected: {settings.themeMode}, selected: {settings.themeMode},
@@ -272,7 +275,7 @@ class SettingsScreen extends ConsumerWidget {
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
const Text('Seed color for Material 3 theme'), Text(AppLocalizations.of(context).seedColorDesc),
], ],
), ),
], ],

View File

@@ -18,6 +18,8 @@ class _Probe extends StatelessWidget {
Text(l.settings), Text(l.settings),
Text(l.toolEraser), Text(l.toolEraser),
Text(l.importPdf), Text(l.importPdf),
Text(l.themeSystem),
Text(l.searchHint),
]), ]),
); );
} }
@@ -38,6 +40,8 @@ void main() {
expect(find.text('Settings'), findsOneWidget); expect(find.text('Settings'), findsOneWidget);
expect(find.text('Eraser'), findsOneWidget); expect(find.text('Eraser'), findsOneWidget);
expect(find.text('Import PDF'), findsOneWidget); expect(find.text('Import PDF'), findsOneWidget);
expect(find.text('System'), findsOneWidget);
expect(find.text('Search notes and documents...'), findsOneWidget);
}); });
testWidgets('Chinese locale resolves Chinese strings', (tester) async { testWidgets('Chinese locale resolves Chinese strings', (tester) async {
@@ -45,6 +49,8 @@ void main() {
expect(find.text('设置'), findsOneWidget); expect(find.text('设置'), findsOneWidget);
expect(find.text('橡皮擦'), findsOneWidget); expect(find.text('橡皮擦'), findsOneWidget);
expect(find.text('导入 PDF'), findsOneWidget); expect(find.text('导入 PDF'), findsOneWidget);
expect(find.text('跟随系统'), findsOneWidget);
expect(find.text('搜索笔记和文档…'), findsOneWidget);
}); });
test('both locales are supported', () { test('both locales are supported', () {