mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-10-05 18:44:46 +02:00
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:
parent
858c25cc80
commit
fb82a400f3
72
src/client/stores/formatters.ts
Normal file
72
src/client/stores/formatters.ts
Normal 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)
|
@ -13,19 +13,31 @@ export function waitLocale(locale?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { init } from './configs'
|
export { init } from './configs'
|
||||||
|
|
||||||
export { $locale as locale } from './stores/locale'
|
export { $locale as locale } from './stores/locale'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
$dictionary as dictionary,
|
$dictionary as dictionary,
|
||||||
$locales as locales,
|
$locales as locales,
|
||||||
addMessages,
|
addMessages,
|
||||||
} from './stores/dictionary'
|
} from './stores/dictionary'
|
||||||
|
export { registerLocaleLoader as register } from './includes/loaderQueue'
|
||||||
|
|
||||||
export { $isLoading as isLoading } from './stores/loading'
|
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 {
|
export {
|
||||||
getDateFormatter,
|
getDateFormatter,
|
||||||
getNumberFormatter,
|
getNumberFormatter,
|
||||||
getTimeFormatter,
|
getTimeFormatter,
|
||||||
getMessageFormatter,
|
getMessageFormatter,
|
||||||
} from './includes/formatters'
|
} from './includes/formatters'
|
||||||
// utilities
|
|
||||||
export { registerLocaleLoader as register } from './includes/loaderQueue'
|
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import { derived } from 'svelte/store'
|
import { derived } from 'svelte/store'
|
||||||
|
|
||||||
import { Formatter, MessageObject } from '../types'
|
import {
|
||||||
|
MessageFormatter,
|
||||||
|
MessageObject,
|
||||||
|
TimeFormatter,
|
||||||
|
DateFormatter,
|
||||||
|
NumberFormatter,
|
||||||
|
} from '../types'
|
||||||
import { lookup } from '../includes/lookup'
|
import { lookup } from '../includes/lookup'
|
||||||
import { hasLocaleQueue } from '../includes/loaderQueue'
|
import { hasLocaleQueue } from '../includes/loaderQueue'
|
||||||
import { capital, upper, lower, title } from '../includes/utils'
|
|
||||||
import {
|
import {
|
||||||
getMessageFormatter,
|
getMessageFormatter,
|
||||||
getTimeFormatter,
|
getTimeFormatter,
|
||||||
@ -15,7 +20,7 @@ import { getOptions } from '../configs'
|
|||||||
import { $dictionary } from './dictionary'
|
import { $dictionary } from './dictionary'
|
||||||
import { getCurrentLocale, getRelatedLocalesOf, $locale } from './locale'
|
import { getCurrentLocale, getRelatedLocalesOf, $locale } from './locale'
|
||||||
|
|
||||||
const formatMessage: Formatter = (id, options = {}) => {
|
const formatMessage: MessageFormatter = (id, options = {}) => {
|
||||||
if (typeof id === 'object') {
|
if (typeof id === 'object') {
|
||||||
options = id as MessageObject
|
options = id as MessageObject
|
||||||
id = options.id
|
id = options.id
|
||||||
@ -52,14 +57,16 @@ const formatMessage: Formatter = (id, options = {}) => {
|
|||||||
return getMessageFormatter(message, locale).format(values)
|
return getMessageFormatter(message, locale).format(values)
|
||||||
}
|
}
|
||||||
|
|
||||||
formatMessage.time = (t, options) => getTimeFormatter(options).format(t)
|
const formatTime: TimeFormatter = (t, options) =>
|
||||||
formatMessage.date = (d, options) => getDateFormatter(options).format(d)
|
getTimeFormatter(options).format(t)
|
||||||
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 $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)
|
||||||
|
@ -79,17 +79,6 @@ describe('collecting format calls', () => {
|
|||||||
expect(calls[2]).toMatchObject({ type: 'CallExpression' })
|
expect(calls[2]).toMatchObject({ type: 'CallExpression' })
|
||||||
expect(calls[3]).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', () => {
|
describe('collecting message definitions', () => {
|
||||||
|
112
test/client/stores/formatters.test.ts
Normal file
112
test/client/stores/formatters.test.ts
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
@ -1,11 +1,39 @@
|
|||||||
|
<<<<<<< HEAD:test/runtime/stores/format.test.ts
|
||||||
import { Formatter } from '../../../src/runtime/types'
|
import { Formatter } from '../../../src/runtime/types'
|
||||||
import { $format } from '../../../src/runtime/stores/format'
|
import { $format } from '../../../src/runtime/stores/format'
|
||||||
import { init } from '../../../src/runtime/configs'
|
import { init } from '../../../src/runtime/configs'
|
||||||
import { addMessages } from '../../../src/runtime/stores/dictionary'
|
import { addMessages } from '../../../src/runtime/stores/dictionary'
|
||||||
import { $locale } from '../../../src/runtime/stores/locale'
|
import { $locale } from '../../../src/runtime/stores/locale'
|
||||||
|
=======
|
||||||
|
import { get } from 'svelte/store'
|
||||||
|
|
||||||
let format: Formatter
|
import {
|
||||||
$format.subscribe(f => (format = f))
|
$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', require('../../fixtures/en.json'))
|
||||||
addMessages('en-GB', require('../../fixtures/en-GB.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', () => {
|
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', () => {
|
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', () => {
|
test('formats a message with interpolated values', () => {
|
||||||
expect(format({ id: 'photos', values: { n: 0 } })).toBe('You have no photos.')
|
expect(formatMessage({ id: 'photos', values: { n: 0 } })).toBe(
|
||||||
expect(format({ id: 'photos', values: { n: 1 } })).toBe('You have one photo.')
|
'You have no photos.'
|
||||||
expect(format({ id: 'photos', values: { n: 21 } })).toBe(
|
)
|
||||||
|
expect(formatMessage({ id: 'photos', values: { n: 1 } })).toBe(
|
||||||
|
'You have one photo.'
|
||||||
|
)
|
||||||
|
expect(formatMessage({ id: 'photos', values: { n: 21 } })).toBe(
|
||||||
'You have 21 photos.'
|
'You have 21 photos.'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('accepts a message id as first argument', () => {
|
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', () => {
|
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', () => {
|
test('throws if no locale is set', () => {
|
||||||
$locale.set(null)
|
$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.'
|
'[svelte-i18n] Cannot format a message without first setting the initial locale.'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('uses a missing message default value', () => {
|
test('uses a missing message default value', () => {
|
||||||
expect(format('missing', { default: 'Missing Default' })).toBe(
|
expect(formatMessage('missing', { default: 'Missing Default' })).toBe(
|
||||||
'Missing Default'
|
'Missing Default'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -58,7 +90,7 @@ test('warn on missing messages', () => {
|
|||||||
const warn = global.console.warn
|
const warn = global.console.warn
|
||||||
global.console.warn = jest.fn()
|
global.console.warn = jest.fn()
|
||||||
|
|
||||||
format('missing')
|
formatMessage('missing')
|
||||||
|
|
||||||
expect(console.warn).toBeCalledWith(
|
expect(console.warn).toBeCalledWith(
|
||||||
`[svelte-i18n] The message "missing" was not found in "en".`
|
`[svelte-i18n] The message "missing" was not found in "en".`
|
||||||
@ -69,24 +101,12 @@ test('warn on missing messages', () => {
|
|||||||
|
|
||||||
describe('format utilities', () => {
|
describe('format utilities', () => {
|
||||||
test('time', () => {
|
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', () => {
|
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', () => {
|
test('number', () => {
|
||||||
expect(format.number(123123123)).toBe('123,123,123')
|
expect(formatNumber(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')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user