diff --git a/frontend/l10n.yaml b/frontend/l10n.yaml new file mode 100644 index 0000000..15338f2 --- /dev/null +++ b/frontend/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart diff --git a/frontend/lib/l10n/app_en.arb b/frontend/lib/l10n/app_en.arb new file mode 100644 index 0000000..7a8e2e7 --- /dev/null +++ b/frontend/lib/l10n/app_en.arb @@ -0,0 +1,70 @@ +{ + "@@locale": "en", + "settings": "Settings", + "networkConfiguration": "Network Configuration", + "backendServerUrl": "Backend Server URL", + "saveNetworkSettings": "Save Network Settings", + "serverUrlUpdated": "Server URL Updated", + "themeCustomization": "Theme Customization", + "appearanceMode": "Appearance Mode", + "system": "System", + "light": "Light", + "dark": "Dark", + "accentColor": "Accent Color", + "explore": "Explore", + "livePreviewThumbnails": "Live Preview Thumbnails", + "livePreviewThumbnailsDesc": "Show cached snapshot covers for live rooms when available.", + "security": "Security", + "oldPassword": "Old Password", + "newPassword": "New Password", + "changePassword": "Change Password", + "logout": "Logout", + "confirmLogout": "Confirm Logout", + "confirmLogoutDesc": "Are you sure you want to log out now?", + "cancel": "Cancel", + "language": "Language", + "selectLanguage": "Select Language", + "english": "English", + "simplifiedChinese": "简体中文", + "traditionalChinese": "繁體中文", + "japanese": "日本語", + "console": "Console", + "failedToLoadRooms": "Failed to load rooms", + "goLive": "Go Live", + "noActiveRooms": "No active rooms. Be the first!", + "hostId": "Host ID", + "username": "Username", + "password": "Password", + "fillAllFields": "Please fill in all fields", + "networkError": "Network Error: Could not connect to server", + "loginFailed": "Login Failed", + "login": "LOGIN", + "dontHaveAccount": "Don't have an account? Create one", + "createAccount": "Create Account", + "joinHightube": "Join Hightube", + "desiredUsername": "Desired Username", + "register": "REGISTER", + "alreadyHaveAccount": "Already have an account? Login here", + "accountCreated": "Account created! Please login.", + "playbackResolution": "Playback Resolution", + "availableNow": "Available now", + "waitingForTranscoding": "Waiting for backend transcoding output", + "sendMessage": "Send a message...", + "liveChat": "Live Chat", + "refresh": "Refresh", + "volume": "Volume", + "danmakuOn": "Danmaku On", + "danmakuOff": "Danmaku Off", + "fullscreen": "Fullscreen", + "exitFullscreen": "Exit Fullscreen", + "resolution": "Resolution", + "playbackOptionsDesc": "Select an available transcoded stream.", + "sourceOnlyDesc": "Only the source stream is available right now.", + "myStreamConsole": "My Stream Console", + "noRoomInfo": "No room info found.", + "roomTitle": "Room Title", + "rtmpServerUrl": "RTMP Server URL", + "streamKey": "Stream Key (Keep Secret!)", + "copiedToClipboard": "Copied to clipboard", + "failedToFetchRoomInfo": "Failed to fetch room info" +} diff --git a/frontend/lib/l10n/app_ja.arb b/frontend/lib/l10n/app_ja.arb new file mode 100644 index 0000000..b7e41b3 --- /dev/null +++ b/frontend/lib/l10n/app_ja.arb @@ -0,0 +1,70 @@ +{ + "@@locale": "ja", + "settings": "設定", + "networkConfiguration": "ネットワーク設定", + "backendServerUrl": "バックエンドサーバーURL", + "saveNetworkSettings": "ネットワーク設定を保存", + "serverUrlUpdated": "サーバーURLが更新されました", + "themeCustomization": "テーマのカスタマイズ", + "appearanceMode": "外観モード", + "system": "システム", + "light": "ライト", + "dark": "ダーク", + "accentColor": "アクセントカラー", + "explore": "探索", + "livePreviewThumbnails": "ライブプレビューサムネイル", + "livePreviewThumbnailsDesc": "利用可能な場合、ライブルームのキャッシュされたスナップショットカバーを表示します。", + "security": "セキュリティ", + "oldPassword": "現在のパスワード", + "newPassword": "新しいパスワード", + "changePassword": "パスワードを変更", + "logout": "ログアウト", + "confirmLogout": "ログアウトの確認", + "confirmLogoutDesc": "今すぐログアウトしてもよろしいですか?", + "cancel": "キャンセル", + "language": "言語", + "selectLanguage": "言語を選択", + "english": "English", + "simplifiedChinese": "简体中文", + "traditionalChinese": "繁體中文", + "japanese": "日本語", + "console": "コンソール", + "failedToLoadRooms": "ルームの読み込みに失敗しました", + "goLive": "ライブ配信を開始", + "noActiveRooms": "配信中のルームはありません。最初の配信者になりましょう!", + "hostId": "配信者 ID", + "username": "ユーザー名", + "password": "パスワード", + "fillAllFields": "すべての項目を入力してください", + "networkError": "ネットワークエラー:サーバーに接続できませんでした", + "loginFailed": "ログインに失敗しました", + "login": "ログイン", + "dontHaveAccount": "アカウントをお持ちでないですか?新規登録", + "createAccount": "アカウント作成", + "joinHightube": "Hightube に参加", + "desiredUsername": "ユーザー名", + "register": "登録", + "alreadyHaveAccount": "既にアカウントをお持ちですか?ログイン", + "accountCreated": "アカウントが作成されました!ログインしてください。", + "playbackResolution": "再生解像度", + "availableNow": "利用可能", + "waitingForTranscoding": "バックエンドのトランスコード出力を待機中", + "sendMessage": "メッセージを送信...", + "liveChat": "ライブチャット", + "refresh": "更新", + "volume": "音量", + "danmakuOn": "弾幕オン", + "danmakuOff": "弾幕オフ", + "fullscreen": "全画面", + "exitFullscreen": "全画面終了", + "resolution": "解像度", + "playbackOptionsDesc": "利用可能なトランスコード済みストリームを選択します。", + "sourceOnlyDesc": "現在、ソースストリームのみが利用可能です。", + "myStreamConsole": "配信コンソール", + "noRoomInfo": "ルーム情報が見つかりません。", + "roomTitle": "ルームタイトル", + "rtmpServerUrl": "RTMP サーバー URL", + "streamKey": "ストリームキー (秘密にしてください!)", + "copiedToClipboard": "クリップボードにコピーしました", + "failedToFetchRoomInfo": "ルーム情報の取得に失敗しました" +} diff --git a/frontend/lib/l10n/app_localizations.dart b/frontend/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..5305fbf --- /dev/null +++ b/frontend/lib/l10n/app_localizations.dart @@ -0,0 +1,553 @@ +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_ja.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('ja'), + Locale('zh'), + Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), + ]; + + /// No description provided for @settings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settings; + + /// No description provided for @networkConfiguration. + /// + /// In en, this message translates to: + /// **'Network Configuration'** + String get networkConfiguration; + + /// No description provided for @backendServerUrl. + /// + /// In en, this message translates to: + /// **'Backend Server URL'** + String get backendServerUrl; + + /// No description provided for @saveNetworkSettings. + /// + /// In en, this message translates to: + /// **'Save Network Settings'** + String get saveNetworkSettings; + + /// No description provided for @serverUrlUpdated. + /// + /// In en, this message translates to: + /// **'Server URL Updated'** + String get serverUrlUpdated; + + /// No description provided for @themeCustomization. + /// + /// In en, this message translates to: + /// **'Theme Customization'** + String get themeCustomization; + + /// No description provided for @appearanceMode. + /// + /// In en, this message translates to: + /// **'Appearance Mode'** + String get appearanceMode; + + /// No description provided for @system. + /// + /// In en, this message translates to: + /// **'System'** + String get system; + + /// No description provided for @light. + /// + /// In en, this message translates to: + /// **'Light'** + String get light; + + /// No description provided for @dark. + /// + /// In en, this message translates to: + /// **'Dark'** + String get dark; + + /// No description provided for @accentColor. + /// + /// In en, this message translates to: + /// **'Accent Color'** + String get accentColor; + + /// No description provided for @explore. + /// + /// In en, this message translates to: + /// **'Explore'** + String get explore; + + /// No description provided for @livePreviewThumbnails. + /// + /// In en, this message translates to: + /// **'Live Preview Thumbnails'** + String get livePreviewThumbnails; + + /// No description provided for @livePreviewThumbnailsDesc. + /// + /// In en, this message translates to: + /// **'Show cached snapshot covers for live rooms when available.'** + String get livePreviewThumbnailsDesc; + + /// No description provided for @security. + /// + /// In en, this message translates to: + /// **'Security'** + String get security; + + /// No description provided for @oldPassword. + /// + /// In en, this message translates to: + /// **'Old Password'** + String get oldPassword; + + /// No description provided for @newPassword. + /// + /// In en, this message translates to: + /// **'New Password'** + String get newPassword; + + /// No description provided for @changePassword. + /// + /// In en, this message translates to: + /// **'Change Password'** + String get changePassword; + + /// No description provided for @logout. + /// + /// In en, this message translates to: + /// **'Logout'** + String get logout; + + /// No description provided for @confirmLogout. + /// + /// In en, this message translates to: + /// **'Confirm Logout'** + String get confirmLogout; + + /// No description provided for @confirmLogoutDesc. + /// + /// In en, this message translates to: + /// **'Are you sure you want to log out now?'** + String get confirmLogoutDesc; + + /// No description provided for @cancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; + + /// No description provided for @language. + /// + /// In en, this message translates to: + /// **'Language'** + String get language; + + /// No description provided for @selectLanguage. + /// + /// In en, this message translates to: + /// **'Select Language'** + String get selectLanguage; + + /// No description provided for @english. + /// + /// In en, this message translates to: + /// **'English'** + String get english; + + /// No description provided for @simplifiedChinese. + /// + /// In en, this message translates to: + /// **'简体中文'** + String get simplifiedChinese; + + /// No description provided for @traditionalChinese. + /// + /// In en, this message translates to: + /// **'繁體中文'** + String get traditionalChinese; + + /// No description provided for @japanese. + /// + /// In en, this message translates to: + /// **'日本語'** + String get japanese; + + /// No description provided for @console. + /// + /// In en, this message translates to: + /// **'Console'** + String get console; + + /// No description provided for @failedToLoadRooms. + /// + /// In en, this message translates to: + /// **'Failed to load rooms'** + String get failedToLoadRooms; + + /// No description provided for @goLive. + /// + /// In en, this message translates to: + /// **'Go Live'** + String get goLive; + + /// No description provided for @noActiveRooms. + /// + /// In en, this message translates to: + /// **'No active rooms. Be the first!'** + String get noActiveRooms; + + /// No description provided for @hostId. + /// + /// In en, this message translates to: + /// **'Host ID'** + String get hostId; + + /// No description provided for @username. + /// + /// In en, this message translates to: + /// **'Username'** + String get username; + + /// No description provided for @password. + /// + /// In en, this message translates to: + /// **'Password'** + String get password; + + /// No description provided for @fillAllFields. + /// + /// In en, this message translates to: + /// **'Please fill in all fields'** + String get fillAllFields; + + /// No description provided for @networkError. + /// + /// In en, this message translates to: + /// **'Network Error: Could not connect to server'** + String get networkError; + + /// No description provided for @loginFailed. + /// + /// In en, this message translates to: + /// **'Login Failed'** + String get loginFailed; + + /// No description provided for @login. + /// + /// In en, this message translates to: + /// **'LOGIN'** + String get login; + + /// No description provided for @dontHaveAccount. + /// + /// In en, this message translates to: + /// **'Don\'t have an account? Create one'** + String get dontHaveAccount; + + /// No description provided for @createAccount. + /// + /// In en, this message translates to: + /// **'Create Account'** + String get createAccount; + + /// No description provided for @joinHightube. + /// + /// In en, this message translates to: + /// **'Join Hightube'** + String get joinHightube; + + /// No description provided for @desiredUsername. + /// + /// In en, this message translates to: + /// **'Desired Username'** + String get desiredUsername; + + /// No description provided for @register. + /// + /// In en, this message translates to: + /// **'REGISTER'** + String get register; + + /// No description provided for @alreadyHaveAccount. + /// + /// In en, this message translates to: + /// **'Already have an account? Login here'** + String get alreadyHaveAccount; + + /// No description provided for @accountCreated. + /// + /// In en, this message translates to: + /// **'Account created! Please login.'** + String get accountCreated; + + /// No description provided for @playbackResolution. + /// + /// In en, this message translates to: + /// **'Playback Resolution'** + String get playbackResolution; + + /// No description provided for @availableNow. + /// + /// In en, this message translates to: + /// **'Available now'** + String get availableNow; + + /// No description provided for @waitingForTranscoding. + /// + /// In en, this message translates to: + /// **'Waiting for backend transcoding output'** + String get waitingForTranscoding; + + /// No description provided for @sendMessage. + /// + /// In en, this message translates to: + /// **'Send a message...'** + String get sendMessage; + + /// No description provided for @liveChat. + /// + /// In en, this message translates to: + /// **'Live Chat'** + String get liveChat; + + /// No description provided for @refresh. + /// + /// In en, this message translates to: + /// **'Refresh'** + String get refresh; + + /// No description provided for @volume. + /// + /// In en, this message translates to: + /// **'Volume'** + String get volume; + + /// No description provided for @danmakuOn. + /// + /// In en, this message translates to: + /// **'Danmaku On'** + String get danmakuOn; + + /// No description provided for @danmakuOff. + /// + /// In en, this message translates to: + /// **'Danmaku Off'** + String get danmakuOff; + + /// No description provided for @fullscreen. + /// + /// In en, this message translates to: + /// **'Fullscreen'** + String get fullscreen; + + /// No description provided for @exitFullscreen. + /// + /// In en, this message translates to: + /// **'Exit Fullscreen'** + String get exitFullscreen; + + /// No description provided for @resolution. + /// + /// In en, this message translates to: + /// **'Resolution'** + String get resolution; + + /// No description provided for @playbackOptionsDesc. + /// + /// In en, this message translates to: + /// **'Select an available transcoded stream.'** + String get playbackOptionsDesc; + + /// No description provided for @sourceOnlyDesc. + /// + /// In en, this message translates to: + /// **'Only the source stream is available right now.'** + String get sourceOnlyDesc; + + /// No description provided for @myStreamConsole. + /// + /// In en, this message translates to: + /// **'My Stream Console'** + String get myStreamConsole; + + /// No description provided for @noRoomInfo. + /// + /// In en, this message translates to: + /// **'No room info found.'** + String get noRoomInfo; + + /// No description provided for @roomTitle. + /// + /// In en, this message translates to: + /// **'Room Title'** + String get roomTitle; + + /// No description provided for @rtmpServerUrl. + /// + /// In en, this message translates to: + /// **'RTMP Server URL'** + String get rtmpServerUrl; + + /// No description provided for @streamKey. + /// + /// In en, this message translates to: + /// **'Stream Key (Keep Secret!)'** + String get streamKey; + + /// No description provided for @copiedToClipboard. + /// + /// In en, this message translates to: + /// **'Copied to clipboard'** + String get copiedToClipboard; + + /// No description provided for @failedToFetchRoomInfo. + /// + /// In en, this message translates to: + /// **'Failed to fetch room info'** + String get failedToFetchRoomInfo; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'ja', 'zh'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when language+script codes are specified. + switch (locale.languageCode) { + case 'zh': + { + switch (locale.scriptCode) { + case 'Hant': + return AppLocalizationsZhHant(); + } + break; + } + } + + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'ja': + return AppLocalizationsJa(); + 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/frontend/lib/l10n/app_localizations_en.dart b/frontend/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..dbea0cf --- /dev/null +++ b/frontend/lib/l10n/app_localizations_en.dart @@ -0,0 +1,212 @@ +// 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 settings => 'Settings'; + + @override + String get networkConfiguration => 'Network Configuration'; + + @override + String get backendServerUrl => 'Backend Server URL'; + + @override + String get saveNetworkSettings => 'Save Network Settings'; + + @override + String get serverUrlUpdated => 'Server URL Updated'; + + @override + String get themeCustomization => 'Theme Customization'; + + @override + String get appearanceMode => 'Appearance Mode'; + + @override + String get system => 'System'; + + @override + String get light => 'Light'; + + @override + String get dark => 'Dark'; + + @override + String get accentColor => 'Accent Color'; + + @override + String get explore => 'Explore'; + + @override + String get livePreviewThumbnails => 'Live Preview Thumbnails'; + + @override + String get livePreviewThumbnailsDesc => + 'Show cached snapshot covers for live rooms when available.'; + + @override + String get security => 'Security'; + + @override + String get oldPassword => 'Old Password'; + + @override + String get newPassword => 'New Password'; + + @override + String get changePassword => 'Change Password'; + + @override + String get logout => 'Logout'; + + @override + String get confirmLogout => 'Confirm Logout'; + + @override + String get confirmLogoutDesc => 'Are you sure you want to log out now?'; + + @override + String get cancel => 'Cancel'; + + @override + String get language => 'Language'; + + @override + String get selectLanguage => 'Select Language'; + + @override + String get english => 'English'; + + @override + String get simplifiedChinese => '简体中文'; + + @override + String get traditionalChinese => '繁體中文'; + + @override + String get japanese => '日本語'; + + @override + String get console => 'Console'; + + @override + String get failedToLoadRooms => 'Failed to load rooms'; + + @override + String get goLive => 'Go Live'; + + @override + String get noActiveRooms => 'No active rooms. Be the first!'; + + @override + String get hostId => 'Host ID'; + + @override + String get username => 'Username'; + + @override + String get password => 'Password'; + + @override + String get fillAllFields => 'Please fill in all fields'; + + @override + String get networkError => 'Network Error: Could not connect to server'; + + @override + String get loginFailed => 'Login Failed'; + + @override + String get login => 'LOGIN'; + + @override + String get dontHaveAccount => 'Don\'t have an account? Create one'; + + @override + String get createAccount => 'Create Account'; + + @override + String get joinHightube => 'Join Hightube'; + + @override + String get desiredUsername => 'Desired Username'; + + @override + String get register => 'REGISTER'; + + @override + String get alreadyHaveAccount => 'Already have an account? Login here'; + + @override + String get accountCreated => 'Account created! Please login.'; + + @override + String get playbackResolution => 'Playback Resolution'; + + @override + String get availableNow => 'Available now'; + + @override + String get waitingForTranscoding => 'Waiting for backend transcoding output'; + + @override + String get sendMessage => 'Send a message...'; + + @override + String get liveChat => 'Live Chat'; + + @override + String get refresh => 'Refresh'; + + @override + String get volume => 'Volume'; + + @override + String get danmakuOn => 'Danmaku On'; + + @override + String get danmakuOff => 'Danmaku Off'; + + @override + String get fullscreen => 'Fullscreen'; + + @override + String get exitFullscreen => 'Exit Fullscreen'; + + @override + String get resolution => 'Resolution'; + + @override + String get playbackOptionsDesc => 'Select an available transcoded stream.'; + + @override + String get sourceOnlyDesc => 'Only the source stream is available right now.'; + + @override + String get myStreamConsole => 'My Stream Console'; + + @override + String get noRoomInfo => 'No room info found.'; + + @override + String get roomTitle => 'Room Title'; + + @override + String get rtmpServerUrl => 'RTMP Server URL'; + + @override + String get streamKey => 'Stream Key (Keep Secret!)'; + + @override + String get copiedToClipboard => 'Copied to clipboard'; + + @override + String get failedToFetchRoomInfo => 'Failed to fetch room info'; +} diff --git a/frontend/lib/l10n/app_localizations_ja.dart b/frontend/lib/l10n/app_localizations_ja.dart new file mode 100644 index 0000000..7a266b6 --- /dev/null +++ b/frontend/lib/l10n/app_localizations_ja.dart @@ -0,0 +1,212 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Japanese (`ja`). +class AppLocalizationsJa extends AppLocalizations { + AppLocalizationsJa([String locale = 'ja']) : super(locale); + + @override + String get settings => '設定'; + + @override + String get networkConfiguration => 'ネットワーク設定'; + + @override + String get backendServerUrl => 'バックエンドサーバーURL'; + + @override + String get saveNetworkSettings => 'ネットワーク設定を保存'; + + @override + String get serverUrlUpdated => 'サーバーURLが更新されました'; + + @override + String get themeCustomization => 'テーマのカスタマイズ'; + + @override + String get appearanceMode => '外観モード'; + + @override + String get system => 'システム'; + + @override + String get light => 'ライト'; + + @override + String get dark => 'ダーク'; + + @override + String get accentColor => 'アクセントカラー'; + + @override + String get explore => '探索'; + + @override + String get livePreviewThumbnails => 'ライブプレビューサムネイル'; + + @override + String get livePreviewThumbnailsDesc => + '利用可能な場合、ライブルームのキャッシュされたスナップショットカバーを表示します。'; + + @override + String get security => 'セキュリティ'; + + @override + String get oldPassword => '現在のパスワード'; + + @override + String get newPassword => '新しいパスワード'; + + @override + String get changePassword => 'パスワードを変更'; + + @override + String get logout => 'ログアウト'; + + @override + String get confirmLogout => 'ログアウトの確認'; + + @override + String get confirmLogoutDesc => '今すぐログアウトしてもよろしいですか?'; + + @override + String get cancel => 'キャンセル'; + + @override + String get language => '言語'; + + @override + String get selectLanguage => '言語を選択'; + + @override + String get english => 'English'; + + @override + String get simplifiedChinese => '简体中文'; + + @override + String get traditionalChinese => '繁體中文'; + + @override + String get japanese => '日本語'; + + @override + String get console => 'コンソール'; + + @override + String get failedToLoadRooms => 'ルームの読み込みに失敗しました'; + + @override + String get goLive => 'ライブ配信を開始'; + + @override + String get noActiveRooms => '配信中のルームはありません。最初の配信者になりましょう!'; + + @override + String get hostId => '配信者 ID'; + + @override + String get username => 'ユーザー名'; + + @override + String get password => 'パスワード'; + + @override + String get fillAllFields => 'すべての項目を入力してください'; + + @override + String get networkError => 'ネットワークエラー:サーバーに接続できませんでした'; + + @override + String get loginFailed => 'ログインに失敗しました'; + + @override + String get login => 'ログイン'; + + @override + String get dontHaveAccount => 'アカウントをお持ちでないですか?新規登録'; + + @override + String get createAccount => 'アカウント作成'; + + @override + String get joinHightube => 'Hightube に参加'; + + @override + String get desiredUsername => 'ユーザー名'; + + @override + String get register => '登録'; + + @override + String get alreadyHaveAccount => '既にアカウントをお持ちですか?ログイン'; + + @override + String get accountCreated => 'アカウントが作成されました!ログインしてください。'; + + @override + String get playbackResolution => '再生解像度'; + + @override + String get availableNow => '利用可能'; + + @override + String get waitingForTranscoding => 'バックエンドのトランスコード出力を待機中'; + + @override + String get sendMessage => 'メッセージを送信...'; + + @override + String get liveChat => 'ライブチャット'; + + @override + String get refresh => '更新'; + + @override + String get volume => '音量'; + + @override + String get danmakuOn => '弾幕オン'; + + @override + String get danmakuOff => '弾幕オフ'; + + @override + String get fullscreen => '全画面'; + + @override + String get exitFullscreen => '全画面終了'; + + @override + String get resolution => '解像度'; + + @override + String get playbackOptionsDesc => '利用可能なトランスコード済みストリームを選択します。'; + + @override + String get sourceOnlyDesc => '現在、ソースストリームのみが利用可能です。'; + + @override + String get myStreamConsole => '配信コンソール'; + + @override + String get noRoomInfo => 'ルーム情報が見つかりません。'; + + @override + String get roomTitle => 'ルームタイトル'; + + @override + String get rtmpServerUrl => 'RTMP サーバー URL'; + + @override + String get streamKey => 'ストリームキー (秘密にしてください!)'; + + @override + String get copiedToClipboard => 'クリップボードにコピーしました'; + + @override + String get failedToFetchRoomInfo => 'ルーム情報の取得に失敗しました'; +} diff --git a/frontend/lib/l10n/app_localizations_zh.dart b/frontend/lib/l10n/app_localizations_zh.dart new file mode 100644 index 0000000..01dee5e --- /dev/null +++ b/frontend/lib/l10n/app_localizations_zh.dart @@ -0,0 +1,417 @@ +// 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 settings => '设置'; + + @override + String get networkConfiguration => '网络配置'; + + @override + String get backendServerUrl => '后端服务器地址'; + + @override + String get saveNetworkSettings => '保存网络设置'; + + @override + String get serverUrlUpdated => '服务器地址已更新'; + + @override + String get themeCustomization => '主题自定义'; + + @override + String get appearanceMode => '外观模式'; + + @override + String get system => '系统'; + + @override + String get light => '浅色'; + + @override + String get dark => '深色'; + + @override + String get accentColor => '强调色'; + + @override + String get explore => '探索'; + + @override + String get livePreviewThumbnails => '直播预览图'; + + @override + String get livePreviewThumbnailsDesc => '在可用时显示直播房间的缓存快照封面。'; + + @override + String get security => '安全'; + + @override + String get oldPassword => '旧密码'; + + @override + String get newPassword => '新密码'; + + @override + String get changePassword => '修改密码'; + + @override + String get logout => '退出登录'; + + @override + String get confirmLogout => '确认退出'; + + @override + String get confirmLogoutDesc => '您确定现在要退出登录吗?'; + + @override + String get cancel => '取消'; + + @override + String get language => '语言'; + + @override + String get selectLanguage => '选择语言'; + + @override + String get english => 'English'; + + @override + String get simplifiedChinese => '简体中文'; + + @override + String get traditionalChinese => '繁體中文'; + + @override + String get japanese => '日本語'; + + @override + String get console => '控制台'; + + @override + String get failedToLoadRooms => '加载房间失败'; + + @override + String get goLive => '开始直播'; + + @override + String get noActiveRooms => '暂无直播房间。快来开播吧!'; + + @override + String get hostId => '主播 ID'; + + @override + String get username => '用户名'; + + @override + String get password => '密码'; + + @override + String get fillAllFields => '请填写所有字段'; + + @override + String get networkError => '网络错误:无法连接到服务器'; + + @override + String get loginFailed => '登录失败'; + + @override + String get login => '登录'; + + @override + String get dontHaveAccount => '没有账号?立即注册'; + + @override + String get createAccount => '创建账号'; + + @override + String get joinHightube => '加入 Hightube'; + + @override + String get desiredUsername => '用户名'; + + @override + String get register => '注册'; + + @override + String get alreadyHaveAccount => '已有账号?立即登录'; + + @override + String get accountCreated => '账号创建成功!请登录。'; + + @override + String get playbackResolution => '播放分辨率'; + + @override + String get availableNow => '当前可用'; + + @override + String get waitingForTranscoding => '正在等待后端转码输出'; + + @override + String get sendMessage => '发送消息...'; + + @override + String get liveChat => '实时聊天'; + + @override + String get refresh => '刷新'; + + @override + String get volume => '音量'; + + @override + String get danmakuOn => '弹幕开启'; + + @override + String get danmakuOff => '弹幕关闭'; + + @override + String get fullscreen => '全屏'; + + @override + String get exitFullscreen => '退出全屏'; + + @override + String get resolution => '分辨率'; + + @override + String get playbackOptionsDesc => '选择可用的转码流。'; + + @override + String get sourceOnlyDesc => '目前仅源流可用。'; + + @override + String get myStreamConsole => '我的直播控制台'; + + @override + String get noRoomInfo => '未找到房间信息。'; + + @override + String get roomTitle => '房间标题'; + + @override + String get rtmpServerUrl => 'RTMP 服务器地址'; + + @override + String get streamKey => '推流码 (请务必保密!)'; + + @override + String get copiedToClipboard => '已复制到剪贴板'; + + @override + String get failedToFetchRoomInfo => '获取房间信息失败'; +} + +/// The translations for Chinese, using the Han script (`zh_Hant`). +class AppLocalizationsZhHant extends AppLocalizationsZh { + AppLocalizationsZhHant() : super('zh_Hant'); + + @override + String get settings => '設定'; + + @override + String get networkConfiguration => '網路設定'; + + @override + String get backendServerUrl => '後端伺服器地址'; + + @override + String get saveNetworkSettings => '儲存網路設定'; + + @override + String get serverUrlUpdated => '伺服器地址已更新'; + + @override + String get themeCustomization => '主題自訂'; + + @override + String get appearanceMode => '外觀模式'; + + @override + String get system => '系統'; + + @override + String get light => '淺色'; + + @override + String get dark => '深色'; + + @override + String get accentColor => '強調色'; + + @override + String get explore => '探索'; + + @override + String get livePreviewThumbnails => '直播預覽圖'; + + @override + String get livePreviewThumbnailsDesc => '在可用時顯示直播房間的快取快照封面。'; + + @override + String get security => '安全'; + + @override + String get oldPassword => '舊密碼'; + + @override + String get newPassword => '新密碼'; + + @override + String get changePassword => '修改密碼'; + + @override + String get logout => '登出'; + + @override + String get confirmLogout => '確認登出'; + + @override + String get confirmLogoutDesc => '您確定現在要登出嗎?'; + + @override + String get cancel => '取消'; + + @override + String get language => '語言'; + + @override + String get selectLanguage => '選擇語言'; + + @override + String get english => 'English'; + + @override + String get simplifiedChinese => '简体中文'; + + @override + String get traditionalChinese => '繁體中文'; + + @override + String get japanese => '日本語'; + + @override + String get console => '控制台'; + + @override + String get failedToLoadRooms => '載入房間失敗'; + + @override + String get goLive => '開始直播'; + + @override + String get noActiveRooms => '暫無直播房間。快來開播吧!'; + + @override + String get hostId => '主播 ID'; + + @override + String get username => '用戶名'; + + @override + String get password => '密碼'; + + @override + String get fillAllFields => '請填寫所有欄位'; + + @override + String get networkError => '網路錯誤:無法連接到伺服器'; + + @override + String get loginFailed => '登錄失敗'; + + @override + String get login => '登錄'; + + @override + String get dontHaveAccount => '沒有帳號?立即註冊'; + + @override + String get createAccount => '建立帳號'; + + @override + String get joinHightube => '加入 Hightube'; + + @override + String get desiredUsername => '用戶名'; + + @override + String get register => '註冊'; + + @override + String get alreadyHaveAccount => '已有帳號?立即登錄'; + + @override + String get accountCreated => '帳號建立成功!請登錄。'; + + @override + String get playbackResolution => '播放解析度'; + + @override + String get availableNow => '目前可用'; + + @override + String get waitingForTranscoding => '正在等待後端轉碼輸出'; + + @override + String get sendMessage => '發送訊息...'; + + @override + String get liveChat => '即時聊天'; + + @override + String get refresh => '重新整理'; + + @override + String get volume => '音量'; + + @override + String get danmakuOn => '彈幕開啟'; + + @override + String get danmakuOff => '彈幕關閉'; + + @override + String get fullscreen => '全屏'; + + @override + String get exitFullscreen => '退出全屏'; + + @override + String get resolution => '解析度'; + + @override + String get playbackOptionsDesc => '選擇可用的轉碼流。'; + + @override + String get sourceOnlyDesc => '目前僅源流可用。'; + + @override + String get myStreamConsole => '我的直播控制台'; + + @override + String get noRoomInfo => '未找到房間資訊。'; + + @override + String get roomTitle => '房間標題'; + + @override + String get rtmpServerUrl => 'RTMP 伺服器地址'; + + @override + String get streamKey => '推流碼 (請務必保密!)'; + + @override + String get copiedToClipboard => '已複製到剪貼板'; + + @override + String get failedToFetchRoomInfo => '獲取房間資訊失敗'; +} diff --git a/frontend/lib/l10n/app_zh.arb b/frontend/lib/l10n/app_zh.arb new file mode 100644 index 0000000..ebd1710 --- /dev/null +++ b/frontend/lib/l10n/app_zh.arb @@ -0,0 +1,70 @@ +{ + "@@locale": "zh", + "settings": "设置", + "networkConfiguration": "网络配置", + "backendServerUrl": "后端服务器地址", + "saveNetworkSettings": "保存网络设置", + "serverUrlUpdated": "服务器地址已更新", + "themeCustomization": "主题自定义", + "appearanceMode": "外观模式", + "system": "系统", + "light": "浅色", + "dark": "深色", + "accentColor": "强调色", + "explore": "探索", + "livePreviewThumbnails": "直播预览图", + "livePreviewThumbnailsDesc": "在可用时显示直播房间的缓存快照封面。", + "security": "安全", + "oldPassword": "旧密码", + "newPassword": "新密码", + "changePassword": "修改密码", + "logout": "退出登录", + "confirmLogout": "确认退出", + "confirmLogoutDesc": "您确定现在要退出登录吗?", + "cancel": "取消", + "language": "语言", + "selectLanguage": "选择语言", + "english": "English", + "simplifiedChinese": "简体中文", + "traditionalChinese": "繁體中文", + "japanese": "日本語", + "console": "控制台", + "failedToLoadRooms": "加载房间失败", + "goLive": "开始直播", + "noActiveRooms": "暂无直播房间。快来开播吧!", + "hostId": "主播 ID", + "username": "用户名", + "password": "密码", + "fillAllFields": "请填写所有字段", + "networkError": "网络错误:无法连接到服务器", + "loginFailed": "登录失败", + "login": "登录", + "dontHaveAccount": "没有账号?立即注册", + "createAccount": "创建账号", + "joinHightube": "加入 Hightube", + "desiredUsername": "用户名", + "register": "注册", + "alreadyHaveAccount": "已有账号?立即登录", + "accountCreated": "账号创建成功!请登录。", + "playbackResolution": "播放分辨率", + "availableNow": "当前可用", + "waitingForTranscoding": "正在等待后端转码输出", + "sendMessage": "发送消息...", + "liveChat": "实时聊天", + "refresh": "刷新", + "volume": "音量", + "danmakuOn": "弹幕开启", + "danmakuOff": "弹幕关闭", + "fullscreen": "全屏", + "exitFullscreen": "退出全屏", + "resolution": "分辨率", + "playbackOptionsDesc": "选择可用的转码流。", + "sourceOnlyDesc": "目前仅源流可用。", + "myStreamConsole": "我的直播控制台", + "noRoomInfo": "未找到房间信息。", + "roomTitle": "房间标题", + "rtmpServerUrl": "RTMP 服务器地址", + "streamKey": "推流码 (请务必保密!)", + "copiedToClipboard": "已复制到剪贴板", + "failedToFetchRoomInfo": "获取房间信息失败" +} diff --git a/frontend/lib/l10n/app_zh_Hant.arb b/frontend/lib/l10n/app_zh_Hant.arb new file mode 100644 index 0000000..b7809fb --- /dev/null +++ b/frontend/lib/l10n/app_zh_Hant.arb @@ -0,0 +1,70 @@ +{ + "@@locale": "zh_Hant", + "settings": "設定", + "networkConfiguration": "網路設定", + "backendServerUrl": "後端伺服器地址", + "saveNetworkSettings": "儲存網路設定", + "serverUrlUpdated": "伺服器地址已更新", + "themeCustomization": "主題自訂", + "appearanceMode": "外觀模式", + "system": "系統", + "light": "淺色", + "dark": "深色", + "accentColor": "強調色", + "explore": "探索", + "livePreviewThumbnails": "直播預覽圖", + "livePreviewThumbnailsDesc": "在可用時顯示直播房間的快取快照封面。", + "security": "安全", + "oldPassword": "舊密碼", + "newPassword": "新密碼", + "changePassword": "修改密碼", + "logout": "登出", + "confirmLogout": "確認登出", + "confirmLogoutDesc": "您確定現在要登出嗎?", + "cancel": "取消", + "language": "語言", + "selectLanguage": "選擇語言", + "english": "English", + "simplifiedChinese": "简体中文", + "traditionalChinese": "繁體中文", + "japanese": "日本語", + "console": "控制台", + "failedToLoadRooms": "載入房間失敗", + "goLive": "開始直播", + "noActiveRooms": "暫無直播房間。快來開播吧!", + "hostId": "主播 ID", + "username": "用戶名", + "password": "密碼", + "fillAllFields": "請填寫所有欄位", + "networkError": "網路錯誤:無法連接到伺服器", + "loginFailed": "登錄失敗", + "login": "登錄", + "dontHaveAccount": "沒有帳號?立即註冊", + "createAccount": "建立帳號", + "joinHightube": "加入 Hightube", + "desiredUsername": "用戶名", + "register": "註冊", + "alreadyHaveAccount": "已有帳號?立即登錄", + "accountCreated": "帳號建立成功!請登錄。", + "playbackResolution": "播放解析度", + "availableNow": "目前可用", + "waitingForTranscoding": "正在等待後端轉碼輸出", + "sendMessage": "發送訊息...", + "liveChat": "即時聊天", + "refresh": "重新整理", + "volume": "音量", + "danmakuOn": "彈幕開啟", + "danmakuOff": "彈幕關閉", + "fullscreen": "全屏", + "exitFullscreen": "退出全屏", + "resolution": "解析度", + "playbackOptionsDesc": "選擇可用的轉碼流。", + "sourceOnlyDesc": "目前僅源流可用。", + "myStreamConsole": "我的直播控制台", + "noRoomInfo": "未找到房間資訊。", + "roomTitle": "房間標題", + "rtmpServerUrl": "RTMP 伺服器地址", + "streamKey": "推流碼 (請務必保密!)", + "copiedToClipboard": "已複製到剪貼板", + "failedToFetchRoomInfo": "獲取房間資訊失敗" +} diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 56b0bbc..c95f0bd 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; import 'package:fvp/fvp.dart' as fvp; import 'providers/auth_provider.dart'; import 'providers/settings_provider.dart'; import 'pages/home_page.dart'; import 'pages/login_page.dart'; +import 'l10n/app_localizations.dart'; void main() { fvp.registerWith(); @@ -30,6 +32,14 @@ class HightubeApp extends StatelessWidget { return MaterialApp( title: 'Hightube', + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + locale: settings.locale, theme: ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed( diff --git a/frontend/lib/pages/home_page.dart b/frontend/lib/pages/home_page.dart index bbedad4..6578eee 100644 --- a/frontend/lib/pages/home_page.dart +++ b/frontend/lib/pages/home_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; 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'; import '../services/api_service.dart'; @@ -22,6 +23,7 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { bool isWide = MediaQuery.of(context).size.width > 600; + final l10n = AppLocalizations.of(context)!; final List pages = [ _ExploreView(onGoLive: () => setState(() => _selectedIndex = 1)), @@ -38,18 +40,18 @@ class _HomePageState extends State { onDestinationSelected: (int index) => setState(() => _selectedIndex = index), labelType: NavigationRailLabelType.all, - destinations: const [ + destinations: [ NavigationRailDestination( - icon: Icon(Icons.explore), - label: Text('Explore'), + icon: const Icon(Icons.explore), + label: Text(l10n.explore), ), NavigationRailDestination( - icon: Icon(Icons.videocam), - label: Text('Console'), + icon: const Icon(Icons.videocam), + label: Text(l10n.console), ), NavigationRailDestination( - icon: Icon(Icons.settings), - label: Text('Settings'), + icon: const Icon(Icons.settings), + label: Text(l10n.settings), ), ], ), @@ -61,18 +63,18 @@ class _HomePageState extends State { selectedIndex: _selectedIndex, onDestinationSelected: (int index) => setState(() => _selectedIndex = index), - destinations: const [ + destinations: [ NavigationDestination( - icon: Icon(Icons.explore), - label: 'Explore', + icon: const Icon(Icons.explore), + label: l10n.explore, ), NavigationDestination( - icon: Icon(Icons.videocam), - label: 'Console', + icon: const Icon(Icons.videocam), + label: l10n.console, ), NavigationDestination( - icon: Icon(Icons.settings), - label: 'Settings', + icon: const Icon(Icons.settings), + label: l10n.settings, ), ], ) @@ -131,9 +133,10 @@ class _ExploreViewState extends State<_ExploreView> { } } catch (e) { if (!isAuto && mounted) { + final l10n = AppLocalizations.of(context); ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text("Failed to load rooms"))); + ).showSnackBar(SnackBar(content: Text(l10n?.failedToLoadRooms ?? "Failed to load rooms"))); } } finally { if (!isAuto && mounted) setState(() => _isLoading = false); @@ -141,20 +144,21 @@ class _ExploreViewState extends State<_ExploreView> { } Future _confirmLogout() async { + final l10n = AppLocalizations.of(context)!; final confirmed = await showDialog( 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), ), ], ); @@ -169,44 +173,45 @@ class _ExploreViewState extends State<_ExploreView> { @override Widget build(BuildContext context) { final settings = context.watch(); + final l10n = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar( - title: Text("Explore", style: TextStyle(fontWeight: FontWeight.bold)), + title: Text(l10n.explore, style: const TextStyle(fontWeight: FontWeight.bold)), actions: [ IconButton( - icon: Icon(Icons.refresh), + icon: const Icon(Icons.refresh), onPressed: () => _refreshRooms(), ), - IconButton(icon: Icon(Icons.logout), onPressed: _confirmLogout), + IconButton(icon: const Icon(Icons.logout), onPressed: _confirmLogout), ], ), floatingActionButton: FloatingActionButton.extended( onPressed: widget.onGoLive, - label: Text("Go Live"), - icon: Icon(Icons.videocam), + label: Text(l10n.goLive), + icon: const Icon(Icons.videocam), ), body: RefreshIndicator( onRefresh: _refreshRooms, child: _isLoading && _activeRooms.isEmpty - ? Center(child: CircularProgressIndicator()) + ? const Center(child: CircularProgressIndicator()) : _activeRooms.isEmpty ? ListView( children: [ Padding( - padding: EdgeInsets.only(top: 100), + padding: const EdgeInsets.only(top: 100), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( + const Icon( Icons.live_tv_outlined, size: 80, color: Colors.grey, ), - SizedBox(height: 16), + const SizedBox(height: 16), Text( - "No active rooms. Be the first!", - style: TextStyle(color: Colors.grey, fontSize: 16), + l10n.noActiveRooms, + style: const TextStyle(color: Colors.grey, fontSize: 16), ), ], ), @@ -214,8 +219,8 @@ class _ExploreViewState extends State<_ExploreView> { ], ) : GridView.builder( - padding: EdgeInsets.all(12), - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + padding: const EdgeInsets.all(12), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 400, childAspectRatio: 1.2, crossAxisSpacing: 12, @@ -224,14 +229,14 @@ class _ExploreViewState extends State<_ExploreView> { itemCount: _activeRooms.length, itemBuilder: (context, index) { final room = _activeRooms[index]; - return _buildRoomCard(room, settings); + return _buildRoomCard(room, settings, l10n); }, ), ), ); } - Widget _buildRoomCard(dynamic room, SettingsProvider settings) { + Widget _buildRoomCard(dynamic room, SettingsProvider settings, AppLocalizations l10n) { final roomId = room['room_id'].toString(); return Card( elevation: 4, @@ -294,12 +299,12 @@ class _ExploreViewState extends State<_ExploreView> { top: 8, left: 8, child: Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(4), ), - child: Row( + child: const Row( children: [ Icon(Icons.circle, size: 8, color: Colors.white), SizedBox(width: 4), @@ -331,7 +336,7 @@ class _ExploreViewState extends State<_ExploreView> { radius: 16, child: Text(room['user_id'].toString().substring(0, 1)), ), - SizedBox(width: 12), + const SizedBox(width: 12), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -341,14 +346,14 @@ class _ExploreViewState extends State<_ExploreView> { room['title'], maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), Text( - "Host ID: ${room['user_id']}", - style: TextStyle(fontSize: 12, color: Colors.grey), + "${l10n.hostId}: ${room['user_id']}", + style: const TextStyle(fontSize: 12, color: Colors.grey), ), ], ), diff --git a/frontend/lib/pages/login_page.dart b/frontend/lib/pages/login_page.dart index ab1b21b..16632ed 100644 --- a/frontend/lib/pages/login_page.dart +++ b/frontend/lib/pages/login_page.dart @@ -1,6 +1,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'; import '../services/api_service.dart'; @@ -29,10 +30,11 @@ class _LoginPageState extends State { } void _handleLogin() async { + final l10n = AppLocalizations.of(context)!; if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) { ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text("Please fill in all fields"))); + ).showSnackBar(SnackBar(content: Text(l10n.fillAllFields))); return; } @@ -53,7 +55,7 @@ class _LoginPageState extends State { if (!mounted) { return; } - final error = jsonDecode(response.body)['error'] ?? "Login Failed"; + final error = jsonDecode(response.body)['error'] ?? l10n.loginFailed; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(error))); @@ -63,7 +65,7 @@ class _LoginPageState extends State { return; } ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Network Error: Could not connect to server")), + SnackBar(content: Text(l10n.networkError)), ); } finally { if (mounted) setState(() => _isLoading = false); @@ -72,11 +74,13 @@ class _LoginPageState extends State { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Scaffold( appBar: AppBar( actions: [ IconButton( - icon: Icon(Icons.settings), + icon: const Icon(Icons.settings), onPressed: () => Navigator.push( context, MaterialPageRoute(builder: (_) => const SettingsPage()), @@ -88,7 +92,7 @@ class _LoginPageState extends State { child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 400), + constraints: const BoxConstraints(maxWidth: 400), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -98,7 +102,7 @@ class _LoginPageState extends State { size: 80, color: Theme.of(context).colorScheme.primary, ), - SizedBox(height: 16), + const SizedBox(height: 16), Text( "HIGHTUBE", style: TextStyle( @@ -108,11 +112,11 @@ class _LoginPageState extends State { color: Theme.of(context).colorScheme.primary, ), ), - Text( + const Text( "Open Source Live Platform", style: TextStyle(color: Colors.grey), ), - SizedBox(height: 48), + const SizedBox(height: 48), // Fields TextField( @@ -120,14 +124,14 @@ class _LoginPageState extends State { textInputAction: TextInputAction.next, onSubmitted: (_) => _passwordFocusNode.requestFocus(), decoration: InputDecoration( - labelText: "Username", - prefixIcon: Icon(Icons.person), + labelText: l10n.username, + prefixIcon: const Icon(Icons.person), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), TextField( controller: _passwordController, focusNode: _passwordFocusNode, @@ -139,14 +143,14 @@ class _LoginPageState extends State { } }, decoration: InputDecoration( - labelText: "Password", - prefixIcon: Icon(Icons.lock), + labelText: l10n.password, + prefixIcon: const Icon(Icons.lock), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), ), ), - SizedBox(height: 32), + const SizedBox(height: 32), // Login Button SizedBox( @@ -160,14 +164,14 @@ class _LoginPageState extends State { ), ), child: _isLoading - ? CircularProgressIndicator() + ? const CircularProgressIndicator() : Text( - "LOGIN", - style: TextStyle(fontWeight: FontWeight.bold), + l10n.login, + style: const TextStyle(fontWeight: FontWeight.bold), ), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), // Register Link TextButton( @@ -175,7 +179,7 @@ class _LoginPageState extends State { context, MaterialPageRoute(builder: (_) => RegisterPage()), ), - child: Text("Don't have an account? Create one"), + child: Text(l10n.dontHaveAccount), ), ], ), diff --git a/frontend/lib/pages/my_stream_page.dart b/frontend/lib/pages/my_stream_page.dart index 06f0559..252a04b 100644 --- a/frontend/lib/pages/my_stream_page.dart +++ b/frontend/lib/pages/my_stream_page.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter/services.dart'; +import '../l10n/app_localizations.dart'; import '../providers/auth_provider.dart'; import '../providers/settings_provider.dart'; import '../services/api_service.dart'; @@ -42,9 +43,10 @@ class _MyStreamPageState extends State { if (!mounted) { return; } + final l10n = AppLocalizations.of(context); ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text("Failed to fetch room info"))); + ).showSnackBar(SnackBar(content: Text(l10n?.failedToFetchRoomInfo ?? "Failed to fetch room info"))); } finally { if (mounted) { setState(() => _isLoading = false); @@ -55,46 +57,47 @@ class _MyStreamPageState extends State { @override Widget build(BuildContext context) { final settings = context.watch(); + final l10n = AppLocalizations.of(context)!; return Scaffold( - appBar: AppBar(title: Text("My Stream Console")), + appBar: AppBar(title: Text(l10n.myStreamConsole)), body: _isLoading - ? Center(child: CircularProgressIndicator()) + ? const Center(child: CircularProgressIndicator()) : _roomInfo == null - ? Center(child: Text("No room info found.")) + ? Center(child: Text(l10n.noRoomInfo)) : SingleChildScrollView( - padding: EdgeInsets.all(20), + padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInfoCard( - title: "Room Title", + title: l10n.roomTitle, value: _roomInfo!['title'], icon: Icons.edit, onTap: () { // TODO: Implement title update API later ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Title editing coming soon!")), + const SnackBar(content: Text("Title editing coming soon!")), ); }, ), - SizedBox(height: 20), + const SizedBox(height: 20), _buildInfoCard( - title: "RTMP Server URL", + title: l10n.rtmpServerUrl, value: settings.rtmpUrl, icon: Icons.copy, onTap: () { Clipboard.setData(ClipboardData(text: settings.rtmpUrl)); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text("Server URL copied to clipboard"), + content: Text(l10n.copiedToClipboard), ), ); }, ), - SizedBox(height: 20), + const SizedBox(height: 20), _buildInfoCard( - title: "Stream Key (Keep Secret!)", + title: l10n.streamKey, value: _roomInfo!['stream_key'], icon: Icons.copy, isSecret: true, @@ -104,18 +107,18 @@ class _MyStreamPageState extends State { ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text("Stream Key copied to clipboard"), + content: Text(l10n.copiedToClipboard), ), ); }, ), - SizedBox(height: 24), + const SizedBox(height: 24), AndroidQuickStreamPanel( rtmpBaseUrl: settings.rtmpUrl, streamKey: _roomInfo!['stream_key'], ), - SizedBox(height: 30), - Center( + const SizedBox(height: 30), + const Center( child: Column( children: [ Icon(Icons.info_outline, color: Colors.grey), @@ -143,10 +146,10 @@ class _MyStreamPageState extends State { }) { return Card( child: ListTile( - title: Text(title, style: TextStyle(fontSize: 12, color: Colors.grey)), + title: Text(title, style: const TextStyle(fontSize: 12, color: Colors.grey)), subtitle: Text( isSecret ? "••••••••••••••••" : value, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), trailing: IconButton(icon: Icon(icon), onPressed: onTap), ), diff --git a/frontend/lib/pages/player_page.dart b/frontend/lib/pages/player_page.dart index fbc7cd4..3702731 100644 --- a/frontend/lib/pages/player_page.dart +++ b/frontend/lib/pages/player_page.dart @@ -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 { Future _openVolumeSheet() async { _showControls(); + final l10n = AppLocalizations.of(context)!; await showModalBottomSheet( context: context, builder: (context) { @@ -284,7 +286,7 @@ class _PlayerPageState extends State { 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 { if (!mounted) { return; } + final l10n = AppLocalizations.of(context)!; final nextResolution = await showModalBottomSheet( context: context, @@ -364,11 +367,11 @@ class _PlayerPageState extends State { 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 { ), 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 { } Widget _buildPlaybackControls() { + final l10n = AppLocalizations.of(context)!; return IgnorePointer( ignoring: !_controlsVisible, child: AnimatedOpacity( @@ -620,7 +624,7 @@ class _PlayerPageState extends State { children: [ _buildControlButton( icon: Icons.refresh, - label: "Refresh", + label: l10n.refresh, onPressed: _refreshPlayer, ), _buildControlButton( @@ -629,19 +633,19 @@ class _PlayerPageState extends State { : _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 { // 抽离聊天区域组件 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 { }, ), ), - Divider(height: 1), + const Divider(height: 1), Padding( padding: const EdgeInsets.all(8.0), child: Row( @@ -712,11 +717,11 @@ class _PlayerPageState extends State { 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, ), diff --git a/frontend/lib/pages/register_page.dart b/frontend/lib/pages/register_page.dart index 2b2902a..dde6419 100644 --- a/frontend/lib/pages/register_page.dart +++ b/frontend/lib/pages/register_page.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../l10n/app_localizations.dart'; import '../providers/settings_provider.dart'; import '../services/api_service.dart'; @@ -15,8 +16,9 @@ class _RegisterPageState extends State { bool _isLoading = false; void _handleRegister() async { + final l10n = AppLocalizations.of(context)!; if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Please fill in all fields"))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(l10n.fillAllFields))); return; } @@ -27,14 +29,14 @@ class _RegisterPageState extends State { try { final response = await api.register(_usernameController.text, _passwordController.text); if (response.statusCode == 201) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Account created! Please login."))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(l10n.accountCreated))); Navigator.pop(context); } else { final error = jsonDecode(response.body)['error'] ?? "Registration Failed"; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error))); } } catch (e) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Network Error"))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(l10n.networkError))); } finally { if (mounted) setState(() => _isLoading = false); } @@ -42,42 +44,43 @@ class _RegisterPageState extends State { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return Scaffold( - appBar: AppBar(title: Text("Create Account")), + appBar: AppBar(title: Text(l10n.createAccount)), body: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 400), + constraints: const BoxConstraints(maxWidth: 400), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.person_add_outlined, size: 64, color: Theme.of(context).colorScheme.primary), - SizedBox(height: 24), + const SizedBox(height: 24), Text( - "Join Hightube", + l10n.joinHightube, style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold), ), - SizedBox(height: 48), + const SizedBox(height: 48), TextField( controller: _usernameController, decoration: InputDecoration( - labelText: "Desired Username", - prefixIcon: Icon(Icons.person), + labelText: l10n.desiredUsername, + prefixIcon: const Icon(Icons.person), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), TextField( controller: _passwordController, obscureText: true, decoration: InputDecoration( - labelText: "Password", - prefixIcon: Icon(Icons.lock), + labelText: l10n.password, + prefixIcon: const Icon(Icons.lock), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), - SizedBox(height: 32), + const SizedBox(height: 32), SizedBox( width: double.infinity, height: 50, @@ -86,13 +89,13 @@ class _RegisterPageState extends State { style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), - child: _isLoading ? CircularProgressIndicator() : Text("REGISTER", style: TextStyle(fontWeight: FontWeight.bold)), + child: _isLoading ? const CircularProgressIndicator() : Text(l10n.register, style: const TextStyle(fontWeight: FontWeight.bold)), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), TextButton( onPressed: () => Navigator.pop(context), - child: Text("Already have an account? Login here"), + child: Text(l10n.alreadyHaveAccount), ), ], ), diff --git a/frontend/lib/pages/settings_page.dart b/frontend/lib/pages/settings_page.dart index 5041550..d4bebc2 100644 --- a/frontend/lib/pages/settings_page.dart +++ b/frontend/lib/pages/settings_page.dart @@ -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 { } } - Future _confirmLogout(AuthProvider auth) async { + Future _confirmLogout(AuthProvider auth, AppLocalizations l10n) async { final confirmed = await showDialog( 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 { final auth = context.watch(); final settings = context.watch(); 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 { _buildProfileSection(auth), const SizedBox(height: 32), ], - _buildSectionTitle("Network Configuration"), + + _buildSectionTitle(l10n.language), + const SizedBox(height: 16), + DropdownButtonFormField( + initialValue: settings.locale == null + ? null + : AppLocalizations.supportedLocales.cast().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 { ); 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 { ), 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( - segments: const [ + segments: [ ButtonSegment( value: ThemeMode.system, - label: Text("System"), + label: Text(l10n.system), icon: Icon(Icons.brightness_auto), ), ButtonSegment( value: ThemeMode.light, - label: Text("Light"), + label: Text(l10n.light), icon: Icon(Icons.light_mode), ), ButtonSegment( value: ThemeMode.dark, - label: Text("Dark"), + label: Text(l10n.dark), icon: Icon(Icons.dark_mode), ), ], @@ -210,7 +258,7 @@ class _SettingsPageState extends State { }, ), 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 { }).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 { 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 { 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 { 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), diff --git a/frontend/lib/providers/settings_provider.dart b/frontend/lib/providers/settings_provider.dart index 5a8329a..c0b8204 100644 --- a/frontend/lib/providers/settings_provider.dart +++ b/frontend/lib/providers/settings_provider.dart @@ -13,11 +13,13 @@ class SettingsProvider with ChangeNotifier { Color _themeColor = Colors.blue; ThemeMode _themeMode = ThemeMode.system; bool _livePreviewThumbnailsEnabled = false; + Locale? _locale; String get baseUrl => _baseUrl; Color get themeColor => _themeColor; ThemeMode get themeMode => _themeMode; bool get livePreviewThumbnailsEnabled => _livePreviewThumbnailsEnabled; + Locale? get locale => _locale; SettingsProvider() { _loadSettings(); @@ -36,6 +38,41 @@ class SettingsProvider with ChangeNotifier { } _livePreviewThumbnailsEnabled = prefs.getBool('livePreviewThumbnailsEnabled') ?? false; + + final languageCode = prefs.getString('languageCode'); + final scriptCode = prefs.getString('scriptCode'); + final countryCode = prefs.getString('countryCode'); + if (languageCode != null) { + _locale = Locale.fromSubtags( + languageCode: languageCode, + scriptCode: scriptCode, + countryCode: countryCode, + ); + } + + notifyListeners(); + } + + void setLocale(Locale? newLocale) async { + _locale = newLocale; + final prefs = await SharedPreferences.getInstance(); + if (newLocale == null) { + await prefs.remove('languageCode'); + await prefs.remove('scriptCode'); + await prefs.remove('countryCode'); + } else { + await prefs.setString('languageCode', newLocale.languageCode); + if (newLocale.scriptCode != null) { + await prefs.setString('scriptCode', newLocale.scriptCode!); + } else { + await prefs.remove('scriptCode'); + } + if (newLocale.countryCode != null) { + await prefs.setString('countryCode', newLocale.countryCode!); + } else { + await prefs.remove('countryCode'); + } + } notifyListeners(); } diff --git a/frontend/lib/widgets/android_quick_stream_panel.dart b/frontend/lib/widgets/android_quick_stream_panel.dart index 60f2a58..a7158fe 100644 --- a/frontend/lib/widgets/android_quick_stream_panel.dart +++ b/frontend/lib/widgets/android_quick_stream_panel.dart @@ -58,6 +58,13 @@ class _AndroidQuickStreamPanelState extends State { } Future _initialize() async { + if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) { + setState(() { + _isPreparing = false; + _statusMessage = 'Quick stream is only supported on Android.'; + }); + return; + } if (!mounted) { return; } diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 2938c45..85089e8 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -150,6 +150,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_test: dependency: "direct dev" description: flutter @@ -216,6 +221,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" json_annotation: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index afc7f26..bda1d6e 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -42,6 +42,9 @@ dependencies: web_socket_channel: ^3.0.3 permission_handler: ^12.0.1 rtmp_streaming: ^1.0.5 + intl: ^0.20.2 + flutter_localizations: + sdk: flutter dev_dependencies: flutter_test: @@ -75,6 +78,7 @@ flutter_launcher_icons: # The following section is specific to Flutter packages. flutter: + generate: true # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in