fix(web): persisted store (#18385)

* fix(web): persisted store

* fix: translation

* fix: test

* fix: test

* revert i18n changes

* fix blank locale
This commit is contained in:
Daimolean 2025-06-04 03:27:23 +08:00 committed by GitHub
parent 6b4d5e3beb
commit daf1bee7ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 64 additions and 40 deletions

View File

@ -694,6 +694,7 @@
"daily_title_text_date": "E, MMM dd", "daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy", "daily_title_text_date_year": "E, MMM dd, yyyy",
"dark": "Dark", "dark": "Dark",
"darkTheme": "Toggle dark theme",
"date_after": "Date after", "date_after": "Date after",
"date_and_time": "Date and Time", "date_and_time": "Date and Time",
"date_before": "Date before", "date_before": "Date before",
@ -1817,7 +1818,6 @@
"to_parent": "Go to parent", "to_parent": "Go to parent",
"to_trash": "Trash", "to_trash": "Trash",
"toggle_settings": "Toggle settings", "toggle_settings": "Toggle settings",
"toggle_theme": "Toggle dark theme",
"total": "Total", "total": "Total",
"total_usage": "Total usage", "total_usage": "Total usage",
"trash": "Trash", "trash": "Trash",

View File

@ -1,13 +1,20 @@
<script lang="ts"> <script lang="ts">
import { Theme } from '$lib/constants'; import { defaultLang, langs, Theme } from '$lib/constants';
import { themeManager } from '$lib/managers/theme-manager.svelte'; import { themeManager } from '$lib/managers/theme-manager.svelte';
import { lang } from '$lib/stores/preferences.store';
import { ThemeSwitcher } from '@immich/ui'; import { ThemeSwitcher } from '@immich/ui';
import { get } from 'svelte/store';
</script> </script>
{#if !themeManager.theme.system} {#if !themeManager.theme.system}
<ThemeSwitcher {#await langs
size="medium" .find((item) => item.code === get(lang))
color="secondary" ?.loader() ?? defaultLang.loader() then { default: translations }}
onChange={(theme) => themeManager.setTheme(theme == 'dark' ? Theme.DARK : Theme.LIGHT)} <ThemeSwitcher
/> size="medium"
color="secondary"
{translations}
onChange={(theme) => themeManager.setTheme(theme == 'dark' ? Theme.DARK : Theme.LIGHT)}
/>
{/await}
{/if} {/if}

View File

@ -39,7 +39,7 @@
}; };
const handleToggleLocaleBrowser = () => { const handleToggleLocaleBrowser = () => {
$locale = $locale ? undefined : fallbackLocale.code; $locale = $locale === 'default' ? fallbackLocale.code : 'default';
}; };
const handleLocaleChange = (newLocale: string | undefined) => { const handleLocaleChange = (newLocale: string | undefined) => {
@ -89,13 +89,13 @@
<SettingSwitch <SettingSwitch
title={$t('default_locale')} title={$t('default_locale')}
subtitle={$t('default_locale_description')} subtitle={$t('default_locale_description')}
checked={$locale == undefined} checked={$locale == 'default'}
onToggle={handleToggleLocaleBrowser} onToggle={handleToggleLocaleBrowser}
> >
<p class="mt-2 dark:text-gray-400">{selectedDate}</p> <p class="mt-2 dark:text-gray-400">{selectedDate}</p>
</SettingSwitch> </SettingSwitch>
</div> </div>
{#if $locale !== undefined} {#if $locale !== 'default'}
<div class="ms-4"> <div class="ms-4">
<SettingCombobox <SettingCombobox
comboboxPlaceholder={$t('searching_locales')} comboboxPlaceholder={$t('searching_locales')}
@ -113,7 +113,6 @@
title={$t('display_original_photos')} title={$t('display_original_photos')}
subtitle={$t('display_original_photos_setting_description')} subtitle={$t('display_original_photos_setting_description')}
bind:checked={$alwaysLoadOriginalFile} bind:checked={$alwaysLoadOriginalFile}
onToggle={() => ($alwaysLoadOriginalFile = !$alwaysLoadOriginalFile)}
/> />
</div> </div>
<div class="ms-4"> <div class="ms-4">
@ -121,16 +120,10 @@
title={$t('video_hover_setting')} title={$t('video_hover_setting')}
subtitle={$t('video_hover_setting_description')} subtitle={$t('video_hover_setting_description')}
bind:checked={$playVideoThumbnailOnHover} bind:checked={$playVideoThumbnailOnHover}
onToggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)}
/> />
</div> </div>
<div class="ms-4"> <div class="ms-4">
<SettingSwitch <SettingSwitch title={$t('loop_videos')} subtitle={$t('loop_videos_description')} bind:checked={$loopVideo} />
title={$t('loop_videos')}
subtitle={$t('loop_videos_description')}
bind:checked={$loopVideo}
onToggle={() => ($loopVideo = !$loopVideo)}
/>
</div> </div>
<div class="ms-4"> <div class="ms-4">

View File

@ -273,9 +273,17 @@ export const locales = [
{ code: 'zu-ZA', name: 'Zulu (South Africa)' }, { code: 'zu-ZA', name: 'Zulu (South Africa)' },
]; ];
export const defaultLang = { name: 'English', code: 'en', loader: () => import('$i18n/en.json') }; interface Lang {
name: string;
code: string;
loader: () => Promise<{ default: object }>;
rtl?: boolean;
weblateCode?: string;
}
export const langs = [ export const defaultLang: Lang = { name: 'English', code: 'en', loader: () => import('$i18n/en.json') };
export const langs: Lang[] = [
{ name: 'Afrikaans', code: 'af', loader: () => import('$i18n/af.json') }, { name: 'Afrikaans', code: 'af', loader: () => import('$i18n/af.json') },
{ name: 'Arabic', code: 'ar', loader: () => import('$i18n/ar.json'), rtl: true }, { name: 'Arabic', code: 'ar', loader: () => import('$i18n/ar.json'), rtl: true },
{ name: 'Azerbaijani', code: 'az', loader: () => import('$i18n/az.json'), rtl: true }, { name: 'Azerbaijani', code: 'az', loader: () => import('$i18n/az.json'), rtl: true },
@ -359,7 +367,7 @@ export const langs = [
weblateCode: 'zh_SIMPLIFIED', weblateCode: 'zh_SIMPLIFIED',
loader: () => import('$i18n/zh_SIMPLIFIED.json'), loader: () => import('$i18n/zh_SIMPLIFIED.json'),
}, },
{ name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({}) }, { name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({ default: {} }) },
]; ];
export enum ImmichProduct { export enum ImmichProduct {

View File

@ -9,9 +9,9 @@ export interface ThemeSetting {
} }
// Locale to use for formatting dates, numbers, etc. // Locale to use for formatting dates, numbers, etc.
export const locale = persisted<string | undefined>('locale', undefined, { export const locale = persisted<string | undefined>('locale', 'default', {
serializer: { serializer: {
parse: (text) => (text == '' ? 'en-US' : text), parse: (text) => text || 'default',
stringify: (object) => object ?? '', stringify: (object) => object ?? '',
}, },
}); });

View File

@ -1,3 +1,4 @@
import { locale } from '$lib/stores/preferences.store';
import { parseUtcDate } from '$lib/utils/date-time'; import { parseUtcDate } from '$lib/utils/date-time';
import { formatGroupTitle } from '$lib/utils/timeline-util'; import { formatGroupTitle } from '$lib/utils/timeline-util';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
@ -16,48 +17,63 @@ describe('formatGroupTitle', () => {
it('formats today', () => { it('formats today', () => {
const date = parseUtcDate('2024-07-27T01:00:00Z'); const date = parseUtcDate('2024-07-27T01:00:00Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('today'); locale.set('en');
expect(formatGroupTitle(date.setLocale('es'))).toBe('hoy'); expect(formatGroupTitle(date)).toBe('today');
locale.set('es');
expect(formatGroupTitle(date)).toBe('hoy');
}); });
it('formats yesterday', () => { it('formats yesterday', () => {
const date = parseUtcDate('2024-07-26T23:59:59Z'); const date = parseUtcDate('2024-07-26T23:59:59Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('yesterday'); locale.set('en');
expect(formatGroupTitle(date.setLocale('fr'))).toBe('hier'); expect(formatGroupTitle(date)).toBe('yesterday');
locale.set('fr');
expect(formatGroupTitle(date)).toBe('hier');
}); });
it('formats last week', () => { it('formats last week', () => {
const date = parseUtcDate('2024-07-21T00:00:00Z'); const date = parseUtcDate('2024-07-21T00:00:00Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('Sunday'); locale.set('en');
expect(formatGroupTitle(date.setLocale('ar-SA'))).toBe('الأحد'); expect(formatGroupTitle(date)).toBe('Sunday');
locale.set('ar-SA');
expect(formatGroupTitle(date)).toBe('الأحد');
}); });
it('formats date 7 days ago', () => { it('formats date 7 days ago', () => {
const date = parseUtcDate('2024-07-20T00:00:00Z'); const date = parseUtcDate('2024-07-20T00:00:00Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('Sat, Jul 20'); locale.set('en');
expect(formatGroupTitle(date.setLocale('de'))).toBe('Sa., 20. Juli'); expect(formatGroupTitle(date)).toBe('Sat, Jul 20');
locale.set('de');
expect(formatGroupTitle(date)).toBe('Sa., 20. Juli');
}); });
it('formats date this year', () => { it('formats date this year', () => {
const date = parseUtcDate('2020-01-01T00:00:00Z'); const date = parseUtcDate('2020-01-01T00:00:00Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('Wed, Jan 1, 2020'); locale.set('en');
expect(formatGroupTitle(date.setLocale('ja'))).toBe('2020年1月1日(水)'); expect(formatGroupTitle(date)).toBe('Wed, Jan 1, 2020');
locale.set('ja');
expect(formatGroupTitle(date)).toBe('2020年1月1日(水)');
}); });
it('formats future date', () => { it('formats future date', () => {
const tomorrow = parseUtcDate('2024-07-28T00:00:00Z'); const tomorrow = parseUtcDate('2024-07-28T00:00:00Z');
expect(formatGroupTitle(tomorrow.setLocale('en'))).toBe('Sun, Jul 28'); locale.set('en');
expect(formatGroupTitle(tomorrow)).toBe('Sun, Jul 28');
const nextMonth = parseUtcDate('2024-08-28T00:00:00Z'); const nextMonth = parseUtcDate('2024-08-28T00:00:00Z');
expect(formatGroupTitle(nextMonth.setLocale('en'))).toBe('Wed, Aug 28'); locale.set('en');
expect(formatGroupTitle(nextMonth)).toBe('Wed, Aug 28');
const nextYear = parseUtcDate('2025-01-10T12:00:00Z'); const nextYear = parseUtcDate('2025-01-10T12:00:00Z');
expect(formatGroupTitle(nextYear.setLocale('en'))).toBe('Fri, Jan 10, 2025'); locale.set('en');
expect(formatGroupTitle(nextYear)).toBe('Fri, Jan 10, 2025');
}); });
it('returns "Invalid DateTime" when date is invalid', () => { it('returns "Invalid DateTime" when date is invalid', () => {
const date = DateTime.invalid('test'); const date = DateTime.invalid('test');
expect(formatGroupTitle(date.setLocale('en'))).toBe('Invalid DateTime'); locale.set('en');
expect(formatGroupTitle(date.setLocale('es'))).toBe('Invalid DateTime'); expect(formatGroupTitle(date)).toBe('Invalid DateTime');
locale.set('es');
expect(formatGroupTitle(date)).toBe('Invalid DateTime');
}); });
}); });

View File

@ -62,12 +62,12 @@ export function formatGroupTitle(_date: DateTime): string {
// Today // Today
if (today.hasSame(date, 'day')) { if (today.hasSame(date, 'day')) {
return date.toRelativeCalendar(); return date.toRelativeCalendar({ locale: get(locale) });
} }
// Yesterday // Yesterday
if (today.minus({ days: 1 }).hasSame(date, 'day')) { if (today.minus({ days: 1 }).hasSame(date, 'day')) {
return date.toRelativeCalendar(); return date.toRelativeCalendar({ locale: get(locale) });
} }
// Last week // Last week