mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-11-16 09:59:58 +01:00
refactor: 💡 separation of a lot of concerns
This commit is contained in:
parent
3dce407f4f
commit
bf5ad6e387
@ -1,6 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"printWidth": 100,
|
||||
"printWidth": 80,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
|
@ -40,7 +40,6 @@ export default {
|
||||
}),
|
||||
commonjs(),
|
||||
json(),
|
||||
|
||||
legacy &&
|
||||
babel({
|
||||
extensions: ['.js', '.mjs', '.html', '.svelte'],
|
||||
@ -73,7 +72,6 @@ export default {
|
||||
|
||||
onwarn,
|
||||
},
|
||||
|
||||
server: {
|
||||
input: config.server.input(),
|
||||
output: config.server.output(),
|
||||
@ -94,9 +92,8 @@ export default {
|
||||
],
|
||||
external: Object.keys(pkg.dependencies).concat(
|
||||
require('module').builtinModules ||
|
||||
Object.keys(process.binding('natives')),
|
||||
Object.keys(process.binding('natives'))
|
||||
),
|
||||
|
||||
onwarn,
|
||||
},
|
||||
|
||||
|
@ -12,7 +12,7 @@ polka() // You can also use Express
|
||||
.use(
|
||||
compression({ threshold: 0 }),
|
||||
sirv('static', { dev }),
|
||||
sapper.middleware(),
|
||||
sapper.middleware()
|
||||
)
|
||||
.listen(PORT, err => {
|
||||
if (err) console.log('error', err)
|
||||
|
@ -1,82 +1,85 @@
|
||||
import { timestamp, files, shell, routes } from '@sapper/service-worker';
|
||||
import { timestamp, files, shell, routes } from '@sapper/service-worker'
|
||||
|
||||
const ASSETS = `cache${timestamp}`;
|
||||
const ASSETS = `cache${timestamp}`
|
||||
|
||||
// `shell` is an array of all the files generated by the bundler,
|
||||
// `files` is an array of everything in the `static` directory
|
||||
const to_cache = shell.concat(files);
|
||||
const cached = new Set(to_cache);
|
||||
const to_cache = shell.concat(files)
|
||||
const cached = new Set(to_cache)
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(ASSETS)
|
||||
.then(cache => cache.addAll(to_cache))
|
||||
.then(() => {
|
||||
self.skipWaiting();
|
||||
})
|
||||
);
|
||||
});
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(ASSETS)
|
||||
.then(cache => cache.addAll(to_cache))
|
||||
.then(() => {
|
||||
self.skipWaiting()
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(async keys => {
|
||||
// delete old caches
|
||||
for (const key of keys) {
|
||||
if (key !== ASSETS) await caches.delete(key);
|
||||
}
|
||||
event.waitUntil(
|
||||
caches.keys().then(async keys => {
|
||||
// delete old caches
|
||||
for (const key of keys) {
|
||||
if (key !== ASSETS) await caches.delete(key)
|
||||
}
|
||||
|
||||
self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
self.clients.claim()
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.method !== 'GET' || event.request.headers.has('range')) return;
|
||||
if (event.request.method !== 'GET' || event.request.headers.has('range'))
|
||||
return
|
||||
|
||||
const url = new URL(event.request.url);
|
||||
const url = new URL(event.request.url)
|
||||
|
||||
// don't try to handle e.g. data: URIs
|
||||
if (!url.protocol.startsWith('http')) return;
|
||||
// don't try to handle e.g. data: URIs
|
||||
if (!url.protocol.startsWith('http')) return
|
||||
|
||||
// ignore dev server requests
|
||||
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
|
||||
// ignore dev server requests
|
||||
if (
|
||||
url.hostname === self.location.hostname &&
|
||||
url.port !== self.location.port
|
||||
)
|
||||
return
|
||||
|
||||
// always serve static files and bundler-generated assets from cache
|
||||
if (url.host === self.location.host && cached.has(url.pathname)) {
|
||||
event.respondWith(caches.match(event.request));
|
||||
return;
|
||||
}
|
||||
// always serve static files and bundler-generated assets from cache
|
||||
if (url.host === self.location.host && cached.has(url.pathname)) {
|
||||
event.respondWith(caches.match(event.request))
|
||||
return
|
||||
}
|
||||
|
||||
// for pages, you might want to serve a shell `service-worker-index.html` file,
|
||||
// which Sapper has generated for you. It's not right for every
|
||||
// app, but if it's right for yours then uncomment this section
|
||||
/*
|
||||
// for pages, you might want to serve a shell `service-worker-index.html` file,
|
||||
// which Sapper has generated for you. It's not right for every
|
||||
// app, but if it's right for yours then uncomment this section
|
||||
/*
|
||||
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
|
||||
event.respondWith(caches.match('/service-worker-index.html'));
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
if (event.request.cache === 'only-if-cached') return;
|
||||
if (event.request.cache === 'only-if-cached') return
|
||||
|
||||
// for everything else, try the network first, falling back to
|
||||
// cache if the user is offline. (If the pages never change, you
|
||||
// might prefer a cache-first approach to a network-first one.)
|
||||
event.respondWith(
|
||||
caches
|
||||
.open(`offline${timestamp}`)
|
||||
.then(async cache => {
|
||||
try {
|
||||
const response = await fetch(event.request);
|
||||
cache.put(event.request, response.clone());
|
||||
return response;
|
||||
} catch(err) {
|
||||
const response = await cache.match(event.request);
|
||||
if (response) return response;
|
||||
// for everything else, try the network first, falling back to
|
||||
// cache if the user is offline. (If the pages never change, you
|
||||
// might prefer a cache-first approach to a network-first one.)
|
||||
event.respondWith(
|
||||
caches.open(`offline${timestamp}`).then(async cache => {
|
||||
try {
|
||||
const response = await fetch(event.request)
|
||||
cache.put(event.request, response.clone())
|
||||
return response
|
||||
} catch (err) {
|
||||
const response = await cache.match(event.request)
|
||||
if (response) return response
|
||||
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
throw err
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
@ -691,9 +691,9 @@
|
||||
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
|
||||
|
||||
"@types/node@*":
|
||||
version "12.12.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.9.tgz#0b5ae05516b757cbff2e82c04500190aef986c7b"
|
||||
integrity sha512-kV3w4KeLsRBW+O2rKhktBwENNJuqAUQHS3kf4ia2wIaF/MN6U7ANgTsx7tGremcA0Pk3Yh0Hl0iKiLPuBdIgmw==
|
||||
version "12.12.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.11.tgz#bec2961975888d964196bf0016a2f984d793d3ce"
|
||||
integrity sha512-O+x6uIpa6oMNTkPuHDa9MhMMehlxLAd5QcOvKRjAFsBVpeFWTOPnXbDvILvFgFFZfQ1xh1EZi1FbXxUix+zpsQ==
|
||||
|
||||
"@types/resolve@0.0.8":
|
||||
version "0.0.8"
|
||||
@ -775,9 +775,9 @@ camel-case@^3.0.0:
|
||||
upper-case "^1.1.1"
|
||||
|
||||
caniuse-lite@^1.0.30001010:
|
||||
version "1.0.30001010"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001010.tgz#397a14034d384260453cc81994f494626d34b938"
|
||||
integrity sha512-RA5GH9YjFNea4ZQszdWgh2SC+dpLiRAg4VDQS2b5JRI45OxmbGrYocYHTa9x0bKMQUE7uvHkNPNffUr+pCxSGw==
|
||||
version "1.0.30001011"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001011.tgz#0d6c4549c78c4a800bb043a83ca0cbe0aee6c6e1"
|
||||
integrity sha512-h+Eqyn/YA6o6ZTqpS86PyRmNWOs1r54EBDcd2NTwwfsXQ8re1B38SnB+p2RKF8OUsyEIjeDU8XGec1RGO/wYCg==
|
||||
|
||||
chalk@^2.0.0, chalk@^2.4.1:
|
||||
version "2.4.2"
|
||||
@ -890,9 +890,9 @@ define-properties@^1.1.2, define-properties@^1.1.3:
|
||||
object-keys "^1.0.12"
|
||||
|
||||
electron-to-chromium@^1.3.306:
|
||||
version "1.3.306"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.306.tgz#e8265301d053d5f74e36cb876486830261fbe946"
|
||||
integrity sha512-frDqXvrIROoYvikSKTIKbHbzO6M3/qC6kCIt/1FOa9kALe++c4VAJnwjSFvf1tYLEUsP2n9XZ4XSCyqc3l7A/A==
|
||||
version "1.3.307"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.307.tgz#e9b98901371d20164af7c0ca5bc820bd4305ccd3"
|
||||
integrity sha512-01rTsAqHwf3D2X6NtlUvzB2hxDj67kiTVIO5GWdFb2unA0QvFvrjyrtc993ByRLF+surlr+9AvJdD0UYs5HzwA==
|
||||
|
||||
error-ex@^1.3.1:
|
||||
version "1.3.2"
|
||||
@ -1492,9 +1492,9 @@ rollup-pluginutils@^2.3.3, rollup-pluginutils@^2.5.0, rollup-pluginutils@^2.6.0,
|
||||
estree-walker "^0.6.1"
|
||||
|
||||
rollup@^1.12.0:
|
||||
version "1.27.2"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.27.2.tgz#caf54a93df228bf7864f13dddcdb363d3e958509"
|
||||
integrity sha512-sD3iyd0zlvgK1S3MmICi6F/Y+R/QWY5XxzsTGN4pAd+nCasDUizmAhgq2hdh1t2eLux974NHU2TW41fhuGPv+Q==
|
||||
version "1.27.3"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.27.3.tgz#0e427d9bee9e30e6017e89b942d88276ce26f9e2"
|
||||
integrity sha512-79AEh4m5NPCz97GTuIoXpSFIMPyk2AiqVQp040baSRPXk/I4YMGt5/CR9GX5oEYEkxwBZoWLheaS1/w/FidfJw==
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
"@types/node" "*"
|
||||
|
@ -78,7 +78,9 @@
|
||||
"commander": "^4.0.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
"estree-walker": "^0.9.0",
|
||||
"fast-memoize": "^2.5.1",
|
||||
"intl-messageformat": "^7.5.2",
|
||||
"nano-memoize": "^1.1.7",
|
||||
"object-resolve-path": "^1.1.1",
|
||||
"tiny-glob": "^0.2.6"
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ function getLibImportDeclarations(ast: Ast) {
|
||||
return (ast.instance
|
||||
? ast.instance.content.body.filter(
|
||||
node =>
|
||||
node.type === 'ImportDeclaration' && node.source.value === LIB_NAME,
|
||||
node.type === 'ImportDeclaration' && node.source.value === LIB_NAME
|
||||
)
|
||||
: []) as ImportDeclaration[]
|
||||
}
|
||||
@ -82,13 +82,13 @@ function getLibImportDeclarations(ast: Ast) {
|
||||
function getDefineMessagesSpecifier(decl: ImportDeclaration) {
|
||||
return decl.specifiers.find(
|
||||
spec =>
|
||||
'imported' in spec && spec.imported.name === DEFINE_MESSAGES_METHOD_NAME,
|
||||
'imported' in spec && spec.imported.name === DEFINE_MESSAGES_METHOD_NAME
|
||||
) as ImportSpecifier
|
||||
}
|
||||
|
||||
function getFormatSpecifiers(decl: ImportDeclaration) {
|
||||
return decl.specifiers.filter(
|
||||
spec => 'imported' in spec && FORMAT_METHOD_NAMES.has(spec.imported.name),
|
||||
spec => 'imported' in spec && FORMAT_METHOD_NAMES.has(spec.imported.name)
|
||||
) as ImportSpecifier[]
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@ function getObjFromExpression(exprNode: Node | ObjectExpression) {
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{ node: exprNode, meta: {} },
|
||||
{ node: exprNode, meta: {} }
|
||||
)
|
||||
}
|
||||
|
||||
@ -115,8 +115,8 @@ export function collectFormatCalls(ast: Ast) {
|
||||
|
||||
const imports = new Set(
|
||||
importDecls.flatMap(decl =>
|
||||
getFormatSpecifiers(decl).map(n => n.local.name),
|
||||
),
|
||||
getFormatSpecifiers(decl).map(n => n.local.name)
|
||||
)
|
||||
)
|
||||
|
||||
if (imports.size === 0) return []
|
||||
@ -137,7 +137,7 @@ export function collectFormatCalls(ast: Ast) {
|
||||
export function collectMessageDefinitions(ast: Ast) {
|
||||
const definitions: ObjectExpression[] = []
|
||||
const defineImportDecl = getLibImportDeclarations(ast).find(
|
||||
getDefineMessagesSpecifier,
|
||||
getDefineMessagesSpecifier
|
||||
)
|
||||
|
||||
if (defineImportDecl == null) return []
|
||||
@ -157,7 +157,7 @@ export function collectMessageDefinitions(ast: Ast) {
|
||||
})
|
||||
|
||||
return definitions.flatMap(definitionDict =>
|
||||
definitionDict.properties.map(propNode => propNode.value),
|
||||
definitionDict.properties.map(propNode => propNode.value)
|
||||
)
|
||||
}
|
||||
|
||||
@ -193,7 +193,7 @@ export function collectMessages(markup: string): Message[] {
|
||||
|
||||
export function extractMessages(
|
||||
markup: string,
|
||||
{ accumulator = {}, shallow = false, overwrite = false } = {} as any,
|
||||
{ accumulator = {}, shallow = false, overwrite = false } = {} as any
|
||||
) {
|
||||
collectMessages(markup).forEach(message => {
|
||||
let defaultValue = message.meta.default
|
||||
|
@ -20,21 +20,21 @@ program
|
||||
.option(
|
||||
'-s, --shallow',
|
||||
'extract to a shallow dictionary (ids with dots interpreted as strings, not paths)',
|
||||
false,
|
||||
false
|
||||
)
|
||||
.option(
|
||||
'--overwrite',
|
||||
'overwrite the content of the output file instead of just appending new properties',
|
||||
false,
|
||||
false
|
||||
)
|
||||
.option(
|
||||
'-c, --config <dir>',
|
||||
'path to the "svelte.config.js" file',
|
||||
process.cwd(),
|
||||
process.cwd()
|
||||
)
|
||||
.action(async (globStr, output, { shallow, overwrite, config }) => {
|
||||
const filesToExtract = (await glob(globStr)).filter(file =>
|
||||
file.match(/\.html|svelte$/i),
|
||||
file.match(/\.html|svelte$/i)
|
||||
)
|
||||
const svelteConfig = await import(
|
||||
resolve(config, 'svelte.config.js')
|
||||
|
@ -1,7 +1,8 @@
|
||||
import IntlMessageFormat, { Formats } from 'intl-messageformat'
|
||||
import memoize from 'micro-memoize'
|
||||
import memoize from 'fast-memoize'
|
||||
|
||||
import { MemoizedIntlFormatter } from './types'
|
||||
import { MemoizedIntlFormatter } from '../types'
|
||||
import { getCurrentLocale } from '../stores/locale'
|
||||
|
||||
export const customFormats: any = {
|
||||
number: {
|
||||
@ -22,7 +23,7 @@ export function addCustomFormats(formats: Partial<Formats>) {
|
||||
|
||||
const getIntlFormatterOptions = (
|
||||
type: 'time' | 'number' | 'date',
|
||||
name: string,
|
||||
name: string
|
||||
): any => {
|
||||
if (type in customFormats && name in customFormats[type]) {
|
||||
return customFormats[type][name]
|
||||
@ -41,8 +42,8 @@ const getIntlFormatterOptions = (
|
||||
export const getNumberFormatter: MemoizedIntlFormatter<
|
||||
Intl.NumberFormat,
|
||||
Intl.NumberFormatOptions
|
||||
> = memoize((locale, options = {}) => {
|
||||
if (options.locale) locale = options.locale
|
||||
> = memoize((options = {}) => {
|
||||
const locale = options.locale || getCurrentLocale()
|
||||
if (options.format) {
|
||||
const format = getIntlFormatterOptions('number', options.format)
|
||||
if (format) options = format
|
||||
@ -53,28 +54,28 @@ export const getNumberFormatter: MemoizedIntlFormatter<
|
||||
export const getDateFormatter: MemoizedIntlFormatter<
|
||||
Intl.DateTimeFormat,
|
||||
Intl.DateTimeFormatOptions
|
||||
> = memoize((locale, options = { format: 'short' }) => {
|
||||
if (options.locale) locale = options.locale
|
||||
if (options.format) {
|
||||
const format = getIntlFormatterOptions('date', options.format)
|
||||
if (format) options = format
|
||||
}
|
||||
> = memoize((options = { format: 'short' }) => {
|
||||
const locale = options.locale || getCurrentLocale()
|
||||
|
||||
const format = getIntlFormatterOptions('date', options.format)
|
||||
if (format) options = format
|
||||
|
||||
return new Intl.DateTimeFormat(locale, options)
|
||||
})
|
||||
|
||||
export const getTimeFormatter: MemoizedIntlFormatter<
|
||||
Intl.DateTimeFormat,
|
||||
Intl.DateTimeFormatOptions
|
||||
> = memoize((locale, options = { format: 'short' }) => {
|
||||
if (options.locale) locale = options.locale
|
||||
if (options.format) {
|
||||
const format = getIntlFormatterOptions('time', options.format)
|
||||
if (format) options = format
|
||||
}
|
||||
> = memoize((options = { format: 'short' }) => {
|
||||
const locale = options.locale || getCurrentLocale()
|
||||
|
||||
const format = getIntlFormatterOptions('time', options.format)
|
||||
if (format) options = format
|
||||
|
||||
return new Intl.DateTimeFormat(locale, options)
|
||||
})
|
||||
|
||||
export const getMessageFormatter = memoize(
|
||||
(message: string, locale: string) =>
|
||||
new IntlMessageFormat(message, locale, customFormats),
|
||||
new IntlMessageFormat(message, locale, customFormats)
|
||||
)
|
67
src/client/includes/loaderQueue.ts
Normal file
67
src/client/includes/loaderQueue.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import merge from 'deepmerge'
|
||||
|
||||
import { LocaleLoader } from '../types'
|
||||
import { hasLocaleDictionary, $dictionary } from '../stores/dictionary'
|
||||
import { getCurrentLocale } from '../stores/locale'
|
||||
import { $loading } from '../stores/loading'
|
||||
|
||||
import { removeFromLookupCache } from './lookup'
|
||||
|
||||
const loaderQueue: Record<string, Set<LocaleLoader>> = {}
|
||||
|
||||
function getLocaleQueue(locale: string) {
|
||||
return loaderQueue[locale]
|
||||
}
|
||||
|
||||
function createLocaleQueue(locale: string) {
|
||||
loaderQueue[locale] = new Set()
|
||||
}
|
||||
|
||||
function removeLocaleFromQueue(locale: string) {
|
||||
delete loaderQueue[locale]
|
||||
}
|
||||
|
||||
export function addLoaderToQueue(locale: string, loader: LocaleLoader) {
|
||||
loaderQueue[locale].add(loader)
|
||||
}
|
||||
|
||||
export async function flushQueue(locale: string = getCurrentLocale()) {
|
||||
if (!getLocaleQueue(locale)) return
|
||||
|
||||
const queue = [...getLocaleQueue(locale)]
|
||||
|
||||
if (queue.length === 0) return
|
||||
|
||||
removeLocaleFromQueue(locale)
|
||||
$loading.set(true)
|
||||
|
||||
// todo what happens if some loader fails?
|
||||
return Promise.all(queue.map(loader => loader()))
|
||||
.then(partials => {
|
||||
partials = partials.map(partial => partial.default || partial)
|
||||
|
||||
removeFromLookupCache(locale)
|
||||
|
||||
$dictionary.update(d => {
|
||||
d[locale] = merge.all<any>([d[locale] || {}].concat(partials))
|
||||
return d
|
||||
})
|
||||
})
|
||||
.then(() => $loading.set(false))
|
||||
}
|
||||
|
||||
export function registerLocaleLoader(locale: string, loader: LocaleLoader) {
|
||||
if (!getLocaleQueue(locale)) createLocaleQueue(locale)
|
||||
|
||||
const queue = getLocaleQueue(locale)
|
||||
if (queue.has(loader)) return
|
||||
|
||||
if (!hasLocaleDictionary(locale)) {
|
||||
$dictionary.update(d => {
|
||||
d[locale] = {}
|
||||
return d
|
||||
})
|
||||
}
|
||||
|
||||
queue.add(loader)
|
||||
}
|
42
src/client/includes/lookup.ts
Normal file
42
src/client/includes/lookup.ts
Normal file
@ -0,0 +1,42 @@
|
||||
// todo invalidate only keys with null values
|
||||
import resolvePath from 'object-resolve-path'
|
||||
|
||||
import { hasLocaleDictionary } from '../stores/dictionary'
|
||||
|
||||
import { getGenericLocaleFrom } from './utils'
|
||||
|
||||
const lookupCache: Record<string, Record<string, string>> = {}
|
||||
|
||||
const addToCache = (path: string, locale: string, message: string) => {
|
||||
if (!(locale in lookupCache)) lookupCache[locale] = {}
|
||||
if (!(path in lookupCache[locale])) lookupCache[locale][path] = message
|
||||
return message
|
||||
}
|
||||
|
||||
export const removeFromLookupCache = (locale: string) => {
|
||||
delete lookupCache[locale]
|
||||
}
|
||||
|
||||
export const lookupMessage = (
|
||||
dictionary: any,
|
||||
path: string,
|
||||
locale: string
|
||||
): string => {
|
||||
if (locale == null) return null
|
||||
if (locale in lookupCache && path in lookupCache[locale]) {
|
||||
return lookupCache[locale][path]
|
||||
}
|
||||
if (hasLocaleDictionary(locale)) {
|
||||
if (path in dictionary[locale]) {
|
||||
return addToCache(path, locale, dictionary[locale][path])
|
||||
}
|
||||
const message = resolvePath(dictionary[locale], path)
|
||||
if (message) return addToCache(path, locale, message)
|
||||
}
|
||||
|
||||
return addToCache(
|
||||
path,
|
||||
locale,
|
||||
lookupMessage(dictionary, path, getGenericLocaleFrom(locale))
|
||||
)
|
||||
}
|
@ -1,7 +1,18 @@
|
||||
export const capital = (str: string) => str.replace(/(^|\s)\S/, l => l.toLocaleUpperCase())
|
||||
export const title = (str: string) => str.replace(/(^|\s)\S/g, l => l.toLocaleUpperCase())
|
||||
export const upper = (str: string) => str.toLocaleUpperCase()
|
||||
export const lower = (str: string) => str.toLocaleLowerCase()
|
||||
export function capital(str: string) {
|
||||
return str.replace(/(^|\s)\S/, l => l.toLocaleUpperCase())
|
||||
}
|
||||
|
||||
export function title(str: string) {
|
||||
return str.replace(/(^|\s)\S/g, l => l.toLocaleUpperCase())
|
||||
}
|
||||
|
||||
export function upper(str: string) {
|
||||
return str.toLocaleUpperCase()
|
||||
}
|
||||
|
||||
export function lower(str: string) {
|
||||
return str.toLocaleLowerCase()
|
||||
}
|
||||
|
||||
export function getGenericLocaleFrom(locale: string) {
|
||||
const index = locale.lastIndexOf('-')
|
||||
@ -12,6 +23,7 @@ export function getLocalesFrom(locale: string) {
|
||||
return locale.split('-').map((_, i, arr) => arr.slice(0, i + 1).join('-'))
|
||||
}
|
||||
|
||||
// todo add a urlPattern method/regexp
|
||||
export const getClientLocale = ({
|
||||
navigator,
|
||||
hash,
|
@ -1,170 +1,23 @@
|
||||
import { writable, derived } from 'svelte/store'
|
||||
import resolvePath from 'object-resolve-path'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
import {
|
||||
capital,
|
||||
title,
|
||||
upper,
|
||||
lower,
|
||||
getClientLocale,
|
||||
getGenericLocaleFrom,
|
||||
getLocalesFrom,
|
||||
} from './utils'
|
||||
import { MessageObject, Formatter } from './types'
|
||||
import {
|
||||
getMessageFormatter,
|
||||
getDateFormatter,
|
||||
getNumberFormatter,
|
||||
getTimeFormatter,
|
||||
} from './formatters'
|
||||
|
||||
const $loading = writable(false)
|
||||
|
||||
let currentLocale: string
|
||||
let currentDictionary: Record<string, Record<string, any>>
|
||||
const dictQueue: Record<string, (() => Promise<any>)[]> = {}
|
||||
|
||||
const hasLocale = (locale: string) => locale in currentDictionary
|
||||
|
||||
async function registerLocaleLoader(locale: string, loader: any) {
|
||||
if (!(locale in currentDictionary)) {
|
||||
$dictionary.update(d => {
|
||||
d[locale] = {}
|
||||
return d
|
||||
})
|
||||
}
|
||||
if (!(locale in dictQueue)) dictQueue[locale] = []
|
||||
dictQueue[locale].push(loader)
|
||||
}
|
||||
function getAvailableLocale(locale: string): string | null {
|
||||
if (locale in currentDictionary || locale == null) return locale
|
||||
return getAvailableLocale(getGenericLocaleFrom(locale))
|
||||
}
|
||||
|
||||
const lookupCache: Record<string, Record<string, string>> = {}
|
||||
const addToCache = (path: string, locale: string, message: string) => {
|
||||
if (!(locale in lookupCache)) lookupCache[locale] = {}
|
||||
if (!(path in lookupCache[locale])) lookupCache[locale][path] = message
|
||||
return message
|
||||
}
|
||||
const invalidateLookupCache = (locale: string) => {
|
||||
delete lookupCache[locale]
|
||||
}
|
||||
const lookupMessage = (path: string, locale: string): string => {
|
||||
if (locale == null) return null
|
||||
if (locale in lookupCache && path in lookupCache[locale]) {
|
||||
return lookupCache[locale][path]
|
||||
}
|
||||
if (hasLocale(locale)) {
|
||||
if (path in currentDictionary[locale]) {
|
||||
return addToCache(path, locale, currentDictionary[locale][path])
|
||||
}
|
||||
const message = resolvePath(currentDictionary[locale], path)
|
||||
if (message) return addToCache(path, locale, message)
|
||||
}
|
||||
|
||||
return lookupMessage(path, getGenericLocaleFrom(locale))
|
||||
}
|
||||
|
||||
const formatMessage: Formatter = (id, options = {}) => {
|
||||
if (typeof id === 'object') {
|
||||
options = id as MessageObject
|
||||
id = options.id
|
||||
}
|
||||
|
||||
const { values, locale = currentLocale, default: defaultValue } = options
|
||||
|
||||
if (locale == null) {
|
||||
throw new Error(
|
||||
'[svelte-i18n] Cannot format a message without first setting the initial locale.'
|
||||
)
|
||||
}
|
||||
|
||||
const message = lookupMessage(id, locale)
|
||||
|
||||
if (!message) {
|
||||
console.warn(
|
||||
`[svelte-i18n] The message "${id}" was not found in "${getLocalesFrom(locale).join('", "')}".`
|
||||
)
|
||||
return defaultValue || id
|
||||
}
|
||||
|
||||
if (!values) return message
|
||||
return getMessageFormatter(message, locale).format(values)
|
||||
}
|
||||
|
||||
formatMessage.time = (t, options) => getTimeFormatter(currentLocale, options).format(t)
|
||||
formatMessage.date = (d, options) => getDateFormatter(currentLocale, options).format(d)
|
||||
formatMessage.number = (n, options) => getNumberFormatter(currentLocale, 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 $dictionary = writable<Record<string, Record<string, any>>>({})
|
||||
$dictionary.subscribe(newDictionary => (currentDictionary = newDictionary))
|
||||
|
||||
function loadLocale(localeToLoad: string) {
|
||||
return Promise.all(
|
||||
getLocalesFrom(localeToLoad).map(localeItem =>
|
||||
flushLocaleQueue(localeItem)
|
||||
.then(() => [localeItem, { err: undefined }])
|
||||
.catch(e => [localeItem, { err: e }])
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async function flushLocaleQueue(locale: string = currentLocale) {
|
||||
if (!(locale in dictQueue)) return
|
||||
|
||||
$loading.set(true)
|
||||
|
||||
return Promise.all(dictQueue[locale].map((loader: any) => loader()))
|
||||
.then(partials => {
|
||||
dictQueue[locale] = []
|
||||
|
||||
partials = partials.map(partial => partial.default || partial)
|
||||
invalidateLookupCache(locale)
|
||||
$dictionary.update(d => {
|
||||
d[locale] = merge.all<any>([d[locale] || {}].concat(partials))
|
||||
return d
|
||||
})
|
||||
})
|
||||
.then(() => $loading.set(false))
|
||||
}
|
||||
|
||||
const $locale = writable(null)
|
||||
const localeSet = $locale.set
|
||||
$locale.set = (newLocale: string): void | Promise<void> => {
|
||||
const locale = getAvailableLocale(newLocale)
|
||||
if (locale) {
|
||||
return flushLocaleQueue(newLocale).then(() => localeSet(newLocale))
|
||||
}
|
||||
|
||||
throw Error(`[svelte-i18n] Locale "${newLocale}" not found.`)
|
||||
}
|
||||
$locale.update = (fn: (locale: string) => void | Promise<void>) => localeSet(fn(currentLocale))
|
||||
$locale.subscribe((newLocale: string) => (currentLocale = newLocale))
|
||||
|
||||
const $format = derived([$locale, $dictionary], () => formatMessage)
|
||||
const $locales = derived([$dictionary], ([$dictionary]) => Object.keys($dictionary))
|
||||
import { MessageObject } from './types'
|
||||
|
||||
// defineMessages allow us to define and extract dynamic message ids
|
||||
const defineMessages = (i: Record<string, MessageObject>) => i
|
||||
|
||||
export { customFormats, addCustomFormats } from './formatters'
|
||||
export { $locale as locale, loadLocale as preloadLocale } from './stores/locale'
|
||||
export {
|
||||
$loading as loading,
|
||||
$locale as locale,
|
||||
$dictionary as dictionary,
|
||||
$format as _,
|
||||
$format as format,
|
||||
$locales as locales,
|
||||
getClientLocale,
|
||||
defineMessages,
|
||||
loadLocale as preloadLocale,
|
||||
} from './stores/dictionary'
|
||||
export { $loading as loading } from './stores/loading'
|
||||
export { $format as format, $format as _, $format as t } from './stores/format'
|
||||
|
||||
// utilities
|
||||
export { defineMessages, merge }
|
||||
export { customFormats, addCustomFormats } from './includes/formats'
|
||||
export { getClientLocale } from './includes/utils'
|
||||
export {
|
||||
flushQueue as waitLocale,
|
||||
registerLocaleLoader as register,
|
||||
flushLocaleQueue as waitLocale,
|
||||
merge,
|
||||
}
|
||||
} from './includes/loaderQueue'
|
||||
|
1
src/client/modules.d.ts
vendored
1
src/client/modules.d.ts
vendored
@ -1 +0,0 @@
|
||||
declare module 'object-resolve-path'
|
22
src/client/stores/dictionary.ts
Normal file
22
src/client/stores/dictionary.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { writable, derived } from 'svelte/store'
|
||||
|
||||
let dictionary: Record<string, Record<string, any>>
|
||||
|
||||
const $dictionary = writable<typeof dictionary>({})
|
||||
$dictionary.subscribe(newDictionary => {
|
||||
dictionary = newDictionary
|
||||
})
|
||||
|
||||
function getDictionary() {
|
||||
return dictionary
|
||||
}
|
||||
|
||||
function hasLocaleDictionary(locale: string) {
|
||||
return locale in dictionary
|
||||
}
|
||||
|
||||
const $locales = derived([$dictionary], ([$dictionary]) =>
|
||||
Object.keys($dictionary)
|
||||
)
|
||||
|
||||
export { $dictionary, $locales, getDictionary, hasLocaleDictionary }
|
55
src/client/stores/format.ts
Normal file
55
src/client/stores/format.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { derived } from 'svelte/store'
|
||||
|
||||
import { Formatter, MessageObject } from '../types'
|
||||
import { lookupMessage } from '../includes/lookup'
|
||||
import { getLocalesFrom, capital, upper, lower, title } from '../includes/utils'
|
||||
import {
|
||||
getMessageFormatter,
|
||||
getTimeFormatter,
|
||||
getDateFormatter,
|
||||
getNumberFormatter,
|
||||
} from '../includes/formats'
|
||||
|
||||
import { getDictionary, $dictionary } from './dictionary'
|
||||
import { getCurrentLocale, $locale } from './locale'
|
||||
|
||||
const formatMessage: Formatter = (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 = lookupMessage(getDictionary(), id, locale)
|
||||
|
||||
if (!message) {
|
||||
console.warn(
|
||||
`[svelte-i18n] The message "${id}" was not found in "${getLocalesFrom(
|
||||
locale
|
||||
).join('", "')}".`
|
||||
)
|
||||
return defaultValue || id
|
||||
}
|
||||
|
||||
if (!values) return message
|
||||
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 $format = derived([$locale, $dictionary], () => formatMessage)
|
||||
|
||||
export { $format }
|
3
src/client/stores/loading.ts
Normal file
3
src/client/stores/loading.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
export const $loading = writable(false)
|
46
src/client/stores/locale.ts
Normal file
46
src/client/stores/locale.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
import { getGenericLocaleFrom, getLocalesFrom } from '../includes/utils'
|
||||
import { flushQueue } from '../includes/loaderQueue'
|
||||
|
||||
import { getDictionary } from './dictionary'
|
||||
|
||||
let current: string
|
||||
const $locale = writable(null)
|
||||
|
||||
function getCurrentLocale() {
|
||||
return current
|
||||
}
|
||||
|
||||
function getAvailableLocale(locale: string): string | null {
|
||||
if (locale in getDictionary() || locale == null) return locale
|
||||
return getAvailableLocale(getGenericLocaleFrom(locale))
|
||||
}
|
||||
|
||||
function loadLocale(localeToLoad: string) {
|
||||
return Promise.all(
|
||||
getLocalesFrom(localeToLoad).map(localeItem =>
|
||||
flushQueue(localeItem)
|
||||
.then(() => [localeItem, { err: undefined }])
|
||||
.catch(e => [localeItem, { err: e }])
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
$locale.subscribe((newLocale: string) => {
|
||||
current = newLocale
|
||||
})
|
||||
|
||||
const localeSet = $locale.set
|
||||
$locale.set = (newLocale: string): void | Promise<void> => {
|
||||
if (getAvailableLocale(newLocale)) {
|
||||
return flushQueue(newLocale).then(() => localeSet(newLocale))
|
||||
}
|
||||
|
||||
throw Error(`[svelte-i18n] Locale "${newLocale}" not found.`)
|
||||
}
|
||||
|
||||
$locale.update = (fn: (locale: string) => void | Promise<void>) =>
|
||||
localeSet(fn(current))
|
||||
|
||||
export { $locale, loadLocale, flushQueue, getCurrentLocale }
|
@ -16,24 +16,28 @@ type IntlFormatterOptions<T> = T & {
|
||||
}
|
||||
|
||||
export interface MemoizedIntlFormatter<T, U> {
|
||||
(locale: string, options: IntlFormatterOptions<U>): T
|
||||
(options: IntlFormatterOptions<U>): T
|
||||
}
|
||||
|
||||
export interface Formatter extends FormatterFn {
|
||||
time: (
|
||||
d: Date | number,
|
||||
options?: IntlFormatterOptions<Intl.DateTimeFormatOptions>,
|
||||
options?: IntlFormatterOptions<Intl.DateTimeFormatOptions>
|
||||
) => string
|
||||
date: (
|
||||
d: Date | number,
|
||||
options?: IntlFormatterOptions<Intl.DateTimeFormatOptions>,
|
||||
options?: IntlFormatterOptions<Intl.DateTimeFormatOptions>
|
||||
) => string
|
||||
number: (
|
||||
d: number,
|
||||
options?: IntlFormatterOptions<Intl.NumberFormatOptions>,
|
||||
options?: IntlFormatterOptions<Intl.NumberFormatOptions>
|
||||
) => string
|
||||
capital: FormatterFn
|
||||
title: FormatterFn
|
||||
upper: FormatterFn
|
||||
lower: FormatterFn
|
||||
}
|
||||
|
||||
export interface LocaleLoader {
|
||||
(): Promise<any>
|
||||
}
|
2
src/client/types/modules.d.ts
vendored
Normal file
2
src/client/types/modules.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
declare module 'object-resolve-path'
|
||||
declare module 'nano-memoize'
|
@ -1,3 +1,4 @@
|
||||
// todo this is a mess
|
||||
import { Formatter } from '../../src/client/types'
|
||||
import {
|
||||
dictionary,
|
||||
@ -7,8 +8,8 @@ import {
|
||||
addCustomFormats,
|
||||
customFormats,
|
||||
preloadLocale,
|
||||
registerLocaleLoader,
|
||||
flushLocaleQueue,
|
||||
register,
|
||||
waitLocale,
|
||||
} from '../../src/client'
|
||||
|
||||
global.Intl = require('intl')
|
||||
@ -20,10 +21,10 @@ const dict = {
|
||||
en: require('../fixtures/en.json'),
|
||||
}
|
||||
|
||||
registerLocaleLoader('en-GB', () => import('../fixtures/en-GB.json'))
|
||||
registerLocaleLoader('pt', () => import('../fixtures/pt.json'))
|
||||
registerLocaleLoader('pt-BR', () => import('../fixtures/pt-BR.json'))
|
||||
registerLocaleLoader('pt-PT', () => import('../fixtures/pt-PT.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
|
||||
@ -34,11 +35,11 @@ locale.subscribe((l: string) => {
|
||||
})
|
||||
|
||||
describe('locale', () => {
|
||||
it('should change locale', () => {
|
||||
locale.set('en')
|
||||
it('should change locale', async () => {
|
||||
await locale.set('en')
|
||||
expect(currentLocale).toBe('en')
|
||||
|
||||
locale.set('en-US')
|
||||
await locale.set('en-US')
|
||||
expect(currentLocale).toBe('en-US')
|
||||
})
|
||||
|
||||
@ -48,16 +49,6 @@ describe('locale', () => {
|
||||
})
|
||||
|
||||
describe('dictionary', () => {
|
||||
// todo test this better
|
||||
it('allows to dynamically import a dictionary', async () => {
|
||||
dictionary.update((dict: any) => {
|
||||
dict.es = () => import('../fixtures/es.json')
|
||||
return dict
|
||||
})
|
||||
await locale.set('es')
|
||||
expect(currentLocale).toBe('es')
|
||||
})
|
||||
|
||||
it('load a locale and its derived locales if dictionary is a loader', async () => {
|
||||
const loaded = await preloadLocale('pt-PT')
|
||||
expect(loaded[0][0]).toEqual('pt')
|
||||
@ -65,28 +56,28 @@ describe('dictionary', () => {
|
||||
})
|
||||
|
||||
it('load a partial dictionary and merge it with the existing one', async () => {
|
||||
locale.set('en')
|
||||
registerLocaleLoader('en', () => import('../fixtures/partials/en.json'))
|
||||
await locale.set('en')
|
||||
register('en', () => import('../fixtures/partials/en.json'))
|
||||
expect(_('page.title_about')).toBe('page.title_about')
|
||||
|
||||
await flushLocaleQueue('en')
|
||||
await waitLocale('en')
|
||||
expect(_('page.title_about')).toBe('About')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatting', () => {
|
||||
it('should translate to current locale', () => {
|
||||
locale.set('en')
|
||||
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', () => {
|
||||
locale.set('en')
|
||||
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', () => {
|
||||
locale.set('en')
|
||||
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')
|
||||
})
|
||||
|
||||
@ -95,23 +86,25 @@ describe('formatting', () => {
|
||||
expect(_('sneakers', { locale: 'en-GB' })).toBe('trainers')
|
||||
})
|
||||
|
||||
it('should fallback to generic locale XX if id not found in XX-YY', () => {
|
||||
locale.set('en-GB')
|
||||
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', () => {
|
||||
locale.set('en')
|
||||
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', () => {
|
||||
expect(_('switch.lang', { locale: 'en' })).toBe('Switch language')
|
||||
expect(_('switch.lang', { locale: 'pt' })).toBe('Trocar idioma')
|
||||
})
|
||||
|
||||
it('should interpolate message with variables', () => {
|
||||
locale.set('en')
|
||||
expect(_('greeting.message', { values: { name: 'Chris' } })).toBe('Hello Chris, how are you?')
|
||||
it('should interpolate message with variables', async () => {
|
||||
await locale.set('en')
|
||||
expect(_('greeting.message', { values: { name: 'Chris' } })).toBe(
|
||||
'Hello Chris, how are you?'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -138,7 +131,9 @@ describe('utilities', () => {
|
||||
})
|
||||
|
||||
it('should get the locale based on the navigator language', () => {
|
||||
expect(getClientLocale({ navigator: true })).toBe(window.navigator.language)
|
||||
expect(getClientLocale({ navigator: true })).toBe(
|
||||
window.navigator.language
|
||||
)
|
||||
})
|
||||
|
||||
it('should get the fallback locale', () => {
|
||||
@ -148,8 +143,8 @@ describe('utilities', () => {
|
||||
})
|
||||
|
||||
describe('format utils', () => {
|
||||
beforeAll(() => {
|
||||
locale.set('en')
|
||||
beforeAll(async () => {
|
||||
await locale.set('en')
|
||||
})
|
||||
|
||||
it('should capital a translated message', () => {
|
||||
@ -169,11 +164,13 @@ describe('utilities', () => {
|
||||
})
|
||||
|
||||
const date = new Date(2019, 3, 24, 23, 45)
|
||||
it('should format a time value', () => {
|
||||
locale.set('en')
|
||||
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')
|
||||
expect(_.time(date, { format: 'medium', locale: 'pt-BR' })).toBe(
|
||||
'23:45:00'
|
||||
)
|
||||
})
|
||||
|
||||
it('should format a date value', () => {
|
||||
@ -188,8 +185,8 @@ describe('utilities', () => {
|
||||
})
|
||||
|
||||
describe('custom formats', () => {
|
||||
beforeAll(() => {
|
||||
locale.set('pt-BR')
|
||||
beforeAll(async () => {
|
||||
await locale.set('pt-BR')
|
||||
})
|
||||
|
||||
it('should have default number custom formats', () => {
|
||||
@ -213,7 +210,7 @@ describe('custom formats', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should format messages with custom formats', () => {
|
||||
it('should format messages with custom formats', async () => {
|
||||
addCustomFormats({
|
||||
number: {
|
||||
usd: { style: 'currency', currency: 'USD' },
|
||||
@ -227,12 +224,16 @@ describe('custom formats', () => {
|
||||
},
|
||||
})
|
||||
|
||||
locale.set('en-US')
|
||||
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(_.date(new Date(2019, 0, 1), { format: 'customDate' })).toEqual(
|
||||
'2019 AD'
|
||||
)
|
||||
|
||||
expect(_.time(new Date(2019, 0, 1, 2, 0, 0), { format: 'customTime' })).toEqual('Jan, 02')
|
||||
expect(
|
||||
_.time(new Date(2019, 0, 1, 2, 0, 0), { format: 'customTime' })
|
||||
).toEqual('Jan, 02')
|
||||
})
|
||||
})
|
||||
|
15
yarn.lock
15
yarn.lock
@ -2104,6 +2104,11 @@ fast-levenshtein@~2.0.6:
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
||||
|
||||
fast-memoize@^2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.1.tgz#c3519241e80552ce395e1a32dcdde8d1fd680f5d"
|
||||
integrity sha512-xdmw296PCL01tMOXx9mdJSmWY29jQgxyuZdq0rEHMu+Tpe1eOEtCycoG6chzlcrWsNgpZP7oL8RiQr7+G6Bl6g==
|
||||
|
||||
fb-watchman@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
|
||||
@ -3453,11 +3458,6 @@ merge-stream@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
||||
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
|
||||
|
||||
micro-memoize@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/micro-memoize/-/micro-memoize-4.0.8.tgz#b2bd9fb57817fe5dc1eb1010b37b3f7695aef9a8"
|
||||
integrity sha512-Mzlo15iWNrP5EwokGjx0Wlh2b3aMjTPdpsD+ryQtkYJBD67IxBddWU2fO3MIXRtXDH8NsuhaotTrtDbfb+k6jw==
|
||||
|
||||
micromatch@^3.1.10, micromatch@^3.1.4:
|
||||
version "3.1.10"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
|
||||
@ -3566,6 +3566,11 @@ nan@^2.12.1:
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
|
||||
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
|
||||
|
||||
nano-memoize@^1.1.7:
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/nano-memoize/-/nano-memoize-1.1.7.tgz#e7850c73b95916b637224c4664a7bff18554c6ad"
|
||||
integrity sha512-zUDNakN4iXDPerGRjnxkZmhhjMzuQipwMDuIPSrRNu+4lpiAt5pTxE5qoAXzMNv+11vYFfTqWRVEkCQ9hdSSZg==
|
||||
|
||||
nanomatch@^1.2.9:
|
||||
version "1.2.13"
|
||||
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
||||
|
Loading…
Reference in New Issue
Block a user