diff --git a/localizely.yml b/localizely.yml new file mode 100644 index 0000000000..c9ad629aa0 --- /dev/null +++ b/localizely.yml @@ -0,0 +1,15 @@ +config_version: 1.0 +project_id: ead34689-ec52-41d9-b675-09bc85a6cbd7 +file_type: flutter_arb +upload: + files: + - file: mobile/assets/i18n/en-US.json + locale_code: en + - file: mobile/assets/i18n/de-DE.json + locale_code: de +download: + files: + - file: mobile/assets/i18n/en-US.json + locale_code: en + - file: mobile/assets/i18n/de-DE.json + locale_code: de \ No newline at end of file diff --git a/mobile/assets/i18n/de-DE.json b/mobile/assets/i18n/de-DE.json new file mode 100644 index 0000000000..bef961441f --- /dev/null +++ b/mobile/assets/i18n/de-DE.json @@ -0,0 +1,98 @@ +{ + "date_format": "E d. LLL y \u2022 hh:mm", + "daily_title_text_date": "E, dd MMM", + "daily_title_text_date_year": "E, dd MMM, yyyy", + "monthly_title_text_date_format": "MMMM y", + "login_form_button_text": "Anmelden", + "login_form_save_login": "Angemeldet bleiben", + "login_form_endpoint_url": "Server URL", + "login_form_endpoint_hint": "http://deine-server-ip:port/api", + "login_form_err_trailing_whitespace": "Folgendes Leerzeichen", + "login_form_err_leading_whitespace": "Führendes Leerzichen", + "login_form_err_invalid_email": "Ungültige E-Mail", + "login_form_err_http": "Bitte gebe http:// oder https:// an", + "login_form_label_email": "E-Mail", + "login_form_email_hint": "deine@email.de", + "login_form_label_password": "Passwort", + "login_form_password_hint": "password", + "share_add_title": "Titel hinzufügen", + "album_viewer_appbar_share_err_delete": "Album konnte nicht gelöscht werden", + "album_viewer_appbar_share_err_leave": "Album konnte nicht verlassen werden", + "album_viewer_appbar_share_err_remove": "Beim Löschen von Elementen aus dem Album ist ein Problem aufgetreten", + "album_viewer_appbar_share_err_title": "Der Titel konnte nicht geändert werden", + "album_viewer_appbar_share_remove": "Entferne vom Album", + "album_viewer_appbar_share_delete": "Album löschen", + "album_viewer_appbar_share_leave": "Album verlassen", + "sharing_silver_appbar_create_shared_album": "Neues geteiltes Album", + "sharing_silver_appbar_share_partner": "Teile mit Partner", + "share_add_photos": "Fotos hinzufügen", + "album_viewer_page_share_add_users": "Nutzer hinzufügen", + "share_add": "Hinzufügen", + "create_shared_album_page_share_add_assets": "ELEMENTE HINZUFÜGEN", + "create_shared_album_page_share_select_photos": "Fotos auswählen", + "share_create_album": "Album erstellen", + "create_shared_album_page_share": "Teilen", + "select_additional_user_for_sharing_page_suggestions": "Vorschläge", + "share_invite": "Zum Album einladen", + "select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden", + "sharing_page_empty_list": "LEERE LISTE", + "sharing_page_description": "Erstelle ein geteiltes Album um Fotos und Videos mit Personen in deinem Netzwerk zu teilen.", + "sharing_page_album": "Geteilte Alben", + "exif_bottom_sheet_description": "Beschreibung hinzufügen...", + "exif_bottom_sheet_location": "STANDORT", + "exif_bottom_sheet_details": "DETAILS", + "backup_err_only_album": "Das einzige Album kann nicht entfernt werden", + "backup_controller_page_server_storage": "Server Speicher", + "backup_controller_page_status_on": "Sicherung ist aktiv", + "backup_controller_page_status_off": "Sicherung ist inaktiv", + "backup_controller_page_turn_off": "Sicherung ausschalten", + "backup_controller_page_turn_on": "Sicherung einschalten", + "backup_controller_page_desc_backup": "Aktiviere die Sicherung um Elemente automatisch auf den Server zu laden.", + "backup_controller_page_backup_selected": "Ausgewählt: ", + "backup_all": "Alle", + "backup_controller_page_none_selected": "Keine ausgewählt", + "backup_controller_page_excluded": "Ausgeschlossen: ", + "backup_controller_page_albums": "Gesicherte Alben", + "backup_controller_page_to_backup": "Zu sichernde Alben", + "backup_controller_page_select": "Auswählen", + "backup_controller_page_backup": "Sicherung", + "backup_controller_page_info": "Informationen zur Sicherung", + "backup_controller_page_total": "Gesamt", + "backup_controller_page_total_sub": "Alle Fotos und Videos", + "backup_controller_page_backup_sub": "Gesicherte Fotos und Videos", + "backup_controller_page_remainder": "Übrig", + "backup_controller_page_remainder_sub": "Noch zu sichernde Fotos und Videos", + "backup_controller_page_cancel": "Abbrechen", + "backup_controller_page_start_backup": "Sicherung starten", + "album_info_card_backup_album_included": "EINGESCHLOSSEN", + "album_info_card_backup_album_excluded": "AUSGESCHLOSSEN", + "backup_info_card_assets": "Elemente", + "backup_album_selection_page_select_albums": "Alben auswählen", + "backup_album_selection_page_selection_info": "Auswahl", + "backup_album_selection_page_total_assets": "Elemente", + "backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})", + "backup_album_selection_page_albums_tap": "Tippen um einzuschließen, doppelt tippen um zu entfernen", + "backup_album_selection_page_assets_scatter": "Elemente können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden", + "backup_controller_page_storage_format": "{} von {} genutzt", + "tab_controller_nav_photos": "Fotos", + "tab_controller_nav_search": "Suche", + "tab_controller_nav_sharing": "Teilen", + "control_bottom_app_bar_delete": "Löschen", + "delete_dialog_title": "Für immer löschen", + "delete_dialog_alert": "Diese Elemente werden unwiderruflich von Immich und dem Gerät entfernt", + "delete_dialog_cancel": "Abbrechen", + "delete_dialog_ok": "Löschen", + "profile_drawer_sign_out": "Abmelden", + "profile_drawer_client_server_up_to_date": "App und Server sind aktuell", + "search_bar_hint": "Durchsuche deine Fotos", + "search_page_places": "Orte", + "search_page_things": "Dinge", + "search_result_page_new_search_hint": "Neue Suche", + "search_page_no_places": "Keine Informationen über Orte verfügbar", + "version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89", + "version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von", + "version_announcement_overlay_text_2": "Bitte nehm dir die Zeit und lese das ", + "version_announcement_overlay_release_notes": "Änderungsprotokoll", + "version_announcement_overlay_text_3": " und achte darauf, dass deine docker-compose und .env Dateien aktuell sind, vor allem wenn du ein System für automatische Updates benutzt (z.B. Watchtower).", + "version_announcement_overlay_ack": "Ich habe verstanden" +} diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json new file mode 100644 index 0000000000..d995acadb3 --- /dev/null +++ b/mobile/assets/i18n/en-US.json @@ -0,0 +1,98 @@ +{ + "date_format": "E, LLL d, y \u2022 h:mm a", + "daily_title_text_date": "E, MMM dd", + "daily_title_text_date_year": "E, MMM dd, yyyy", + "monthly_title_text_date_format": "MMMM y", + "login_form_button_text": "Login", + "login_form_save_login": "Stay logged in", + "login_form_endpoint_url": "Server Endpoint URL", + "login_form_endpoint_hint": "http://your-server-ip:port/api", + "login_form_err_trailing_whitespace": "Trailing whitespace", + "login_form_err_leading_whitespace": "Leading whitespace", + "login_form_err_invalid_email": "Invalid Email", + "login_form_err_http": "Please specify http:// or https://", + "login_form_label_email": "Email", + "login_form_email_hint": "youremail@email.com", + "login_form_label_password": "Password", + "login_form_password_hint": "password", + "share_add_title": "Add a title", + "album_viewer_appbar_share_err_delete": "Failed to delete album", + "album_viewer_appbar_share_err_leave": "Failed to leave album", + "album_viewer_appbar_share_err_remove": "There are problems in removing assets from album", + "album_viewer_appbar_share_err_title": "Failed to change album title", + "album_viewer_appbar_share_remove": "Remove from album", + "album_viewer_appbar_share_delete": "Delete album", + "album_viewer_appbar_share_leave": "Leave album", + "sharing_silver_appbar_create_shared_album": "Create shared album", + "sharing_silver_appbar_share_partner": "Share with partner", + "share_add_photos": "Add photos", + "album_viewer_page_share_add_users": "Add users", + "share_add": "Add", + "create_shared_album_page_share_add_assets": "ADD ASSETS", + "create_shared_album_page_share_select_photos": "Select Photos", + "share_create_album": "Create album", + "create_shared_album_page_share": "Share", + "select_additional_user_for_sharing_page_suggestions": "Suggestions", + "share_invite": "Invite to album", + "select_user_for_sharing_page_err_album": "Failed to create album", + "sharing_page_empty_list": "EMPTY LIST", + "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", + "sharing_page_album": "Shared albums", + "exif_bottom_sheet_description": "Add Description...", + "exif_bottom_sheet_location": "LOCATION", + "exif_bottom_sheet_details": "DETAILS", + "backup_err_only_album": "Cannot remove the only album", + "backup_controller_page_server_storage": "Server Storage", + "backup_controller_page_status_on": "Backup is on", + "backup_controller_page_status_off": "Backup is off", + "backup_controller_page_turn_off": "Turn off Backup", + "backup_controller_page_turn_on": "Turn on Backup", + "backup_controller_page_desc_backup": "Turn on backup to automatically upload new assets to the server.", + "backup_controller_page_backup_selected": "Selected: ", + "backup_all": "All", + "backup_controller_page_none_selected": "None selected", + "backup_controller_page_excluded": "Excluded: ", + "backup_controller_page_albums": "Backup Albums", + "backup_controller_page_to_backup": "Albums to be backup", + "backup_controller_page_select": "Select", + "backup_controller_page_backup": "Backup", + "backup_controller_page_info": "Backup Information", + "backup_controller_page_total": "Total", + "backup_controller_page_total_sub": "All unique photos and videos from selected albums", + "backup_controller_page_backup_sub": "Backed up photos and videos", + "backup_controller_page_remainder": "Remainder", + "backup_controller_page_remainder_sub": "Remaining photos and albums to back up from selection", + "backup_controller_page_cancel": "Cancel", + "backup_controller_page_start_backup": "Start Backup", + "album_info_card_backup_album_included": "INCLUDED", + "album_info_card_backup_album_excluded": "EXCLUDED", + "backup_info_card_assets": "assets", + "backup_album_selection_page_select_albums": "Select Albums", + "backup_album_selection_page_selection_info": "Selection Info", + "backup_album_selection_page_total_assets": "Total unique assets", + "backup_album_selection_page_albums_device": "Albums on device ({})", + "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", + "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", + "backup_controller_page_storage_format": "{} of {} used", + "tab_controller_nav_photos": "Photos", + "tab_controller_nav_search": "Search", + "tab_controller_nav_sharing": "Sharing", + "control_bottom_app_bar_delete": "Delete", + "delete_dialog_title": "Delete Permanently", + "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device", + "delete_dialog_cancel": "Cancel", + "delete_dialog_ok": "Delete", + "profile_drawer_sign_out": "Sign Out", + "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", + "search_bar_hint": "Search your photos", + "search_page_places": "Places", + "search_page_things": "Things", + "search_result_page_new_search_hint": "New Search", + "search_page_no_places": "No Places Info Available", + "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", + "version_announcement_overlay_text_1": "Hi friend, there is a new release of", + "version_announcement_overlay_text_2": "please take your time to visit the ", + "version_announcement_overlay_release_notes": "release notes", + "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", + "version_announcement_overlay_ack": "Acknowledge" +} diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index ffdad9da73..0c0a5984c8 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -82,5 +82,11 @@ https + + CFBundleLocalizations + + en + de + \ No newline at end of file diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 144bddd278..5e0e51a77d 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hive_flutter/hive_flutter.dart'; @@ -36,7 +37,21 @@ void main() async { ), ); - runApp(const ProviderScope(child: ImmichApp())); + await EasyLocalization.ensureInitialized(); + + var locales = const [ + // Default locale + Locale('en', 'US'), + // Additional locales + Locale('de', 'DE') + ]; + + runApp(EasyLocalization( + supportedLocales: locales, + path: 'assets/i18n', + useFallbackTranslations: true, + fallbackLocale: locales.first, + child: const ProviderScope(child: ImmichApp()))); } class ImmichApp extends ConsumerStatefulWidget { @@ -112,6 +127,9 @@ class ImmichAppState extends ConsumerState ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo(); return MaterialApp( + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, debugShowCheckedModeBanner: false, home: Stack( children: [ diff --git a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart index 334d41c820..892ef04c88 100644 --- a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart +++ b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -72,7 +73,7 @@ class ExifBottomSheet extends ConsumerWidget { children: [ if (assetDetail.exifInfo?.dateTimeOriginal != null) Text( - DateFormat('E, LLL d, y • h:mm a').format( + DateFormat('date_format'.tr()).format( DateTime.parse(assetDetail.exifInfo!.dateTimeOriginal!), ), style: TextStyle( @@ -84,12 +85,12 @@ class ExifBottomSheet extends ConsumerWidget { Padding( padding: const EdgeInsets.only(top: 16.0), child: Text( - "Add Description...", + "exif_bottom_sheet_description", style: TextStyle( color: Colors.grey[500], fontSize: 11, ), - ), + ).tr(), ), // Location @@ -104,9 +105,9 @@ class ExifBottomSheet extends ConsumerWidget { color: Colors.grey[600], ), Text( - "LOCATION", + "exif_bottom_sheet_location", style: TextStyle(fontSize: 11, color: Colors.grey[400]), - ), + ).tr(), if (assetDetail.exifInfo?.latitude != null && assetDetail.exifInfo?.longitude != null) _buildMap(), @@ -134,9 +135,9 @@ class ExifBottomSheet extends ConsumerWidget { Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Text( - "DETAILS", + "exif_bottom_sheet_details", style: TextStyle(fontSize: 11, color: Colors.grey[400]), - ), + ).tr(), ), ListTile( contentPadding: const EdgeInsets.all(0), diff --git a/mobile/lib/modules/backup/ui/album_info_card.dart b/mobile/lib/modules/backup/ui/album_info_card.dart index 4a9ff46e63..90a1e8976d 100644 --- a/mobile/lib/modules/backup/ui/album_info_card.dart +++ b/mobile/lib/modules/backup/ui/album_info_card.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -37,10 +38,10 @@ class AlbumInfoCard extends HookConsumerWidget { visualDensity: VisualDensity.compact, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), label: const Text( - "INCLUDED", + "album_info_card_backup_album_included", style: TextStyle( fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold), - ), + ).tr(), backgroundColor: Theme.of(context).primaryColor, ); } else if (isExcluded) { @@ -48,10 +49,10 @@ class AlbumInfoCard extends HookConsumerWidget { visualDensity: VisualDensity.compact, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), label: const Text( - "EXCLUDED", + "album_info_card_backup_album_excluded", style: TextStyle( fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold), - ), + ).tr(), backgroundColor: Colors.red[300], ); } @@ -77,7 +78,7 @@ class AlbumInfoCard extends HookConsumerWidget { if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) { ImmichToast.show( context: context, - msg: "Cannot remove the only album", + msg: "backup_err_only_album".tr(), toastType: ToastType.error, gravity: ToastGravity.BOTTOM, ); @@ -104,7 +105,7 @@ class AlbumInfoCard extends HookConsumerWidget { .contains(albumInfo)) { ImmichToast.show( context: context, - msg: "Cannot exclude the only album", + msg: "backup_err_only_album".tr(), toastType: ToastType.error, gravity: ToastGravity.BOTTOM, ); @@ -180,7 +181,10 @@ class AlbumInfoCard extends HookConsumerWidget { Padding( padding: const EdgeInsets.only(top: 2.0), child: Text( - '${albumInfo.assetCount} ${(albumInfo.isAll ? " (ALL)" : "")}', + albumInfo.assetCount.toString() + + (albumInfo.isAll + ? " (${'backup_all'.tr()})" + : ""), style: TextStyle( fontSize: 12, color: Colors.grey[600]), ), diff --git a/mobile/lib/modules/backup/ui/backup_info_card.dart b/mobile/lib/modules/backup/ui/backup_info_card.dart index e69ff0fc78..300fa1feba 100644 --- a/mobile/lib/modules/backup/ui/backup_info_card.dart +++ b/mobile/lib/modules/backup/ui/backup_info_card.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; class BackupInfoCard extends StatelessWidget { @@ -44,7 +45,7 @@ class BackupInfoCard extends StatelessWidget { info, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), - const Text("assets"), + const Text("backup_info_card_assets").tr(), ], ), ), diff --git a/mobile/lib/modules/backup/views/backup_album_selection_page.dart b/mobile/lib/modules/backup/views/backup_album_selection_page.dart index 917480def3..27cf556842 100644 --- a/mobile/lib/modules/backup/views/backup_album_selection_page.dart +++ b/mobile/lib/modules/backup/views/backup_album_selection_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -55,7 +56,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) { ImmichToast.show( context: context, - msg: "Cannot remove the only album", + msg: "backup_err_only_album".tr(), toastType: ToastType.error, gravity: ToastGravity.BOTTOM, ); @@ -136,20 +137,21 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { icon: const Icon(Icons.arrow_back_ios_rounded), ), title: const Text( - "Select Albums", + "backup_album_selection_page_select_albums", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), + ).tr(), elevation: 0, ), body: ListView( physics: const ClampingScrollPhysics(), children: [ - const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Text( - "Selection Info", + Padding( + padding: + const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: const Text( + "backup_album_selection_page_selection_info", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), - ), + ).tr(), ), // Selected Album Chips @@ -181,14 +183,18 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ListTile( visualDensity: VisualDensity.compact, title: Text( - "Total unique assets", + "backup_album_selection_page_total_assets", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: Colors.grey[700]), - ), + ).tr(), trailing: Text( - '${ref.watch(backupProvider).allUniqueAssets.length}', + ref + .watch(backupProvider) + .allUniqueAssets + .length + .toString(), style: const TextStyle(fontWeight: FontWeight.bold), ), ), @@ -199,19 +205,20 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ListTile( title: Text( - "Albums on device (${availableAlbums.length})", + "backup_album_selection_page_albums_device" + .tr(args: [availableAlbums.length.toString()]), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), subtitle: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Text( - "Tap to include, double tap to exclude", + "backup_album_selection_page_albums_tap", style: TextStyle( fontSize: 12, color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold, ), - ), + ).tr(), ), trailing: IconButton( splashRadius: 16, @@ -230,21 +237,21 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { borderRadius: BorderRadius.circular(12)), elevation: 5, title: Text( - 'Selection Info', + 'backup_album_selection_page_selection_info', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor, ), - ), + ).tr(), content: SingleChildScrollView( child: ListBody( children: [ Text( - 'Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.', + 'backup_album_selection_page_assets_scatter', style: TextStyle( fontSize: 14, color: Colors.grey[700]), - ), + ).tr(), ], ), ), diff --git a/mobile/lib/modules/backup/views/backup_controller_page.dart b/mobile/lib/modules/backup/views/backup_controller_page.dart index dd2afd7b11..cc968755bd 100644 --- a/mobile/lib/modules/backup/views/backup_controller_page.dart +++ b/mobile/lib/modules/backup/views/backup_controller_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -44,9 +45,9 @@ class BackupControllerPage extends HookConsumerWidget { color: Theme.of(context).primaryColor, ), title: const Text( - "Server storage", + "backup_controller_page_server_storage", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), - ), + ).tr(), subtitle: Padding( padding: const EdgeInsets.only(top: 8.0), child: Column( @@ -66,8 +67,11 @@ class BackupControllerPage extends HookConsumerWidget { ), Padding( padding: const EdgeInsets.only(top: 12.0), - child: Text( - '${backupState.serverInfo.diskUse} of ${backupState.serverInfo.diskSize} used'), + child: const Text('backup_controller_page_storage_format').tr( + args: [ + backupState.serverInfo.diskUse, + backupState.serverInfo.diskSize + ]), ), ], ), @@ -76,11 +80,13 @@ class BackupControllerPage extends HookConsumerWidget { } ListTile _buildBackupController() { - var backUpOption = - authenticationState.deviceInfo.isAutoBackup ? "on" : "off"; + var backUpOption = authenticationState.deviceInfo.isAutoBackup + ? "backup_controller_page_status_on".tr() + : "backup_controller_page_status_off".tr(); var isAutoBackup = authenticationState.deviceInfo.isAutoBackup; - var backupBtnText = - authenticationState.deviceInfo.isAutoBackup ? "off" : "on"; + var backupBtnText = authenticationState.deviceInfo.isAutoBackup + ? "backup_controller_page_turn_off".tr() + : "backup_controller_page_turn_on".tr(); return ListTile( isThreeLine: true, leading: isAutoBackup @@ -90,7 +96,7 @@ class BackupControllerPage extends HookConsumerWidget { ) : const Icon(Icons.cloud_off_rounded), title: Text( - "Back up is $backUpOption", + backUpOption, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), subtitle: Padding( @@ -100,9 +106,9 @@ class BackupControllerPage extends HookConsumerWidget { children: [ if (!isAutoBackup) const Text( - "Turn on backup to automatically upload new assets to the server.", + "backup_controller_page_desc_backup", style: TextStyle(fontSize: 14), - ), + ).tr(), Padding( padding: const EdgeInsets.only(top: 8.0), child: OutlinedButton( @@ -123,7 +129,7 @@ class BackupControllerPage extends HookConsumerWidget { .setAutoBackup(true); } }, - child: Text("Turn $backupBtnText Backup", + child: Text(backupBtnText, style: const TextStyle(fontWeight: FontWeight.bold)), ), ) @@ -134,13 +140,13 @@ class BackupControllerPage extends HookConsumerWidget { } Widget _buildSelectedAlbumName() { - var text = "Selected: "; + var text = "backup_controller_page_backup_selected".tr(); var albums = ref.watch(backupProvider).selectedBackupAlbums; if (albums.isNotEmpty) { for (var album in albums) { if (album.name == "Recent" || album.name == "Recents") { - text += "${album.name} (All), "; + text += "${album.name} (${'backup_all'.tr()}), "; } else { text += "${album.name}, "; } @@ -160,7 +166,7 @@ class BackupControllerPage extends HookConsumerWidget { return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( - "None selected", + "backup_controller_page_none_selected".tr(), style: TextStyle( color: Theme.of(context).primaryColor, fontSize: 12, @@ -171,7 +177,7 @@ class BackupControllerPage extends HookConsumerWidget { } Widget _buildExcludedAlbumName() { - var text = "Excluded: "; + var text = "backup_controller_page_excluded".tr(); var albums = ref.watch(backupProvider).excludedBackupAlbums; if (albums.isNotEmpty) { @@ -207,17 +213,18 @@ class BackupControllerPage extends HookConsumerWidget { borderOnForeground: false, child: ListTile( minVerticalPadding: 15, - title: const Text("Backup Albums", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)), + title: const Text("backup_controller_page_albums", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)) + .tr(), subtitle: Padding( padding: const EdgeInsets.only(top: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - "Albums to be backed up", + "backup_controller_page_to_backup", style: TextStyle(color: Color(0xFF808080), fontSize: 12), - ), + ).tr(), _buildSelectedAlbumName(), _buildExcludedAlbumName() ], @@ -234,14 +241,14 @@ class BackupControllerPage extends HookConsumerWidget { onPressed: () { AutoRouter.of(context).push(const BackupAlbumSelectionRoute()); }, - child: const Padding( - padding: EdgeInsets.symmetric( + child: Padding( + padding: const EdgeInsets.symmetric( vertical: 16.0, ), - child: Text( - "Select", + child: const Text( + "backup_controller_page_select", style: TextStyle(fontWeight: FontWeight.bold), - ), + ).tr(), ), ), ), @@ -387,9 +394,9 @@ class BackupControllerPage extends HookConsumerWidget { appBar: AppBar( elevation: 0, title: const Text( - "Backup", + "backup_controller_page_backup", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), + ).tr(), leading: IconButton( onPressed: () { ref.watch(websocketProvider.notifier).listenUploadEvent(); @@ -405,27 +412,27 @@ class BackupControllerPage extends HookConsumerWidget { child: ListView( // crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Padding( - padding: EdgeInsets.all(8.0), - child: Text( - "Backup Information", + Padding( + padding: const EdgeInsets.all(8.0), + child: const Text( + "backup_controller_page_info", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), + ).tr(), ), _buildFolderSelectionTile(), BackupInfoCard( - title: "Total", - subtitle: "All unique photos and videos from selected albums", + title: "backup_controller_page_total".tr(), + subtitle: "backup_controller_page_total_sub".tr(), info: "${backupState.allUniqueAssets.length}", ), BackupInfoCard( - title: "Backup", - subtitle: "Backed up photos and videos", + title: "backup_controller_page_backup".tr(), + subtitle: "backup_controller_page_backup_sub".tr(), info: "${backupState.selectedAlbumsBackupAssetsIds.length}", ), BackupInfoCard( - title: "Remainder", - subtitle: "Remaining photos and albums to back up from selection", + title: "backup_controller_page_remainder".tr(), + subtitle: "backup_controller_page_remainder_sub".tr(), info: "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}", ), @@ -452,12 +459,12 @@ class BackupControllerPage extends HookConsumerWidget { ref.read(backupProvider.notifier).cancelBackup(); }, child: const Text( - "CANCEL", + "backup_controller_page_cancel", style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, ), - ), + ).tr(), ) : ElevatedButton( style: ElevatedButton.styleFrom( @@ -467,12 +474,12 @@ class BackupControllerPage extends HookConsumerWidget { ), onPressed: shouldBackup ? startBackup : null, child: const Text( - "START BACKUP", + "backup_controller_page_start_backup", style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, ), - ), + ).tr(), ), ), ) diff --git a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart index 0ce5d6d20d..2ff56fb997 100644 --- a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart +++ b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart'; @@ -26,7 +27,7 @@ class ControlBottomAppBar extends StatelessWidget { children: [ ControlBoxButton( iconData: Icons.delete_forever_rounded, - label: "Delete", + label: "control_bottom_app_bar_delete".tr(), onPressed: () { showDialog( context: context, diff --git a/mobile/lib/modules/home/ui/daily_title_text.dart b/mobile/lib/modules/home/ui/daily_title_text.dart index 870dacb3f0..15e941f9a0 100644 --- a/mobile/lib/modules/home/ui/daily_title_text.dart +++ b/mobile/lib/modules/home/ui/daily_title_text.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; @@ -19,7 +20,7 @@ class DailyTitleText extends ConsumerWidget { var currentYear = DateTime.now().year; var groupYear = DateTime.parse(isoDate).year; var formatDateTemplate = - currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy'; + currentYear == groupYear ? "daily_title_text_date".tr() : "daily_title_text_date_year".tr(); var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(isoDate)); var isMultiSelectEnable = diff --git a/mobile/lib/modules/home/ui/delete_diaglog.dart b/mobile/lib/modules/home/ui/delete_diaglog.dart index 5d02303058..3adef135a8 100644 --- a/mobile/lib/modules/home/ui/delete_diaglog.dart +++ b/mobile/lib/modules/home/ui/delete_diaglog.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; @@ -13,18 +14,17 @@ class DeleteDialog extends ConsumerWidget { return AlertDialog( backgroundColor: Colors.grey[200], shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - title: const Text("Delete Permanently"), - content: const Text( - "These items will be permanently deleted from Immich and from your device"), + title: const Text("delete_dialog_title").tr(), + content: const Text("delete_dialog_alert").tr(), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: const Text( - "Cancel", + "delete_dialog_cancel", style: TextStyle(color: Colors.blueGrey), - ), + ).tr(), ), TextButton( onPressed: () { @@ -36,9 +36,9 @@ class DeleteDialog extends ConsumerWidget { Navigator.of(context).pop(); }, child: Text( - "Delete", + "delete_dialog_ok", style: TextStyle(color: Colors.red[400]), - ), + ).tr(), ), ], ); diff --git a/mobile/lib/modules/home/ui/monthly_title_text.dart b/mobile/lib/modules/home/ui/monthly_title_text.dart index 8df0334317..ad9998c8f2 100644 --- a/mobile/lib/modules/home/ui/monthly_title_text.dart +++ b/mobile/lib/modules/home/ui/monthly_title_text.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -11,7 +12,7 @@ class MonthlyTitleText extends StatelessWidget { @override Widget build(BuildContext context) { - var monthTitleText = DateFormat('MMMM y').format(DateTime.parse(isoDate)); + var monthTitleText = DateFormat("monthly_title_text_date_format".tr()).format(DateTime.parse(isoDate)); return SliverToBoxAdapter( child: Padding( diff --git a/mobile/lib/modules/home/ui/profile_drawer.dart b/mobile/lib/modules/home/ui/profile_drawer.dart index 759b613f35..20ef4d139a 100644 --- a/mobile/lib/modules/home/ui/profile_drawer.dart +++ b/mobile/lib/modules/home/ui/profile_drawer.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hive_flutter/hive_flutter.dart'; @@ -183,12 +184,12 @@ class ProfileDrawer extends HookConsumerWidget { color: Colors.black54, ), title: const Text( - "Sign Out", + "profile_drawer_sign_out", style: TextStyle( color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold), - ), + ).tr(), onTap: () async { bool res = await ref.watch(authenticationProvider.notifier).logout(); @@ -227,7 +228,7 @@ class ProfileDrawer extends HookConsumerWidget { child: Text( serverInfoState.isVersionMismatch ? serverInfoState.versionMismatchErrorMessage - : "Client and Server are up-to-date", + : "profile_drawer_client_server_up_to_date".tr(), textAlign: TextAlign.center, style: TextStyle( fontSize: 11, diff --git a/mobile/lib/modules/login/ui/login_form.dart b/mobile/lib/modules/login/ui/login_form.dart index e8d77c6b39..b109254a77 100644 --- a/mobile/lib/modules/login/ui/login_form.dart +++ b/mobile/lib/modules/login/ui/login_form.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hive/hive.dart'; @@ -21,7 +22,7 @@ class LoginForm extends HookConsumerWidget { final passwordController = useTextEditingController.fromValue(TextEditingValue.empty); final serverEndpointController = - useTextEditingController(text: 'http://your-server-ip:2283/api'); + useTextEditingController(text: 'login_endpoint_hint'.tr()); final isSaveLoginInfo = useState(false); useEffect(() { @@ -73,12 +74,12 @@ class LoginForm extends HookConsumerWidget { borderRadius: BorderRadius.circular(5)), enableFeedback: true, title: const Text( - "Stay logged in", + "login_form_save_login", style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey), - ), + ).tr(), value: isSaveLoginInfo.value, onChanged: (switchValue) { if (switchValue != null) { @@ -107,11 +108,11 @@ class ServerEndpointInput extends StatelessWidget { : super(key: key); String? _validateInput(String? url) { - + if (url?.startsWith(RegExp(r'https?://')) == true) { return null; } else { - return 'Please specify http:// or https://'; + return 'login_form_err_http'.tr(); } } @@ -119,10 +120,10 @@ class ServerEndpointInput extends StatelessWidget { Widget build(BuildContext context) { return TextFormField( controller: controller, - decoration: const InputDecoration( - labelText: 'Server Endpoint URL', + decoration: InputDecoration( + labelText: 'login_form_endpoint_url'.tr(), border: OutlineInputBorder(), - hintText: 'http://your-server-ip:port', + hintText: 'login_form_endpoint_hint'.tr(), ), validator: _validateInput, autovalidateMode: AutovalidateMode.always, @@ -137,9 +138,10 @@ class EmailInput extends StatelessWidget { String? _validateInput(String? email) { if (email == null || email == '') return null; - if (email.endsWith(' ')) return 'Trailing whitespace'; - if (email.startsWith(' ')) return 'Leading whitespace'; - if (email.contains(' ') || !email.contains('@')) return 'Invalid Email'; + if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr(); + if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr(); + if (email.contains(' ') || !email.contains('@')) + return 'login_form_err_invalid_email'.tr(); return null; } @@ -147,10 +149,10 @@ class EmailInput extends StatelessWidget { Widget build(BuildContext context) { return TextFormField( controller: controller, - decoration: const InputDecoration( - labelText: 'Email', + decoration: InputDecoration( + labelText: 'login_form_label_email'.tr(), border: OutlineInputBorder(), - hintText: 'youremail@email.com', + hintText: 'login_form_email_hint'.tr(), ), validator: _validateInput, autovalidateMode: AutovalidateMode.always, @@ -168,10 +170,10 @@ class PasswordInput extends StatelessWidget { return TextFormField( obscureText: true, controller: controller, - decoration: const InputDecoration( - labelText: 'Password', + decoration: InputDecoration( + labelText: 'login_form_label_password'.tr(), border: OutlineInputBorder(), - hintText: 'password'), + hintText: 'login_form_password_hint'.tr()), ); } } @@ -222,15 +224,14 @@ class LoginButton extends ConsumerWidget { } else { ImmichToast.show( context: context, - msg: - "Error logging you in, check server url, email and password!", + msg: "login_failed".tr(), toastType: ToastType.error, ); } }, child: const Text( - "Login", + "login_form_button_text", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), - )); + ).tr()); } } diff --git a/mobile/lib/modules/search/ui/search_bar.dart b/mobile/lib/modules/search/ui/search_bar.dart index 3d6f3e85a1..f1b67749af 100644 --- a/mobile/lib/modules/search/ui/search_bar.dart +++ b/mobile/lib/modules/search/ui/search_bar.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -47,8 +48,8 @@ class SearchBar extends HookConsumerWidget with PreferredSizeWidget { onChanged: (value) { ref.watch(searchPageStateProvider.notifier).setSearchTerm(value); }, - decoration: const InputDecoration( - hintText: 'Search your photos', + decoration: InputDecoration( + hintText: 'search_bar_hint'.tr(), enabledBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.transparent), ), diff --git a/mobile/lib/modules/search/ui/thumbnail_with_info.dart b/mobile/lib/modules/search/ui/thumbnail_with_info.dart index e3d95e099d..36956b9a74 100644 --- a/mobile/lib/modules/search/ui/thumbnail_with_info.dart +++ b/mobile/lib/modules/search/ui/thumbnail_with_info.dart @@ -55,7 +55,7 @@ class ThumbnailWithInfo extends StatelessWidget { child: SizedBox( width: MediaQuery.of(context).size.width / 3, child: Text( - textInfo.capitalizeFirstLetter(), + textInfo, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart index bc0c27037d..9a2a5fb54d 100644 --- a/mobile/lib/modules/search/views/search_page.dart +++ b/mobile/lib/modules/search/views/search_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hive_flutter/hive_flutter.dart'; @@ -82,7 +83,7 @@ class SearchPage extends HookConsumerWidget { return ThumbnailWithInfo( imageUrl: 'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60', - textInfo: 'No Places Info Available', + textInfo: 'search_page_no_places'.tr(), onTap: () {}, ); }), @@ -134,7 +135,7 @@ class SearchPage extends HookConsumerWidget { return ThumbnailWithInfo( imageUrl: 'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60', - textInfo: 'No Object Info Available', + textInfo: 'sarch_no_objects'.tr(), onTap: () {}, ); }), @@ -158,20 +159,20 @@ class SearchPage extends HookConsumerWidget { children: [ ListView( children: [ - const Padding( + Padding( padding: EdgeInsets.all(16.0), - child: Text( - "Places", + child: const Text( + "search_page_places", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), + ).tr(), ), _buildPlaces(), - const Padding( + Padding( padding: EdgeInsets.all(16.0), - child: Text( - "Things", + child: const Text( + "search_page_things", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), + ).tr(), ), _buildThings() ], diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart index 3902c6f967..7fd14abfa9 100644 --- a/mobile/lib/modules/search/views/search_result_page.dart +++ b/mobile/lib/modules/search/views/search_result_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; @@ -66,8 +67,8 @@ class SearchResultPage extends HookConsumerWidget { onChanged: (value) { ref.watch(searchPageStateProvider.notifier).setSearchTerm(value); }, - decoration: const InputDecoration( - hintText: 'New Search', + decoration: InputDecoration( + hintText: 'search_result_page_new_search_hint'.tr(), enabledBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.transparent), ), diff --git a/mobile/lib/modules/sharing/ui/album_title_text_field.dart b/mobile/lib/modules/sharing/ui/album_title_text_field.dart index 8eca564307..1556c87fad 100644 --- a/mobile/lib/modules/sharing/ui/album_title_text_field.dart +++ b/mobile/lib/modules/sharing/ui/album_title_text_field.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/sharing/providers/album_title.provider.dart'; @@ -59,7 +60,7 @@ class AlbumTitleTextField extends ConsumerWidget { borderSide: const BorderSide(color: Colors.transparent), borderRadius: BorderRadius.circular(10), ), - hintText: 'Add a title', + hintText: 'share_add_title'.tr(), focusColor: Colors.grey[300], fillColor: Colors.grey[200], filled: isAlbumTitleTextFieldFocus.value, diff --git a/mobile/lib/modules/sharing/ui/album_viewer_appbar.dart b/mobile/lib/modules/sharing/ui/album_viewer_appbar.dart index e364522e68..bcb0690a24 100644 --- a/mobile/lib/modules/sharing/ui/album_viewer_appbar.dart +++ b/mobile/lib/modules/sharing/ui/album_viewer_appbar.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -45,7 +46,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { } else { ImmichToast.show( context: context, - msg: "Failed to delete album", + msg: "album_viewer_appbar_share_err_delete".tr(), toastType: ToastType.error, gravity: ToastGravity.BOTTOM, ); @@ -67,7 +68,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { Navigator.pop(context); ImmichToast.show( context: context, - msg: "Failed to leave album", + msg: "album_viewer_appbar_share_err_leave".tr(), toastType: ToastType.error, gravity: ToastGravity.BOTTOM, ); @@ -93,7 +94,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { Navigator.pop(context); ImmichToast.show( context: context, - msg: "There are problems in removing assets from album", + msg: "album_viewer_appbar_share_err_remove".tr(), toastType: ToastType.error, gravity: ToastGravity.BOTTOM, ); @@ -108,9 +109,9 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { return ListTile( leading: const Icon(Icons.delete_sweep_rounded), title: const Text( - 'Remove from album', + 'album_viewer_appbar_share_remove', style: TextStyle(fontWeight: FontWeight.bold), - ), + ).tr(), onTap: () => _onRemoveFromAlbumPressed(albumId), ); } else { @@ -121,18 +122,18 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { return ListTile( leading: const Icon(Icons.delete_forever_rounded), title: const Text( - 'Delete album', + 'album_viewer_appbar_share_delete', style: TextStyle(fontWeight: FontWeight.bold), - ), + ).tr(), onTap: () => _onDeleteAlbumPressed(albumId), ); } else { return ListTile( leading: const Icon(Icons.person_remove_rounded), title: const Text( - 'Leave album', + 'album_viewer_appbar_share_leave', style: TextStyle(fontWeight: FontWeight.bold), - ), + ).tr(), onTap: () => _onLeaveAlbumPressed(albumId), ); } @@ -176,7 +177,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { if (!isSuccess) { ImmichToast.show( context: context, - msg: "Failed to change album title", + msg: "album_viewer_appbar_share_err_title".tr(), gravity: ToastGravity.BOTTOM, toastType: ToastType.error, ); diff --git a/mobile/lib/modules/sharing/ui/album_viewer_editable_title.dart b/mobile/lib/modules/sharing/ui/album_viewer_editable_title.dart index 7a9b0127b1..52d0938680 100644 --- a/mobile/lib/modules/sharing/ui/album_viewer_editable_title.dart +++ b/mobile/lib/modules/sharing/ui/album_viewer_editable_title.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -74,7 +75,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget { focusColor: Colors.grey[300], fillColor: Colors.grey[200], filled: titleFocusNode.hasFocus, - hintText: 'Add a title', + hintText: 'share_add_title'.tr(), ), ); } diff --git a/mobile/lib/modules/sharing/ui/sharing_sliver_appbar.dart b/mobile/lib/modules/sharing/ui/sharing_sliver_appbar.dart index 2290e6bd74..32adf2138c 100644 --- a/mobile/lib/modules/sharing/ui/sharing_sliver_appbar.dart +++ b/mobile/lib/modules/sharing/ui/sharing_sliver_appbar.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -51,10 +52,10 @@ class SharingSliverAppBar extends StatelessWidget { size: 20, ), label: const Text( - "Create shared album", + "sharing_silver_appbar_create_shared_album", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ), + ).tr(), ), ), ), @@ -73,10 +74,10 @@ class SharingSliverAppBar extends StatelessWidget { size: 20, ), label: const Text( - "Share with partner", + "sharing_silver_appbar_share_partner", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ), + ).tr(), ), ), ) diff --git a/mobile/lib/modules/sharing/views/album_viewer_page.dart b/mobile/lib/modules/sharing/views/album_viewer_page.dart index b450a01e8d..1f26a643ec 100644 --- a/mobile/lib/modules/sharing/views/album_viewer_page.dart +++ b/mobile/lib/modules/sharing/views/album_viewer_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -204,13 +205,13 @@ class AlbumViewerPage extends HookConsumerWidget { AlbumActionOutlinedButton( iconData: Icons.add_photo_alternate_outlined, onPressed: () => _onAddPhotosPressed(albumInfo), - labelText: "Add photos", + labelText: "share_add_photos".tr(), ), if (userId == albumInfo.ownerId) AlbumActionOutlinedButton( iconData: Icons.person_add_alt_rounded, onPressed: () => _onAddUsersPressed(albumInfo), - labelText: "Add users", + labelText: "album_viewer_page_share_add_users".tr(), ), ], ), diff --git a/mobile/lib/modules/sharing/views/asset_selection_page.dart b/mobile/lib/modules/sharing/views/asset_selection_page.dart index 2ab5371f43..b12a96de7b 100644 --- a/mobile/lib/modules/sharing/views/asset_selection_page.dart +++ b/mobile/lib/modules/sharing/views/asset_selection_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -65,9 +66,9 @@ class AssetSelectionPage extends HookConsumerWidget { ), title: selectedAssets.isEmpty ? const Text( - 'Add photos', + 'share_add_photos', style: TextStyle(fontSize: 18), - ) + ).tr() : Text( _buildAssetCountText(), style: const TextStyle(fontSize: 18), @@ -86,9 +87,9 @@ class AssetSelectionPage extends HookConsumerWidget { AutoRouter.of(context).pop(payload); }, child: const Text( - "Add", + "share_add", style: TextStyle(fontWeight: FontWeight.bold), - ), + ).tr(), ), ], ), diff --git a/mobile/lib/modules/sharing/views/create_shared_album_page.dart b/mobile/lib/modules/sharing/views/create_shared_album_page.dart index b74ee283c9..2da6eae0a9 100644 --- a/mobile/lib/modules/sharing/views/create_shared_album_page.dart +++ b/mobile/lib/modules/sharing/views/create_shared_album_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -64,13 +65,13 @@ class CreateSharedAlbumPage extends HookConsumerWidget { _buildTitle() { if (selectedAssets.isEmpty) { - return const SliverToBoxAdapter( + return SliverToBoxAdapter( child: Padding( padding: EdgeInsets.only(top: 200, left: 18), child: Text( - 'ADD ASSETS', + 'create_shared_album_page_share_add_assets', style: TextStyle(fontSize: 12), - ), + ).tr(), ), ); } @@ -97,12 +98,12 @@ class CreateSharedAlbumPage extends HookConsumerWidget { label: Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( - 'Select Photos', + 'create_shared_album_page_share_select_photos', style: TextStyle( fontSize: 16, color: Colors.grey[700], fontWeight: FontWeight.bold), - ), + ).tr(), ), ), ), @@ -123,7 +124,7 @@ class CreateSharedAlbumPage extends HookConsumerWidget { AlbumActionOutlinedButton( iconData: Icons.add_photo_alternate_outlined, onPressed: _onSelectPhotosButtonPressed, - labelText: "Add photos", + labelText: "share_add_photos".tr(), ), ], ), @@ -169,16 +170,16 @@ class CreateSharedAlbumPage extends HookConsumerWidget { }, icon: const Icon(Icons.close_rounded)), title: const Text( - 'Create album', + 'share_create_album', style: TextStyle(color: Colors.black), - ), + ).tr(), actions: [ TextButton( onPressed: albumTitleController.text.isNotEmpty ? _showSelectUserPage : null, - child: const Text( - 'Share', + child: Text( + 'create_shared_album_page_share'.tr(), style: TextStyle( fontWeight: FontWeight.bold, ), diff --git a/mobile/lib/modules/sharing/views/select_additional_user_for_sharing_page.dart b/mobile/lib/modules/sharing/views/select_additional_user_for_sharing_page.dart index 87ba9b6ba0..59ebc7e009 100644 --- a/mobile/lib/modules/sharing/views/select_additional_user_for_sharing_page.dart +++ b/mobile/lib/modules/sharing/views/select_additional_user_for_sharing_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -68,10 +69,10 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { Wrap( children: [...usersChip], ), - const Padding( + Padding( padding: EdgeInsets.all(16.0), child: Text( - 'Suggestions', + 'select_additional_user_for_sharing_page_suggestions'.tr(), style: TextStyle( fontSize: 14, color: Colors.grey, @@ -112,9 +113,9 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( title: const Text( - 'Invite to album', + 'share_invite', style: TextStyle(color: Colors.black), - ), + ).tr(), elevation: 0, centerTitle: false, leading: IconButton( @@ -128,9 +129,9 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { onPressed: sharedUsersList.value.isEmpty ? null : _addNewUsersHandler, child: const Text( - "Add", + "share_add", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), - ), + ).tr(), ) ], ), diff --git a/mobile/lib/modules/sharing/views/select_user_for_sharing_page.dart b/mobile/lib/modules/sharing/views/select_user_for_sharing_page.dart index 0001edd206..0e0efc4878 100644 --- a/mobile/lib/modules/sharing/views/select_user_for_sharing_page.dart +++ b/mobile/lib/modules/sharing/views/select_user_for_sharing_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -36,8 +37,7 @@ class SelectUserForSharingPage extends HookConsumerWidget { .navigate(const TabControllerRoute(children: [SharingRoute()])); } - const ScaffoldMessenger( - child: SnackBar(content: Text('Failed to create album'))); + ScaffoldMessenger(child: SnackBar(content: Text('select_user_for_sharing_page_err_album').tr())); } _buildTileIcon(User user) { @@ -84,15 +84,15 @@ class SelectUserForSharingPage extends HookConsumerWidget { Wrap( children: [...usersChip], ), - const Padding( + Padding( padding: EdgeInsets.all(16.0), child: Text( - 'Suggestions', + 'share_suggestions', style: TextStyle( fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), - ), + ).tr(), ), ListView.builder( shrinkWrap: true, @@ -128,9 +128,9 @@ class SelectUserForSharingPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( title: const Text( - 'Invite to album', + 'share_invite', style: TextStyle(color: Colors.black), - ), + ).tr(), elevation: 0, centerTitle: false, leading: IconButton( @@ -144,9 +144,9 @@ class SelectUserForSharingPage extends HookConsumerWidget { onPressed: sharedUsersList.value.isEmpty ? null : _createSharedAlbum, child: const Text( - "Create Album", + "share_create_album", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), - )) + ).tr()) ], ), body: suggestedShareUsers.when( diff --git a/mobile/lib/modules/sharing/views/sharing_page.dart b/mobile/lib/modules/sharing/views/sharing_page.dart index 45a717de91..616b55b4b9 100644 --- a/mobile/lib/modules/sharing/views/sharing_page.dart +++ b/mobile/lib/modules/sharing/views/sharing_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hive/hive.dart'; @@ -104,20 +105,20 @@ class SharingPage extends HookConsumerWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - 'EMPTY LIST', + 'sharing_page_empty_list', style: TextStyle( fontSize: 12, color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold, ), - ), + ).tr(), ), Padding( padding: const EdgeInsets.all(8.0), child: Text( - 'Create shared albums to share photos and videos with people in your network.', + 'sharing_page_description', style: TextStyle(fontSize: 12, color: Colors.grey[700]), - ), + ).tr(), ), ], ), @@ -131,15 +132,15 @@ class SharingPage extends HookConsumerWidget { body: CustomScrollView( slivers: [ const SharingSliverAppBar(), - const SliverPadding( + SliverPadding( padding: EdgeInsets.symmetric(horizontal: 12, vertical: 12), sliver: SliverToBoxAdapter( child: Text( - "Shared albums", + "sharing_page_album", style: TextStyle( fontWeight: FontWeight.bold, ), - ), + ).tr(), ), ), sharedAlbums.isNotEmpty diff --git a/mobile/lib/shared/views/tab_controller_page.dart b/mobile/lib/shared/views/tab_controller_page.dart index c099c15c09..b32baf33bf 100644 --- a/mobile/lib/shared/views/tab_controller_page.dart +++ b/mobile/lib/shared/views/tab_controller_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; @@ -41,13 +42,16 @@ class TabControllerPage extends ConsumerWidget { onTap: (index) { tabsRouter.setActiveIndex(index); }, - items: const [ + items: [ BottomNavigationBarItem( - label: 'Photos', icon: Icon(Icons.photo)), + label: 'tab_controller_nav_photos'.tr(), + icon: const Icon(Icons.photo)), BottomNavigationBarItem( - label: 'Search', icon: Icon(Icons.search)), + label: 'tab_controller_nav_search'.tr(), + icon: const Icon(Icons.search)), BottomNavigationBarItem( - label: 'Sharing', icon: Icon(Icons.group_outlined)), + label: 'tab_controller_nav_sharing'.tr(), + icon: const Icon(Icons.group_outlined)), ], ), ), diff --git a/mobile/lib/shared/views/version_announcement_overlay.dart b/mobile/lib/shared/views/version_announcement_overlay.dart index 50db4ba17c..90095ef27f 100644 --- a/mobile/lib/shared/views/version_announcement_overlay.dart +++ b/mobile/lib/shared/views/version_announcement_overlay.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -40,14 +41,14 @@ class VersionAnnouncementOverlay extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - "New Server Version Available 🎉", + "version_announcement_overlay_title", style: TextStyle( fontSize: 16, fontFamily: 'WorkSans', fontWeight: FontWeight.bold, color: Colors.indigo, ), - ), + ).tr(), Padding( padding: const EdgeInsets.only(top: 16.0), child: RichText( @@ -58,9 +59,8 @@ class VersionAnnouncementOverlay extends HookConsumerWidget { color: Colors.black87, height: 1.2), children: [ - const TextSpan( - text: - 'Hi friend, there is a new release of', + TextSpan( + text: 'version_announcement_overlay_text_1'.tr(), ), const TextSpan( text: ' Immich ', @@ -70,22 +70,21 @@ class VersionAnnouncementOverlay extends HookConsumerWidget { fontWeight: FontWeight.bold, ), ), - const TextSpan( - text: - "please take your time to visit the ", + TextSpan( + text: "version_announcement_overlay_text_2".tr(), ), TextSpan( - text: "release note", + text: "version_announcement_overlay_release_notes" + .tr(), style: const TextStyle( decoration: TextDecoration.underline, ), recognizer: TapGestureRecognizer() ..onTap = goToReleaseNote, ), - const TextSpan( - text: - " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", - ), + TextSpan( + text: "version_announcement_overlay_text_3".tr(), + ) ], ), ), @@ -93,24 +92,23 @@ class VersionAnnouncementOverlay extends HookConsumerWidget { Padding( padding: const EdgeInsets.only(top: 16.0), child: ElevatedButton( - style: ElevatedButton.styleFrom( - shape: const StadiumBorder(), - visualDensity: VisualDensity.standard, - primary: Colors.indigo, - onPrimary: Colors.grey[50], - elevation: 2, - padding: const EdgeInsets.symmetric( - vertical: 10, horizontal: 25), - ), - onPressed: onAcknowledgeTapped, - child: const Text( - "Acknowledge", - style: TextStyle( - fontSize: 14, + style: ElevatedButton.styleFrom( + shape: const StadiumBorder(), + visualDensity: VisualDensity.standard, + primary: Colors.indigo, + onPrimary: Colors.grey[50], + elevation: 2, + padding: const EdgeInsets.symmetric( + vertical: 10, horizontal: 25), ), - ), - ), - ), + onPressed: onAcknowledgeTapped, + child: const Text( + "version_announcement_overlay_ack", + style: TextStyle( + fontSize: 14, + ), + ).tr()), + ) ], ), ), diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 90caf6d1d9..911cc10b19 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -253,6 +253,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.6" + easy_localization: + dependency: "direct main" + description: + name: easy_localization + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + easy_logger: + dependency: transitive + description: + name: easy_logger + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2" equatable: dependency: "direct main" description: @@ -335,6 +349,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_map: dependency: "direct main" description: @@ -842,6 +861,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.27.3" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.15" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.12" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" shelf: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 2c1828e5e9..6633f1146e 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: url_launcher: ^6.1.3 http: 0.13.4 cancellation_token_http: ^1.1.0 + easy_localization: ^3.0.1 path: ^1.8.1 path_provider: ^2.0.11 @@ -59,6 +60,7 @@ flutter: uses-material-design: true assets: - assets/ + - assets/i18n/ fonts: - family: WorkSans fonts: diff --git a/mobile/scripts/check_i18n_keys.py b/mobile/scripts/check_i18n_keys.py new file mode 100644 index 0000000000..8d748ceb06 --- /dev/null +++ b/mobile/scripts/check_i18n_keys.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +import json +import subprocess + +def main(): + with open('assets/i18n/en-US.json', 'r') as f: + data = json.load(f) + + for k in data.keys(): + print(k) + sp = subprocess.run(['sh', '-c', f'grep -r --include="*.dart" "{k}"']) + + if sp.returncode != 0: + print("Not found in source code!") + return 1 + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/mobile/scripts/check_key_uniform.py b/mobile/scripts/check_key_uniform.py new file mode 100644 index 0000000000..3140f55a12 --- /dev/null +++ b/mobile/scripts/check_key_uniform.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +import json +import subprocess + +def main(): + print("CHECK GERMAN TRANSLATIONS") + with open('assets/i18n/de-DE.json', 'r') as f: + data = json.load(f) + + for k in data.keys(): + print(k) + sp = subprocess.run(['sh', '-c', f'grep -r --include="./assets/i18n/en-US.json" "{k}"']) + + if sp.returncode != 0: + print(f"Outdated Key! {k}") + return 1 + +if __name__ == '__main__': + main() \ No newline at end of file