Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,7 @@
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"dark": "Dark",
"darkTheme": "Toggle dark theme",
"date_after": "Date after",
"date_and_time": "Date and Time",
"date_before": "Date before",
Expand Down Expand Up @@ -1817,7 +1818,6 @@
"to_parent": "Go to parent",
"to_trash": "Trash",
"toggle_settings": "Toggle settings",
"toggle_theme": "Toggle dark theme",
"total": "Total",
"total_usage": "Total usage",
"trash": "Trash",
Expand Down
19 changes: 13 additions & 6 deletions web/src/lib/components/shared-components/theme-button.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
<script lang="ts">
import { Theme } from '$lib/constants';
import { defaultLang, langs, Theme } from '$lib/constants';
import { themeManager } from '$lib/managers/theme-manager.svelte';
import { lang } from '$lib/stores/preferences.store';
import { ThemeSwitcher } from '@immich/ui';
import { get } from 'svelte/store';
</script>

{#if !themeManager.theme.system}
<ThemeSwitcher
size="medium"
color="secondary"
onChange={(theme) => themeManager.setTheme(theme == 'dark' ? Theme.DARK : Theme.LIGHT)}
/>
{#await langs
.find((item) => item.code === get(lang))
?.loader() ?? defaultLang.loader() then { default: translations }}
<ThemeSwitcher
size="medium"
color="secondary"
{translations}
onChange={(theme) => themeManager.setTheme(theme == 'dark' ? Theme.DARK : Theme.LIGHT)}
/>
{/await}
{/if}
15 changes: 4 additions & 11 deletions web/src/lib/components/user-settings-page/app-settings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
};

const handleToggleLocaleBrowser = () => {
$locale = $locale ? undefined : fallbackLocale.code;
$locale = $locale === 'default' ? fallbackLocale.code : 'default';
};

const handleLocaleChange = (newLocale: string | undefined) => {
Expand Down Expand Up @@ -89,13 +89,13 @@
<SettingSwitch
title={$t('default_locale')}
subtitle={$t('default_locale_description')}
checked={$locale == undefined}
checked={$locale == 'default'}
onToggle={handleToggleLocaleBrowser}
>
<p class="mt-2 dark:text-gray-400">{selectedDate}</p>
</SettingSwitch>
</div>
{#if $locale !== undefined}
{#if $locale !== 'default'}
<div class="ms-4">
<SettingCombobox
comboboxPlaceholder={$t('searching_locales')}
Expand All @@ -113,24 +113,17 @@
title={$t('display_original_photos')}
subtitle={$t('display_original_photos_setting_description')}
bind:checked={$alwaysLoadOriginalFile}
onToggle={() => ($alwaysLoadOriginalFile = !$alwaysLoadOriginalFile)}
/>
</div>
<div class="ms-4">
<SettingSwitch
title={$t('video_hover_setting')}
subtitle={$t('video_hover_setting_description')}
bind:checked={$playVideoThumbnailOnHover}
onToggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)}
/>
</div>
<div class="ms-4">
<SettingSwitch
title={$t('loop_videos')}
subtitle={$t('loop_videos_description')}
bind:checked={$loopVideo}
onToggle={() => ($loopVideo = !$loopVideo)}
/>
<SettingSwitch title={$t('loop_videos')} subtitle={$t('loop_videos_description')} bind:checked={$loopVideo} />
</div>

<div class="ms-4">
Expand Down
14 changes: 11 additions & 3 deletions web/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,17 @@ export const locales = [
{ 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 defaultLang: Lang = { name: 'English', code: 'en', loader: () => import('$i18n/en.json') };

export const langs = [
export const langs: Lang[] = [
{ name: 'Afrikaans', code: 'af', loader: () => import('$i18n/af.json') },
{ name: 'Arabic', code: 'ar', loader: () => import('$i18n/ar.json'), rtl: true },
{ name: 'Azerbaijani', code: 'az', loader: () => import('$i18n/az.json'), rtl: true },
Expand Down Expand Up @@ -359,7 +367,7 @@ export const langs = [
weblateCode: 'zh_SIMPLIFIED',
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 {
Expand Down
4 changes: 2 additions & 2 deletions web/src/lib/stores/preferences.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ export interface ThemeSetting {
}

// 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: {
parse: (text) => (text == '' ? 'en-US' : text),
parse: (text) => text || 'default',
stringify: (object) => object ?? '',
},
});
Expand Down
46 changes: 31 additions & 15 deletions web/src/lib/utils/timeline-util.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { locale } from '$lib/stores/preferences.store';
import { parseUtcDate } from '$lib/utils/date-time';
import { formatGroupTitle } from '$lib/utils/timeline-util';
import { DateTime } from 'luxon';
Expand All @@ -16,48 +17,63 @@ describe('formatGroupTitle', () => {

it('formats today', () => {
const date = parseUtcDate('2024-07-27T01:00:00Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('today');
expect(formatGroupTitle(date.setLocale('es'))).toBe('hoy');
locale.set('en');
expect(formatGroupTitle(date)).toBe('today');
locale.set('es');
expect(formatGroupTitle(date)).toBe('hoy');
});

it('formats yesterday', () => {
const date = parseUtcDate('2024-07-26T23:59:59Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('yesterday');
expect(formatGroupTitle(date.setLocale('fr'))).toBe('hier');
locale.set('en');
expect(formatGroupTitle(date)).toBe('yesterday');
locale.set('fr');
expect(formatGroupTitle(date)).toBe('hier');
});

it('formats last week', () => {
const date = parseUtcDate('2024-07-21T00:00:00Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('Sunday');
expect(formatGroupTitle(date.setLocale('ar-SA'))).toBe('الأحد');
locale.set('en');
expect(formatGroupTitle(date)).toBe('Sunday');
locale.set('ar-SA');
expect(formatGroupTitle(date)).toBe('الأحد');
});

it('formats date 7 days ago', () => {
const date = parseUtcDate('2024-07-20T00:00:00Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('Sat, Jul 20');
expect(formatGroupTitle(date.setLocale('de'))).toBe('Sa., 20. Juli');
locale.set('en');
expect(formatGroupTitle(date)).toBe('Sat, Jul 20');
locale.set('de');
expect(formatGroupTitle(date)).toBe('Sa., 20. Juli');
});

it('formats date this year', () => {
const date = parseUtcDate('2020-01-01T00:00:00Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('Wed, Jan 1, 2020');
expect(formatGroupTitle(date.setLocale('ja'))).toBe('2020年1月1日(水)');
locale.set('en');
expect(formatGroupTitle(date)).toBe('Wed, Jan 1, 2020');
locale.set('ja');
expect(formatGroupTitle(date)).toBe('2020年1月1日(水)');
});

it('formats future date', () => {
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');
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');
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', () => {
const date = DateTime.invalid('test');
expect(formatGroupTitle(date.setLocale('en'))).toBe('Invalid DateTime');
expect(formatGroupTitle(date.setLocale('es'))).toBe('Invalid DateTime');
locale.set('en');
expect(formatGroupTitle(date)).toBe('Invalid DateTime');
locale.set('es');
expect(formatGroupTitle(date)).toBe('Invalid DateTime');
});
});
4 changes: 2 additions & 2 deletions web/src/lib/utils/timeline-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ export function formatGroupTitle(_date: DateTime): string {

// Today
if (today.hasSame(date, 'day')) {
return date.toRelativeCalendar();
return date.toRelativeCalendar({ locale: get(locale) });
}

// Yesterday
if (today.minus({ days: 1 }).hasSame(date, 'day')) {
return date.toRelativeCalendar();
return date.toRelativeCalendar({ locale: get(locale) });
}

// Last week
Expand Down