diff --git a/mobile/lib/extensions/translate_extensions.dart b/mobile/lib/extensions/translate_extensions.dart new file mode 100644 index 0000000000..122830843d --- /dev/null +++ b/mobile/lib/extensions/translate_extensions.dart @@ -0,0 +1,50 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:intl/message_format.dart'; +import 'package:flutter/material.dart'; + +extension StringTranslateExtension on String { + String t({BuildContext? context, Map? args}) { + return _translateHelper(context, this, args); + } +} + +extension TextTranslateExtension on Text { + Text t({BuildContext? context, Map? args}) { + return Text( + _translateHelper(context, data ?? '', args), + key: key, + style: style, + strutStyle: strutStyle, + textAlign: textAlign, + textDirection: textDirection, + locale: locale, + softWrap: softWrap, + overflow: overflow, + textScaler: textScaler, + maxLines: maxLines, + semanticsLabel: semanticsLabel, + textWidthBasis: textWidthBasis, + textHeightBehavior: textHeightBehavior, + ); + } +} + +String _translateHelper( + BuildContext? context, + String key, [ + Map? args, +]) { + if (key.isEmpty) { + return ''; + } + try { + final translatedMessage = key.tr(context: context); + return args != null + ? MessageFormat(translatedMessage, locale: Intl.defaultLocale ?? 'en') + .format(args) + : translatedMessage; + } catch (e) { + debugPrint('Translation failed for key "$key". Error: $e'); + return key; + } +} diff --git a/mobile/lib/pages/albums/albums.page.dart b/mobile/lib/pages/albums/albums.page.dart index 9d8ebb7673..2a13ccccd7 100644 --- a/mobile/lib/pages/albums/albums.page.dart +++ b/mobile/lib/pages/albums/albums.page.dart @@ -8,13 +8,13 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/translation.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; @@ -230,11 +230,17 @@ class AlbumsPage extends HookConsumerWidget { ), subtitle: sorted[index].ownerId != null ? Text( - '${t('items_count', { - 'count': sorted[index].assetCount, - })} • ${sorted[index].ownerId != userId ? t('shared_by_user', { - 'user': sorted[index].ownerName!, - }) : 'owned'.tr()}', + '${'items_count'.t( + context: context, + args: { + 'count': sorted[index].assetCount, + }, + )} • ${sorted[index].ownerId != userId ? 'shared_by_user'.t( + context: context, + args: { + 'user': sorted[index].ownerName!, + }, + ) : 'owned'.t(context: context)}', overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith( diff --git a/mobile/lib/pages/library/local_albums.page.dart b/mobile/lib/pages/library/local_albums.page.dart index 5ce6d453ae..9eceaca205 100644 --- a/mobile/lib/pages/library/local_albums.page.dart +++ b/mobile/lib/pages/library/local_albums.page.dart @@ -2,9 +2,9 @@ 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/utils/translation.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -46,7 +46,10 @@ class LocalAlbumsPage extends HookConsumerWidget { ), ), subtitle: Text( - t('items_count', {'count': albums[index].assetCount}), + 'items_count'.t( + context: context, + args: {'count': albums[index].assetCount}, + ), style: context.textTheme.bodyMedium?.copyWith( color: context.colorScheme.onSurfaceSecondary, ), diff --git a/mobile/lib/services/memory.service.dart b/mobile/lib/services/memory.service.dart index d6c44278c7..ab0e685778 100644 --- a/mobile/lib/services/memory.service.dart +++ b/mobile/lib/services/memory.service.dart @@ -1,10 +1,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/models/memories/memory.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/utils/translation.dart'; import 'package:logging/logging.dart'; final memoryServiceProvider = StateProvider((ref) { @@ -40,7 +40,11 @@ class MemoryService { .getAllByRemoteId(memory.assets.map((e) => e.id)); final yearsAgo = now.year - memory.data.year; if (dbAssets.isNotEmpty) { - final String title = t('years_ago', {'years': yearsAgo.toString()}); + final String title = 'years_ago'.t( + args: { + 'years': yearsAgo.toString(), + }, + ); memories.add( Memory( title: title, diff --git a/mobile/lib/utils/selection_handlers.dart b/mobile/lib/utils/selection_handlers.dart index e22076aae9..a5466c83a2 100644 --- a/mobile/lib/utils/selection_handlers.dart +++ b/mobile/lib/utils/selection_handlers.dart @@ -6,10 +6,10 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/asset_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/services/asset.service.dart'; import 'package:immich_mobile/services/share.service.dart'; -import 'package:immich_mobile/utils/translation.dart'; import 'package:immich_mobile/widgets/common/date_time_picker.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/location_picker.dart'; @@ -59,10 +59,11 @@ Future handleArchiveAssets( await ref .read(assetProvider.notifier) .toggleArchive(selection, shouldArchive); - final message = shouldArchive - ? t('moved_to_archive', {'count': selection.length}) - : t('moved_to_library', {'count': selection.length}); + ? 'moved_to_archive' + .t(context: context, args: {'count': selection.length}) + : 'moved_to_library' + .t(context: context, args: {'count': selection.length}); if (context.mounted) { ImmichToast.show( context: context, diff --git a/mobile/lib/utils/translation.dart b/mobile/lib/utils/translation.dart deleted file mode 100644 index 1a33161dbc..0000000000 --- a/mobile/lib/utils/translation.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:intl/message_format.dart'; - -String t(String key, [Map? args]) { - try { - String message = key.tr(); - if (args != null) { - return MessageFormat(message, locale: Intl.defaultLocale ?? 'en') - .format(args); - } - return message; - } catch (e) { - return key; - } -} diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart index 9f78b6066d..5e89cd7db3 100644 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ b/mobile/lib/widgets/album/album_thumbnail_card.dart @@ -4,8 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/utils/translation.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; class AlbumThumbnailCard extends ConsumerWidget { @@ -62,7 +62,12 @@ class AlbumThumbnailCard extends ConsumerWidget { if (album.ownerId == ref.read(currentUserProvider)?.id) { owner = 'owned'.tr(); } else if (album.ownerName != null) { - owner = t('shared_by_user', {'user': album.ownerName!}); + owner = 'shared_by_user'.t( + context: context, + args: { + 'user': album.ownerName!, + }, + ); } } @@ -70,7 +75,12 @@ class AlbumThumbnailCard extends ConsumerWidget { TextSpan( children: [ TextSpan( - text: t('items_count', {'count': album.assetCount}), + text: 'items_count'.t( + context: context, + args: { + 'count': album.assetCount, + }, + ), ), if (owner != null) const TextSpan(text: ' • '), if (owner != null) TextSpan(text: owner), diff --git a/mobile/lib/widgets/album/album_thumbnail_listtile.dart b/mobile/lib/widgets/album/album_thumbnail_listtile.dart index 11ef5d329b..f35d4b7ede 100644 --- a/mobile/lib/widgets/album/album_thumbnail_listtile.dart +++ b/mobile/lib/widgets/album/album_thumbnail_listtile.dart @@ -4,10 +4,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/utils/translation.dart'; import 'package:openapi/api.dart'; class AlbumThumbnailListTile extends StatelessWidget { @@ -91,7 +91,12 @@ class AlbumThumbnailListTile extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - t('items_count', {'count': album.assetCount}), + 'items_count'.t( + context: context, + args: { + 'count': album.assetCount, + }, + ), style: const TextStyle( fontSize: 12, ), diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart index 9904447569..98b1c6f601 100644 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ b/mobile/lib/widgets/asset_grid/multiselect_grid.dart @@ -11,6 +11,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/asset_selection_state.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; @@ -24,7 +25,6 @@ import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/stack.service.dart'; import 'package:immich_mobile/utils/immich_loading_overlay.dart'; import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/utils/translation.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart'; import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; @@ -257,13 +257,19 @@ class MultiselectGrid extends HookConsumerWidget { final failedCount = totalCount - successCount; final msg = failedCount > 0 - ? t('assets_downloaded_failed', { - 'count': successCount, - 'error': failedCount, - }) - : t('assets_downloaded_successfully', { - 'count': successCount, - }); + ? 'assets_downloaded_failed'.t( + context: context, + args: { + 'count': successCount, + 'error': failedCount, + }, + ) + : 'assets_downloaded_successfully'.t( + context: context, + args: { + 'count': successCount, + }, + ); ImmichToast.show( context: context, diff --git a/mobile/lib/widgets/backup/ios_debug_info_tile.dart b/mobile/lib/widgets/backup/ios_debug_info_tile.dart index 04be0c00dc..de80b3bfd1 100644 --- a/mobile/lib/widgets/backup/ios_debug_info_tile.dart +++ b/mobile/lib/widgets/backup/ios_debug_info_tile.dart @@ -1,9 +1,9 @@ -import 'package:easy_localization/easy_localization.dart'; +import 'package:intl/intl.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/utils/translation.dart'; /// This is a simple debug widget which should be removed later on when we are /// more confident about background sync @@ -22,29 +22,28 @@ class IosDebugInfoTile extends HookConsumerWidget { final String title; if (processes == 0) { - title = 'ios_debug_info_no_processes_queued'.tr(); + title = 'ios_debug_info_no_processes_queued'.t(context: context); } else { - title = t('ios_debug_info_processes_queued', {'count': processes}); + title = 'ios_debug_info_processes_queued' + .t(context: context, args: {'count': processes}); } final df = DateFormat.yMd().add_jm(); final String subtitle; if (fetch == null && processing == null) { - subtitle = 'ios_debug_info_no_sync_yet'.tr(); + subtitle = 'ios_debug_info_no_sync_yet'.t(context: context); } else if (fetch != null && processing == null) { - subtitle = - t('ios_debug_info_fetch_ran_at', {'dateTime': df.format(fetch)}); + subtitle = 'ios_debug_info_fetch_ran_at' + .t(context: context, args: {'dateTime': df.format(fetch)}); } else if (processing != null && fetch == null) { - subtitle = t( - 'ios_debug_info_processing_ran_at', - {'dateTime': df.format(processing)}, - ); + subtitle = 'ios_debug_info_processing_ran_at' + .t(context: context, args: {'dateTime': df.format(processing)}); } else { final fetchOrProcessing = fetch!.isAfter(processing!) ? fetch : processing; - subtitle = t( - 'ios_debug_info_last_sync_at', - {'dateTime': df.format(fetchOrProcessing)}, + subtitle = 'ios_debug_info_last_sync_at'.t( + context: context, + args: {'dateTime': df.format(fetchOrProcessing)}, ); } diff --git a/mobile/lib/widgets/settings/language_settings.dart b/mobile/lib/widgets/settings/language_settings.dart index 7dc7f89ea1..4d41d5b19b 100644 --- a/mobile/lib/widgets/settings/language_settings.dart +++ b/mobile/lib/widgets/settings/language_settings.dart @@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/constants/locales.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; @@ -91,6 +92,7 @@ class LanguageSettings extends HookConsumerWidget { padding: const EdgeInsets.all(8), itemCount: filteredLocaleEntries.value.length, itemExtent: 64.0, + cacheExtent: 100, itemBuilder: (context, index) { final countryName = filteredLocaleEntries.value[index].key; @@ -100,6 +102,7 @@ class LanguageSettings extends HookConsumerWidget { selectedLocale.value == localeValue; return _LanguageItem( + key: ValueKey(localeValue.toString()), countryName: countryName, localeValue: localeValue, isSelected: isSelected, @@ -162,7 +165,7 @@ class _LanguageSearchBar extends StatelessWidget { child: SearchField( autofocus: false, contentPadding: const EdgeInsets.all(12), - hintText: 'language_search_hint'.tr(), + hintText: 'language_search_hint'.t(context: context), prefixIcon: const Icon(Icons.search_rounded), suffixIcon: controller.text.isNotEmpty ? IconButton( @@ -196,14 +199,14 @@ class _LanguageNotFound extends StatelessWidget { ), const SizedBox(height: 8), Text( - 'language_no_results_title'.tr(), + 'language_no_results_title'.t(context: context), style: context.textTheme.titleMedium?.copyWith( color: context.colorScheme.onSurface, ), ), const SizedBox(height: 4), Text( - 'language_no_results_subtitle'.tr(), + 'language_no_results_subtitle'.t(context: context), style: context.textTheme.bodyMedium?.copyWith( color: context.colorScheme.onSurface.withValues(alpha: 0.8), ), @@ -246,7 +249,7 @@ class _LanguageApplyButton extends StatelessWidget { ), ) : Text( - 'setting_languages_apply'.tr(), + 'setting_languages_apply'.t(context: context), style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 16.0, @@ -261,6 +264,7 @@ class _LanguageApplyButton extends StatelessWidget { class _LanguageItem extends StatelessWidget { const _LanguageItem({ + super.key, required this.countryName, required this.localeValue, required this.isSelected,