From 6526245bf9d40d25af14ec1e7acb34772a9f3f0e Mon Sep 17 00:00:00 2001 From: Christian Kaisermann Date: Wed, 15 Jan 2020 21:47:43 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20make=20date,time=20and?= =?UTF-8?q?=20number=20formatters=20tree-shakeable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: Changes completely the API. Now, to format a number, date or time, the developer must explicitly import the formatter store: `import { time, date, number } from 'svelte-i18n'` --- src/client/stores/formatters.ts | 72 +++++++++++++++++ src/runtime/index.ts | 18 ++++- src/runtime/stores/format.ts | 31 ++++--- test/cli/extract.test.ts | 11 --- test/client/stores/formatters.test.ts | 112 ++++++++++++++++++++++++++ test/runtime/stores/format.test.ts | 74 ++++++++++------- 6 files changed, 265 insertions(+), 53 deletions(-) create mode 100644 src/client/stores/formatters.ts create mode 100644 test/client/stores/formatters.test.ts diff --git a/src/client/stores/formatters.ts b/src/client/stores/formatters.ts new file mode 100644 index 0000000..184ea35 --- /dev/null +++ b/src/client/stores/formatters.ts @@ -0,0 +1,72 @@ +import { derived } from 'svelte/store' + +import { + MessageFormatter, + MessageObject, + TimeFormatter, + DateFormatter, + NumberFormatter, +} from '../types' +import { lookup } from '../includes/lookup' +import { hasLocaleQueue } from '../includes/loaderQueue' +import { + getMessageFormatter, + getTimeFormatter, + getDateFormatter, + getNumberFormatter, +} from '../includes/formatters' +import { getOptions } from '../configs' + +import { $dictionary } from './dictionary' +import { getCurrentLocale, getRelatedLocalesOf, $locale } from './locale' + +const formatMessage: MessageFormatter = (id, options = {}) => { + if (typeof id === 'object') { + options = id as MessageObject + id = options.id + } + + const { values, locale = getCurrentLocale(), default: defaultValue } = options + + if (locale == null) { + throw new Error( + '[svelte-i18n] Cannot format a message without first setting the initial locale.' + ) + } + + const message = lookup(id, locale) + + if (!message) { + if (getOptions().warnOnMissingMessages) { + // istanbul ignore next + console.warn( + `[svelte-i18n] The message "${id}" was not found in "${getRelatedLocalesOf( + locale + ).join('", "')}".${ + hasLocaleQueue(getCurrentLocale()) + ? `\n\nNote: there are at least one loader still registered to this locale that wasn't executed.` + : '' + }` + ) + } + + return defaultValue || id + } + + if (!values) return message + return getMessageFormatter(message, locale).format(values) +} + +const formatTime: TimeFormatter = (t, options) => + getTimeFormatter(options).format(t) + +const formatDate: DateFormatter = (d, options) => + getDateFormatter(options).format(d) + +const formatNumber: NumberFormatter = (n, options) => + getNumberFormatter(options).format(n) + +export const $format = derived([$locale, $dictionary], () => formatMessage) +export const $formatTime = derived([$locale], () => formatTime) +export const $formatDate = derived([$locale], () => formatDate) +export const $formatNumber = derived([$locale], () => formatNumber) diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 5e6d522..a521f02 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -13,19 +13,31 @@ export function waitLocale(locale?: string) { } export { init } from './configs' + export { $locale as locale } from './stores/locale' + export { $dictionary as dictionary, $locales as locales, addMessages, } from './stores/dictionary' +export { registerLocaleLoader as register } from './includes/loaderQueue' + export { $isLoading as isLoading } from './stores/loading' -export { $format as format, $format as _, $format as t } from './stores/format' + +export { + $format as format, + $format as _, + $format as t, + $formatDate as date, + $formatNumber as number, + $formatTime as time, +} from './stores/formatters' + +// low-level export { getDateFormatter, getNumberFormatter, getTimeFormatter, getMessageFormatter, } from './includes/formatters' -// utilities -export { registerLocaleLoader as register } from './includes/loaderQueue' diff --git a/src/runtime/stores/format.ts b/src/runtime/stores/format.ts index aa25e35..184ea35 100644 --- a/src/runtime/stores/format.ts +++ b/src/runtime/stores/format.ts @@ -1,9 +1,14 @@ import { derived } from 'svelte/store' -import { Formatter, MessageObject } from '../types' +import { + MessageFormatter, + MessageObject, + TimeFormatter, + DateFormatter, + NumberFormatter, +} from '../types' import { lookup } from '../includes/lookup' import { hasLocaleQueue } from '../includes/loaderQueue' -import { capital, upper, lower, title } from '../includes/utils' import { getMessageFormatter, getTimeFormatter, @@ -15,7 +20,7 @@ import { getOptions } from '../configs' import { $dictionary } from './dictionary' import { getCurrentLocale, getRelatedLocalesOf, $locale } from './locale' -const formatMessage: Formatter = (id, options = {}) => { +const formatMessage: MessageFormatter = (id, options = {}) => { if (typeof id === 'object') { options = id as MessageObject id = options.id @@ -52,14 +57,16 @@ const formatMessage: Formatter = (id, options = {}) => { return getMessageFormatter(message, locale).format(values) } -formatMessage.time = (t, options) => getTimeFormatter(options).format(t) -formatMessage.date = (d, options) => getDateFormatter(options).format(d) -formatMessage.number = (n, options) => getNumberFormatter(options).format(n) -formatMessage.capital = (id, options) => capital(formatMessage(id, options)) -formatMessage.title = (id, options) => title(formatMessage(id, options)) -formatMessage.upper = (id, options) => upper(formatMessage(id, options)) -formatMessage.lower = (id, options) => lower(formatMessage(id, options)) +const formatTime: TimeFormatter = (t, options) => + getTimeFormatter(options).format(t) -const $format = derived([$locale, $dictionary], () => formatMessage) +const formatDate: DateFormatter = (d, options) => + getDateFormatter(options).format(d) -export { $format } +const formatNumber: NumberFormatter = (n, options) => + getNumberFormatter(options).format(n) + +export const $format = derived([$locale, $dictionary], () => formatMessage) +export const $formatTime = derived([$locale], () => formatTime) +export const $formatDate = derived([$locale], () => formatDate) +export const $formatNumber = derived([$locale], () => formatNumber) diff --git a/test/cli/extract.test.ts b/test/cli/extract.test.ts index 0711447..7a3e015 100644 --- a/test/cli/extract.test.ts +++ b/test/cli/extract.test.ts @@ -79,17 +79,6 @@ describe('collecting format calls', () => { expect(calls[2]).toMatchObject({ type: 'CallExpression' }) expect(calls[3]).toMatchObject({ type: 'CallExpression' }) }) - - test('ignores date, time and number calls', () => { - const ast = parse(``) - const calls = collectFormatCalls(ast) - expect(calls).toHaveLength(0) - }) }) describe('collecting message definitions', () => { diff --git a/test/client/stores/formatters.test.ts b/test/client/stores/formatters.test.ts new file mode 100644 index 0000000..b6de057 --- /dev/null +++ b/test/client/stores/formatters.test.ts @@ -0,0 +1,112 @@ +<<<<<<< HEAD:test/runtime/stores/format.test.ts +import { Formatter } from '../../../src/runtime/types' +import { $format } from '../../../src/runtime/stores/format' +import { init } from '../../../src/runtime/configs' +import { addMessages } from '../../../src/runtime/stores/dictionary' +import { $locale } from '../../../src/runtime/stores/locale' +======= +import { get } from 'svelte/store' + +import { + $format, + $formatTime, + $formatDate, + $formatNumber, +} from '../../../src/client/stores/formatters' +import { init } from '../../../src/client/configs' +import { addMessages } from '../../../src/client/stores/dictionary' +import { $locale } from '../../../src/client/stores/locale' +import { MessageFormatter } from '../../../src/client/types' +import { + TimeFormatter, + DateFormatter, + NumberFormatter, +} from '../../../src/client/types/index' +>>>>>>> feat: 🎸 make date,time and number formatters tree-shakeable:test/client/stores/formatters.test.ts + +let formatMessage: MessageFormatter +let formatTime: TimeFormatter +let formatDate: DateFormatter +let formatNumber: NumberFormatter +$locale.subscribe(() => { + formatMessage = get($format) + formatTime = get($formatTime) + formatDate = get($formatDate) + formatNumber = get($formatNumber) +}) + +addMessages('en', require('../../fixtures/en.json')) +addMessages('en-GB', require('../../fixtures/en-GB.json')) +addMessages('pt', require('../../fixtures/pt.json')) +addMessages('pt-BR', require('../../fixtures/pt-BR.json')) +addMessages('pt-PT', require('../../fixtures/pt-PT.json')) + +beforeEach(() => { + init({ fallbackLocale: 'en' }) +}) + +test('formats a message by its id and the current locale', () => { + expect(formatMessage({ id: 'form.field_1_name' })).toBe('Name') +}) + +test('formats a message by its id and the a passed locale', () => { + expect(formatMessage({ id: 'form.field_1_name', locale: 'pt' })).toBe('Nome') +}) + +test('formats a message with interpolated values', () => { + expect(formatMessage({ id: 'photos', values: { n: 0 } })).toBe( + 'You have no photos.' + ) + expect(formatMessage({ id: 'photos', values: { n: 1 } })).toBe( + 'You have one photo.' + ) + expect(formatMessage({ id: 'photos', values: { n: 21 } })).toBe( + 'You have 21 photos.' + ) +}) + +test('accepts a message id as first argument', () => { + expect(formatMessage('form.field_1_name')).toBe('Name') +}) + +test('accepts a message id as first argument and formatting options as second', () => { + expect(formatMessage('form.field_1_name', { locale: 'pt' })).toBe('Nome') +}) + +test('throws if no locale is set', () => { + $locale.set(null) + expect(() => formatMessage('form.field_1_name')).toThrow( + '[svelte-i18n] Cannot format a message without first setting the initial locale.' + ) +}) + +test('uses a missing message default value', () => { + expect(formatMessage('missing', { default: 'Missing Default' })).toBe( + 'Missing Default' + ) +}) + +test('warn on missing messages', () => { + const warn = global.console.warn + global.console.warn = jest.fn() + + formatMessage('missing') + + expect(console.warn).toBeCalledWith( + `[svelte-i18n] The message "missing" was not found in "en".` + ) + + global.console.warn = warn +}) + +describe('format utilities', () => { + test('time', () => { + expect(formatTime(new Date(2019, 0, 1, 20, 37))).toBe('8:37 PM') + }) + test('date', () => { + expect(formatDate(new Date(2019, 0, 1, 20, 37))).toBe('1/1/19') + }) + test('number', () => { + expect(formatNumber(123123123)).toBe('123,123,123') + }) +}) diff --git a/test/runtime/stores/format.test.ts b/test/runtime/stores/format.test.ts index b80fdbe..b6de057 100644 --- a/test/runtime/stores/format.test.ts +++ b/test/runtime/stores/format.test.ts @@ -1,11 +1,39 @@ +<<<<<<< HEAD:test/runtime/stores/format.test.ts import { Formatter } from '../../../src/runtime/types' import { $format } from '../../../src/runtime/stores/format' import { init } from '../../../src/runtime/configs' import { addMessages } from '../../../src/runtime/stores/dictionary' import { $locale } from '../../../src/runtime/stores/locale' +======= +import { get } from 'svelte/store' -let format: Formatter -$format.subscribe(f => (format = f)) +import { + $format, + $formatTime, + $formatDate, + $formatNumber, +} from '../../../src/client/stores/formatters' +import { init } from '../../../src/client/configs' +import { addMessages } from '../../../src/client/stores/dictionary' +import { $locale } from '../../../src/client/stores/locale' +import { MessageFormatter } from '../../../src/client/types' +import { + TimeFormatter, + DateFormatter, + NumberFormatter, +} from '../../../src/client/types/index' +>>>>>>> feat: 🎸 make date,time and number formatters tree-shakeable:test/client/stores/formatters.test.ts + +let formatMessage: MessageFormatter +let formatTime: TimeFormatter +let formatDate: DateFormatter +let formatNumber: NumberFormatter +$locale.subscribe(() => { + formatMessage = get($format) + formatTime = get($formatTime) + formatDate = get($formatDate) + formatNumber = get($formatNumber) +}) addMessages('en', require('../../fixtures/en.json')) addMessages('en-GB', require('../../fixtures/en-GB.json')) @@ -18,38 +46,42 @@ beforeEach(() => { }) test('formats a message by its id and the current locale', () => { - expect(format({ id: 'form.field_1_name' })).toBe('Name') + expect(formatMessage({ id: 'form.field_1_name' })).toBe('Name') }) test('formats a message by its id and the a passed locale', () => { - expect(format({ id: 'form.field_1_name', locale: 'pt' })).toBe('Nome') + expect(formatMessage({ id: 'form.field_1_name', locale: 'pt' })).toBe('Nome') }) test('formats a message with interpolated values', () => { - expect(format({ id: 'photos', values: { n: 0 } })).toBe('You have no photos.') - expect(format({ id: 'photos', values: { n: 1 } })).toBe('You have one photo.') - expect(format({ id: 'photos', values: { n: 21 } })).toBe( + expect(formatMessage({ id: 'photos', values: { n: 0 } })).toBe( + 'You have no photos.' + ) + expect(formatMessage({ id: 'photos', values: { n: 1 } })).toBe( + 'You have one photo.' + ) + expect(formatMessage({ id: 'photos', values: { n: 21 } })).toBe( 'You have 21 photos.' ) }) test('accepts a message id as first argument', () => { - expect(format('form.field_1_name')).toBe('Name') + expect(formatMessage('form.field_1_name')).toBe('Name') }) test('accepts a message id as first argument and formatting options as second', () => { - expect(format('form.field_1_name', { locale: 'pt' })).toBe('Nome') + expect(formatMessage('form.field_1_name', { locale: 'pt' })).toBe('Nome') }) test('throws if no locale is set', () => { $locale.set(null) - expect(() => format('form.field_1_name')).toThrow( + expect(() => formatMessage('form.field_1_name')).toThrow( '[svelte-i18n] Cannot format a message without first setting the initial locale.' ) }) test('uses a missing message default value', () => { - expect(format('missing', { default: 'Missing Default' })).toBe( + expect(formatMessage('missing', { default: 'Missing Default' })).toBe( 'Missing Default' ) }) @@ -58,7 +90,7 @@ test('warn on missing messages', () => { const warn = global.console.warn global.console.warn = jest.fn() - format('missing') + formatMessage('missing') expect(console.warn).toBeCalledWith( `[svelte-i18n] The message "missing" was not found in "en".` @@ -69,24 +101,12 @@ test('warn on missing messages', () => { describe('format utilities', () => { test('time', () => { - expect(format.time(new Date(2019, 0, 1, 20, 37))).toBe('8:37 PM') + expect(formatTime(new Date(2019, 0, 1, 20, 37))).toBe('8:37 PM') }) test('date', () => { - expect(format.date(new Date(2019, 0, 1, 20, 37))).toBe('1/1/19') + expect(formatDate(new Date(2019, 0, 1, 20, 37))).toBe('1/1/19') }) test('number', () => { - expect(format.number(123123123)).toBe('123,123,123') - }) - test('capital', () => { - expect(format.capital('title')).toBe('Page title') - }) - test('title', () => { - expect(format.title('title')).toBe('Page Title') - }) - test('upper', () => { - expect(format.upper('title')).toBe('PAGE TITLE') - }) - test('lower', () => { - expect(format.lower('title')).toBe('page title') + expect(formatNumber(123123123)).toBe('123,123,123') }) })