use navigator preferred languages to select locale

This commit is contained in:
Gauthier Roebroeck 2025-07-23 11:58:33 +08:00
parent d95acc203f
commit 648f71d908
6 changed files with 83 additions and 50 deletions

View file

@ -8,6 +8,7 @@
"name": "next-ui",
"version": "0.0.0",
"dependencies": {
"@formatjs/intl-localematcher": "^0.6.1",
"@pinia/colada": "^0.17.1",
"@pinia/colada-plugin-auto-refetch": "^0.2.0",
"@vueuse/core": "^13.5.0",

View file

@ -28,6 +28,7 @@
"chromatic": "npm run chromatic"
},
"dependencies": {
"@formatjs/intl-localematcher": "^0.6.1",
"@pinia/colada": "^0.17.1",
"@pinia/colada-plugin-auto-refetch": "^0.2.0",
"@vueuse/core": "^13.5.0",

View file

@ -1,10 +1,10 @@
import { createIntl } from 'vue-intl'
import { currentLocale, defaultLocale, loadLocale } from '@/utils/i18n/locale-helper'
import { currentLocale, fallbackLocale, loadLocale } from '@/utils/i18n/locale-helper'
const messages = loadLocale(currentLocale)
export const vueIntl = createIntl({
locale: currentLocale,
defaultLocale: defaultLocale,
defaultLocale: fallbackLocale,
messages,
})

View file

@ -16,7 +16,7 @@ import { md3 } from 'vuetify/blueprints'
import { VIconBtn } from 'vuetify/labs/components'
import { createRulesPlugin } from 'vuetify/labs/rules'
import { availableLocales, currentLocale, defaultLocale } from '@/utils/i18n/locale-helper'
import { availableLocales, currentLocale, fallbackLocale } from '@/utils/i18n/locale-helper'
// load vuetify locales only for the available locales in i18n
async function loadVuetifyLocale(locale: string) {
@ -34,7 +34,7 @@ void (async () => {
export const vuetify = createVuetify({
locale: {
locale: currentLocale,
fallback: defaultLocale,
fallback: fallbackLocale,
messages,
},
icons: {

View file

@ -1,42 +1,64 @@
import { beforeEach, expect, test, vi } from 'vitest'
import { loadLocale, defaultLocale, setLocale, getLocale, availableLocales } from './locale-helper'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { loadLocale, fallbackLocale, setLocale, getLocale, availableLocales } from './locale-helper'
beforeEach(() => {
// mock the available locales, as locales are checked against what's available
vi.mock('@/i18n?dir2json&ext=.json&1', () => {
return {
default: {
en: {
sample: 'sample',
},
fr: {
sample: 'échantillon',
},
} as Record<string, Record<string, string>>,
}
describe('locale', () => {
beforeEach(() => {
// mock the available locales, as locales are checked against what's available
vi.mock('@/i18n?dir2json&ext=.json&1', () => {
return {
default: {
en: {
sample: 'sample',
},
fr: {
sample: 'échantillon',
},
} as Record<string, Record<string, string>>,
}
})
})
afterEach(() => {
localStorage.clear()
})
test('given available locales when getting available locales then they are returned', () => {
expect(Object.keys(availableLocales)).toStrictEqual(['en', 'fr'])
})
test('when trying to load unknown locale then fallback locale is loaded', () => {
const localeFallback = loadLocale(fallbackLocale)
const localeUnknown = loadLocale('unknown')
expect(localeUnknown).toBe(localeFallback)
})
test('when setting locale then it is persisted', () => {
const spy = vi.fn(() => {})
vi.spyOn(window, 'location', 'get').mockReturnValue({
reload: spy,
} as unknown as Location)
setLocale('fr')
const newLocale = getLocale()
expect(newLocale).toBe('fr')
expect(spy).toHaveBeenCalledOnce()
})
test('given browser preferred languages in available locales when getting locale then preferred language is used', () => {
vi.spyOn(navigator, 'languages', 'get').mockReturnValue(['fr-XX', 'zh'])
const locale = getLocale()
expect(locale).toBe('fr')
})
test('given browser preferred languages not in available locales when getting locale then fallback locale is used', () => {
vi.spyOn(navigator, 'languages', 'get').mockReturnValue(['ja-JP', 'zh-CN'])
const locale = getLocale()
expect(locale).toBe(fallbackLocale)
})
})
test('given available locales when getting available locales then they are returned', () => {
expect(Object.keys(availableLocales)).toStrictEqual(['en', 'fr'])
})
test('when trying to load unknown locale then default locale is loaded', () => {
const localeDefault = loadLocale(defaultLocale)
const localeUnknown = loadLocale('unknown')
expect(localeUnknown).toBe(localeDefault)
})
test('when setting locale then it is persisted', () => {
const spy = vi.fn(() => {})
vi.spyOn(window, 'location', 'get').mockReturnValue({
reload: spy,
} as unknown as Location)
setLocale('fr')
const newLocale = getLocale()
expect(newLocale).toBe('fr')
expect(spy).toHaveBeenCalledOnce()
})

View file

@ -1,7 +1,8 @@
import { defineMessage } from 'vue-intl'
import localeMessages from '@/i18n?dir2json&ext=.json&1'
import { match } from '@formatjs/intl-localematcher'
export const defaultLocale = 'en'
export const fallbackLocale = 'en'
const USER_LOCALE_KEY = 'komga.userLocale'
@ -14,11 +15,11 @@ const localeName = defineMessage({
/**
* Loads messages from a translation file by its locale code.
* If the translation file does not exist, loads the `defaultLocale` instead.
* If the translation file does not exist, loads the `fallbackLocale` instead.
* @param locale the locale code, e.g. 'fr'
*/
export function loadLocale(locale: string): Record<string, string> {
const localeToLoad = locale in availableLocales ? locale : defaultLocale
const localeToLoad = locale in availableLocales ? locale : fallbackLocale
return (localeMessages as unknown as Record<string, Record<string, string>>)[localeToLoad]!
}
@ -41,12 +42,20 @@ function loadAvailableLocales(): Record<string, string> {
export const availableLocales = loadAvailableLocales()
/**
* Gets the saved locale from localStorage.
* If the locale is not valid, defaults to 'en'.
* Gets the saved locale from localStorage if defined and valid.
* Else tries to get the best matching language from the brower's preferred languages.
* If the locale is not valid, defaults to 'fallbackLocale'.
*/
export function getLocale(): string {
const storageLocale = localStorage.getItem(USER_LOCALE_KEY) ?? defaultLocale
return storageLocale in availableLocales ? storageLocale : defaultLocale
const storageLocale = localStorage.getItem(USER_LOCALE_KEY)
console.log('locale from storage:', storageLocale)
if (storageLocale && storageLocale in availableLocales) return storageLocale
// get the browser's preferred languages and see if we can match it to an available locale
console.log('preferred languages:', navigator.languages)
const s = match(navigator.languages, Object.keys(availableLocales), fallbackLocale)
console.log('match:', s)
return s
}
export const currentLocale = getLocale()