From 85ac0512a6b64244bd2fd475ecd6e17d5e13d4f9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20Tollk=C3=B6tter?=
 <1518021+atollk@users.noreply.github.com>
Date: Mon, 28 Apr 2025 15:53:26 +0200
Subject: [PATCH] fix(web): Make date-time formatting follow locale (#17899)

* fixed missing $locale parameter to .toLocaleString

* Remove unused types and functions in timeline-util

* remove unused export

* re-enable export because it is needed for tests

* format
---
 .../memory-page/memory-viewer.svelte          |  4 +-
 .../server-about-modal.svelte                 | 16 +++--
 .../user-settings-page/device-card.svelte     |  4 +-
 web/src/lib/utils/byte-units.ts               |  1 +
 web/src/lib/utils/thumbnail-util.ts           |  5 +-
 web/src/lib/utils/timeline-util.ts            | 71 +++----------------
 6 files changed, 31 insertions(+), 70 deletions(-)

diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte
index e39a3cfa74..45aaf85b67 100644
--- a/web/src/lib/components/memory-page/memory-viewer.svelte
+++ b/web/src/lib/components/memory-page/memory-viewer.svelte
@@ -544,7 +544,9 @@
 
             <div class="absolute left-8 top-4 text-sm font-medium text-white">
               <p>
-                {fromLocalDateTime(current.memory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL)}
+                {fromLocalDateTime(current.memory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL, {
+                  locale: $locale,
+                })}
               </p>
               <p>
                 {current.asset.exifInfo?.city || ''}
diff --git a/web/src/lib/components/shared-components/server-about-modal.svelte b/web/src/lib/components/shared-components/server-about-modal.svelte
index cf935cd314..1284bb126d 100644
--- a/web/src/lib/components/shared-components/server-about-modal.svelte
+++ b/web/src/lib/components/shared-components/server-about-modal.svelte
@@ -6,6 +6,7 @@
   import { t } from 'svelte-i18n';
   import { mdiAlert } from '@mdi/js';
   import Icon from '$lib/components/elements/icon.svelte';
+  import { locale } from '$lib/stores/preferences.store';
 
   interface Props {
     onClose: () => void;
@@ -177,16 +178,19 @@
               <span
                 class="immich-form-label pb-2 text-xs"
                 id="version-history"
-                title={createdAt.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS)}
+                title={createdAt.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS, { locale: $locale })}
               >
                 {$t('version_history_item', {
                   values: {
                     version: item.version,
-                    date: createdAt.toLocaleString({
-                      month: 'short',
-                      day: 'numeric',
-                      year: 'numeric',
-                    }),
+                    date: createdAt.toLocaleString(
+                      {
+                        month: 'short',
+                        day: 'numeric',
+                        year: 'numeric',
+                      },
+                      { locale: $locale },
+                    ),
                   },
                 })}
               </span>
diff --git a/web/src/lib/components/user-settings-page/device-card.svelte b/web/src/lib/components/user-settings-page/device-card.svelte
index 5b70b006be..74e6579dd0 100644
--- a/web/src/lib/components/user-settings-page/device-card.svelte
+++ b/web/src/lib/components/user-settings-page/device-card.svelte
@@ -64,7 +64,9 @@
         <span>{DateTime.fromISO(device.updatedAt, { locale: $locale }).toRelativeCalendar(options)}</span>
         <span class="text-xs text-gray-500 dark:text-gray-400"> - </span>
         <span class="text-xs text-gray-500 dark:text-gray-400">
-          {DateTime.fromISO(device.updatedAt, { locale: $locale }).toLocaleString(DateTime.DATETIME_MED)}
+          {DateTime.fromISO(device.updatedAt, { locale: $locale }).toLocaleString(DateTime.DATETIME_MED, {
+            locale: $locale,
+          })}
         </span>
       </div>
     </div>
diff --git a/web/src/lib/utils/byte-units.ts b/web/src/lib/utils/byte-units.ts
index dae44009e2..218e22f671 100644
--- a/web/src/lib/utils/byte-units.ts
+++ b/web/src/lib/utils/byte-units.ts
@@ -34,6 +34,7 @@ export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, Byte
  * * de: `1,5 KiB`
  *
  * @param bytes number of bytes
+ * @param locale locale to use, default is `navigator.language`
  * @param maxPrecision maximum number of decimal places, default is `1`
  * @returns localized bytes with unit as string
  */
diff --git a/web/src/lib/utils/thumbnail-util.ts b/web/src/lib/utils/thumbnail-util.ts
index a53691e716..f0043790ea 100644
--- a/web/src/lib/utils/thumbnail-util.ts
+++ b/web/src/lib/utils/thumbnail-util.ts
@@ -1,6 +1,7 @@
+import { locale } from '$lib/stores/preferences.store';
 import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
 import { t } from 'svelte-i18n';
-import { derived } from 'svelte/store';
+import { derived, get } from 'svelte/store';
 import { fromLocalDateTime } from './timeline-util';
 
 /**
@@ -43,7 +44,7 @@ export const getAltText = derived(t, ($t) => {
       return asset.exifInfo.description;
     }
 
-    const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' });
+    const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' }, { locale: get(locale) });
     const hasPlace = !!asset.exifInfo?.city && !!asset.exifInfo?.country;
     const names = asset.people?.filter((p) => p.name).map((p) => p.name) ?? [];
     const peopleCount = names.length;
diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts
index f40e2bc3eb..21a7d23953 100644
--- a/web/src/lib/utils/timeline-util.ts
+++ b/web/src/lib/utils/timeline-util.ts
@@ -1,41 +1,13 @@
-import type { AssetBucket } from '$lib/stores/assets-store.svelte';
 import { locale } from '$lib/stores/preferences.store';
-import { type CommonJustifiedLayout } from '$lib/utils/layout-utils';
-
-import type { AssetResponseDto } from '@immich/sdk';
 import { memoize } from 'lodash-es';
 import { DateTime, type LocaleOptions } from 'luxon';
 import { get } from 'svelte/store';
 
-export type DateGroup = {
-  bucket: AssetBucket;
-  index: number;
-  row: number;
-  col: number;
-  date: DateTime;
-  groupTitle: string;
-  assets: AssetResponseDto[];
-  assetsIntersecting: boolean[];
-  height: number;
-  intersecting: boolean;
-  geometry: CommonJustifiedLayout;
-};
 export type ScrubberListener = (
   bucketDate: string | undefined,
   overallScrollPercent: number,
   bucketScrollPercent: number,
 ) => void | Promise<void>;
-export type ScrollTargetListener = ({
-  bucket,
-  dateGroup,
-  asset,
-  offset,
-}: {
-  bucket: AssetBucket;
-  dateGroup: DateGroup;
-  asset: AssetResponseDto;
-  offset: number;
-}) => void;
 
 export const fromLocalDateTime = (localDateTime: string) =>
   DateTime.fromISO(localDateTime, { zone: 'UTC', locale: get(locale) });
@@ -43,31 +15,6 @@ export const fromLocalDateTime = (localDateTime: string) =>
 export const fromDateTimeOriginal = (dateTimeOriginal: string, timeZone: string) =>
   DateTime.fromISO(dateTimeOriginal, { zone: timeZone });
 
-export type LayoutBox = {
-  aspectRatio: number;
-  top: number;
-  width: number;
-  height: number;
-  left: number;
-  forcedAspectRatio?: boolean;
-};
-
-export function findTotalOffset(element: HTMLElement, stop: HTMLElement) {
-  let offset = 0;
-  while (element.offsetParent && element !== stop) {
-    offset += element.offsetTop;
-    element = element.offsetParent as HTMLElement;
-  }
-  return offset;
-}
-
-export const groupDateFormat: Intl.DateTimeFormatOptions = {
-  weekday: 'short',
-  month: 'short',
-  day: 'numeric',
-  year: 'numeric',
-};
-
 export function formatGroupTitle(_date: DateTime): string {
   if (!_date.isValid) {
     return _date.toString();
@@ -87,20 +34,24 @@ export function formatGroupTitle(_date: DateTime): string {
 
   // Last week
   if (date >= today.minus({ days: 6 }) && date < today) {
-    return date.toLocaleString({ weekday: 'long' });
+    return date.toLocaleString({ weekday: 'long' }, { locale: get(locale) });
   }
 
   // This year
   if (today.hasSame(date, 'year')) {
-    return date.toLocaleString({
-      weekday: 'short',
-      month: 'short',
-      day: 'numeric',
-    });
+    return date.toLocaleString(
+      {
+        weekday: 'short',
+        month: 'short',
+        day: 'numeric',
+      },
+      { locale: get(locale) },
+    );
   }
 
-  return getDateLocaleString(date);
+  return getDateLocaleString(date, { locale: get(locale) });
 }
+
 export const getDateLocaleString = (date: DateTime, opts?: LocaleOptions): string =>
   date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY, opts);