diff --git a/README.md b/README.md index d106a8f..55dd1d9 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The `locale` store defines what is the current locale. ```js -import { locale, dictionary } from 'svelte-i18n' +import { locale, dictionary, getClientLocale } from 'svelte-i18n' // Set the current locale to en-US locale.set('en-US') @@ -24,8 +24,26 @@ locale.set('en-US') locale.subscribe(() => { console.log('locale change') }) + +// svelte-i18n exports a method to help getting the current client locale +locale.set( + getClientLocale({ + // the fallback locale, if didn't find any + fallback: 'en-US', + // set to 'true' to check the 'window.navigator.language' + navigator: true, + // set the key name to look for a locale on 'window.location.search' + // 'example.com?locale=en-US' + search: 'lang', + // set the key name to look for a locale on 'window.location.hash' + // 'example.com#locale=en-US' + hash: 'locale', + }), +) ``` +If a locale with the format `xx-YY` is not found, `svelte-i18n` looks for the locale `xx` as well. + --- ### The dictionary diff --git a/example/src/i18n.js b/example/src/i18n.js index 57d33ba..ff242aa 100644 --- a/example/src/i18n.js +++ b/example/src/i18n.js @@ -1,12 +1,4 @@ -import { locale, dictionary } from 'svelte-i18n' - -// setting the locale -locale.set('pt') - -// subscribe to locale changes -locale.subscribe(() => { - console.log('locale change') -}) +import { locale, dictionary, getClientLocale } from 'svelte-i18n' // defining a locale dictionary dictionary.set({ @@ -16,7 +8,8 @@ dictionary.set({ ask: 'Por favor, digite seu nome', message: 'Olá {name}, como vai?', }, - photos: 'Você {n, plural, =0 {não tem fotos.} =1 {tem uma foto.} other {tem # fotos.}}', + photos: + 'Você {n, plural, =0 {não tem fotos.} =1 {tem uma foto.} other {tem # fotos.}}', cats: 'Tenho {n, number} {n,plural,=0{gatos}one{gato}other{gatos}}', }, en: { @@ -25,7 +18,20 @@ dictionary.set({ ask: 'Please type your name', message: 'Hello {name}, how are you?', }, - photos: 'You have {n, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}', - cats: 'I have {n, number} {n,plural,one{cat}other{cats}}' + photos: + 'You have {n, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}', + cats: 'I have {n, number} {n,plural,one{cat}other{cats}}', }, -}) \ No newline at end of file +}) + +locale.set( + getClientLocale({ + navigator: true, + hash: 'lang', + default: 'pt', + }), +) + +locale.subscribe(l => { + console.log('locale change', l) +}) diff --git a/package-lock.json b/package-lock.json index f18ee6f..f1826c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "svelte-i18n", - "version": "1.0.6-beta", + "version": "1.0.7", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/index.js b/src/index.js index 2b58918..32eecfa 100644 --- a/src/index.js +++ b/src/index.js @@ -2,11 +2,26 @@ import { writable, derived } from 'svelte/store' import resolvePath from 'object-resolve-path' import IntlMessageFormat from 'intl-messageformat' import memoize from 'micro-memoize' -import { capital, title, upper, lower } from './utils.js' +import { capital, title, upper, lower, getClientLocale } from './utils.js' let currentLocale let currentDictionary +const getAvailableLocale = newLocale => { + if (currentDictionary[newLocale]) return newLocale + + // istanbul ignore else + if (typeof newLocale === 'string') { + const fallbackLocale = newLocale.split('-').shift() + + if (currentDictionary[fallbackLocale]) { + return fallbackLocale + } + } + + return null +} + const getMessageFormatter = memoize( (message, locale, formats) => new IntlMessageFormat(message, locale, formats), ) @@ -37,22 +52,16 @@ const getLocalizedMessage = (path, interpolations, locale = currentLocale) => { getLocalizedMessage.time = (t, format = 'short', locale) => formatMessage(`{t,time,${format}}`, { t }, locale) - getLocalizedMessage.date = (d, format = 'short', locale) => formatMessage(`{d,date,${format}}`, { d }, locale) - getLocalizedMessage.number = (n, locale) => formatMessage('{n,number}', { n }, locale) - getLocalizedMessage.capital = (path, interpolations, locale) => capital(getLocalizedMessage(path, interpolations, locale)) - getLocalizedMessage.title = (path, interpolations, locale) => title(getLocalizedMessage(path, interpolations, locale)) - getLocalizedMessage.upper = (path, interpolations, locale) => upper(getLocalizedMessage(path, interpolations, locale)) - getLocalizedMessage.lower = (path, interpolations, locale) => lower(getLocalizedMessage(path, interpolations, locale)) @@ -62,10 +71,21 @@ dictionary.subscribe(newDictionary => { }) const locale = writable({}) +const localeSet = locale.set +locale.set = newLocale => { + const availableLocale = getAvailableLocale(newLocale) + if (availableLocale) { + return localeSet(availableLocale) + } + + console.warn(`[svelte-i18n] Locale "${newLocale}" not found.`) + return localeSet(newLocale) +} +locale.update = fn => localeSet(fn(currentLocale)) locale.subscribe(newLocale => { currentLocale = newLocale }) -const format = derived(locale, () => getLocalizedMessage) +const format = derived([locale, dictionary], () => getLocalizedMessage) -export { locale, format as _, format, dictionary } +export { locale, format as _, format, dictionary, getClientLocale } diff --git a/src/utils.js b/src/utils.js index 9967d94..35436c0 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,3 +2,36 @@ export const capital = str => str.replace(/(^|\s)\S/, l => l.toUpperCase()) export const title = str => str.replace(/(^|\s)\S/g, l => l.toUpperCase()) export const upper = str => str.toLocaleUpperCase() export const lower = str => str.toLocaleLowerCase() + +export const getClientLocale = ({ navigator, hash, search, fallback } = {}) => { + let locale + + const getFromURL = (urlPart, key) => { + const keyVal = urlPart + .substr(1) + .split('&') + .find(i => i.indexOf(key) === 0) + + if (keyVal) { + return keyVal.split('=').pop() + } + } + + // istanbul ignore else + if (typeof window !== 'undefined') { + if (navigator) { + // istanbul ignore next + locale = window.navigator.language || window.navigator.languages[0] + } + + if (search) { + locale = getFromURL(window.location.search, search) + } + + if (hash) { + locale = getFromURL(window.location.hash, hash) + } + } + + return locale || fallback +} diff --git a/test/index.test.js b/test/index.test.js index 1e50f8b..6b29ccb 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,4 +1,4 @@ -import { dictionary, locale, format } from '../src/index' +import { dictionary, locale, format, getClientLocale } from '../src/index' let _ let currentLocale @@ -42,7 +42,19 @@ it('should change locale', () => { expect(currentLocale).toBe('en') }) -it('should fallback to message id if id is not found', () => { +it('should fallback to existing locale', () => { + locale.set('pt-BR') + expect(currentLocale).toBe('pt') + + locale.set('en-US') + expect(currentLocale).toBe('en') + + locale.set('non-existent') + expect(currentLocale).toBe('non-existent') +}) + +it('should fallback to message id if id is not found', () => { + locale.set('en') expect(_('batatinha')).toBe('batatinha') }) @@ -71,39 +83,74 @@ it('should interpolate message with variables according to passed locale', () => }) describe('utilities', () => { - beforeAll(() => { - locale.set('en') + describe('get locale', () => { + beforeEach(() => { + delete window.location + window.location = { + hash: '', + search: '', + } + }) + + it('should get the locale based on the passed hash parameter', () => { + window.location.hash = '#locale=en-US&lang=pt-BR' + expect(getClientLocale({ hash: 'locale' })).toBe('en-US') + expect(getClientLocale({ hash: 'lang' })).toBe('pt-BR') + }) + + it('should get the locale based on the passed search parameter', () => { + window.location.search = '?locale=en-US&lang=pt-BR' + expect(getClientLocale({ search: 'locale' })).toBe('en-US') + expect(getClientLocale({ search: 'lang' })).toBe('pt-BR') + }) + + it('should get the locale based on the navigator language', () => { + expect(getClientLocale({ navigator: true })).toBe( + window.navigator.language, + ) + }) + + it('should get the fallback locale', () => { + expect(getClientLocale({ navigator: false, fallback: 'pt' })).toBe('pt') + expect(getClientLocale({ hash: 'locale', fallback: 'pt' })).toBe('pt') + }) }) - it('should capital a translated message', () => { - expect(_.capital('hi')).toBe('Hi yo') - }) + describe('format utils', () => { + beforeAll(() => { + locale.set('en') + }) - it('should title a translated message', () => { - expect(_.title('hi')).toBe('Hi Yo') - }) + it('should capital a translated message', () => { + expect(_.capital('hi')).toBe('Hi yo') + }) - it('should lowercase a translated message', () => { - expect(_.lower('hi')).toBe('hi yo') - }) + it('should title a translated message', () => { + expect(_.title('hi')).toBe('Hi Yo') + }) - it('should uppercase a translated message', () => { - expect(_.upper('hi')).toBe('HI YO') - }) + it('should lowercase a translated message', () => { + expect(_.lower('hi')).toBe('hi yo') + }) - const date = new Date(2019, 3, 24, 23, 45) - it('should format a time value', () => { - locale.set('en') - expect(_.time(date)).toBe('11:45 PM') - expect(_.time(date, 'medium')).toBe('11:45:00 PM') - }) + it('should uppercase a translated message', () => { + expect(_.upper('hi')).toBe('HI YO') + }) - it('should format a date value', () => { - expect(_.date(date)).toBe('4/24/19') - expect(_.date(date, 'medium')).toBe('Apr 24, 2019') - }) - // number + const date = new Date(2019, 3, 24, 23, 45) + it('should format a time value', () => { + locale.set('en') + expect(_.time(date)).toBe('11:45 PM') + expect(_.time(date, 'medium')).toBe('11:45:00 PM') + }) + + it('should format a date value', () => { + expect(_.date(date)).toBe('4/24/19') + expect(_.date(date, 'medium')).toBe('Apr 24, 2019') + }) + // number it('should format a date value', () => { expect(_.number(123123123)).toBe('123,123,123') }) + }) })