diff --git a/.prettierrc b/.prettierrc
index beaab0d..9b4e1ca 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,6 +1,6 @@
{
"semi": false,
- "printWidth": 100,
+ "printWidth": 80,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false,
diff --git a/example/rollup.config.js b/example/rollup.config.js
index e977918..2ee80ca 100644
--- a/example/rollup.config.js
+++ b/example/rollup.config.js
@@ -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,
},
diff --git a/example/src/server.js b/example/src/server.js
index 86f7466..056b74d 100644
--- a/example/src/server.js
+++ b/example/src/server.js
@@ -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)
diff --git a/example/src/service-worker.js b/example/src/service-worker.js
index 2289a55..1578336 100644
--- a/example/src/service-worker.js
+++ b/example/src/service-worker.js
@@ -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
+ }
+ })
+ )
+})
diff --git a/example/yarn.lock b/example/yarn.lock
index 54b36bf..32f0a46 100644
--- a/example/yarn.lock
+++ b/example/yarn.lock
@@ -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" "*"
diff --git a/package.json b/package.json
index 73a308a..f29ec80 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
diff --git a/src/cli/extract.ts b/src/cli/extract.ts
index 331c12f..dde5868 100644
--- a/src/cli/extract.ts
+++ b/src/cli/extract.ts
@@ -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
diff --git a/src/cli/index.ts b/src/cli/index.ts
index 58c40e9..70133f1 100644
--- a/src/cli/index.ts
+++ b/src/cli/index.ts
@@ -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
',
'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')
diff --git a/src/client/formatters.ts b/src/client/includes/formats.ts
similarity index 70%
rename from src/client/formatters.ts
rename to src/client/includes/formats.ts
index 791385b..93029ba 100644
--- a/src/client/formatters.ts
+++ b/src/client/includes/formats.ts
@@ -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) {
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)
)
diff --git a/src/client/includes/loaderQueue.ts b/src/client/includes/loaderQueue.ts
new file mode 100644
index 0000000..3d7df88
--- /dev/null
+++ b/src/client/includes/loaderQueue.ts
@@ -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> = {}
+
+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([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)
+}
diff --git a/src/client/includes/lookup.ts b/src/client/includes/lookup.ts
new file mode 100644
index 0000000..47b1c5a
--- /dev/null
+++ b/src/client/includes/lookup.ts
@@ -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> = {}
+
+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))
+ )
+}
diff --git a/src/client/utils.ts b/src/client/includes/utils.ts
similarity index 74%
rename from src/client/utils.ts
rename to src/client/includes/utils.ts
index 126f8e6..48b7d17 100644
--- a/src/client/utils.ts
+++ b/src/client/includes/utils.ts
@@ -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,
diff --git a/src/client/index.ts b/src/client/index.ts
index 7d8ee25..0888603 100644
--- a/src/client/index.ts
+++ b/src/client/index.ts
@@ -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>
-const dictQueue: Record Promise)[]> = {}
-
-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> = {}
-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>>({})
-$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([d[locale] || {}].concat(partials))
- return d
- })
- })
- .then(() => $loading.set(false))
-}
-
-const $locale = writable(null)
-const localeSet = $locale.set
-$locale.set = (newLocale: string): void | Promise => {
- 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) => 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) => 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'
diff --git a/src/client/modules.d.ts b/src/client/modules.d.ts
deleted file mode 100644
index 1c95bf8..0000000
--- a/src/client/modules.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module 'object-resolve-path'
\ No newline at end of file
diff --git a/src/client/stores/dictionary.ts b/src/client/stores/dictionary.ts
new file mode 100644
index 0000000..c6aef6a
--- /dev/null
+++ b/src/client/stores/dictionary.ts
@@ -0,0 +1,22 @@
+import { writable, derived } from 'svelte/store'
+
+let dictionary: Record>
+
+const $dictionary = writable({})
+$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 }
diff --git a/src/client/stores/format.ts b/src/client/stores/format.ts
new file mode 100644
index 0000000..3ae5c60
--- /dev/null
+++ b/src/client/stores/format.ts
@@ -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 }
diff --git a/src/client/stores/loading.ts b/src/client/stores/loading.ts
new file mode 100644
index 0000000..41afb6a
--- /dev/null
+++ b/src/client/stores/loading.ts
@@ -0,0 +1,3 @@
+import { writable } from 'svelte/store'
+
+export const $loading = writable(false)
diff --git a/src/client/stores/locale.ts b/src/client/stores/locale.ts
new file mode 100644
index 0000000..800ee9b
--- /dev/null
+++ b/src/client/stores/locale.ts
@@ -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 => {
+ if (getAvailableLocale(newLocale)) {
+ return flushQueue(newLocale).then(() => localeSet(newLocale))
+ }
+
+ throw Error(`[svelte-i18n] Locale "${newLocale}" not found.`)
+}
+
+$locale.update = (fn: (locale: string) => void | Promise) =>
+ localeSet(fn(current))
+
+export { $locale, loadLocale, flushQueue, getCurrentLocale }
diff --git a/src/client/types.ts b/src/client/types/index.ts
similarity index 69%
rename from src/client/types.ts
rename to src/client/types/index.ts
index 92799bb..5e2d787 100644
--- a/src/client/types.ts
+++ b/src/client/types/index.ts
@@ -16,24 +16,28 @@ type IntlFormatterOptions = T & {
}
export interface MemoizedIntlFormatter {
- (locale: string, options: IntlFormatterOptions): T
+ (options: IntlFormatterOptions): T
}
export interface Formatter extends FormatterFn {
time: (
d: Date | number,
- options?: IntlFormatterOptions,
+ options?: IntlFormatterOptions
) => string
date: (
d: Date | number,
- options?: IntlFormatterOptions,
+ options?: IntlFormatterOptions
) => string
number: (
d: number,
- options?: IntlFormatterOptions,
+ options?: IntlFormatterOptions
) => string
capital: FormatterFn
title: FormatterFn
upper: FormatterFn
lower: FormatterFn
}
+
+export interface LocaleLoader {
+ (): Promise
+}
diff --git a/src/client/types/modules.d.ts b/src/client/types/modules.d.ts
new file mode 100644
index 0000000..e9a9a3a
--- /dev/null
+++ b/src/client/types/modules.d.ts
@@ -0,0 +1,2 @@
+declare module 'object-resolve-path'
+declare module 'nano-memoize'
diff --git a/test/client/index.test.ts b/test/client/index.test.ts
index cbbec40..dc1800c 100644
--- a/test/client/index.test.ts
+++ b/test/client/index.test.ts
@@ -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')
})
})
diff --git a/yarn.lock b/yarn.lock
index 779fad2..219791f 100644
--- a/yarn.lock
+++ b/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"