From e6fdb56bcd080628823e03c8b9cb5804fbae736d Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:38:30 +0530 Subject: [PATCH] fix: unsupported locale for date format (#32267) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Santo Shakil --- mobile/analysis_options.yaml | 6 ++++++ .../repositories/timeline.repository.dart | 2 +- mobile/lib/pages/common/app_log.page.dart | 3 ++- mobile/lib/pages/library/folder/folder.page.dart | 3 ++- .../shared_link/shared_link_edit.page.dart | 3 ++- .../widgets/memory/memory_bottom_info.widget.dart | 3 ++- .../people/person_edit_birthday_modal.widget.dart | 3 ++- .../widgets/search/quick_date_picker.dart | 7 ++++--- .../widgets/timeline/header.widget.dart | 15 ++++++++------- .../widgets/timeline/scrubber.widget.dart | 3 ++- mobile/lib/widgets/common/date_time_picker.dart | 8 ++++++-- .../lib/widgets/common/person_sliver_app_bar.dart | 3 ++- .../map/map_settings/map_custom_time_range.dart | 5 +++-- .../widgets/settings/free_up_space_settings.dart | 10 +++++++--- 14 files changed, 49 insertions(+), 25 deletions(-) diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 06377279d8..10944ceb4f 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -127,6 +127,12 @@ dart_code_metrics: severity: perf - name: tr description: 'Use context.t. or StaticTranslations.instance. instead of tr().' + - type: Locale + entries: + - name: toLanguageTag + description: 'Pass resolvedDateTimeLocale() to DateFormat, not the raw locale. DateFormat throws for locales intl has no date data for (e.g. mi).' + exclude-paths: + - 'lib/main.dart' - type: String entries: - name: tr diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 71acae3816..2adc862299 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:drift/drift.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/data/db/main/database.dart'; import 'package:immich_mobile/data/db/main/table/local/asset.dart'; import 'package:immich_mobile/data/db/main/table/remote/asset.dart'; @@ -13,6 +12,7 @@ import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/infrastructure/repositories/map.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.drift.dart'; +import 'package:intl/intl.dart'; import 'package:stream_transform/stream_transform.dart'; @DriftAccessor() diff --git a/mobile/lib/pages/common/app_log.page.dart b/mobile/lib/pages/common/app_log.page.dart index d0b5096288..d6ee939971 100644 --- a/mobile/lib/pages/common/app_log.page.dart +++ b/mobile/lib/pages/common/app_log.page.dart @@ -6,6 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -105,7 +106,7 @@ class AppLogPage extends HookWidget { style: TextStyle(fontSize: 14.0, color: context.colorScheme.onSurface, fontFamily: "GoogleSansCode"), ), subtitle: Text( - "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.logger}", + "at ${DateFormat("HH:mm:ss.SSS", resolvedDateTimeLocale()).format(logMessage.createdAt)} in ${logMessage.logger}", style: TextStyle(fontSize: 12.0, color: context.colorScheme.onSurfaceSecondary), ), leading: buildLeadingIcon(logMessage.level), diff --git a/mobile/lib/pages/library/folder/folder.page.dart b/mobile/lib/pages/library/folder/folder.page.dart index c915bf72a0..0b12674963 100644 --- a/mobile/lib/pages/library/folder/folder.page.dart +++ b/mobile/lib/pages/library/folder/folder.page.dart @@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; @@ -201,7 +202,7 @@ class FolderContent extends HookConsumerWidget { style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), ), subtitle: Text( - "${asset.exifInfo.fileSize != null ? "${formatBytes(asset.exifInfo.fileSize ?? 0)} • " : ""}${DateFormat.yMMMd().format(asset.createdAt.toLocal())}", + "${asset.exifInfo.fileSize != null ? "${formatBytes(asset.exifInfo.fileSize ?? 0)} • " : ""}${DateFormat.yMMMd(resolvedDateTimeLocale()).format(asset.createdAt.toLocal())}", style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ), diff --git a/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart index bc7087c898..da7352f356 100644 --- a/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart +++ b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart @@ -6,6 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -179,7 +180,7 @@ class SharedLinkEditPage extends HookConsumerWidget { ); } - String formatDateTime(DateTime dateTime) => DateFormat.yMMMd(context.locale.toString()).add_Hm().format(dateTime); + String formatDateTime(DateTime dateTime) => DateFormat.yMMMd(resolvedDateTimeLocale()).add_Hm().format(dateTime); DateTime? getExpiresAtFromPreset(Duration preset) => preset == Duration.zero ? null : DateTime.now().add(preset); diff --git a/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart b/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart index 80eca7ab1a..503edcdd24 100644 --- a/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart @@ -4,6 +4,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/memory/memory_title.widget.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -16,7 +17,7 @@ class MemoryBottomInfo extends StatelessWidget { @override Widget build(BuildContext context) { - final df = DateFormat.yMMMMd(); + final df = DateFormat.yMMMMd(resolvedDateTimeLocale()); final fileCreatedDate = asset.createdAt; return Padding( padding: const EdgeInsets.all(16.0), diff --git a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart index 55dc277cc8..3133e12275 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/data/store.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -61,7 +62,7 @@ class _PersonBirthdayEditFormState extends ConsumerState child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(16.0)), child: ScrollDatePicker( - viewType: datePickerColumnOrder(DateFormat.yMd(context.locale.toLanguageTag()).pattern), + viewType: datePickerColumnOrder(DateFormat.yMd(resolvedDateTimeLocale()).pattern), options: DatePickerOptions( backgroundColor: context.colorScheme.surfaceContainerHigh, itemExtent: 50, diff --git a/mobile/lib/presentation/widgets/search/quick_date_picker.dart b/mobile/lib/presentation/widgets/search/quick_date_picker.dart index b1cead7eb0..e9e8baa1af 100644 --- a/mobile/lib/presentation/widgets/search/quick_date_picker.dart +++ b/mobile/lib/presentation/widgets/search/quick_date_picker.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:intl/intl.dart'; @@ -11,11 +12,11 @@ sealed class DateFilterInputModel { // If date range is less than 24 hours, set the end date to the end of the day final date = asDateTimeRange(); if (date.end.difference(date.start).inHours < 24) { - return DateFormat.yMMMd().format(date.start.toLocal()); + return DateFormat.yMMMd(resolvedDateTimeLocale()).format(date.start.toLocal()); } else { return context.t.search_filter_date_interval( - start: DateFormat.yMMMd().format(date.start.toLocal()), - end: DateFormat.yMMMd().format(date.end.toLocal()), + start: DateFormat.yMMMd(resolvedDateTimeLocale()).format(date.start.toLocal()), + end: DateFormat.yMMMd(resolvedDateTimeLocale()).format(date.end.toLocal()), ); } } diff --git a/mobile/lib/presentation/widgets/timeline/header.widget.dart b/mobile/lib/presentation/widgets/timeline/header.widget.dart index 4f8e2820e2..f668375771 100644 --- a/mobile/lib/presentation/widgets/timeline/header.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/header.widget.dart @@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; @@ -27,15 +28,15 @@ class TimelineHeader extends HookWidget { required this.assetOffset, }); - String _formatMonth(BuildContext context, DateTime date) { + String _formatMonth(DateTime date) { final formatter = date.year == DateTime.now().year - ? DateFormat.MMMM(context.locale.toLanguageTag()) - : DateFormat.yMMMM(context.locale.toLanguageTag()); + ? DateFormat.MMMM(resolvedDateTimeLocale()) + : DateFormat.yMMMM(resolvedDateTimeLocale()); return formatter.format(date); } - String _formatDay(BuildContext context, DateTime date) { - final formatter = DateFormat.yMMMEd(context.locale.toLanguageTag()); + String _formatDay(DateTime date) { + final formatter = DateFormat.yMMMEd(resolvedDateTimeLocale()); return formatter.format(date); } @@ -61,7 +62,7 @@ class TimelineHeader extends HookWidget { Row( children: [ Text( - toBeginningOfSentenceCase(_formatMonth(context, date)), + toBeginningOfSentenceCase(_formatMonth(date)), style: context.textTheme.labelLarge?.copyWith(fontSize: 24), ), const Spacer(), @@ -72,7 +73,7 @@ class TimelineHeader extends HookWidget { Row( children: [ Text( - toBeginningOfSentenceCase(_formatDay(context, date)), + toBeginningOfSentenceCase(_formatDay(date)), style: context.textTheme.labelLarge?.copyWith(fontSize: 15), ), const Spacer(), diff --git a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart index 8eca9a3ab2..11a9cd33e9 100644 --- a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart @@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; @@ -62,7 +63,7 @@ List<_Segment> _buildSegments({required List layoutSegments, required d return []; } - final formatter = DateFormat.yMMM(); + final formatter = DateFormat.yMMM(resolvedDateTimeLocale()); DateTime? lastDate; double lastOffset = -offsetThreshold; for (final layoutSegment in layoutSegments) { diff --git a/mobile/lib/widgets/common/date_time_picker.dart b/mobile/lib/widgets/common/date_time_picker.dart index 6b30efe820..0701db61b3 100644 --- a/mobile/lib/widgets/common/date_time_picker.dart +++ b/mobile/lib/widgets/common/date_time_picker.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/extensions/duration_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/widgets/common/dropdown_search_menu.dart'; @@ -108,7 +109,7 @@ class _DateTimePicker extends HookWidget { } void popWithDateTime() { - final formattedDateTime = DateFormat("yyyy-MM-dd'T'HH:mm:ss").format(date.value); + final formattedDateTime = DateFormat("yyyy-MM-dd'T'HH:mm:ss", 'en').format(date.value); final dtWithOffset = formattedDateTime + Duration(milliseconds: tzOffset.value.offsetInMilliseconds).formatAsOffset(); context.pop(dtWithOffset); @@ -149,7 +150,10 @@ class _DateTimePicker extends HookWidget { 1, ), trailing: Icon(Icons.edit_outlined, size: 18, color: context.primaryColor), - title: Text(DateFormat("dd-MM-yyyy hh:mm a").format(date.value), style: context.textTheme.bodyMedium), + title: Text( + DateFormat("dd-MM-yyyy hh:mm a", resolvedDateTimeLocale()).format(date.value), + style: context.textTheme.bodyMedium, + ), onTap: pickDate, ), const SizedBox(height: 24), diff --git a/mobile/lib/widgets/common/person_sliver_app_bar.dart b/mobile/lib/widgets/common/person_sliver_app_bar.dart index d8bf361e4e..d26ca789ed 100644 --- a/mobile/lib/widgets/common/person_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/person_sliver_app_bar.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/sliver_app_bar/item_count_text.widget.dart'; @@ -290,7 +291,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S if (widget.person.birthDate != null) Text( - "${DateFormat.yMMMd(context.locale.toString()).format(widget.person.birthDate!)} (${formatAge(widget.person.birthDate!, DateTime.now())})", + "${DateFormat.yMMMd(resolvedDateTimeLocale()).format(widget.person.birthDate!)} (${formatAge(widget.person.birthDate!, DateTime.now())})", style: context.textTheme.labelLarge?.copyWith( color: Colors.white, height: 1.2, diff --git a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart index 6fa929852a..61cce54fd6 100644 --- a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart +++ b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/time_range.model.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/utils/option.dart'; @@ -19,7 +20,7 @@ class MapTimeRange extends StatelessWidget { title: Text(context.t.date_after), subtitle: Text( timeRange.from != null - ? DateFormat.yMMMd(context.locale.toLanguageTag()).add_jm().format(timeRange.from!) + ? DateFormat.yMMMd(resolvedDateTimeLocale()).add_jm().format(timeRange.from!) : context.t.not_set, ), trailing: timeRange.from != null @@ -45,7 +46,7 @@ class MapTimeRange extends StatelessWidget { title: Text(context.t.date_before), subtitle: Text( timeRange.to != null - ? DateFormat.yMMMd(context.locale.toLanguageTag()).add_jm().format(timeRange.to!) + ? DateFormat.yMMMd(resolvedDateTimeLocale()).add_jm().format(timeRange.to!) : context.t.not_set, ), trailing: timeRange.to != null diff --git a/mobile/lib/widgets/settings/free_up_space_settings.dart b/mobile/lib/widgets/settings/free_up_space_settings.dart index 07dfe95b21..412e3faefa 100644 --- a/mobile/lib/widgets/settings/free_up_space_settings.dart +++ b/mobile/lib/widgets/settings/free_up_space_settings.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/cleanup.provider.dart'; @@ -423,7 +424,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { ), subtitle: hasDate ? Text( - DateFormat.yMMMd().format(state.selectedDate!), + DateFormat.yMMMd(resolvedDateTimeLocale()).format(state.selectedDate!), style: context.textTheme.bodyMedium?.copyWith( color: context.colorScheme.primary, fontWeight: FontWeight.w500, @@ -628,7 +629,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { Text( context.t.cleanup_step4_summary( count: state.assetsToDelete.length, - date: DateFormat.yMMMd().format(state.selectedDate!), + date: DateFormat.yMMMd(resolvedDateTimeLocale()).format(state.selectedDate!), ), style: context.textTheme.labelLarge?.copyWith(fontSize: 15), ), @@ -687,7 +688,10 @@ class _DeleteConfirmationDialog extends StatelessWidget { return AlertDialog( title: Text(context.t.cleanup_confirm_prompt_title), content: Text( - context.t.cleanup_confirm_description(count: assetCount, date: DateFormat.yMMMd().format(cutoffDate)), + context.t.cleanup_confirm_description( + count: assetCount, + date: DateFormat.yMMMd(resolvedDateTimeLocale()).format(cutoffDate), + ), style: context.textTheme.labelLarge?.copyWith(fontSize: 15), ), actions: [