feat: 🎸 make date,time and number formatters tree-shakeable

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'`
This commit is contained in:
Christian Kaisermann 2020-01-15 21:47:43 -03:00
parent 858c25cc80
commit 6526245bf9
6 changed files with 265 additions and 53 deletions

View File

@ -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)

View File

@ -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'

View File

@ -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)

View File

@ -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(`<script>
import { _ } from 'svelte-i18n'
$_.number(1000)
$_.date(new Date())
$_.time(new Date())
</script>`)
const calls = collectFormatCalls(ast)
expect(calls).toHaveLength(0)
})
})
describe('collecting message definitions', () => {

View File

@ -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')
})
})

View File

@ -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')
})
})