diff --git a/assets/translations/af.json b/assets/translations/af.json new file mode 100644 index 0000000..5a34872 --- /dev/null +++ b/assets/translations/af.json @@ -0,0 +1,166 @@ +{ + "navigation": { + "chat": "xxx", + "settings": "xxx", + "sessions": "xxx", + "worlds": "xxx", + "inventory": "xxx", + "findUsers": "xxx", + "myProfile": "xxx" + }, + "settings": { + "title": "xxx", + "notifications": { + "title": "xxx", + "enabled": "xxx", + "showusernames": "xxx" + }, + "appearance": { + "title": "xxx", + "theme": "xxx" + }, + "other": { + "title": "xxx", + "signout": "xxx", + "signout.confirm": "xxx", + "about": "xxx", + "error": "xxx" + } + }, + "general": { + "yes": "xxx", + "no": "xxx", + "confirm": "xxx", + "cancel": "xxx", + "retry": "xxx", + "search": "xxx", + "enabled": "xxx", + "disabled": "xxx" + }, + "contacts": { + "status": { + "sociable": "xxx", + "online": "xxx", + "away": "xxx", + "busy": "xxx", + "invisible": "xxx", + "offline": "xxx" + }, + "headlessHost": "xxx", + "inWorld": "xxx", + "inWorldNoDetails": "xxx", + "onVersion": "xxx", + "bot": "xxx" + }, + "messaging": { + "placeholder": "xxx", + "noMessages": "xxx", + "failedSend": "xxx", + "recording": "xxx", + "cancelRecording": "xxx", + "removeAttachments": "xxx", + "removeAttachments.description": "xxx", + "invite": { + "requestedSelf": "xxx", + "granted": "xxx", + "addAsContact": "xxx", + "requested": "xxx", + "unknownResponse": "xxx", + "requestForwarded": "xxx", + "requestToYou": "xxx", + "requestGeneric": "xxx", + "hostedBy": "xxx" + }, + "send": "xxx", + "copiedToClipboard": "xxx" + }, + "sessions": { + "title": "xxx", + "headless": "xxx", + "access": "xxx", + "users": "xxx", + "sessionUsers": "xxx", + "noSessionsFound": "xxx", + "noSessionsFound.description": "xxx", + "noLongerValid": "xxx", + "accessLevel": { + "unknown": "xxx", + "private": "xxx", + "lan": "xxx", + "contacts": "xxx", + "contactsPlus": "xxx", + "registeredUsers": "xxx", + "anyone": "xxx" + } + }, + "worlds": { + "title": "xxx", + "preview": "xxx", + "search": "xxx", + "inputTags": "xxx", + "failedToLoad": "xxx", + "noWorldsFound": "xxx", + "noWorldsFound.description": "xxx", + "description": "xxx", + "noDescription": "xxx", + "details": "xxx", + "tags": "xxx", + "noTags": "xxx", + "createdAt": "xxx", + "lastModified": "xxx", + "visits": "xxx", + "rating": "xxx", + "sorting": { + "ascending": "xxx", + "descending": "xxx", + "creationDate": "xxx", + "lastUpdateDate": "xxx", + "firstPublishTime": "xxx", + "totalVisits": "xxx", + "name": "xxx", + "random": "xxx" + } + }, + "inventory": { + "title": "xxx", + "selection": "xxx", + "sorting": { + "ascending": "xxx", + "descending": "xxx", + "name": "xxx", + "date": "xxx", + "resonite": "xxx" + }, + "download": { + "title": "xxx", + "selectionAborted": "xxx", + "selectionInvalid": "xxx", + "downloadSuccess": "xxx", + "downloadFailed": "xxx", + "downloadError": "xxx" + } + }, + "login": { + "title": "xxx", + "signin": "xxx", + "username": "xxx", + "password": "xxx", + "totp": "xxx", + "invalidCredentials": "xxx", + "invalidServerResponse": "xxx", + "totpRequired": "xxx", + "invalidTotp": "xxx", + "unsuccessful": "xxx" + }, + "update": { + "title": "xxx", + "description": "xxx", + "localVersion": "xxx", + "remoteVersion": "xxx", + "openDownload": "xxx", + "dismiss": "xxx" + }, + "uncategorized": { + "featureUnavailable": "xxx" + } +} diff --git a/assets/translations/de.json b/assets/translations/de.json new file mode 100644 index 0000000..2405be8 --- /dev/null +++ b/assets/translations/de.json @@ -0,0 +1,8 @@ +{ + "general": { + "yes": "Ja", + "no": "Nein", + "confirm": "Bestätigen", + "cancel": "Abbrechen" + } +} \ No newline at end of file diff --git a/assets/translations/en.json b/assets/translations/en.json new file mode 100644 index 0000000..b8e87a8 --- /dev/null +++ b/assets/translations/en.json @@ -0,0 +1,166 @@ +{ + "navigation": { + "chat": "Chat", + "settings": "@:settings.title", + "sessions": "@:sessions.title", + "worlds": "@:worlds.title", + "inventory": "@:inventory.title", + "findUsers": "Find Users", + "myProfile": "My Profile" + }, + "settings": { + "title": "Settings", + "notifications": { + "title": "Notifications", + "enabled": "Enable Notifications", + "showusernames": "Show Usernames" + }, + "appearance": { + "title": "Appearance", + "theme": "Theme Mode" + }, + "other": { + "title": "Other", + "signout": "Sign out", + "signout.confirm": "Are you sure you want to sign out?", + "about": "About ReCon", + "error": "Failed to open link." + } + }, + "general": { + "yes": "Yes", + "no": "No", + "confirm": "Confirm", + "cancel": "Cancel", + "retry": "Retry", + "search": "Search", + "enabled": "Enabled", + "disabled": "Disabled" + }, + "contacts": { + "status": { + "sociable": "Sociable", + "online": "Online", + "away": "Away", + "busy": "Busy", + "invisible": "Invisible", + "offline": "Offline" + }, + "headlessHost": "Headless Host", + "inWorld": "{} in ", + "inWorldNoDetails": "{} in {} World", + "onVersion": "{} on version {}", + "bot": "Bot" + }, + "messaging": { + "placeholder": "Message {}...", + "noMessages": "There are no messages here\nWhy not say hello?", + "failedSend": "Failed to send a message: {}", + "recording": "Recording: {}", + "cancelRecording": "Cancel Recording", + "removeAttachments": "Remove all attachments", + "removeAttachments.description": "This will remove all attachments, are you sure?", + "invite": { + "requestedSelf": "Requested invite", + "granted": "Granted {} invite to session {}", + "addAsContact": "Adding {} as a contact of the headless", + "unknownResponse": "Unknown response: {} for invite request", + "requestForwarded": "{} wants to invite {} to {}", + "requestToYou": "{} would like to join you in '{}'", + "requestGeneric": "{} would like to join you", + "hostedBy": "Hosted by {}" + }, + "send": "Send", + "copiedToClipboard": "Copied to clipboard" + }, + "sessions": { + "title": "Sessions", + "preview": "Session Preview", + "headless": "Headless:", + "access": "Access:", + "users": "Users:", + "sessionUsers": "{} / {} Online", + "noSessionsFound": "No Sessions Found", + "noSessionsFound.description": "Try to adjust your filters", + "noLongerValid": "Session is no longer valid", + "accessLevel": { + "unknown": "Unknown", + "private": "Private", + "lan": "LAN", + "contacts": "Contacts", + "contactsPlus": "Contacts+", + "registeredUsers": "Registered users", + "anyone": "Public" + } + }, + "worlds": { + "title": "Worlds", + "preview": "World Preview", + "search": "Search for worlds", + "inputTags": "Enter tags", + "failedToLoad": "Failed to load Worlds", + "noWorldsFound": "No Worlds Found", + "noWorldsFound.description": "Try to adjust your filters", + "description": "Description:", + "noDescription": "No description", + "details": "Details:", + "tags": "Tags:", + "noTags": "None", + "createdAt": "Created at:", + "lastModified": "Last modified:", + "visits": "Visits:", + "rating": "Rating:", + "sorting": { + "ascending": "Ascending", + "descending": "Descending", + "creationDate": "Created", + "lastUpdateDate": "Last Updated", + "firstPublishTime": "First Published", + "totalVisits": "Total Visits", + "name": "Name", + "random": "Random" + } + }, + "inventory": { + "title": "Inventory", + "selection": "{} Selected", + "sorting": { + "ascending": "Ascending", + "descending": "Descending", + "name": "Name", + "date": "Date", + "resonite": "Resonite" + }, + "download": { + "title": "What would you like to download?", + "selectionAborted": "Selection aborted", + "selectionInvalid": "Selected directory is invalid", + "downloadSuccess": "Downloaded {}", + "downloadFailed": "Failed to download {} : {}", + "downloadError": "Unknown Error" + } + }, + "login": { + "title": "Login", + "signin": "Sign In", + "username": "Username", + "password": "Password", + "totp": "2FA Code", + "invalidCredentials": "Please enter a valid username/password combination.", + "invalidServerResponse":"Login unsuccessful: Server sent invalid response.", + "totpRequired": "Please enter your 2FA-Code", + "invalidTotp": "The given 2FA code is not valid.", + "unsuccessful": "Login unsuccessful: {}" + }, + "update": { + "title": "Update Available", + "description": "There is a new version available for download!", + "localVersion": "Your version: {}", + "remoteVersion": "New version: {}", + "openDownload": "Get it on Github", + "dismiss": "I'll do it later." + }, + "uncategorized": { + "featureUnavailable": "Sorry, this feature is not yet available" + } +} diff --git a/assets/translations/eo.json b/assets/translations/eo.json new file mode 100644 index 0000000..74dc8cc --- /dev/null +++ b/assets/translations/eo.json @@ -0,0 +1,81 @@ +{ + "navigation": { + "chat": "Kontaktoj" + }, + "settings": { + "title": "Agordoj", + "other": { + "title": "Alia" + } + }, + "general": { + "yes": "Jes", + "no": "Ne", + "confirm": "Ĉu vere", + "cancel": "Nuligi", + "retry": "Retry", + "search": "Serĉi", + "enabled": "Ebligita", + "disabled": "Malŝaltita" + }, + "contacts": { + "status": { + "sociable": "Sociema", + "online": "Enrete", + "away": "For", + "busy": "Okupata", + "invisible": "Nevidebla", + "offline": "Eksterrete" + }, + "headlessHost": "Senkapa Gastigo", + "bot": "Robot-konto" + }, + "messaging": { + "send": "Sendi", + "copiedToClipboard": "Kopiita al tondejo" + }, + "sessions": { + "access": "Videblo:", + "users": "Uzantoj:", + "sessionUsers": "{} / {} Uzantoj", + "accessLevel": { + "unknown": "Neniu seanco", + "private": "Privata", + "lan": "Loka Reto", + "contacts": "Kontaktoj", + "contactsPlus": "Kontaktoj+", + "registeredUsers": "Registritaj Uzantoj", + "anyone": "Iu ajn" + } + }, + "worlds": { + "title": "Mondoj", + "description": "Priskribo:", + "tags": "Etikedoj:", + "noTags": "Neniu", + "lastModified": "Laste ĝisdatigita:", + "visits": "Vizitoj:", + "sorting": { + "ascending": "Kreskante", + "descending": "Malkreskante", + "creationDate": "Dato de Kreo", + "lastUpdateDate": "Dato de Modifo", + "firstPublishTime": "Dato de Publikigo", + "totalVisits": "Vizitoj Entute", + "name": "Nomo", + "random": "Hazarde" + } + }, + "inventory": { + "title": "Inventaro", + "sorting": { + "ascending": "Kreskante", + "descending": "Malkreskante", + "name": "Nomo", + "date": "Dato" + }, + "download": { + "downloadError": "Nekonanta Eraro" + } + } +} \ No newline at end of file diff --git a/assets/translations/es.json b/assets/translations/es.json new file mode 100644 index 0000000..b7b4efc --- /dev/null +++ b/assets/translations/es.json @@ -0,0 +1,8 @@ +{ + "general": { + "yes": "Sí", + "no": "No", + "confirm": "Ĉu vere", + "cancel": "Cancelar" + } +} \ No newline at end of file diff --git a/assets/translations/fr.json b/assets/translations/fr.json new file mode 100644 index 0000000..c89461e --- /dev/null +++ b/assets/translations/fr.json @@ -0,0 +1,8 @@ +{ + "general": { + "yes": "Oui", + "no": "Non", + "confirm": "Confirmer", + "cancel": "Annuler" + } +} \ No newline at end of file diff --git a/assets/translations/ja.json b/assets/translations/ja.json new file mode 100644 index 0000000..948597c --- /dev/null +++ b/assets/translations/ja.json @@ -0,0 +1,8 @@ +{ + "general": { + "yes": "はい", + "no": "いいえ", + "confirm": "よろしいですか?", + "cancel": "キャンセル" + } +} \ No newline at end of file diff --git a/assets/translations/ko.json b/assets/translations/ko.json new file mode 100644 index 0000000..53aa931 --- /dev/null +++ b/assets/translations/ko.json @@ -0,0 +1,8 @@ +{ + "general": { + "yes": "네", + "no": "아니요", + "confirm": "확실합니까?", + "cancel": "취소" + } +} \ No newline at end of file diff --git a/assets/translations/ru.json b/assets/translations/ru.json new file mode 100644 index 0000000..35a4716 --- /dev/null +++ b/assets/translations/ru.json @@ -0,0 +1,8 @@ +{ + "general": { + "yes": "Да", + "no": "Нет", + "confirm": "Точно", + "cancel": "Отмена" + } +} \ No newline at end of file diff --git a/assets/translations/zh-cn.json b/assets/translations/zh-cn.json new file mode 100644 index 0000000..46a4d6c --- /dev/null +++ b/assets/translations/zh-cn.json @@ -0,0 +1,8 @@ +{ + "general": { + "yes": "是", + "no": "否", + "confirm": "确定吗?", + "cancel": "取消" + } +} \ No newline at end of file diff --git a/build_test_lang.py b/build_test_lang.py new file mode 100644 index 0000000..d43d325 --- /dev/null +++ b/build_test_lang.py @@ -0,0 +1,19 @@ +import json +from pathlib import Path + +base_translation = Path("assets/translations/en.json") + +with open(base_translation, encoding="utf-8") as f: + data = json.load(f) + +# The raw keyname path is shown when a translation is missing. +# Since locales also fallback to english, it was hard to tell when the translation was working. +def replace(data): + if isinstance(data, dict): + return {key: replace(data) for key, data in data.items()} + return "xxx" if isinstance(data, str) else data + +# Uses 'af' as it is a valid language code and we probably won't have it translated to this (unless Resonite adds it as well) +with open("assets/translations/af.json", "w", encoding="utf-8") as f: + json.dump(replace(data), f, indent="\t", ensure_ascii=False) + f.write("\n") \ No newline at end of file diff --git a/lib/auxiliary.dart b/lib/auxiliary.dart index 94d0b01..2675f7e 100644 --- a/lib/auxiliary.dart +++ b/lib/auxiliary.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:html/parser.dart' as htmlparser; import 'package:path/path.dart' as p; diff --git a/lib/main.dart b/lib/main.dart index 3f0bbbb..85eafe5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,12 +4,12 @@ import 'dart:ui'; import 'package:background_downloader/background_downloader.dart'; import 'package:dynamic_color/dynamic_color.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_phoenix/flutter_phoenix.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'package:intl/intl.dart'; import 'package:just_audio_media_kit/just_audio_media_kit.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -30,6 +30,7 @@ import 'models/authentication_data.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await EasyLocalization.ensureInitialized(); try { JustAudioMediaKit.ensureInitialized(); // Windows and Linux are enabled by default. } catch (e) { @@ -64,7 +65,26 @@ void main() async { // Ignore } - runApp(ReCon(settingsClient: settingsClient, cachedAuthentication: cachedAuth)); + runApp( + EasyLocalization( + supportedLocales: const [ + Locale('af'), //testing only + Locale('en'), + Locale('eo'), + Locale('es'), + Locale('fr'), + Locale('de'), + Locale('ja'), + Locale('ko'), + Locale('ru'), + Locale('zh-cn') + ], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + useFallbackTranslations: true, + child: ReCon(settingsClient: settingsClient, cachedAuthentication: cachedAuth) + ) + ); } class ReCon extends StatefulWidget { @@ -230,6 +250,20 @@ class _ReConState extends State { colorScheme: darkDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark), ), themeMode: ThemeMode.values[widget.settingsClient.currentSettings.themeMode.valueOrDefault], + locale: context.locale, + localizationsDelegates: context.localizationDelegates, + supportedLocales: const [ + Locale('af'), // testing only + Locale('en'), + //Locale('eo'), // Not supported by MaterialLocalizations + Locale('es'), + Locale('fr'), + Locale('de'), + Locale('ja'), + Locale('ko'), + Locale('ru'), + Locale('zh') // only base supported, not zh-cn? + ], home: Builder( // Builder is necessary here since we need a context which has access to the ClientHolder builder: (context) { diff --git a/lib/models/records/search_sort.dart b/lib/models/records/search_sort.dart index 6830b70..c66c760 100644 --- a/lib/models/records/search_sort.dart +++ b/lib/models/records/search_sort.dart @@ -14,7 +14,7 @@ enum SearchSortParameter { firstPublishTime, totalVisits, name, - rand; + random; @override String toString() => const { @@ -23,7 +23,7 @@ enum SearchSortParameter { SearchSortParameter.firstPublishTime: "First Published", SearchSortParameter.totalVisits: "Total Visits", SearchSortParameter.name: "Name", - SearchSortParameter.rand: "Random", + SearchSortParameter.random: "Random", }[this]!; String serialize() => toBeginningOfSentenceCase(this.name); diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 1269733..c60b523 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -44,6 +44,7 @@ class Settings { final SettingsEntry sessionViewLastIncludeEnded; final SettingsEntry sessionViewLastIncludeEmpty; final SettingsEntry sessionViewLastIncludeIncompatible; + final SettingsEntry locale; Settings({ SettingsEntry? notificationsDenied, @@ -55,6 +56,7 @@ class Settings { SettingsEntry? sessionViewLastIncludeEnded, SettingsEntry? sessionViewLastIncludeEmpty, SettingsEntry? sessionViewLastIncludeIncompatible, + SettingsEntry? locale, }) : notificationsDenied = notificationsDenied ?? const SettingsEntry(deflt: false), lastOnlineStatus = lastOnlineStatus ?? SettingsEntry(deflt: OnlineStatus.online.index), themeMode = themeMode ?? SettingsEntry(deflt: ThemeMode.dark.index), @@ -64,7 +66,8 @@ class Settings { sessionViewLastIncludeEnded = sessionViewLastIncludeEnded ?? const SettingsEntry(deflt: false), sessionViewLastIncludeEmpty = sessionViewLastIncludeEmpty ?? const SettingsEntry(deflt: true), sessionViewLastIncludeIncompatible = - sessionViewLastIncludeIncompatible ?? const SettingsEntry(deflt: false); + sessionViewLastIncludeIncompatible ?? const SettingsEntry(deflt: false), + locale = locale ?? const SettingsEntry(deflt: 'en'); factory Settings.fromMap(Map map) { return Settings( @@ -77,6 +80,7 @@ class Settings { sessionViewLastIncludeEnded: getEntryOrNull(map["sessionViewLastIncludeEnded"]), sessionViewLastIncludeEmpty: getEntryOrNull(map["sessionViewLastIncludeEmpty"]), sessionViewLastIncludeIncompatible: getEntryOrNull(map["sessionViewLastIncludeIncompatible"]), + locale: getEntryOrNull(map["locale"]), ); } @@ -100,6 +104,7 @@ class Settings { "sessionViewLastIncludeEnded": sessionViewLastIncludeEnded.toMap(), "sessionViewLastIncludeEmpty": sessionViewLastIncludeEmpty.toMap(), "sessionViewLastIncludeIncompatible": sessionViewLastIncludeIncompatible.toMap(), + "locale": locale.toMap(), }; } @@ -115,6 +120,7 @@ class Settings { bool? sessionViewLastIncludeEnded, bool? sessionViewLastIncludeEmpty, bool? sessionViewLastIncludeIncompatible, + String? locale, }) { return Settings( notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied), @@ -127,6 +133,7 @@ class Settings { sessionViewLastIncludeEmpty: this.sessionViewLastIncludeEmpty.passThrough(sessionViewLastIncludeEmpty), sessionViewLastIncludeIncompatible: this.sessionViewLastIncludeIncompatible.passThrough(sessionViewLastIncludeIncompatible), + locale: this.locale.passThrough(locale), ); } } \ No newline at end of file diff --git a/lib/models/users/friend.dart b/lib/models/users/friend.dart index 71f79d6..7deec3b 100644 --- a/lib/models/users/friend.dart +++ b/lib/models/users/friend.dart @@ -29,6 +29,10 @@ class Friend implements Comparable { bool get isHeadless => userStatus.sessionType == UserSessionType.headless; + bool get isOnChatClient => userStatus.sessionType == UserSessionType.chatClient; + + bool get isOnReCon => userStatus.appVersion.contains("ReCon"); + bool get isBot => userStatus.sessionType == UserSessionType.bot || contactUserId == _resoniteBotId; bool get isSociable => (userStatus.onlineStatus == OnlineStatus.sociable) && !isBot && !isHeadless; diff --git a/lib/widgets/default_error_widget.dart b/lib/widgets/default_error_widget.dart index 5a37759..0c5b80c 100644 --- a/lib/widgets/default_error_widget.dart +++ b/lib/widgets/default_error_widget.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; class DefaultErrorWidget extends StatelessWidget { @@ -38,7 +39,7 @@ class DefaultErrorWidget extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), ), icon: const Icon(Icons.refresh), - label: const Text("Retry"), + label: Text('general.retry'.tr()), ), ], ), diff --git a/lib/widgets/friends/friend_list_tile.dart b/lib/widgets/friends/friend_list_tile.dart index 35cd89a..e31a97a 100644 --- a/lib/widgets/friends/friend_list_tile.dart +++ b/lib/widgets/friends/friend_list_tile.dart @@ -1,7 +1,7 @@ import 'dart:async'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:recon/auxiliary.dart'; import 'package:recon/clients/messaging_client.dart'; @@ -57,11 +57,10 @@ class FriendListTile extends StatelessWidget { const SizedBox( width: 4, ), - if (!(friend.isOffline || friend.isHeadless)) ...[ - Text(toBeginningOfSentenceCase(friend.userStatus.onlineStatus.name) ?? "Unknown"), + if (!(friend.isOffline || friend.isHeadless || friend.isBot)) ...[ if (currentSession != null) ...[ - const Text(" in "), - if (currentSession.name.isNotEmpty) + if (currentSession.name.isNotEmpty) ...[ + Text('contacts.inWorld'.tr(args: ['contacts.status.${friend.userStatus.onlineStatus.name}'.tr()])), Expanded( child: FormattedText( currentSession.formattedName, @@ -69,10 +68,11 @@ class FriendListTile extends StatelessWidget { maxLines: 1, ), ) + ] else Expanded( child: Text( - "${currentSession.accessLevel.toReadableString()} World", + 'contacts.inWorldNoDetails'.tr(args: ['contacts.status.${friend.userStatus.onlineStatus.name}'.tr(), 'sessions.accessLevel.${currentSession.accessLevel.name}'.tr()]), overflow: TextOverflow.ellipsis, maxLines: 1, ), @@ -80,23 +80,30 @@ class FriendListTile extends StatelessWidget { ] else if (friend.userStatus.appVersion.isNotEmpty) Expanded( child: Text( - " on version ${friend.userStatus.appVersion}", + 'contacts.onVersion'.tr(args: ['contacts.status.${friend.userStatus.onlineStatus.name}'.tr(), friend.userStatus.appVersion]), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ] else if (friend.isOffline) Text( - "Offline", + 'contacts.status.offline'.tr(), overflow: TextOverflow.ellipsis, maxLines: 1, style: theme.textTheme.bodyMedium?.copyWith( color: OnlineStatus.offline.color(context), ), ) + else if (friend.isBot) + Text( + 'contacts.bot'.tr(), + overflow: TextOverflow.ellipsis, + maxLines: 1, + + ) else Text( - "Headless Host", + 'contacts.headlessHost'.tr(), overflow: TextOverflow.ellipsis, maxLines: 1, style: theme.textTheme.bodyMedium?.copyWith( diff --git a/lib/widgets/friends/friend_online_status_indicator.dart b/lib/widgets/friends/friend_online_status_indicator.dart index dc88e7b..47d0690 100644 --- a/lib/widgets/friends/friend_online_status_indicator.dart +++ b/lib/widgets/friends/friend_online_status_indicator.dart @@ -10,7 +10,7 @@ class FriendOnlineStatusIndicator extends StatelessWidget { Widget build(BuildContext context) { final userStatus = friend.userStatus; final onlineStatus = userStatus.onlineStatus; - return userStatus.appVersion.contains("ReCon") && friend.isOnline + return friend.isOnReCon && friend.isOnline ? Image.asset( "assets/images/logo-white.png", color: onlineStatus.color(context), diff --git a/lib/widgets/friends/friends_list_app_bar.dart b/lib/widgets/friends/friends_list_app_bar.dart index cf49f2b..7790154 100644 --- a/lib/widgets/friends/friends_list_app_bar.dart +++ b/lib/widgets/friends/friends_list_app_bar.dart @@ -1,6 +1,6 @@ import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:recon/client_holder.dart'; import 'package:recon/clients/messaging_client.dart'; @@ -38,7 +38,7 @@ class _FriendsListAppBarState extends State with AutomaticKee color: client.userStatus.onlineStatus.color(context), ), ), - Text(toBeginningOfSentenceCase(client.userStatus.onlineStatus.name) ?? "Unknown"), + Text('contacts.status.${client.userStatus.onlineStatus.name}').tr(), ], ), ), @@ -75,7 +75,7 @@ class _FriendsListAppBarState extends State with AutomaticKee const SizedBox( width: 8, ), - Text(toBeginningOfSentenceCase(item.name)!), + Text('contacts.status.${item.name}').tr(), ], ), ), @@ -93,7 +93,7 @@ class _FriendsListAppBarState extends State with AutomaticKee }, itemBuilder: (context) => [ MenuItemDefinition( - name: "Find Users", + name: 'navigation.findUsers'.tr(), icon: Icons.person_add, onTap: () async { final mClient = Provider.of(context, listen: false); @@ -108,7 +108,7 @@ class _FriendsListAppBarState extends State with AutomaticKee }, ), MenuItemDefinition( - name: "My Profile", + name: 'navigation.myProfile'.tr(), icon: Icons.person, onTap: () async { await showDialog( @@ -120,7 +120,7 @@ class _FriendsListAppBarState extends State with AutomaticKee }, ), MenuItemDefinition( - name: "Settings", + name: 'navigation.settings'.tr(), icon: Icons.settings, onTap: () { Navigator.of(context).push( diff --git a/lib/widgets/homepage.dart b/lib/widgets/homepage.dart index 32c064b..aa776fe 100644 --- a/lib/widgets/homepage.dart +++ b/lib/widgets/homepage.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:recon/widgets/friends/friends_list.dart'; import 'package:recon/widgets/friends/friends_list_app_bar.dart'; @@ -6,7 +7,6 @@ import 'package:recon/widgets/inventory/inventory_browser_app_bar.dart'; import 'package:recon/widgets/sessions/session_list.dart'; import 'package:recon/widgets/sessions/session_list_app_bar.dart'; import 'package:recon/widgets/settings_app_bar.dart'; -import 'package:recon/widgets/settings_page.dart'; import 'package:recon/widgets/worlds/world_list.dart'; import 'package:recon/widgets/worlds/world_list_app_bar.dart'; @@ -71,22 +71,22 @@ class _HomeState extends State { _selectedPage = index; }); }, - destinations: const [ + destinations: [ NavigationDestination( icon: Icon(Icons.message), - label: "Chat", + label: 'navigation.chat'.tr(), ), NavigationDestination( icon: Icon(Icons.groups), - label: "Sessions", + label: 'navigation.sessions'.tr(), ), NavigationDestination( icon: Icon(Icons.public), - label: "Worlds", + label: 'navigation.worlds'.tr(), ), NavigationDestination( icon: Icon(Icons.inventory), - label: "Inventory", + label: 'navigation.inventory'.tr(), ), ], ), diff --git a/lib/widgets/inventory/inventory_browser_app_bar.dart b/lib/widgets/inventory/inventory_browser_app_bar.dart index 861ebba..db0df09 100644 --- a/lib/widgets/inventory/inventory_browser_app_bar.dart +++ b/lib/widgets/inventory/inventory_browser_app_bar.dart @@ -2,9 +2,9 @@ import 'dart:io'; import 'dart:ui'; import 'package:background_downloader/background_downloader.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; @@ -45,7 +45,7 @@ class _InventoryBrowserAppBarState extends State { child: !iClient.isAnyRecordSelected ? AppBar( key: const ValueKey("default-appbar"), - title: const Text("Inventory"), + title: const Text('inventory.title').tr(), actions: [ PopupMenuButton( icon: const Icon(Icons.swap_vert), @@ -66,7 +66,7 @@ class _InventoryBrowserAppBarState extends State { width: 8, ), Text( - "Ascending", + 'inventory.sorting.ascending'.tr(), style: TextStyle( color: !iClient.sortReverse ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface, ), @@ -83,7 +83,7 @@ class _InventoryBrowserAppBarState extends State { width: 8, ), Text( - "Descending", + 'inventory.sorting.descending'.tr(), style: TextStyle( color: iClient.sortReverse ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface, ), @@ -116,7 +116,7 @@ class _InventoryBrowserAppBarState extends State { width: 8, ), Text( - toBeginningOfSentenceCase(e.name) ?? e.name, + 'inventory.sorting.${e.name}'.tr(), style: TextStyle( color: iClient.sortMode == e ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface, ), @@ -133,7 +133,7 @@ class _InventoryBrowserAppBarState extends State { ) : AppBar( key: const ValueKey("selection-appbar"), - title: Text("${iClient.selectedRecordCount} Selected"), + title: Text('inventory.selection'.tr(args: [iClient.selectedRecordCount.toString()])), leading: IconButton( onPressed: () { iClient.clearSelectedRecords(); @@ -161,7 +161,7 @@ class _InventoryBrowserAppBarState extends State { builder: (context) { return AlertDialog( icon: const Icon(Icons.download), - title: const Text("Download what?"), + title: const Text('inventory.download.title').tr(), content: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -197,8 +197,8 @@ class _InventoryBrowserAppBarState extends State { if (directory == null) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Selection aborted."), + SnackBar( + content: const Text('inventory.download.selectionAborted').tr(), ), ); } @@ -207,8 +207,8 @@ class _InventoryBrowserAppBarState extends State { if (directory == "/") { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Selected directory is invalid"), + SnackBar( + content: const Text('inventory.download.selectionInvalid').tr(), ), ); } @@ -238,11 +238,11 @@ class _InventoryBrowserAppBarState extends State { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text("Downloaded ${record.formattedName}"), + content: Text('inventory.download.downloadSuccess'.tr(args: [record.formattedName.toString()])), ), ); } else { - throw downloadStatus.exception ?? "Unknown Error"; + throw downloadStatus.exception ?? 'inventory.download.downloadError'.tr(); } } } catch (e, s) { @@ -251,7 +251,7 @@ class _InventoryBrowserAppBarState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - "Failed to download '${record.formattedName}':\n$e", + 'inventory.download.downloadFailed'.tr(args: [record.formattedName.toString(), e.toString()]), ), ), ); @@ -285,7 +285,7 @@ class _InventoryBrowserAppBarState extends State { : () { Navigator.of(context).pop(false); }, - child: const Text("Cancel"), + child: const Text('general.cancel').tr(), ), Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/widgets/inventory/object_inventory_tile.dart b/lib/widgets/inventory/object_inventory_tile.dart index 6fb78b6..26935eb 100644 --- a/lib/widgets/inventory/object_inventory_tile.dart +++ b/lib/widgets/inventory/object_inventory_tile.dart @@ -1,8 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:recon/auxiliary.dart'; -import 'package:recon/models/records/record.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:recon/auxiliary.dart'; +import 'package:recon/models/records/record.dart'; import '../formatted_text.dart'; diff --git a/lib/widgets/login_screen.dart b/lib/widgets/login_screen.dart index ba4605a..f56ba22 100644 --- a/lib/widgets/login_screen.dart +++ b/lib/widgets/login_screen.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; @@ -50,7 +51,7 @@ class _LoginScreenState extends State { Future submit() async { if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) { setState(() { - _error = "Please enter a valid username/password combination."; + _error = 'login.invalidCredentials'.tr(); }); return; } @@ -66,7 +67,7 @@ class _LoginScreenState extends State { ); if (!authData.isAuthenticated) { setState(() { - _error = "Login unsuccessful: Server sent invalid response."; + _error = 'login.invalidServerResponse'.tr(); _isLoading = false; }); return; @@ -80,7 +81,7 @@ class _LoginScreenState extends State { setState(() { if (e == ApiClient.totpKey) { if (!_needsTotp) { - _error = "Please enter your 2FA-Code"; + _error = 'login.totpRequired'.tr(); _totpFocusNode.requestFocus(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _scrollController.animateTo( @@ -90,11 +91,11 @@ class _LoginScreenState extends State { ); }); } else { - _error = "The given 2FA code is not valid."; + _error = 'login.invalidTotp'.tr(); } _needsTotp = true; } else { - _error = "Login unsuccessful: $e."; + _error = 'login.unsuccessful'.tr(args:[e.toString()]); } if (kDebugMode) { FlutterError.reportError( @@ -126,7 +127,7 @@ class _LoginScreenState extends State { Navigator.of(context).pop(); await settingsClient.changeSettings(settingsClient.currentSettings.copyWith(notificationsDenied: true)); }, - child: const Text("No"), + child: const Text('general.no').tr(), ), TextButton( onPressed: () async { @@ -148,7 +149,7 @@ class _LoginScreenState extends State { settingsClient.currentSettings.copyWith(notificationsDenied: !(requestResult == null) && !requestResult), ); }, - child: const Text("Yes"), + child: const Text('general.yes').tr(), ), ], ); @@ -174,7 +175,7 @@ class _LoginScreenState extends State { Padding( padding: const EdgeInsets.symmetric(vertical: 64), child: Center( - child: Text("Sign In", style: Theme.of(context).textTheme.headlineMedium), + child: Text('login.signin', style: Theme.of(context).textTheme.headlineMedium).tr(), ), ), Padding( @@ -188,7 +189,7 @@ class _LoginScreenState extends State { border: OutlineInputBorder( borderRadius: BorderRadius.circular(32), ), - labelText: 'Username', + labelText: 'login.username'.tr(), filled: true, fillColor: Theme.of(context).colorScheme.surfaceContainerLowest, ), @@ -205,7 +206,7 @@ class _LoginScreenState extends State { decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24), border: OutlineInputBorder(borderRadius: BorderRadius.circular(32)), - labelText: 'Password', + labelText: 'login.password'.tr(), filled: true, fillColor: Theme.of(context).colorScheme.surfaceContainerLowest, ), @@ -224,7 +225,7 @@ class _LoginScreenState extends State { border: OutlineInputBorder( borderRadius: BorderRadius.circular(32), ), - labelText: '2FA Code', + labelText: 'login.totp'.tr(), filled: true, fillColor: Theme.of(context).colorScheme.surfaceContainerLowest, ), @@ -257,11 +258,11 @@ class _LoginScreenState extends State { ), const SizedBox(width: 16), Text( - "Login", + 'login.title', style: Theme.of(context).textTheme.bodyLarge?.apply( color: Theme.of(context).colorScheme.primary, ), - ), + ).tr(), const SizedBox(width: 4), ], ), diff --git a/lib/widgets/messages/camera_image_view.dart b/lib/widgets/messages/camera_image_view.dart index de20b04..0ec6fa4 100644 --- a/lib/widgets/messages/camera_image_view.dart +++ b/lib/widgets/messages/camera_image_view.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; @@ -38,7 +39,7 @@ class CameraImageView extends StatelessWidget { side: BorderSide(width: 1, color: Theme.of(context).colorScheme.error) ), icon: const Icon(Icons.close), - label: const Text("Cancel",), + label: const Text('general.cancel').tr(), ), TextButton.icon( onPressed: () { diff --git a/lib/widgets/messages/message_asset.dart b/lib/widgets/messages/message_asset.dart index 597a615..564c6bc 100644 --- a/lib/widgets/messages/message_asset.dart +++ b/lib/widgets/messages/message_asset.dart @@ -1,14 +1,14 @@ import 'dart:convert'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:photo_view/photo_view.dart'; import 'package:recon/auxiliary.dart'; -import 'package:recon/models/photo_asset.dart'; import 'package:recon/models/message.dart'; +import 'package:recon/models/photo_asset.dart'; import 'package:recon/string_formatter.dart'; import 'package:recon/widgets/formatted_text.dart'; import 'package:recon/widgets/messages/message_state_indicator.dart'; -import 'package:flutter/material.dart'; -import 'package:photo_view/photo_view.dart'; class MessageAsset extends StatelessWidget { const MessageAsset({required this.message, this.foregroundColor, super.key}); diff --git a/lib/widgets/messages/message_attachment_list.dart b/lib/widgets/messages/message_attachment_list.dart index bae2f88..a83006f 100644 --- a/lib/widgets/messages/message_attachment_list.dart +++ b/lib/widgets/messages/message_attachment_list.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; @@ -81,7 +82,7 @@ class _MessageAttachmentListState extends State { onPressed: () { Navigator.of(context).pop(); }, - child: const Text("No"), + child: const Text('general.no').tr(), ), TextButton( onPressed: () async { @@ -91,7 +92,7 @@ class _MessageAttachmentListState extends State { }); await widget.onChange(_loadedFiles); }, - child: const Text("Yes"), + child: const Text('general.yes').tr(), ) ], ), diff --git a/lib/widgets/messages/message_bubble.dart b/lib/widgets/messages/message_bubble.dart index 77cbe73..ba73099 100644 --- a/lib/widgets/messages/message_bubble.dart +++ b/lib/widgets/messages/message_bubble.dart @@ -1,11 +1,11 @@ +import 'package:flutter/material.dart'; import 'package:recon/client_holder.dart'; import 'package:recon/models/message.dart'; import 'package:recon/widgets/messages/message_asset.dart'; import 'package:recon/widgets/messages/message_audio_player.dart'; -import 'package:recon/widgets/messages/message_session_invite.dart'; import 'package:recon/widgets/messages/message_invite_request.dart'; +import 'package:recon/widgets/messages/message_session_invite.dart'; import 'package:recon/widgets/messages/message_text.dart'; -import 'package:flutter/material.dart'; class MessageBubble extends StatelessWidget { const MessageBubble({required this.message, super.key}); diff --git a/lib/widgets/messages/message_input_bar.dart b/lib/widgets/messages/message_input_bar.dart index 9b6d030..0c0c310 100644 --- a/lib/widgets/messages/message_input_bar.dart +++ b/lib/widgets/messages/message_input_bar.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -194,7 +195,7 @@ class _MessageInputBarState extends State { _attachmentPickerOpen = false; } catch (e, s) { FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s)); - sMsgnr.showSnackBar(SnackBar(content: Text("Failed to send a message: $e"))); + sMsgnr.showSnackBar(SnackBar(content: Text('messaging.failedSend'.tr(args: [e.toString()])))); } setState(() { _isSending = false; @@ -423,7 +424,7 @@ class _MessageInputBarState extends State { ? null : () { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Sorry, this feature is not yet available")), + SnackBar(content: const Text('general.featureUnavailable').tr()), ); return; // setState(() { @@ -443,14 +444,14 @@ class _MessageInputBarState extends State { await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text("Remove all attachments"), - content: const Text("This will remove all attachments, are you sure?"), + title: const Text('messaging.removeAttachments').tr(), + content: const Text('messaging.removeAttachmentsDescription').tr(), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, - child: const Text("No"), + child: const Text('general.no').tr(), ), TextButton( onPressed: () { @@ -460,7 +461,7 @@ class _MessageInputBarState extends State { }); Navigator.of(context).pop(); }, - child: const Text("Yes"), + child: const Text('general.yes').tr(), ), ], ), @@ -501,7 +502,7 @@ class _MessageInputBarState extends State { style: Theme.of(context).textTheme.bodyLarge, decoration: InputDecoration( isDense: true, - hintText: _isRecording ? "" : "Message ${widget.recipient.contactUsername}...", + hintText: _isRecording ? "" : 'messaging.placeholder'.tr(args: [widget.recipient.contactUsername]), hintMaxLines: 1, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), fillColor: Colors.black26, @@ -542,7 +543,7 @@ class _MessageInputBarState extends State { size: 16, ), ), - Text("Cancel Recording", style: Theme.of(context).textTheme.titleMedium), + Text('messaging.cancelRecording'.tr(), style: Theme.of(context).textTheme.titleMedium), ], ) : Row( @@ -563,7 +564,7 @@ class _MessageInputBarState extends State { stream: _recordingDurationStream(), builder: (context, snapshot) { return Text( - "Recording: ${snapshot.data?.format()}", + 'messaging.recording'.tr(args: [snapshot.data?.format() ?? "0:00"]), style: Theme.of(context).textTheme.titleMedium, ); }, @@ -602,7 +603,7 @@ class _MessageInputBarState extends State { ? null : (_) async { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Sorry, this feature is not yet available")), + SnackBar(content: Text('uncategorized.featureUnavailable'.tr())), ); return; // HapticFeedback.vibrate(); diff --git a/lib/widgets/messages/message_invite_request.dart b/lib/widgets/messages/message_invite_request.dart index 76de723..92f6622 100644 --- a/lib/widgets/messages/message_invite_request.dart +++ b/lib/widgets/messages/message_invite_request.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:recon/client_holder.dart'; import 'package:recon/models/invite_request.dart'; @@ -25,12 +26,12 @@ class MessageInviteRequest extends StatelessWidget { } else if (fromSelf) { //Forwarded invite request if(inviteInfo.response == "SendInvite") { - text = '''Granted "${inviteInfo.usernameToInvite}" invite to session "${inviteInfo.forSessionName}"'''; + text = 'messaging.invite.granted'.tr(args: [inviteInfo.usernameToInvite, inviteInfo.forSessionName.toString()]); } else if(inviteInfo.response == "AddAsContact") { - text = '''Adding ${inviteInfo.usernameToInvite} as a contact of the headless"'''; + text = 'messaging.invite.addAsContact'.tr(args: [inviteInfo.usernameToInvite]); } else { //If new responses are added to the InviteRequestResponse enum. - text = '''Unknown response "${inviteInfo.response}" for invite request'''; + text = 'messaging.invite.unknownResponse'.tr(args: [inviteInfo.response.toString()]); } } else if (inviteInfo.forSessionName != null) { //Another user requested an invite from you. diff --git a/lib/widgets/messages/message_session_invite.dart b/lib/widgets/messages/message_session_invite.dart index 5bcaf03..2eabfef 100644 --- a/lib/widgets/messages/message_session_invite.dart +++ b/lib/widgets/messages/message_session_invite.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:easy_localization/easy_localization.dart'; import 'package:recon/auxiliary.dart'; import 'package:recon/models/message.dart'; import 'package:recon/models/session.dart'; @@ -75,7 +76,7 @@ class MessageSessionInvite extends StatelessWidget { mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("Hosted by ${sessionInfo.hostUsername}", overflow: TextOverflow.ellipsis, style: Theme + Text('messaging.hostedBy'.tr(args: [sessionInfo.hostUsername]), overflow: TextOverflow.ellipsis, style: Theme .of(context) .textTheme .bodySmall diff --git a/lib/widgets/messages/message_text.dart b/lib/widgets/messages/message_text.dart index fd8eea0..b80c0cd 100644 --- a/lib/widgets/messages/message_text.dart +++ b/lib/widgets/messages/message_text.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:recon/models/message.dart'; @@ -19,12 +20,13 @@ class MessageText extends StatelessWidget { onLongPress: () async { await Clipboard.setData(ClipboardData(text: message.content)); if (context.mounted) { - const content = Text("Copied to clipboard"); ScaffoldMessenger.of(context).showSnackBar( Platform.isIOS - ? const SnackBar(content: content) + ? SnackBar( + content: Text('messaging.copiedToClipboard').tr() + ) : SnackBar( - content: content, + content: Text('messaging.copiedToClipboard').tr(), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), diff --git a/lib/widgets/messages/messages_list.dart b/lib/widgets/messages/messages_list.dart index 28a5b44..ab3f335 100644 --- a/lib/widgets/messages/messages_list.dart +++ b/lib/widgets/messages/messages_list.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:recon/apis/user_api.dart'; @@ -93,11 +94,11 @@ class _MessagesListState extends State with SingleTickerProviderSt actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text("Cancel"), + child: const Text("general.cancel").tr(), ), TextButton( onPressed: () => Navigator.pop(context, true), - child: const Text("Send"), + child: const Text('messaging.send').tr(), ), ], ); @@ -227,7 +228,7 @@ class _MessagesListState extends State with SingleTickerProviderSt Padding( padding: const EdgeInsets.symmetric(vertical: 24), child: Text( - "There are no messages here\nWhy not say hello?", + 'messaging.noMessages'.tr(), textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium, ), diff --git a/lib/widgets/my_profile_dialog.dart b/lib/widgets/my_profile_dialog.dart index 4dfef02..c903f35 100644 --- a/lib/widgets/my_profile_dialog.dart +++ b/lib/widgets/my_profile_dialog.dart @@ -1,5 +1,5 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:recon/apis/user_api.dart'; import 'package:recon/auxiliary.dart'; import 'package:recon/client_holder.dart'; @@ -88,7 +88,7 @@ class _MyProfileDialogState extends State { "2FA: ", style: tt.labelLarge, ), - Text(profile.twoFactor ? "Enabled" : "Disabled") + Text(profile.twoFactor ? "general.enabled" : "general.disabled").tr() ], ), Row( @@ -98,7 +98,7 @@ class _MyProfileDialogState extends State { "Patreon Supporter: ", style: tt.labelLarge, ), - Text(profile.isPatreonSupporter ? "Yes" : "No") + Text(profile.isPatreonSupporter ? "general.yes" : "general.no").tr() ], ), Row( @@ -108,7 +108,7 @@ class _MyProfileDialogState extends State { "Stripe Supporter: ", style: tt.labelLarge, ), - Text(profile.isStripeSupporter ? "Yes" : "No") + Text(profile.isStripeSupporter ? "general.yes" : "general.no").tr() ], ), if (profile.publicBanExpiration?.isAfter(DateTime.now()) ?? false) diff --git a/lib/widgets/sessions/session_list.dart b/lib/widgets/sessions/session_list.dart index cb0c193..7afaa6f 100644 --- a/lib/widgets/sessions/session_list.dart +++ b/lib/widgets/sessions/session_list.dart @@ -1,12 +1,13 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:recon/auxiliary.dart'; import 'package:recon/clients/session_client.dart'; import 'package:recon/models/session.dart'; import 'package:recon/widgets/default_error_widget.dart'; import 'package:recon/widgets/formatted_text.dart'; import 'package:recon/widgets/sessions/session_view.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class SessionList extends StatefulWidget { const SessionList({super.key}); @@ -50,9 +51,9 @@ class _SessionListState extends State with AutomaticKeepAliveClient } }, child: data.isEmpty && snapshot.connectionState == ConnectionState.done - ? const DefaultErrorWidget( - title: "No Sessions Found", - message: "Try to adjust your filters", + ? DefaultErrorWidget( + title: 'sessions.noSessionsFound'.tr(), + message: 'sessions.noSessionsFound.description'.tr(), iconOverride: Icons.public_off, ) : Padding( @@ -137,7 +138,7 @@ class _SessionListState extends State with AutomaticKeepAliveClient children: [ Expanded( child: Text( - "${session.sessionUsers.length.toString().padLeft(2, "0")}/${session.maxUsers.toString().padLeft(2, "0")} Online", + 'sessions.sessionUsers'.tr(args: [session.sessionUsers.length.toString().padLeft(2, "0"), session.maxUsers.toString().padLeft(2, "0")]), maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith( diff --git a/lib/widgets/sessions/session_view.dart b/lib/widgets/sessions/session_view.dart index 05b76d8..2e9abee 100644 --- a/lib/widgets/sessions/session_view.dart +++ b/lib/widgets/sessions/session_view.dart @@ -1,4 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:recon/apis/session_api.dart'; import 'package:recon/auxiliary.dart'; import 'package:recon/client_holder.dart'; @@ -6,7 +8,6 @@ import 'package:recon/models/session.dart'; import 'package:recon/widgets/formatted_text.dart'; import 'package:recon/widgets/panorama.dart'; import 'package:recon/widgets/settings_page.dart'; -import 'package:flutter/material.dart'; class SessionView extends StatefulWidget { const SessionView({required this.session, super.key}); @@ -52,7 +53,7 @@ class _SessionViewState extends State { body: RefreshIndicator( onRefresh: () async { if (session.id == null) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Session is no longer valid"))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: const Text('sessions.noLongerValid').tr())); } setState(() { _sessionFuture = SessionApi.getSession(ClientHolder.of(context).apiClient, sessionId: session.id!); @@ -74,7 +75,7 @@ class _SessionViewState extends State { MaterialPageRoute( builder: (context) => Scaffold( appBar: AppBar( - title: const Text("Session Preview"), + title: const Text('sessions.preview').tr(), ), body: Center( child: Panorama( @@ -120,17 +121,21 @@ class _SessionViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + ListSectionHeader( + leadingText: 'worlds.description'.tr(), + showLine: false, + ), Padding( padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8), child: session.formattedDescription.isEmpty - ? Text("No description", style: Theme.of(context).textTheme.labelLarge) + ? Text('worlds.noDescription', style: Theme.of(context).textTheme.labelLarge).tr() : FormattedText( session.formattedDescription, style: Theme.of(context).textTheme.labelLarge?.apply(fontStyle: FontStyle.italic), ), ), - const ListSectionHeader( - leadingText: "Tags:", + ListSectionHeader( + leadingText: 'worlds.tags'.tr(), showLine: false, ), Padding( @@ -142,8 +147,8 @@ class _SessionViewState extends State { softWrap: true, ), ), - const ListSectionHeader( - leadingText: "Details:", + ListSectionHeader( + leadingText: 'worlds.details'.tr(), showLine: false, ), Padding( @@ -152,13 +157,13 @@ class _SessionViewState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Access: ", + 'sessions.access', style: Theme.of(context).textTheme.labelLarge, - ), + ).tr(), Text( - session.accessLevel.toReadableString(), + 'sessions.accessLevel.${session.accessLevel.name}', style: Theme.of(context).textTheme.labelMedium, - ), + ).tr(), ], ), ), @@ -168,19 +173,19 @@ class _SessionViewState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Headless: ", + 'sessions.headless', style: Theme.of(context).textTheme.labelLarge, - ), + ).tr(), Text( - session.headlessHost ? "Yes" : "No", + session.headlessHost ? 'general.yes' : 'general.no', style: Theme.of(context).textTheme.labelMedium, - ), + ).tr(), ], ), ), ListSectionHeader( - leadingText: "Users", - trailingText: "${session.sessionUsers.length.toString().padLeft(2, "0")}/${session.maxUsers.toString().padLeft(2, "0")}", + leadingText: 'sessions.users'.tr(), + trailingText: 'sessions.sessionUsers'.tr(args: [session.sessionUsers.length.toString().padLeft(2, "0"), session.maxUsers.toString().padLeft(2, "0")]), showLine: false, ), ], diff --git a/lib/widgets/settings_app_bar.dart b/lib/widgets/settings_app_bar.dart index 4a2ed02..6054abf 100644 --- a/lib/widgets/settings_app_bar.dart +++ b/lib/widgets/settings_app_bar.dart @@ -1,3 +1,5 @@ + +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; class SettingsAppBar extends StatelessWidget { @@ -6,7 +8,7 @@ class SettingsAppBar extends StatelessWidget { @override Widget build(BuildContext context) { return AppBar( - title: const Text("Settings"), + title: const Text('settings.title').tr(), ); } } diff --git a/lib/widgets/settings_page.dart b/lib/widgets/settings_page.dart index 63e9da8..271853e 100644 --- a/lib/widgets/settings_page.dart +++ b/lib/widgets/settings_page.dart @@ -1,6 +1,6 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_phoenix/flutter_phoenix.dart'; -import 'package:intl/intl.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:recon/client_holder.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -13,17 +13,17 @@ class SettingsPage extends StatelessWidget { final sClient = ClientHolder.of(context).settingsClient; return Scaffold( appBar: AppBar( - title: Text("Settings"), + title: const Text('settings.title').tr(), ), body: ListView( children: [ - const ListSectionHeader(leadingText: "Notifications"), + ListSectionHeader(leadingText: 'settings.notifications.title'.tr()), BooleanSettingsTile( - title: "Enable Notifications", + title: 'settings.notifications.enabled'.tr(), initialState: !sClient.currentSettings.notificationsDenied.valueOrDefault, onChanged: (value) async => sClient.changeSettings(sClient.currentSettings.copyWith(notificationsDenied: !value)), ), - const ListSectionHeader(leadingText: "Appearance"), + ListSectionHeader(leadingText: 'settings.appearance.title'.tr()), ListTile( trailing: StatefulBuilder( builder: (context, setState) { @@ -52,27 +52,27 @@ class SettingsPage extends StatelessWidget { ); }, ), - title: const Text("Theme Mode"), + title: const Text('settings.appearance.theme').tr(), ), - const ListSectionHeader(leadingText: "Other"), + ListSectionHeader(leadingText: 'settings.other.title'.tr()), ListTile( trailing: const Icon(Icons.logout), - title: const Text("Sign out"), + title: const Text('settings.other.signout').tr(), onTap: () { showDialog( context: context, builder: (context) => AlertDialog( title: Text( - "Are you sure you want to sign out?", + 'settings.other.signout.confirm'.tr(), style: Theme.of(context).textTheme.titleLarge, ), actions: [ - TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text("No")), + TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('general.no').tr()), TextButton( onPressed: () async { await ClientHolder.of(context).apiClient.logout(); }, - child: const Text("Yes"), + child: const Text('general.yes').tr(), ), ], ), @@ -81,7 +81,7 @@ class SettingsPage extends StatelessWidget { ), ListTile( trailing: const Icon(Icons.info_outline), - title: const Text("About ReCon"), + title: const Text('settings.other.about').tr(), onTap: () async { final version = (await PackageInfo.fromPlatform()).version; if (context.mounted) { @@ -92,7 +92,7 @@ class SettingsPage extends StatelessWidget { onTap: () async { if (!await launchUrl(Uri.parse("https://github.com/Nutcake/ReCon"), mode: LaunchMode.externalApplication)) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Failed to open link."))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: const Text('settings.other.error').tr())); } } }, diff --git a/lib/widgets/update_notifier.dart b/lib/widgets/update_notifier.dart index 4fe903a..1103c7d 100644 --- a/lib/widgets/update_notifier.dart +++ b/lib/widgets/update_notifier.dart @@ -1,6 +1,7 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:recon/client_holder.dart'; import 'package:recon/models/sem_ver.dart'; -import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; class UpdateNotifier extends StatelessWidget { @@ -12,23 +13,23 @@ class UpdateNotifier extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text("Update Available", style: Theme + title: Text('update.title'.tr(), style: Theme .of(context) .textTheme .titleLarge), content: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text("There is a new version available for download!"), + Text('update.description'.tr()), const SizedBox(height: 8,), Row( children: [ - Text("Your version: ${localVersion.toString()}"), + Text('update.localVersion'.tr(args: [localVersion.toString()])), ], ), Row( children: [ - Text("New version: ${remoteVersion.toString()}"), + Text('update.remoteVersion'.tr(args: [remoteVersion.toString()])), ], ), const SizedBox(height: 24,), @@ -54,7 +55,7 @@ class UpdateNotifier extends StatelessWidget { .secondary ), icon: const Icon(Icons.download), - label: const Text("Get it on Github"), + label: Text('update.openDownload'.tr()), ), ], ), @@ -69,7 +70,7 @@ class UpdateNotifier extends StatelessWidget { sClient.changeSettings(sClient.currentSettings.copyWith(lastDismissedVersion: remoteVersion.toString())); Navigator.of(context).pop(); }, - child: const Text("I'll do it later."), + child: Text('update.dismiss'.tr()), ), ], ); diff --git a/lib/widgets/worlds/world_list.dart b/lib/widgets/worlds/world_list.dart index 5579227..f171866 100644 --- a/lib/widgets/worlds/world_list.dart +++ b/lib/widgets/worlds/world_list.dart @@ -1,7 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:recon/apis/record_api.dart'; import 'package:recon/auxiliary.dart'; import 'package:recon/client_holder.dart'; @@ -83,7 +83,7 @@ class WorldListState extends State { if (snapshot.hasError) { FlutterError.reportError(FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace)); return DefaultErrorWidget( - title: "Failed to load Worlds", + title: 'worlds.failedToLoad'.tr(), message: snapshot.error.toString(), ); } @@ -96,9 +96,9 @@ class WorldListState extends State { await _recordsFuture; }, child: data.isEmpty && snapshot.connectionState == ConnectionState.done - ? const DefaultErrorWidget( - title: "No Sessions Found", - message: "Try to adjust your filters", + ? DefaultErrorWidget( + title: 'worlds.noWorldsFound'.tr(), + message: 'worlds.noWorldsFound.description'.tr(), iconOverride: Icons.public_off, ) : Padding( @@ -181,7 +181,7 @@ class WorldListState extends State { children: [ Expanded( child: Text( - "Last modified ${_dateFormat.format(world.lastModificationTime)}", + 'worlds.lastModified'.tr(args: [_dateFormat.format(world.lastModificationTime)]), maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith( diff --git a/lib/widgets/worlds/world_list_app_bar.dart b/lib/widgets/worlds/world_list_app_bar.dart index 104a5fd..31e2840 100644 --- a/lib/widgets/worlds/world_list_app_bar.dart +++ b/lib/widgets/worlds/world_list_app_bar.dart @@ -1,5 +1,5 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:recon/models/records/search_sort.dart'; class WorldListAppBar extends StatefulWidget { @@ -23,7 +23,7 @@ class _WorldListAppBarState extends State { @override Widget build(BuildContext context) { return AppBar( - title: const Text("Worlds"), + title: const Text('worlds.title').tr(), actions: [ if (_needle.isEmpty) IconButton( @@ -34,11 +34,11 @@ class _WorldListAppBarState extends State { context: context, builder: (context) { return AlertDialog( - title: const Text("Search for worlds"), + title: const Text('worlds.search').tr(), content: TextField( controller: textController, - decoration: const InputDecoration( - hintText: "Enter tags", + decoration: InputDecoration( + hintText: 'worlds.inputTags'.tr(), border: OutlineInputBorder(), ), ), @@ -47,13 +47,13 @@ class _WorldListAppBarState extends State { onPressed: () { Navigator.of(context).pop(); }, - child: const Text("Cancel"), + child: const Text('general.cancel').tr(), ), TextButton( onPressed: () { Navigator.of(context).pop(textController.text); }, - child: const Text("Search"), + child: const Text('general.search').tr(), ), ], ); @@ -97,7 +97,7 @@ class _WorldListAppBarState extends State { (e) => CheckedPopupMenuItem( value: e, checked: e == _sortParameter, - child: Text(toBeginningOfSentenceCase(e.toString())), + child: Text('worlds.sorting.${e.name}').tr(), ), ) .toList(), @@ -119,7 +119,7 @@ class _WorldListAppBarState extends State { (e) => CheckedPopupMenuItem( value: e, checked: e == _sortDirection, - child: Text(toBeginningOfSentenceCase(e.toString())), + child: Text('worlds.sorting.${e.name}').tr(), ), ) .toList(), diff --git a/lib/widgets/worlds/world_view.dart b/lib/widgets/worlds/world_view.dart index 0afcff2..4e14860 100644 --- a/lib/widgets/worlds/world_view.dart +++ b/lib/widgets/worlds/world_view.dart @@ -1,6 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:recon/auxiliary.dart'; import 'package:recon/models/records/record.dart'; import 'package:recon/widgets/formatted_text.dart'; @@ -62,7 +62,7 @@ class _WorldViewState extends State { MaterialPageRoute( builder: (context) => Scaffold( appBar: AppBar( - title: const Text("Session Preview"), + title: const Text('worlds.preview').tr(), ), body: Center( child: Panorama( @@ -108,34 +108,34 @@ class _WorldViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const ListSectionHeader( - leadingText: "Description:", + ListSectionHeader( + leadingText: 'worlds.description'.tr(), showLine: false, ), Padding( padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8), child: widget.world.formattedDescription.isEmpty - ? Text("No description", style: Theme.of(context).textTheme.labelLarge) + ? Text('worlds.noDescription', style: Theme.of(context).textTheme.labelLarge).tr() : FormattedText( widget.world.formattedDescription, style: Theme.of(context).textTheme.labelLarge?.apply(fontStyle: FontStyle.italic), ), ), - const ListSectionHeader( - leadingText: "Tags:", + ListSectionHeader( + leadingText: 'worlds.tags'.tr(), showLine: false, ), Padding( padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8.0), child: Text( - widget.world.tags.isEmpty ? "None" : widget.world.tags.join(", "), + widget.world.tags.isEmpty ? 'worlds.noTags'.tr() : widget.world.tags.join(", "), style: Theme.of(context).textTheme.labelMedium, textAlign: TextAlign.start, softWrap: true, ), ), - const ListSectionHeader( - leadingText: "Details:", + ListSectionHeader( + leadingText: 'worlds.details'.tr(), showLine: false, ), Padding( @@ -144,7 +144,7 @@ class _WorldViewState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Created at: ", + 'worlds.createdAt'.tr(), style: Theme.of(context).textTheme.labelLarge, ), Text( @@ -160,7 +160,7 @@ class _WorldViewState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Last modified at: ", + 'worlds.lastModified'.tr(), style: Theme.of(context).textTheme.labelLarge, ), Text( @@ -176,7 +176,7 @@ class _WorldViewState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Visits: ", + 'worlds.visits'.tr(), style: Theme.of(context).textTheme.labelLarge, ), Text( @@ -192,7 +192,7 @@ class _WorldViewState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Rating: ", + 'worlds.rating'.tr(), style: Theme.of(context).textTheme.labelLarge, ), Text( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 15e34aa..2f33e99 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -15,6 +15,7 @@ import just_audio import package_info_plus import record_macos import share_plus +import shared_preferences_foundation import sqflite_darwin import url_launcher_macos @@ -29,6 +30,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index d366f2a..37a0968 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -193,6 +193,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.1" + easy_localization: + dependency: "direct main" + description: + name: easy_localization + sha256: "2ccdf9db8fe4d9c5a75c122e6275674508fd0f0d49c827354967b8afcc56bbed" + url: "https://pub.dev" + source: hosted + version: "3.0.8" + easy_logger: + dependency: transitive + description: + name: easy_logger + sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 + url: "https://pub.dev" + source: hosted + version: "0.0.2" fake_async: dependency: transitive description: @@ -342,6 +358,11 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_phoenix: dependency: "direct main" description: @@ -1000,6 +1021,62 @@ packages: url: "https://pub.dev" source: hosted version: "7.1.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5" + url: "https://pub.dev" + source: hosted + version: "2.4.26" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index d38f556..212c18c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,6 +67,7 @@ dependencies: just_audio_media_kit: ^2.1.0 media_kit_libs_linux: ^1.2.1 media_kit_libs_windows_audio: ^1.0.9 + easy_localization: ^3.0.8 dev_dependencies: flutter_test: @@ -92,6 +93,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/images/ + - assets/translations/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware