mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
feat(web): use browser language by default (#10849)
This commit is contained in:
parent
6629bf50ae
commit
6030349a6f
4 changed files with 55 additions and 10 deletions
|
@ -14,9 +14,10 @@
|
||||||
sidebarSettings,
|
sidebarSettings,
|
||||||
} from '$lib/stores/preferences.store';
|
} from '$lib/stores/preferences.store';
|
||||||
import { findLocale } from '$lib/utils';
|
import { findLocale } from '$lib/utils';
|
||||||
|
import { getClosestAvailableLocale, langCodes } from '$lib/utils/i18n';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { locale as i18nLocale, t } from 'svelte-i18n';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { t, init } from 'svelte-i18n';
|
|
||||||
import { invalidateAll } from '$app/navigation';
|
import { invalidateAll } from '$app/navigation';
|
||||||
|
|
||||||
let time = new Date();
|
let time = new Date();
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
value: findLocale(editedLocale).code || fallbackLocale.code,
|
value: findLocale(editedLocale).code || fallbackLocale.code,
|
||||||
label: findLocale(editedLocale).name || fallbackLocale.name,
|
label: findLocale(editedLocale).name || fallbackLocale.name,
|
||||||
};
|
};
|
||||||
|
$: closestLanguage = getClosestAvailableLocale([$lang], langCodes);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
|
@ -78,13 +80,7 @@
|
||||||
const handleLanguageChange = async (newLang: string | undefined) => {
|
const handleLanguageChange = async (newLang: string | undefined) => {
|
||||||
if (newLang) {
|
if (newLang) {
|
||||||
$lang = newLang;
|
$lang = newLang;
|
||||||
|
await i18nLocale.set(newLang);
|
||||||
if (newLang === 'dev') {
|
|
||||||
// Reload required, because fallbackLocale cannot be cleared.
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
await init({ fallbackLocale: defaultLang.code, initialLocale: newLang });
|
|
||||||
await invalidateAll();
|
await invalidateAll();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -111,7 +107,7 @@
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<SettingCombobox
|
<SettingCombobox
|
||||||
comboboxPlaceholder={$t('language')}
|
comboboxPlaceholder={$t('language')}
|
||||||
selectedOption={langOptions.find(({ value }) => value === $lang) || defaultLangOption}
|
selectedOption={langOptions.find(({ value }) => value === closestLanguage) || defaultLangOption}
|
||||||
options={langOptions}
|
options={langOptions}
|
||||||
title={$t('language')}
|
title={$t('language')}
|
||||||
subtitle={$t('language_setting_description')}
|
subtitle={$t('language_setting_description')}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { langs } from '$lib/constants';
|
import { langs } from '$lib/constants';
|
||||||
import messages from '$lib/i18n/en.json';
|
import messages from '$lib/i18n/en.json';
|
||||||
|
import { getClosestAvailableLocale } from '$lib/utils/i18n';
|
||||||
import { exec as execCallback } from 'node:child_process';
|
import { exec as execCallback } from 'node:child_process';
|
||||||
import { readFileSync, readdirSync } from 'node:fs';
|
import { readFileSync, readdirSync } from 'node:fs';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
|
@ -52,4 +53,30 @@ describe('i18n', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getClosestAvailableLocale', () => {
|
||||||
|
const allLocales = ['ar', 'bg', 'en', 'en-US', 'en-DE', 'zh-Hans', 'zh-Hans-HK'];
|
||||||
|
|
||||||
|
it('returns undefined on mismatch', () => {
|
||||||
|
expect(getClosestAvailableLocale([], allLocales)).toBeUndefined();
|
||||||
|
expect(getClosestAvailableLocale(['invalid'], allLocales)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the first matching locale', () => {
|
||||||
|
expect(getClosestAvailableLocale(['invalid', 'ar', 'bg'], allLocales)).toBe('ar');
|
||||||
|
expect(getClosestAvailableLocale(['bg'], allLocales)).toBe('bg');
|
||||||
|
expect(getClosestAvailableLocale(['bg', 'invalid', 'ar'], allLocales)).toBe('bg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the locale for a less specific match', () => {
|
||||||
|
expect(getClosestAvailableLocale(['ar-AE'], allLocales)).toBe('ar-AE');
|
||||||
|
expect(getClosestAvailableLocale(['ar-AE', 'en'], allLocales)).toBe('ar-AE');
|
||||||
|
expect(getClosestAvailableLocale(['zh-Hans-HK', 'zh-Hans'], allLocales)).toBe('zh-Hans-HK');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores the locale for a more specific match', () => {
|
||||||
|
expect(getClosestAvailableLocale(['zh'], allLocales)).toBeUndefined();
|
||||||
|
expect(getClosestAvailableLocale(['de', 'zh', 'en-US'], allLocales)).toBe('en-US');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { Theme, defaultLang } from '$lib/constants';
|
import { Theme, defaultLang } from '$lib/constants';
|
||||||
|
import { getPreferredLocale } from '$lib/utils/i18n';
|
||||||
import { persisted } from 'svelte-local-storage-store';
|
import { persisted } from 'svelte-local-storage-store';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
|
@ -42,7 +43,8 @@ export const locale = persisted<string | undefined>('locale', undefined, {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const lang = persisted('lang', defaultLang.code, {
|
const preferredLocale = browser ? getPreferredLocale() : undefined;
|
||||||
|
export const lang = persisted<string>('lang', preferredLocale || defaultLang.code, {
|
||||||
serializer: {
|
serializer: {
|
||||||
parse: (text) => text,
|
parse: (text) => text,
|
||||||
stringify: (object) => object ?? '',
|
stringify: (object) => object ?? '',
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { langs } from '$lib/constants';
|
||||||
import { locale, t, waitLocale } from 'svelte-i18n';
|
import { locale, t, waitLocale } from 'svelte-i18n';
|
||||||
import { get, type Unsubscriber } from 'svelte/store';
|
import { get, type Unsubscriber } from 'svelte/store';
|
||||||
|
|
||||||
|
@ -11,3 +12,22 @@ export async function getFormatter() {
|
||||||
await waitLocale();
|
await waitLocale();
|
||||||
return get(t);
|
return get(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/kaisermann/svelte-i18n/blob/780932a3e1270d521d348aac8ba03be9df309f04/src/runtime/stores/locale.ts#L11
|
||||||
|
function getSubLocales(refLocale: string) {
|
||||||
|
return refLocale
|
||||||
|
.split('-')
|
||||||
|
.map((_, i, arr) => arr.slice(0, i + 1).join('-'))
|
||||||
|
.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClosestAvailableLocale(locales: readonly string[], allLocales: readonly string[]) {
|
||||||
|
const allLocalesSet = new Set(allLocales);
|
||||||
|
return locales.find((locale) => getSubLocales(locale).some((subLocale) => allLocalesSet.has(subLocale)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const langCodes = langs.map((lang) => lang.code);
|
||||||
|
|
||||||
|
export function getPreferredLocale() {
|
||||||
|
return getClosestAvailableLocale(navigator.languages, langCodes);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue