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

@@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:video_player/video_player.dart';
import '../l10n/app_localizations.dart';
import '../providers/auth_provider.dart';
import '../providers/settings_provider.dart';
import '../services/api_service.dart';
@@ -271,6 +272,7 @@ class _PlayerPageState extends State<PlayerPage> {
Future<void> _openVolumeSheet() async {
_showControls();
final l10n = AppLocalizations.of(context)!;
await showModalBottomSheet<void>(
context: context,
builder: (context) {
@@ -284,7 +286,7 @@ class _PlayerPageState extends State<PlayerPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Volume',
l10n.volume,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
@@ -353,6 +355,7 @@ class _PlayerPageState extends State<PlayerPage> {
if (!mounted) {
return;
}
final l10n = AppLocalizations.of(context)!;
final nextResolution = await showModalBottomSheet<String>(
context: context,
@@ -364,11 +367,11 @@ class _PlayerPageState extends State<PlayerPage> {
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text('Playback Resolution'),
title: Text(l10n.playbackResolution),
subtitle: Text(
available.length > 1
? 'Select an available transcoded stream.'
: 'Only the source stream is available right now.',
? l10n.playbackOptionsDesc
: l10n.sourceOnlyDesc,
),
),
...options.map((option) {
@@ -382,8 +385,8 @@ class _PlayerPageState extends State<PlayerPage> {
),
title: Text(option),
subtitle: enabled
? const Text('Available now')
: const Text('Waiting for backend transcoding output'),
? Text(l10n.availableNow)
: Text(l10n.waitingForTranscoding),
onTap: enabled ? () => Navigator.pop(context, option) : null,
);
}),
@@ -590,6 +593,7 @@ class _PlayerPageState extends State<PlayerPage> {
}
Widget _buildPlaybackControls() {
final l10n = AppLocalizations.of(context)!;
return IgnorePointer(
ignoring: !_controlsVisible,
child: AnimatedOpacity(
@@ -620,7 +624,7 @@ class _PlayerPageState extends State<PlayerPage> {
children: [
_buildControlButton(
icon: Icons.refresh,
label: "Refresh",
label: l10n.refresh,
onPressed: _refreshPlayer,
),
_buildControlButton(
@@ -629,19 +633,19 @@ class _PlayerPageState extends State<PlayerPage> {
: _volume < 0.5
? Icons.volume_down
: Icons.volume_up,
label: "Volume",
label: l10n.volume,
onPressed: _openVolumeSheet,
),
_buildControlButton(
icon: _showDanmaku ? Icons.subtitles : Icons.subtitles_off,
label: _showDanmaku ? "Danmaku On" : "Danmaku Off",
label: _showDanmaku ? l10n.danmakuOn : l10n.danmakuOff,
onPressed: _toggleDanmaku,
),
_buildControlButton(
icon: _isFullscreen
? Icons.fullscreen_exit
: Icons.fullscreen,
label: _isFullscreen ? "Exit Fullscreen" : "Fullscreen",
label: _isFullscreen ? l10n.exitFullscreen : l10n.fullscreen,
onPressed: _toggleFullscreen,
),
_buildControlButton(
@@ -679,23 +683,24 @@ class _PlayerPageState extends State<PlayerPage> {
// 抽离聊天区域组件
Widget _buildChatSection() {
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: Row(
children: [
Icon(Icons.chat_bubble_outline, size: 16),
SizedBox(width: 8),
Text("Live Chat", style: TextStyle(fontWeight: FontWeight.bold)),
const Icon(Icons.chat_bubble_outline, size: 16),
const SizedBox(width: 8),
Text(l10n.liveChat, style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
),
Expanded(
child: ListView.builder(
reverse: true,
padding: EdgeInsets.all(8),
padding: const EdgeInsets.all(8),
itemCount: _messages.length,
itemBuilder: (context, index) {
final m = _messages[index];
@@ -703,7 +708,7 @@ class _PlayerPageState extends State<PlayerPage> {
},
),
),
Divider(height: 1),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
@@ -712,11 +717,11 @@ class _PlayerPageState extends State<PlayerPage> {
child: TextField(
controller: _msgController,
decoration: InputDecoration(
hintText: "Send a message...",
hintText: l10n.sendMessage,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
contentPadding: EdgeInsets.symmetric(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),