diff --git a/.eslintrc b/.eslintrc index 729d68e..043b9a9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,5 +6,8 @@ }, "parserOptions": { "sourceType": "module" + }, + "rules": { + "@typescript-eslint/camelcase": "off" } } diff --git a/src/client/includes/formatters.ts b/src/client/includes/formatters.ts index 0a8a40d..abe1cb9 100644 --- a/src/client/includes/formatters.ts +++ b/src/client/includes/formatters.ts @@ -3,47 +3,52 @@ import memoize from 'fast-memoize' import { MemoizedIntlFormatter } from '../types' import { getCurrentLocale } from '../stores/locale' -import { getFormats } from '../configs' +import { getOptions } from '../configs' const getIntlFormatterOptions = ( type: 'time' | 'number' | 'date', name: string ): any => { - const formats = getFormats() + const formats = getOptions().formats if (type in formats && name in formats[type]) { return formats[type][name] } - if ( - type in IntlMessageFormat.formats && - name in IntlMessageFormat.formats[type] - ) { - return (IntlMessageFormat.formats[type] as any)[name] - } - - return null + throw new Error(`[svelte-i18n] Unknown "${name}" ${type} format.`) } export const getNumberFormatter: MemoizedIntlFormatter< Intl.NumberFormat, Intl.NumberFormatOptions -> = memoize((options = {}) => { - const locale = options.locale || getCurrentLocale() - if (options.format) { - const format = getIntlFormatterOptions('number', options.format) - if (format) options = format +> = memoize(({ locale, format, ...options } = {}) => { + locale = locale || getCurrentLocale() + if (locale == null) { + throw new Error('[svelte-i18n] A "locale" must be set to format numbers') } + + if (format) { + options = getIntlFormatterOptions('number', format) || {} + } + return new Intl.NumberFormat(locale, options) }) export const getDateFormatter: MemoizedIntlFormatter< Intl.DateTimeFormat, Intl.DateTimeFormatOptions -> = memoize((options = { format: 'short' }) => { - const locale = options.locale || getCurrentLocale() +> = memoize(({ locale, format, ...options } = {}) => { + locale = locale || getCurrentLocale() + if (locale == null) { + throw new Error('[svelte-i18n] A "locale" must be set to format dates') + } - const format = getIntlFormatterOptions('date', options.format) - if (format) options = format + const hasInlineArgs = Object.keys(options).length > 0 + if (!hasInlineArgs) { + options = + typeof format === 'string' + ? getIntlFormatterOptions('date', format) + : getIntlFormatterOptions('date', 'short') + } return new Intl.DateTimeFormat(locale, options) }) @@ -51,16 +56,26 @@ export const getDateFormatter: MemoizedIntlFormatter< export const getTimeFormatter: MemoizedIntlFormatter< Intl.DateTimeFormat, Intl.DateTimeFormatOptions -> = memoize((options = { format: 'short' }) => { - const locale = options.locale || getCurrentLocale() +> = memoize(({ locale, format, ...options } = {}) => { + locale = locale || getCurrentLocale() + if (locale == null) { + throw new Error( + '[svelte-i18n] A "locale" must be set to format time values' + ) + } - const format = getIntlFormatterOptions('time', options.format) - if (format) options = format + const hasInlineArgs = Object.keys(options).length > 0 + if (!hasInlineArgs) { + options = + typeof format === 'string' + ? getIntlFormatterOptions('time', format) + : getIntlFormatterOptions('time', 'short') + } return new Intl.DateTimeFormat(locale, options) }) export const getMessageFormatter = memoize( (message: string, locale: string) => - new IntlMessageFormat(message, locale, getFormats()) + new IntlMessageFormat(message, locale, getOptions().formats) ) diff --git a/src/client/types/index.ts b/src/client/types/index.ts index 15fdcd3..8cc4651 100644 --- a/src/client/types/index.ts +++ b/src/client/types/index.ts @@ -22,7 +22,7 @@ type IntlFormatterOptions = T & { } export interface MemoizedIntlFormatter { - (options: IntlFormatterOptions): T + (options?: IntlFormatterOptions): T } export interface Formatter extends FormatterFn { diff --git a/test/client/formatter.test.ts b/test/client/formatter.test.ts new file mode 100644 index 0000000..426d350 --- /dev/null +++ b/test/client/formatter.test.ts @@ -0,0 +1,154 @@ +import { + getNumberFormatter, + getDateFormatter, + getTimeFormatter, + getMessageFormatter, +} from '../../src/client/includes/formatters' +import { init } from '../../src/client/configs' + +beforeEach(() => { + init({ fallbackLocale: undefined }) +}) + +describe('number formatter', () => { + const number = 123123 + test('should format a date according to the current locale', () => { + init({ fallbackLocale: 'en' }) + expect(getNumberFormatter().format(number)).toBe('123,123') + }) + + test('should format a number according to a locale', () => { + init({ fallbackLocale: 'en' }) + expect(getNumberFormatter({ locale: 'pt-BR' }).format(number)).toBe( + '123.123' + ) + }) + + test('should format a number with a custom format', () => { + init({ + fallbackLocale: 'en', + formats: require('../fixtures/formats.json'), + }) + + expect(getNumberFormatter({ format: 'brl' }).format(number)).toBe( + 'R$123,123.00' + ) + }) + + test('should format a number with inline options', () => { + init({ fallbackLocale: 'en' }) + + expect( + getNumberFormatter({ style: 'currency', currency: 'BRL' }).format(number) + ).toBe('R$123,123.00') + }) +}) + +describe('date formatter', () => { + const date = new Date(2019, 1, 1) + + test('should format a date according to the current locale', () => { + init({ fallbackLocale: 'en' }) + expect(getDateFormatter().format(date)).toBe('2/1/19') + }) + + test('should format a date according to a locale', () => { + expect(getDateFormatter({ locale: 'pt-BR' }).format(date)).toBe('01/02/19') + }) + + test('should throw if passed a non-existing format', () => { + init({ + fallbackLocale: 'en', + formats: require('../fixtures/formats.json'), + }) + + expect(() => + getDateFormatter({ locale: 'pt-BR', format: 'foo' }).format(date) + ).toThrowError(`[svelte-i18n] Unknown "foo" date format.`) + }) + + test('should format a date with a custom format', () => { + init({ + fallbackLocale: 'en', + formats: require('../fixtures/formats.json'), + }) + + expect(getDateFormatter({ format: 'customDate' }).format(date)).toBe( + '2019 AD' + ) + }) + + test('should format a date with inline options', () => { + init({ fallbackLocale: 'en' }) + + expect( + getDateFormatter({ year: 'numeric', era: 'short' }).format(date) + ).toBe('2019 AD') + }) +}) + +describe('time formatter', () => { + const time = new Date(2019, 1, 1, 20, 37, 32) + + test('should format a time according to the current locale', () => { + init({ fallbackLocale: 'en' }) + expect(getTimeFormatter().format(time)).toBe('8:37 PM') + }) + + test('should format a time according to a locale', () => { + expect(getTimeFormatter({ locale: 'pt-BR' }).format(time)).toBe('20:37') + }) + + test('should format a time with a custom format', () => { + init({ + fallbackLocale: 'en', + formats: require('../fixtures/formats.json'), + }) + + expect(getTimeFormatter({ format: 'customTime' }).format(time)).toBe( + '08:37:32 PM' + ) + }) + + test('should throw if passed a non-existing format', () => { + init({ + fallbackLocale: 'en', + formats: require('../fixtures/formats.json'), + }) + + expect(() => + getTimeFormatter({ locale: 'pt-BR', format: 'foo' }).format(time) + ).toThrowError(`[svelte-i18n] Unknown "foo" time format.`) + }) + + test('should format a time with inline options', () => { + init({ fallbackLocale: 'en' }) + + expect( + getTimeFormatter({ + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }).format(time) + ).toBe('08:37:32 PM') + }) +}) + +describe('message formatter', () => { + test('formats a message with interpolated values', () => { + expect( + getMessageFormatter('Page: {current,number}/{max,number}', 'en').format({ + current: 2, + max: 10, + }) + ).toBe('Page: 2/10') + }) + + test('formats number with custom formats', () => { + expect( + getMessageFormatter('Number: {n, number, compactShort}', 'en').format({ + n: 2000000, + }) + ).toBe('Number: 2M') + }) +}) diff --git a/test/client/index.test.ts b/test/client/index.test.ts deleted file mode 100644 index 485c311..0000000 --- a/test/client/index.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -// // TODO remake this, it's a mess -// import { Formatter } from '../../src/client/types' -// import { -// dictionary, -// locale, -// format, -// addCustomFormats, -// customFormats, -// register, -// waitLocale, -// } from '../../src/client' -// import { getClientLocale } from '../../src/client/includes/utils' - -// global.Intl = require('intl') - -// let _: Formatter -// let currentLocale: string - -// const dict = { -// en: require('../fixtures/en.json'), -// } - -// register('en-GB', () => import('../fixtures/en-GB.json')) -// register('pt', () => import('../fixtures/pt.json')) -// register('pt-BR', () => import('../fixtures/pt-BR.json')) -// register('pt-PT', () => import('../fixtures/pt-PT.json')) - -// format.subscribe(formatFn => { -// _ = formatFn -// }) -// dictionary.set(dict) -// locale.subscribe((l: string) => { -// currentLocale = l -// }) - -// describe('locale', () => { -// it('should change locale', async () => { -// await locale.set('en') -// expect(currentLocale).toBe('en') - -// await locale.set('en-US') -// expect(currentLocale).toBe('en-US') -// }) -// }) - -// describe('dictionary', () => { -// it('load a partial dictionary and merge it with the existing one', async () => { -// await locale.set('en') -// register('en', () => import('../fixtures/partials/en.json')) -// expect(_('page.title_about')).toBe('page.title_about') - -// await waitLocale('en') -// expect(_('page.title_about')).toBe('About') -// }) -// }) - -// describe('formatting', () => { -// it('should translate to current locale', async () => { -// await locale.set('en') -// expect(_('switch.lang')).toBe('Switch language') -// }) - -// it('should fallback to message id if id is not found', async () => { -// await locale.set('en') -// expect(_('batatinha.quente')).toBe('batatinha.quente') -// }) - -// it('should fallback to default value if id is not found', async () => { -// await locale.set('en') -// expect(_('batatinha.quente', { default: 'Hot Potato' })).toBe('Hot Potato') -// }) - -// it('should fallback to generic locale XX if id not found in XX-YY', async () => { -// await locale.set('en-GB') -// expect(_('sneakers', { locale: 'en-GB' })).toBe('trainers') -// }) - -// it('should fallback to generic locale XX if id not found in XX-YY', async () => { -// await locale.set('en-GB') -// expect(_('switch.lang')).toBe('Switch language') -// }) - -// it('should accept single object with id prop as the message path', async () => { -// await locale.set('en') -// expect(_({ id: 'switch.lang' })).toBe('Switch language') -// }) - -// it('should translate to passed locale', async () => { -// await waitLocale('pt-BR') -// expect(_('switch.lang', { locale: 'pt' })).toBe('Trocar idioma') -// }) - -// it('should interpolate message with variables', async () => { -// await locale.set('en') -// expect(_('greeting.message', { values: { name: 'Chris' } })).toBe( -// 'Hello Chris, how are you?' -// ) -// }) -// }) - -// describe('utilities', () => { - -// describe('format utils', () => { -// beforeAll(async () => { -// await locale.set('en') -// }) - -// it('should capital a translated message', () => { -// expect(_.capital('hi')).toBe('Hi yo') -// }) - -// it('should title a translated message', () => { -// expect(_.title('hi')).toBe('Hi Yo') -// }) - -// it('should lowercase a translated message', () => { -// expect(_.lower('hi')).toBe('hi yo') -// }) - -// it('should uppercase a translated message', () => { -// expect(_.upper('hi')).toBe('HI YO') -// }) - -// const date = new Date(2019, 3, 24, 23, 45) -// it('should format a time value', async () => { -// await locale.set('en') -// expect(_.time(date)).toBe('11:45 PM') -// expect(_.time(date, { format: 'medium' })).toBe('11:45:00 PM') -// expect(_.time(date, { format: 'medium', locale: 'pt-BR' })).toBe( -// '23:45:00' -// ) -// }) - -// it('should format a date value', () => { -// expect(_.date(date)).toBe('4/24/19') -// expect(_.date(date, { format: 'medium' })).toBe('Apr 24, 2019') -// }) -// // number -// it('should format a date value', () => { -// expect(_.number(123123123)).toBe('123,123,123') -// }) -// }) -// }) - -// describe('custom formats', () => { -// it('should format messages with custom formats', async () => { -// addCustomFormats({ -// number: { -// usd: { style: 'currency', currency: 'USD' }, -// brl: { style: 'currency', currency: 'BRL' }, -// }, -// date: { -// customDate: { year: 'numeric', era: 'short' }, -// }, -// time: { -// customTime: { hour: '2-digit', minute: '2-digit' }, -// }, -// }) - -// await locale.set('en-US') - -// expect(_.number(123123123, { format: 'usd' })).toContain('$123,123,123.00') - -// expect(_.date(new Date(2019, 0, 1), { format: 'customDate' })).toEqual( -// '2019 AD' -// ) - -// expect( -// _.time(new Date(2019, 0, 1, 2, 0, 0), { format: 'customTime' }) -// ).toEqual('02:00') -// }) -// }) diff --git a/test/client/stores/dictionary.test.ts b/test/client/stores/dictionary.test.ts index 8541932..2af7f9d 100644 --- a/test/client/stores/dictionary.test.ts +++ b/test/client/stores/dictionary.test.ts @@ -1,3 +1,5 @@ +import { get } from 'svelte/store' + import { getDictionary, hasLocaleDictionary, @@ -8,7 +10,6 @@ import { $locales, getLocaleDictionary, } from '../../../src/client/stores/dictionary' -import { get } from 'svelte/store' beforeEach(() => { $dictionary.set({}) diff --git a/test/client/stores/locale.test.ts b/test/client/stores/locale.test.ts index 953ba9f..01583ae 100644 --- a/test/client/stores/locale.test.ts +++ b/test/client/stores/locale.test.ts @@ -1,5 +1,6 @@ -import { lookupMessage } from './../../../src/client/includes/lookup' import { get } from 'svelte/store' +import { lookupMessage } from '../../../src/client/includes/lookup' + import { isFallbackLocaleOf, diff --git a/test/fixtures/formats.json b/test/fixtures/formats.json index d0e1edd..d36cac1 100644 --- a/test/fixtures/formats.json +++ b/test/fixtures/formats.json @@ -7,6 +7,10 @@ "customDate": { "year": "numeric", "era": "short" } }, "time": { - "customTime": { "hour": "2-digit", "minute": "2-digit" } + "customTime": { + "hour": "2-digit", + "minute": "2-digit", + "second": "2-digit" + } } }