feat(frontend): add multi-language support (en, zh-Hans, zh-Hant, ja)

This commit is contained in:
2026-05-25 11:49:53 +08:00
parent 1539e495e6
commit 261b1ab169
20 changed files with 1955 additions and 139 deletions

View File

@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../l10n/app_localizations.dart';
import '../providers/auth_provider.dart';
import '../providers/settings_provider.dart';
@@ -88,21 +89,21 @@ class _SettingsPageState extends State<SettingsPage> {
}
}
Future<void> _confirmLogout(AuthProvider auth) async {
Future<void> _confirmLogout(AuthProvider auth, AppLocalizations l10n) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("Confirm Logout"),
content: const Text("Are you sure you want to log out now?"),
title: Text(l10n.confirmLogout),
content: Text(l10n.confirmLogoutDesc),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text("Cancel"),
child: Text(l10n.cancel),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text("Logout"),
child: Text(l10n.logout),
),
],
);
@@ -119,10 +120,11 @@ class _SettingsPageState extends State<SettingsPage> {
final auth = context.watch<AuthProvider>();
final settings = context.watch<SettingsProvider>();
final isAuthenticated = auth.isAuthenticated;
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text("Settings", style: TextStyle(fontWeight: FontWeight.bold)),
title: Text(l10n.settings, style: TextStyle(fontWeight: FontWeight.bold)),
centerTitle: true,
),
body: SingleChildScrollView(
@@ -134,12 +136,58 @@ class _SettingsPageState extends State<SettingsPage> {
_buildProfileSection(auth),
const SizedBox(height: 32),
],
_buildSectionTitle("Network Configuration"),
_buildSectionTitle(l10n.language),
const SizedBox(height: 16),
DropdownButtonFormField<Locale?>(
initialValue: settings.locale == null
? null
: AppLocalizations.supportedLocales.cast<Locale?>().firstWhere(
(l) => l?.languageCode == settings.locale?.languageCode &&
l?.scriptCode == settings.locale?.scriptCode,
orElse: () => null,
),
decoration: InputDecoration(
labelText: l10n.selectLanguage,
prefixIcon: const Icon(Icons.language),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
items: [
DropdownMenuItem(
value: null,
child: Text(l10n.system),
),
DropdownMenuItem(
value: const Locale('en'),
child: Text(l10n.english),
),
DropdownMenuItem(
value: const Locale('zh'),
child: Text(l10n.simplifiedChinese),
),
DropdownMenuItem(
value: const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'),
child: Text(l10n.traditionalChinese),
),
DropdownMenuItem(
value: const Locale('ja'),
child: Text(l10n.japanese),
),
],
onChanged: (Locale? newLocale) {
settings.setLocale(newLocale);
},
),
const SizedBox(height: 32),
_buildSectionTitle(l10n.networkConfiguration),
const SizedBox(height: 16),
TextField(
controller: _urlController,
decoration: InputDecoration(
labelText: "Backend Server URL",
labelText: l10n.backendServerUrl,
hintText: "http://127.0.0.1:8080",
prefixIcon: Icon(Icons.lan),
border: OutlineInputBorder(
@@ -157,13 +205,13 @@ class _SettingsPageState extends State<SettingsPage> {
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Server URL Updated"),
content: Text(l10n.serverUrlUpdated),
behavior: SnackBarBehavior.floating,
),
);
},
icon: Icon(Icons.save),
label: Text("Save Network Settings"),
label: Text(l10n.saveNetworkSettings),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(
context,
@@ -179,28 +227,28 @@ class _SettingsPageState extends State<SettingsPage> {
),
const SizedBox(height: 32),
_buildSectionTitle("Theme Customization"),
_buildSectionTitle(l10n.themeCustomization),
const SizedBox(height: 16),
Text(
"Appearance Mode",
l10n.appearanceMode,
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 12),
SegmentedButton<ThemeMode>(
segments: const [
segments: [
ButtonSegment<ThemeMode>(
value: ThemeMode.system,
label: Text("System"),
label: Text(l10n.system),
icon: Icon(Icons.brightness_auto),
),
ButtonSegment<ThemeMode>(
value: ThemeMode.light,
label: Text("Light"),
label: Text(l10n.light),
icon: Icon(Icons.light_mode),
),
ButtonSegment<ThemeMode>(
value: ThemeMode.dark,
label: Text("Dark"),
label: Text(l10n.dark),
icon: Icon(Icons.dark_mode),
),
],
@@ -210,7 +258,7 @@ class _SettingsPageState extends State<SettingsPage> {
},
),
const SizedBox(height: 20),
Text("Accent Color", style: Theme.of(context).textTheme.labelLarge),
Text(l10n.accentColor, style: Theme.of(context).textTheme.labelLarge),
const SizedBox(height: 12),
Wrap(
spacing: 12,
@@ -248,26 +296,26 @@ class _SettingsPageState extends State<SettingsPage> {
}).toList(),
),
const SizedBox(height: 32),
_buildSectionTitle("Explore"),
_buildSectionTitle(l10n.explore),
const SizedBox(height: 8),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: const Text("Live Preview Thumbnails"),
subtitle: const Text(
"Show cached snapshot covers for live rooms when available.",
title: Text(l10n.livePreviewThumbnails),
subtitle: Text(
l10n.livePreviewThumbnailsDesc,
),
value: settings.livePreviewThumbnailsEnabled,
onChanged: settings.setLivePreviewThumbnailsEnabled,
),
if (isAuthenticated) ...[
const SizedBox(height: 32),
_buildSectionTitle("Security"),
_buildSectionTitle(l10n.security),
const SizedBox(height: 16),
TextField(
controller: _oldPasswordController,
obscureText: true,
decoration: InputDecoration(
labelText: "Old Password",
labelText: l10n.oldPassword,
prefixIcon: const Icon(Icons.lock_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
@@ -279,7 +327,7 @@ class _SettingsPageState extends State<SettingsPage> {
controller: _newPasswordController,
obscureText: true,
decoration: InputDecoration(
labelText: "New Password",
labelText: l10n.newPassword,
prefixIcon: const Icon(Icons.lock_reset),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
@@ -292,7 +340,7 @@ class _SettingsPageState extends State<SettingsPage> {
child: OutlinedButton.icon(
onPressed: _handleChangePassword,
icon: const Icon(Icons.update),
label: const Text("Change Password"),
label: Text(l10n.changePassword),
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@@ -304,9 +352,9 @@ class _SettingsPageState extends State<SettingsPage> {
SizedBox(
width: double.infinity,
child: FilledButton.tonalIcon(
onPressed: () => _confirmLogout(auth),
onPressed: () => _confirmLogout(auth, l10n),
icon: const Icon(Icons.logout),
label: const Text("Logout"),
label: Text(l10n.logout),
style: FilledButton.styleFrom(
foregroundColor: Colors.redAccent,
padding: const EdgeInsets.symmetric(vertical: 14),