chore: 🤖 update linter deps and format project
This commit is contained in:
parent
68e8c51a63
commit
e659889f43
|
@ -1,13 +1,10 @@
|
|||
{
|
||||
"extends": ["kaisermann/typescript"],
|
||||
"extends": ["@kiwi"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"jest": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/camelcase": "off"
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1 @@
|
|||
{
|
||||
"semi": false,
|
||||
"printWidth": 80,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
"@kiwi/prettier-config"
|
45
package.json
45
package.json
|
@ -30,7 +30,7 @@
|
|||
"test:ci": "jest --silent",
|
||||
"test:watch": "jest --verbose --watchAll",
|
||||
"lint": "eslint \"src/**/*.ts\"",
|
||||
"format": "prettier --loglevel silent --write \"src/**/*.ts\" && eslint --fix \"src/**/*.ts\"",
|
||||
"format": "prettier --loglevel silent --write \"src/**/*.ts\"",
|
||||
"release": " git add package.json && git commit -m \"chore(release): v$npm_package_version :tada:\"",
|
||||
"pretest": "npm run build",
|
||||
"prebuild": "yarn clean",
|
||||
|
@ -70,34 +70,35 @@
|
|||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.14.1"
|
||||
"svelte": "^3.25.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"@types/estree": "0.0.39",
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@kiwi/eslint-config": "^1.2.0",
|
||||
"@kiwi/prettier-config": "^1.1.0",
|
||||
"@types/estree": "0.0.45",
|
||||
"@types/intl": "^1.2.0",
|
||||
"@types/jest": "^24.0.23",
|
||||
"@types/jest": "^26.0.14",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^24.9.0",
|
||||
"conventional-changelog-cli": "^2.0.28",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-kaisermann": "0.0.3",
|
||||
"husky": "^4.2.1",
|
||||
"jest": "^24.9.0",
|
||||
"lint-staged": "^10.0.4",
|
||||
"babel-jest": "^26.3.0",
|
||||
"conventional-changelog-cli": "^2.1.0",
|
||||
"eslint": "^7.9.0",
|
||||
"husky": "^4.3.0",
|
||||
"jest": "^26.4.2",
|
||||
"lint-staged": "^10.4.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^1.19.1",
|
||||
"rollup": "^1.26.5",
|
||||
"prettier": "^2.1.2",
|
||||
"rollup": "^2.27.1",
|
||||
"rollup-plugin-auto-external": "^2.0.0",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-terser": "^5.1.3",
|
||||
"rollup-plugin-typescript2": "^0.25.2",
|
||||
"sass": "^1.23.6",
|
||||
"svelte": "^3.14.1",
|
||||
"svelte-preprocess": "^3.2.6",
|
||||
"ts-jest": "^24.1.0",
|
||||
"typescript": "^3.7.2"
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-typescript2": "^0.27.2",
|
||||
"sass": "^1.26.11",
|
||||
"svelte": "^3.25.1",
|
||||
"svelte-preprocess": "^4.3.0",
|
||||
"ts-jest": "^26.4.0",
|
||||
"typescript": "^4.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^4.0.1",
|
||||
|
|
|
@ -6,179 +6,199 @@ import {
|
|||
CallExpression,
|
||||
Identifier,
|
||||
Literal,
|
||||
} from 'estree'
|
||||
import { walk } from 'estree-walker'
|
||||
import { Ast } from 'svelte/types/compiler/interfaces'
|
||||
import { parse } from 'svelte/compiler'
|
||||
} from 'estree';
|
||||
import { walk } from 'estree-walker';
|
||||
import { Ast } from 'svelte/types/compiler/interfaces';
|
||||
import { parse } from 'svelte/compiler';
|
||||
|
||||
import { deepGet } from './includes/deepGet'
|
||||
import { deepSet } from './includes/deepSet'
|
||||
import { getObjFromExpression } from './includes/getObjFromExpression'
|
||||
import { Message } from './types'
|
||||
import { deepGet } from './includes/deepGet';
|
||||
import { deepSet } from './includes/deepSet';
|
||||
import { getObjFromExpression } from './includes/getObjFromExpression';
|
||||
import { Message } from './types';
|
||||
|
||||
const LIB_NAME = 'svelte-i18n'
|
||||
const DEFINE_MESSAGES_METHOD_NAME = 'defineMessages'
|
||||
const FORMAT_METHOD_NAMES = new Set(['format', '_', 't'])
|
||||
const IGNORED_UTILITIES = new Set(['number', 'date', 'time'])
|
||||
const LIB_NAME = 'svelte-i18n';
|
||||
const DEFINE_MESSAGES_METHOD_NAME = 'defineMessages';
|
||||
const FORMAT_METHOD_NAMES = new Set(['format', '_', 't']);
|
||||
const IGNORED_UTILITIES = new Set(['number', 'date', 'time']);
|
||||
|
||||
function isFormatCall(node: Node, imports: Set<string>) {
|
||||
if (node.type !== 'CallExpression') return false
|
||||
if (node.type !== 'CallExpression') return false;
|
||||
|
||||
let identifier: Identifier;
|
||||
|
||||
let identifier: Identifier
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.callee.property.type === 'Identifier' &&
|
||||
!IGNORED_UTILITIES.has(node.callee.property.name)
|
||||
) {
|
||||
identifier = node.callee.object as Identifier
|
||||
identifier = node.callee.object as Identifier;
|
||||
} else if (node.callee.type === 'Identifier') {
|
||||
identifier = node.callee
|
||||
}
|
||||
if (!identifier || identifier.type !== 'Identifier') {
|
||||
return false
|
||||
identifier = node.callee;
|
||||
}
|
||||
|
||||
const methodName = identifier.name.slice(1)
|
||||
return imports.has(methodName)
|
||||
if (!identifier || identifier.type !== 'Identifier') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const methodName = identifier.name.slice(1);
|
||||
|
||||
return imports.has(methodName);
|
||||
}
|
||||
|
||||
function isMessagesDefinitionCall(node: Node, methodName: string) {
|
||||
if (node.type !== 'CallExpression') return false
|
||||
if (node.type !== 'CallExpression') return false;
|
||||
|
||||
return (
|
||||
node.callee &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === methodName
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function getLibImportDeclarations(ast: Ast) {
|
||||
return (ast.instance
|
||||
? ast.instance.content.body.filter(
|
||||
node =>
|
||||
node.type === 'ImportDeclaration' && node.source.value === LIB_NAME
|
||||
(node) =>
|
||||
node.type === 'ImportDeclaration' && node.source.value === LIB_NAME,
|
||||
)
|
||||
: []) as ImportDeclaration[]
|
||||
: []) as ImportDeclaration[];
|
||||
}
|
||||
|
||||
function getDefineMessagesSpecifier(decl: ImportDeclaration) {
|
||||
return decl.specifiers.find(
|
||||
spec =>
|
||||
'imported' in spec && spec.imported.name === DEFINE_MESSAGES_METHOD_NAME
|
||||
) as ImportSpecifier
|
||||
(spec) =>
|
||||
'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)
|
||||
) as ImportSpecifier[]
|
||||
(spec) => 'imported' in spec && FORMAT_METHOD_NAMES.has(spec.imported.name),
|
||||
) as ImportSpecifier[];
|
||||
}
|
||||
|
||||
export function collectFormatCalls(ast: Ast) {
|
||||
const importDecls = getLibImportDeclarations(ast)
|
||||
const importDecls = getLibImportDeclarations(ast);
|
||||
|
||||
if (importDecls.length === 0) return []
|
||||
if (importDecls.length === 0) return [];
|
||||
|
||||
const imports = new Set(
|
||||
importDecls.flatMap(decl =>
|
||||
getFormatSpecifiers(decl).map(n => n.local.name)
|
||||
)
|
||||
)
|
||||
importDecls.flatMap((decl) =>
|
||||
getFormatSpecifiers(decl).map((n) => n.local.name),
|
||||
),
|
||||
);
|
||||
|
||||
if (imports.size === 0) return []
|
||||
if (imports.size === 0) return [];
|
||||
|
||||
const calls: CallExpression[] = [];
|
||||
|
||||
const calls: CallExpression[] = []
|
||||
function enter(node: Node) {
|
||||
if (isFormatCall(node, imports)) {
|
||||
calls.push(node as CallExpression)
|
||||
this.skip()
|
||||
calls.push(node as CallExpression);
|
||||
this.skip();
|
||||
}
|
||||
}
|
||||
walk(ast.instance as any, { enter })
|
||||
walk(ast.html as any, { enter })
|
||||
|
||||
return calls
|
||||
walk(ast.instance as any, { enter });
|
||||
walk(ast.html as any, { enter });
|
||||
|
||||
return calls;
|
||||
}
|
||||
|
||||
export function collectMessageDefinitions(ast: Ast) {
|
||||
const definitions: ObjectExpression[] = []
|
||||
const definitions: ObjectExpression[] = [];
|
||||
const defineImportDecl = getLibImportDeclarations(ast).find(
|
||||
getDefineMessagesSpecifier
|
||||
)
|
||||
getDefineMessagesSpecifier,
|
||||
);
|
||||
|
||||
if (defineImportDecl == null) return []
|
||||
if (defineImportDecl == null) return [];
|
||||
|
||||
const defineMethodName = getDefineMessagesSpecifier(defineImportDecl).local
|
||||
.name
|
||||
.name;
|
||||
|
||||
walk(ast.instance as any, {
|
||||
enter(node: Node) {
|
||||
if (isMessagesDefinitionCall(node, defineMethodName) === false) return
|
||||
const [arg] = (node as CallExpression).arguments
|
||||
if (isMessagesDefinitionCall(node, defineMethodName) === false) return;
|
||||
const [arg] = (node as CallExpression).arguments;
|
||||
|
||||
if (arg.type === 'ObjectExpression') {
|
||||
definitions.push(arg)
|
||||
this.skip()
|
||||
definitions.push(arg);
|
||||
this.skip();
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return definitions.flatMap(definitionDict =>
|
||||
definitionDict.properties.map(
|
||||
propNode => propNode.value as ObjectExpression
|
||||
)
|
||||
)
|
||||
return definitions.flatMap((definitionDict) =>
|
||||
definitionDict.properties.map((propNode) => {
|
||||
if (propNode.type !== 'Property') {
|
||||
throw new Error(
|
||||
`Found invalid '${propNode.type}' at L${propNode.loc.start.line}:${propNode.loc.start.column}`,
|
||||
);
|
||||
}
|
||||
|
||||
return propNode.value as ObjectExpression;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function collectMessages(markup: string): Message[] {
|
||||
const ast = parse(markup)
|
||||
const calls = collectFormatCalls(ast)
|
||||
const definitions = collectMessageDefinitions(ast)
|
||||
const ast = parse(markup);
|
||||
const calls = collectFormatCalls(ast);
|
||||
const definitions = collectMessageDefinitions(ast);
|
||||
|
||||
return [
|
||||
...definitions.map(definition => getObjFromExpression(definition)),
|
||||
...calls.map(call => {
|
||||
const [pathNode, options] = call.arguments
|
||||
...definitions.map((definition) => getObjFromExpression(definition)),
|
||||
...calls.map((call) => {
|
||||
const [pathNode, options] = call.arguments;
|
||||
|
||||
if (pathNode.type === 'ObjectExpression') {
|
||||
return getObjFromExpression(pathNode)
|
||||
return getObjFromExpression(pathNode);
|
||||
}
|
||||
|
||||
const node = pathNode as Literal
|
||||
const id = node.value as string
|
||||
const node = pathNode as Literal;
|
||||
const id = node.value as string;
|
||||
|
||||
if (options && options.type === 'ObjectExpression') {
|
||||
const messageObj = getObjFromExpression(options)
|
||||
messageObj.meta.id = id
|
||||
return messageObj
|
||||
const messageObj = getObjFromExpression(options);
|
||||
|
||||
messageObj.meta.id = id;
|
||||
|
||||
return messageObj;
|
||||
}
|
||||
|
||||
return { node, meta: { id } }
|
||||
return { node, meta: { id } };
|
||||
}),
|
||||
].filter(Boolean)
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
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
|
||||
collectMessages(markup).forEach((message) => {
|
||||
let defaultValue = message.meta.default;
|
||||
|
||||
if (typeof defaultValue === 'undefined') {
|
||||
defaultValue = ''
|
||||
defaultValue = '';
|
||||
}
|
||||
|
||||
if (shallow) {
|
||||
if (overwrite === false && message.meta.id in accumulator) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
accumulator[message.meta.id] = defaultValue
|
||||
|
||||
accumulator[message.meta.id] = defaultValue;
|
||||
} else {
|
||||
if (
|
||||
overwrite === false &&
|
||||
typeof deepGet(accumulator, message.meta.id) !== 'undefined'
|
||||
) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
deepSet(accumulator, message.meta.id, defaultValue)
|
||||
|
||||
deepSet(accumulator, message.meta.id, defaultValue);
|
||||
}
|
||||
})
|
||||
return accumulator
|
||||
});
|
||||
|
||||
return accumulator;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
export const deepGet = (o: Record<string, any>, id: string) => {
|
||||
return id.split('.').reduce((acc, path) => {
|
||||
if (typeof acc !== 'object') {
|
||||
return acc
|
||||
return acc;
|
||||
}
|
||||
return acc[path]
|
||||
}, o)
|
||||
}
|
||||
|
||||
return acc[path];
|
||||
}, o);
|
||||
};
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
const isNumberString = (n: string) => !Number.isNaN(parseInt(n))
|
||||
/* eslint-disable no-multi-assign */
|
||||
/* eslint-disable no-return-assign */
|
||||
const isNumberString = (n: string) => !Number.isNaN(parseInt(n, 10));
|
||||
|
||||
export function deepSet(obj: any, path: string, value: any) {
|
||||
const parts = path.replace(/\[(\w+)\]/gi, '.$1').split('.')
|
||||
const parts = path.replace(/\[(\w+)\]/gi, '.$1').split('.');
|
||||
|
||||
return parts.reduce((ref, part, i) => {
|
||||
if (part in ref) return (ref = ref[part])
|
||||
if (part in ref) return (ref = ref[part]);
|
||||
|
||||
if (i < parts.length - 1) {
|
||||
if (isNumberString(parts[i + 1])) {
|
||||
return (ref = ref[part] = [])
|
||||
return (ref = ref[part] = []);
|
||||
}
|
||||
return (ref = ref[part] = {})
|
||||
|
||||
return (ref = ref[part] = {});
|
||||
}
|
||||
|
||||
return (ref[part] = value)
|
||||
}, obj)
|
||||
return (ref[part] = value);
|
||||
}, obj);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ObjectExpression, Property, Identifier } from 'estree'
|
||||
import { ObjectExpression, Property, Identifier } from 'estree';
|
||||
|
||||
import { Message } from '../types'
|
||||
import { Message } from '../types';
|
||||
|
||||
export function getObjFromExpression(exprNode: ObjectExpression) {
|
||||
return exprNode.properties.reduce<Message>(
|
||||
|
@ -10,11 +10,13 @@ export function getObjFromExpression(exprNode: ObjectExpression) {
|
|||
prop.value.type === 'Literal' &&
|
||||
prop.value.value !== Object(prop.value.value)
|
||||
) {
|
||||
const key = (prop.key as Identifier).name as string
|
||||
acc.meta[key] = prop.value.value
|
||||
const key = (prop.key as Identifier).name as string;
|
||||
|
||||
acc.meta[key] = prop.value.value;
|
||||
}
|
||||
return acc
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ node: exprNode, meta: {} }
|
||||
)
|
||||
{ node: exprNode, meta: {} },
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import fs from 'fs'
|
||||
import { dirname, resolve } from 'path'
|
||||
import fs from 'fs';
|
||||
import { dirname, resolve } from 'path';
|
||||
|
||||
import program from 'commander'
|
||||
import glob from 'tiny-glob'
|
||||
import { preprocess } from 'svelte/compiler'
|
||||
import program from 'commander';
|
||||
import glob from 'tiny-glob';
|
||||
import { preprocess } from 'svelte/compiler';
|
||||
|
||||
import { extractMessages } from './extract'
|
||||
import { extractMessages } from './extract';
|
||||
|
||||
const { readFile, writeFile, mkdir, access } = fs.promises
|
||||
const { readFile, writeFile, mkdir, access } = fs.promises;
|
||||
|
||||
const fileExists = (path: string) =>
|
||||
access(path)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
.catch(() => false);
|
||||
|
||||
program
|
||||
.command('extract <glob> [output]')
|
||||
|
@ -20,54 +20,59 @@ 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)
|
||||
)
|
||||
const filesToExtract = (await glob(globStr)).filter((file) =>
|
||||
file.match(/\.html|svelte$/i),
|
||||
);
|
||||
|
||||
const svelteConfig = await import(
|
||||
resolve(config, 'svelte.config.js')
|
||||
).catch(() => null)
|
||||
).catch(() => null);
|
||||
|
||||
let accumulator = {};
|
||||
|
||||
let accumulator = {}
|
||||
if (output != null && overwrite === false && (await fileExists(output))) {
|
||||
accumulator = await readFile(output)
|
||||
.then(file => JSON.parse(file.toString()))
|
||||
.then((file) => JSON.parse(file.toString()))
|
||||
.catch((e: Error) => {
|
||||
console.warn(e)
|
||||
accumulator = {}
|
||||
})
|
||||
console.warn(e);
|
||||
accumulator = {};
|
||||
});
|
||||
}
|
||||
|
||||
for await (const filePath of filesToExtract) {
|
||||
const buffer = await readFile(filePath)
|
||||
let content = buffer.toString()
|
||||
const buffer = await readFile(filePath);
|
||||
let content = buffer.toString();
|
||||
|
||||
if (svelteConfig && svelteConfig.preprocess) {
|
||||
if (svelteConfig?.preprocess) {
|
||||
const processed = await preprocess(content, svelteConfig.preprocess, {
|
||||
filename: filePath,
|
||||
})
|
||||
content = processed.code
|
||||
});
|
||||
|
||||
content = processed.code;
|
||||
}
|
||||
extractMessages(content, { accumulator, shallow })
|
||||
|
||||
extractMessages(content, { filePath, accumulator, shallow });
|
||||
}
|
||||
|
||||
const jsonDictionary = JSON.stringify(accumulator, null, ' ')
|
||||
if (output == null) return console.log(jsonDictionary)
|
||||
const jsonDictionary = JSON.stringify(accumulator, null, ' ');
|
||||
|
||||
await mkdir(dirname(output), { recursive: true })
|
||||
await writeFile(output, jsonDictionary)
|
||||
})
|
||||
if (output == null) return console.log(jsonDictionary);
|
||||
|
||||
program.parse(process.argv)
|
||||
await mkdir(dirname(output), { recursive: true });
|
||||
await writeFile(output, jsonDictionary);
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Node } from 'estree'
|
||||
import { Node } from 'estree';
|
||||
|
||||
export interface Message {
|
||||
node: Node
|
||||
node: Node;
|
||||
meta: {
|
||||
id?: string
|
||||
default?: string
|
||||
[key: string]: any
|
||||
}
|
||||
id?: string;
|
||||
default?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ConfigureOptions } from './types'
|
||||
import { $locale } from './stores/locale'
|
||||
import { ConfigureOptions } from './types';
|
||||
import { $locale } from './stores/locale';
|
||||
|
||||
interface Formats {
|
||||
number: Record<string, any>;
|
||||
|
@ -36,39 +36,41 @@ export const defaultFormats: Formats = {
|
|||
timeZoneName: 'short',
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export const defaultOptions: Options = {
|
||||
export const defaultOptions: ConfigureOptions = {
|
||||
fallbackLocale: null,
|
||||
initialLocale: null,
|
||||
loadingDelay: 200,
|
||||
formats: defaultFormats,
|
||||
warnOnMissingMessages: true,
|
||||
}
|
||||
};
|
||||
|
||||
const options: Options = defaultOptions
|
||||
const options: ConfigureOptions = defaultOptions;
|
||||
|
||||
export function getOptions() {
|
||||
return options
|
||||
return options;
|
||||
}
|
||||
|
||||
export function init(opts: ConfigureOptions) {
|
||||
const { formats, ...rest } = opts
|
||||
const initialLocale = opts.initialLocale || opts.fallbackLocale
|
||||
const { formats, ...rest } = opts;
|
||||
const initialLocale = opts.initialLocale || opts.fallbackLocale;
|
||||
|
||||
Object.assign(options, rest, { initialLocale })
|
||||
Object.assign(options, rest, { initialLocale });
|
||||
|
||||
if (formats) {
|
||||
if ('number' in formats) {
|
||||
Object.assign(options.formats.number, formats.number)
|
||||
Object.assign(options.formats.number, formats.number);
|
||||
}
|
||||
|
||||
if ('date' in formats) {
|
||||
Object.assign(options.formats.date, formats.date)
|
||||
Object.assign(options.formats.date, formats.date);
|
||||
}
|
||||
|
||||
if ('time' in formats) {
|
||||
Object.assign(options.formats.time, formats.time)
|
||||
Object.assign(options.formats.time, formats.time);
|
||||
}
|
||||
}
|
||||
|
||||
return $locale.set(initialLocale)
|
||||
return $locale.set(initialLocale);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
// could use a reduce, but a simple for-in has less footprint
|
||||
export const flatObj = (obj: Record<string, any>, prefix = '') => {
|
||||
const flatted: Record<string, string> = {}
|
||||
const flatted: Record<string, string> = {};
|
||||
|
||||
for (const key in obj) {
|
||||
const flatKey = prefix + key
|
||||
const flatKey = prefix + key;
|
||||
|
||||
// we want plain objects and arrays
|
||||
if (typeof obj[key] === 'object') {
|
||||
Object.assign(flatted, flatObj(obj[key], `${flatKey}.`))
|
||||
Object.assign(flatted, flatObj(obj[key], `${flatKey}.`));
|
||||
} else {
|
||||
flatted[flatKey] = obj[key]
|
||||
flatted[flatKey] = obj[key];
|
||||
}
|
||||
}
|
||||
return flatted
|
||||
}
|
||||
|
||||
return flatted;
|
||||
};
|
||||
|
|
|
@ -1,95 +1,95 @@
|
|||
import IntlMessageFormat from 'intl-messageformat'
|
||||
import IntlMessageFormat from 'intl-messageformat';
|
||||
|
||||
import { MemoizedIntlFormatter } from '../types'
|
||||
import { getCurrentLocale } from '../stores/locale'
|
||||
import { getOptions } from '../configs'
|
||||
|
||||
import { monadicMemoize } from './memoize'
|
||||
import { MemoizedIntlFormatter } from '../types';
|
||||
import { getCurrentLocale } from '../stores/locale';
|
||||
import { getOptions } from '../configs';
|
||||
import { monadicMemoize } from './memoize';
|
||||
|
||||
type MemoizedNumberFormatterFactory = MemoizedIntlFormatter<
|
||||
Intl.NumberFormat,
|
||||
Intl.NumberFormatOptions
|
||||
>
|
||||
>;
|
||||
|
||||
type MemoizedDateTimeFormatterFactory = MemoizedIntlFormatter<
|
||||
Intl.DateTimeFormat,
|
||||
Intl.DateTimeFormatOptions
|
||||
>
|
||||
>;
|
||||
|
||||
const getIntlFormatterOptions = (
|
||||
type: 'time' | 'number' | 'date',
|
||||
name: string
|
||||
name: string,
|
||||
): any => {
|
||||
const formats = getOptions().formats
|
||||
const { formats } = getOptions();
|
||||
|
||||
if (type in formats && name in formats[type]) {
|
||||
return formats[type][name]
|
||||
return formats[type][name];
|
||||
}
|
||||
|
||||
throw new Error(`[svelte-i18n] Unknown "${name}" ${type} format.`)
|
||||
}
|
||||
throw new Error(`[svelte-i18n] Unknown "${name}" ${type} format.`);
|
||||
};
|
||||
|
||||
const createNumberFormatter: MemoizedNumberFormatterFactory = monadicMemoize(
|
||||
({ locale, format, ...options }) => {
|
||||
if (locale == null) {
|
||||
throw new Error('[svelte-i18n] A "locale" must be set to format numbers')
|
||||
throw new Error('[svelte-i18n] A "locale" must be set to format numbers');
|
||||
}
|
||||
|
||||
if (format) {
|
||||
options = getIntlFormatterOptions('number', format)
|
||||
options = getIntlFormatterOptions('number', format);
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat(locale, options)
|
||||
}
|
||||
)
|
||||
return new Intl.NumberFormat(locale, options);
|
||||
},
|
||||
);
|
||||
|
||||
const createDateFormatter: MemoizedDateTimeFormatterFactory = monadicMemoize(
|
||||
({ locale, format, ...options }) => {
|
||||
if (locale == null) {
|
||||
throw new Error('[svelte-i18n] A "locale" must be set to format dates')
|
||||
throw new Error('[svelte-i18n] A "locale" must be set to format dates');
|
||||
}
|
||||
|
||||
if (format) options = getIntlFormatterOptions('date', format)
|
||||
if (format) options = getIntlFormatterOptions('date', format);
|
||||
else if (Object.keys(options).length === 0) {
|
||||
options = getIntlFormatterOptions('date', 'short')
|
||||
options = getIntlFormatterOptions('date', 'short');
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat(locale, options)
|
||||
}
|
||||
)
|
||||
return new Intl.DateTimeFormat(locale, options);
|
||||
},
|
||||
);
|
||||
|
||||
const createTimeFormatter: MemoizedDateTimeFormatterFactory = monadicMemoize(
|
||||
({ locale, format, ...options }) => {
|
||||
if (locale == null) {
|
||||
throw new Error(
|
||||
'[svelte-i18n] A "locale" must be set to format time values'
|
||||
)
|
||||
'[svelte-i18n] A "locale" must be set to format time values',
|
||||
);
|
||||
}
|
||||
|
||||
if (format) options = getIntlFormatterOptions('time', format)
|
||||
if (format) options = getIntlFormatterOptions('time', format);
|
||||
else if (Object.keys(options).length === 0) {
|
||||
options = getIntlFormatterOptions('time', 'short')
|
||||
options = getIntlFormatterOptions('time', 'short');
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat(locale, options)
|
||||
}
|
||||
)
|
||||
return new Intl.DateTimeFormat(locale, options);
|
||||
},
|
||||
);
|
||||
|
||||
export const getNumberFormatter: MemoizedNumberFormatterFactory = ({
|
||||
locale = getCurrentLocale(),
|
||||
...args
|
||||
} = {}) => createNumberFormatter({ locale, ...args })
|
||||
} = {}) => createNumberFormatter({ locale, ...args });
|
||||
|
||||
export const getDateFormatter: MemoizedDateTimeFormatterFactory = ({
|
||||
locale = getCurrentLocale(),
|
||||
...args
|
||||
} = {}) => createDateFormatter({ locale, ...args })
|
||||
} = {}) => createDateFormatter({ locale, ...args });
|
||||
|
||||
export const getTimeFormatter: MemoizedDateTimeFormatterFactory = ({
|
||||
locale = getCurrentLocale(),
|
||||
...args
|
||||
} = {}) => createTimeFormatter({ locale, ...args })
|
||||
} = {}) => createTimeFormatter({ locale, ...args });
|
||||
|
||||
export const getMessageFormatter = monadicMemoize(
|
||||
(message: string, locale: string = getCurrentLocale()) =>
|
||||
new IntlMessageFormat(message, locale, getOptions().formats)
|
||||
)
|
||||
new IntlMessageFormat(message, locale, getOptions().formats),
|
||||
);
|
||||
|
|
|
@ -1,104 +1,111 @@
|
|||
import { MessagesLoader } from '../types'
|
||||
import { MessagesLoader } from '../types';
|
||||
import {
|
||||
hasLocaleDictionary,
|
||||
$dictionary,
|
||||
addMessages,
|
||||
} from '../stores/dictionary'
|
||||
import { getRelatedLocalesOf } from '../stores/locale'
|
||||
} from '../stores/dictionary';
|
||||
import { getRelatedLocalesOf } from '../stores/locale';
|
||||
|
||||
type Queue = Set<MessagesLoader>
|
||||
const queue: Record<string, Queue> = {}
|
||||
type Queue = Set<MessagesLoader>;
|
||||
const queue: Record<string, Queue> = {};
|
||||
|
||||
export function resetQueues() {
|
||||
Object.keys(queue).forEach(key => {
|
||||
delete queue[key]
|
||||
})
|
||||
Object.keys(queue).forEach((key) => {
|
||||
delete queue[key];
|
||||
});
|
||||
}
|
||||
|
||||
function createLocaleQueue(locale: string) {
|
||||
queue[locale] = new Set()
|
||||
queue[locale] = new Set();
|
||||
}
|
||||
|
||||
function removeLoaderFromQueue(locale: string, loader: MessagesLoader) {
|
||||
queue[locale].delete(loader)
|
||||
queue[locale].delete(loader);
|
||||
|
||||
if (queue[locale].size === 0) {
|
||||
delete queue[locale]
|
||||
delete queue[locale];
|
||||
}
|
||||
}
|
||||
|
||||
function getLocaleQueue(locale: string) {
|
||||
return queue[locale]
|
||||
return queue[locale];
|
||||
}
|
||||
|
||||
function getLocalesQueues(locale: string) {
|
||||
return getRelatedLocalesOf(locale)
|
||||
.reverse()
|
||||
.map<[string, MessagesLoader[]]>(localeItem => {
|
||||
const queue = getLocaleQueue(localeItem)
|
||||
return [localeItem, queue ? [...queue] : []]
|
||||
.map<[string, MessagesLoader[]]>((localeItem) => {
|
||||
const localeQueue = getLocaleQueue(localeItem);
|
||||
|
||||
return [localeItem, localeQueue ? [...localeQueue] : []];
|
||||
})
|
||||
.filter(([, queue]) => queue.length > 0)
|
||||
.filter(([, localeQueue]) => localeQueue.length > 0);
|
||||
}
|
||||
|
||||
export function hasLocaleQueue(locale: string) {
|
||||
return getRelatedLocalesOf(locale)
|
||||
.reverse()
|
||||
.some(locale => getLocaleQueue(locale)?.size)
|
||||
.some((localeQueue) => getLocaleQueue(localeQueue)?.size);
|
||||
}
|
||||
|
||||
function loadLocaleQueue(locale: string, queue: MessagesLoader[]) {
|
||||
function loadLocaleQueue(locale: string, localeQueue: MessagesLoader[]) {
|
||||
const allLoadersPromise = Promise.all(
|
||||
queue.map(loader => {
|
||||
localeQueue.map((loader) => {
|
||||
// todo: maybe don't just remove, but add to a `loading` set?
|
||||
removeLoaderFromQueue(locale, loader)
|
||||
removeLoaderFromQueue(locale, loader);
|
||||
|
||||
return loader().then(partial => partial.default || partial)
|
||||
})
|
||||
)
|
||||
return loader().then((partial) => partial.default || partial);
|
||||
}),
|
||||
);
|
||||
|
||||
return allLoadersPromise.then(partials => addMessages(locale, ...partials))
|
||||
return allLoadersPromise.then((partials) => addMessages(locale, ...partials));
|
||||
}
|
||||
|
||||
const activeFlushes: { [key: string]: Promise<void> } = {}
|
||||
const activeFlushes: { [key: string]: Promise<void> } = {};
|
||||
|
||||
export function flush(locale: string): Promise<void> {
|
||||
if (!hasLocaleQueue(locale)) {
|
||||
if (locale in activeFlushes) {
|
||||
return activeFlushes[locale]
|
||||
return activeFlushes[locale];
|
||||
}
|
||||
return
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// get queue of XX-YY and XX locales
|
||||
const queues = getLocalesQueues(locale)
|
||||
const queues = getLocalesQueues(locale);
|
||||
|
||||
// todo: what happens if some loader fails?
|
||||
activeFlushes[locale] = Promise.all(
|
||||
queues.map(([locale, queue]) => loadLocaleQueue(locale, queue))
|
||||
queues.map(([localeName, localeQueue]) =>
|
||||
loadLocaleQueue(localeName, localeQueue),
|
||||
),
|
||||
).then(() => {
|
||||
if (hasLocaleQueue(locale)) {
|
||||
return flush(locale)
|
||||
return flush(locale);
|
||||
}
|
||||
|
||||
delete activeFlushes[locale]
|
||||
})
|
||||
delete activeFlushes[locale];
|
||||
});
|
||||
|
||||
return activeFlushes[locale]
|
||||
return activeFlushes[locale];
|
||||
}
|
||||
|
||||
export function registerLocaleLoader(locale: string, loader: MessagesLoader) {
|
||||
if (!getLocaleQueue(locale)) createLocaleQueue(locale)
|
||||
if (!getLocaleQueue(locale)) createLocaleQueue(locale);
|
||||
|
||||
const localeQueue = getLocaleQueue(locale);
|
||||
|
||||
const queue = getLocaleQueue(locale)
|
||||
// istanbul ignore if
|
||||
if (getLocaleQueue(locale).has(loader)) return
|
||||
if (getLocaleQueue(locale).has(loader)) return;
|
||||
|
||||
if (!hasLocaleDictionary(locale)) {
|
||||
$dictionary.update(d => {
|
||||
d[locale] = {}
|
||||
return d
|
||||
})
|
||||
$dictionary.update((d) => {
|
||||
d[locale] = {};
|
||||
|
||||
return d;
|
||||
});
|
||||
}
|
||||
|
||||
queue.add(loader)
|
||||
localeQueue.add(loader);
|
||||
}
|
||||
|
|
|
@ -1,46 +1,54 @@
|
|||
const getFromQueryString = (queryString: string, key: string) => {
|
||||
const keyVal = queryString.split('&').find(i => i.indexOf(`${key}=`) === 0)
|
||||
const keyVal = queryString.split('&').find((i) => i.indexOf(`${key}=`) === 0);
|
||||
|
||||
if (keyVal) {
|
||||
return keyVal.split('=').pop()
|
||||
return keyVal.split('=').pop();
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getFirstMatch = (base: string, pattern: RegExp) => {
|
||||
const match = pattern.exec(base)
|
||||
const match = pattern.exec(base);
|
||||
|
||||
// istanbul ignore if
|
||||
if (!match) return null
|
||||
if (!match) return null;
|
||||
|
||||
// istanbul ignore else
|
||||
return match[1] || null
|
||||
}
|
||||
return match[1] || null;
|
||||
};
|
||||
|
||||
export const getLocaleFromHostname = (hostname: RegExp) => {
|
||||
// istanbul ignore next
|
||||
if (typeof window === 'undefined') return null
|
||||
return getFirstMatch(window.location.hostname, hostname)
|
||||
}
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
return getFirstMatch(window.location.hostname, hostname);
|
||||
};
|
||||
|
||||
export const getLocaleFromPathname = (pathname: RegExp) => {
|
||||
// istanbul ignore next
|
||||
if (typeof window === 'undefined') return null
|
||||
return getFirstMatch(window.location.pathname, pathname)
|
||||
}
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
return getFirstMatch(window.location.pathname, pathname);
|
||||
};
|
||||
|
||||
export const getLocaleFromNavigator = () => {
|
||||
// istanbul ignore next
|
||||
if (typeof window === 'undefined') return null
|
||||
return window.navigator.language || window.navigator.languages[0]
|
||||
}
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
return window.navigator.language || window.navigator.languages[0];
|
||||
};
|
||||
|
||||
export const getLocaleFromQueryString = (search: string) => {
|
||||
// istanbul ignore next
|
||||
if (typeof window === 'undefined') return null
|
||||
return getFromQueryString(window.location.search.substr(1), search)
|
||||
}
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
return getFromQueryString(window.location.search.substr(1), search);
|
||||
};
|
||||
|
||||
export const getLocaleFromHash = (hash: string) => {
|
||||
// istanbul ignore next
|
||||
if (typeof window === 'undefined') return null
|
||||
return getFromQueryString(window.location.hash.substr(1), hash)
|
||||
}
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
return getFromQueryString(window.location.hash.substr(1), hash);
|
||||
};
|
||||
|
|
|
@ -1,33 +1,36 @@
|
|||
import { getMessageFromDictionary } from '../stores/dictionary'
|
||||
import { getFallbackOf } from '../stores/locale'
|
||||
import { getMessageFromDictionary } from '../stores/dictionary';
|
||||
import { getFallbackOf } from '../stores/locale';
|
||||
|
||||
export const lookupCache: Record<string, Record<string, string>> = {}
|
||||
export const lookupCache: Record<string, Record<string, string>> = {};
|
||||
|
||||
const addToCache = (path: string, locale: string, message: string) => {
|
||||
if (!message) return message
|
||||
if (!(locale in lookupCache)) lookupCache[locale] = {}
|
||||
if (!(path in lookupCache[locale])) lookupCache[locale][path] = message
|
||||
return message
|
||||
}
|
||||
if (!message) return message;
|
||||
if (!(locale in lookupCache)) lookupCache[locale] = {};
|
||||
if (!(path in lookupCache[locale])) lookupCache[locale][path] = message;
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
const searchForMessage = (path: string, locale: string): string => {
|
||||
if (locale == null) return null
|
||||
if (locale == null) return null;
|
||||
|
||||
const message = getMessageFromDictionary(locale, path)
|
||||
if (message) return message
|
||||
const message = getMessageFromDictionary(locale, path);
|
||||
|
||||
return searchForMessage(path, getFallbackOf(locale))
|
||||
}
|
||||
if (message) return message;
|
||||
|
||||
return searchForMessage(path, getFallbackOf(locale));
|
||||
};
|
||||
|
||||
export const lookup = (path: string, locale: string) => {
|
||||
if (locale in lookupCache && path in lookupCache[locale]) {
|
||||
return lookupCache[locale][path]
|
||||
return lookupCache[locale][path];
|
||||
}
|
||||
|
||||
const message = searchForMessage(path, locale)
|
||||
const message = searchForMessage(path, locale);
|
||||
|
||||
if (message) {
|
||||
return addToCache(path, locale, message)
|
||||
return addToCache(path, locale, message);
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
type MemoizedFunction = <F extends any>(fn: F) => F
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
type MemoizedFunction = <F extends Function>(fn: F) => F;
|
||||
|
||||
const monadicMemoize: MemoizedFunction = fn => {
|
||||
const cache = Object.create(null)
|
||||
const monadicMemoize: MemoizedFunction = (fn) => {
|
||||
const cache = Object.create(null);
|
||||
const memoizedFn: any = (arg: unknown) => {
|
||||
const cacheKey = JSON.stringify(arg)
|
||||
const cacheKey = JSON.stringify(arg);
|
||||
|
||||
if (cacheKey in cache) {
|
||||
return cache[cacheKey]
|
||||
return cache[cacheKey];
|
||||
}
|
||||
return (cache[cacheKey] = fn(arg))
|
||||
}
|
||||
return memoizedFn
|
||||
}
|
||||
export { monadicMemoize }
|
||||
|
||||
return (cache[cacheKey] = fn(arg));
|
||||
};
|
||||
|
||||
return memoizedFn;
|
||||
};
|
||||
|
||||
export { monadicMemoize };
|
||||
|
|
|
@ -1,36 +1,36 @@
|
|||
import { MessageObject } from './types'
|
||||
import { getCurrentLocale } from './stores/locale'
|
||||
import { getOptions } from './configs'
|
||||
import { flush } from './includes/loaderQueue'
|
||||
import { MessageObject } from './types';
|
||||
import { getCurrentLocale } from './stores/locale';
|
||||
import { getOptions } from './configs';
|
||||
import { flush } from './includes/loaderQueue';
|
||||
|
||||
// defineMessages allow us to define and extract dynamic message ids
|
||||
export function defineMessages(i: Record<string, MessageObject>) {
|
||||
return i
|
||||
return i;
|
||||
}
|
||||
|
||||
export function waitLocale(locale?: string) {
|
||||
return flush(locale || getCurrentLocale() || getOptions().initialLocale)
|
||||
return flush(locale || getCurrentLocale() || getOptions().initialLocale);
|
||||
}
|
||||
|
||||
export { init } from './configs'
|
||||
export { init } from './configs';
|
||||
export {
|
||||
getLocaleFromHostname,
|
||||
getLocaleFromPathname,
|
||||
getLocaleFromNavigator,
|
||||
getLocaleFromQueryString,
|
||||
getLocaleFromHash,
|
||||
} from './includes/localeGetters'
|
||||
} from './includes/localeGetters';
|
||||
|
||||
export { $locale as locale } from './stores/locale'
|
||||
export { $locale as locale } from './stores/locale';
|
||||
|
||||
export {
|
||||
$dictionary as dictionary,
|
||||
$locales as locales,
|
||||
addMessages,
|
||||
} from './stores/dictionary'
|
||||
export { registerLocaleLoader as register } from './includes/loaderQueue'
|
||||
} 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,
|
||||
|
@ -39,7 +39,7 @@ export {
|
|||
$formatDate as date,
|
||||
$formatNumber as number,
|
||||
$formatTime as time,
|
||||
} from './stores/formatters'
|
||||
} from './stores/formatters';
|
||||
|
||||
// low-level
|
||||
export {
|
||||
|
@ -47,4 +47,4 @@ export {
|
|||
getNumberFormatter,
|
||||
getTimeFormatter,
|
||||
getMessageFormatter,
|
||||
} from './includes/formatters'
|
||||
} from './includes/formatters';
|
||||
|
|
|
@ -1,53 +1,57 @@
|
|||
import { writable, derived } from 'svelte/store'
|
||||
import { writable, derived } from 'svelte/store';
|
||||
|
||||
import { LocaleDictionary, DeepDictionary, Dictionary } from '../types/index'
|
||||
import { flatObj } from '../includes/flatObj'
|
||||
import { LocaleDictionary, DeepDictionary, Dictionary } from '../types/index';
|
||||
import { flatObj } from '../includes/flatObj';
|
||||
import { getFallbackOf } from './locale';
|
||||
|
||||
import { getFallbackOf } from './locale'
|
||||
|
||||
let dictionary: Dictionary
|
||||
const $dictionary = writable<Dictionary>({})
|
||||
let dictionary: Dictionary;
|
||||
const $dictionary = writable<Dictionary>({});
|
||||
|
||||
export function getLocaleDictionary(locale: string) {
|
||||
return (dictionary[locale] as LocaleDictionary) || null
|
||||
return (dictionary[locale] as LocaleDictionary) || null;
|
||||
}
|
||||
|
||||
export function getDictionary() {
|
||||
return dictionary
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
export function hasLocaleDictionary(locale: string) {
|
||||
return locale in dictionary
|
||||
return locale in dictionary;
|
||||
}
|
||||
|
||||
export function getMessageFromDictionary(locale: string, id: string) {
|
||||
if (hasLocaleDictionary(locale)) {
|
||||
const localeDictionary = getLocaleDictionary(locale)
|
||||
const localeDictionary = getLocaleDictionary(locale);
|
||||
|
||||
if (id in localeDictionary) {
|
||||
return localeDictionary[id]
|
||||
return localeDictionary[id];
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getClosestAvailableLocale(locale: string): string | null {
|
||||
if (locale == null || hasLocaleDictionary(locale)) return locale
|
||||
return getClosestAvailableLocale(getFallbackOf(locale))
|
||||
if (locale == null || hasLocaleDictionary(locale)) return locale;
|
||||
|
||||
return getClosestAvailableLocale(getFallbackOf(locale));
|
||||
}
|
||||
|
||||
export function addMessages(locale: string, ...partials: DeepDictionary[]) {
|
||||
const flattedPartials = partials.map(partial => flatObj(partial))
|
||||
const flattedPartials = partials.map((partial) => flatObj(partial));
|
||||
|
||||
$dictionary.update(d => {
|
||||
d[locale] = Object.assign(d[locale] || {}, ...flattedPartials)
|
||||
return d
|
||||
})
|
||||
$dictionary.update((d) => {
|
||||
d[locale] = Object.assign(d[locale] || {}, ...flattedPartials);
|
||||
|
||||
return d;
|
||||
});
|
||||
}
|
||||
|
||||
const $locales = derived([$dictionary], ([$dictionary]) =>
|
||||
Object.keys($dictionary)
|
||||
)
|
||||
// eslint-disable-next-line no-shadow
|
||||
const $locales = derived([$dictionary], ([dictionary]) =>
|
||||
Object.keys(dictionary),
|
||||
);
|
||||
|
||||
$dictionary.subscribe(newDictionary => (dictionary = newDictionary))
|
||||
$dictionary.subscribe((newDictionary) => (dictionary = newDictionary));
|
||||
|
||||
export { $dictionary, $locales }
|
||||
export { $dictionary, $locales };
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { derived } from 'svelte/store'
|
||||
import { derived } from 'svelte/store';
|
||||
|
||||
import {
|
||||
MessageFormatter,
|
||||
|
@ -6,67 +6,71 @@ import {
|
|||
TimeFormatter,
|
||||
DateFormatter,
|
||||
NumberFormatter,
|
||||
} from '../types'
|
||||
import { lookup } from '../includes/lookup'
|
||||
import { hasLocaleQueue } from '../includes/loaderQueue'
|
||||
} 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'
|
||||
} 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
|
||||
options = id as MessageObject;
|
||||
id = options.id;
|
||||
}
|
||||
|
||||
const { values, locale = getCurrentLocale(), default: defaultValue } = options
|
||||
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.'
|
||||
)
|
||||
'[svelte-i18n] Cannot format a message without first setting the initial locale.',
|
||||
);
|
||||
}
|
||||
|
||||
const message = lookup(id, 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
|
||||
locale,
|
||||
).join('", "')}".${
|
||||
hasLocaleQueue(getCurrentLocale())
|
||||
? `\n\nNote: there are at least one loader still registered to this locale that wasn't executed.`
|
||||
: ''
|
||||
}`
|
||||
)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
return defaultValue || id
|
||||
return defaultValue || id;
|
||||
}
|
||||
|
||||
if (!values) return message
|
||||
return getMessageFormatter(message, locale).format(values)
|
||||
}
|
||||
if (!values) return message;
|
||||
|
||||
return getMessageFormatter(message, locale).format(values);
|
||||
};
|
||||
|
||||
const formatTime: TimeFormatter = (t, options) =>
|
||||
getTimeFormatter(options).format(t)
|
||||
getTimeFormatter(options).format(t);
|
||||
|
||||
const formatDate: DateFormatter = (d, options) =>
|
||||
getDateFormatter(options).format(d)
|
||||
getDateFormatter(options).format(d);
|
||||
|
||||
const formatNumber: NumberFormatter = (n, options) =>
|
||||
getNumberFormatter(options).format(n)
|
||||
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)
|
||||
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);
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import { writable } from 'svelte/store'
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const $isLoading = writable(false)
|
||||
export const $isLoading = writable(false);
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import { writable } from 'svelte/store'
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
import { flush, hasLocaleQueue } from '../includes/loaderQueue'
|
||||
import { getOptions } from '../configs'
|
||||
import { flush, hasLocaleQueue } from '../includes/loaderQueue';
|
||||
import { getOptions } from '../configs';
|
||||
import { getClosestAvailableLocale } from './dictionary';
|
||||
import { $isLoading } from './loading';
|
||||
|
||||
import { getClosestAvailableLocale } from './dictionary'
|
||||
import { $isLoading } from './loading'
|
||||
|
||||
let current: string
|
||||
const $locale = writable(null)
|
||||
let current: string;
|
||||
const $locale = writable(null);
|
||||
|
||||
export function isFallbackLocaleOf(localeA: string, localeB: string) {
|
||||
return localeB.indexOf(localeA) === 0 && localeA !== localeB
|
||||
return localeB.indexOf(localeA) === 0 && localeA !== localeB;
|
||||
}
|
||||
|
||||
export function isRelatedLocale(localeA: string, localeB: string) {
|
||||
|
@ -18,51 +17,56 @@ export function isRelatedLocale(localeA: string, localeB: string) {
|
|||
localeA === localeB ||
|
||||
isFallbackLocaleOf(localeA, localeB) ||
|
||||
isFallbackLocaleOf(localeB, localeA)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function getFallbackOf(locale: string) {
|
||||
const index = locale.lastIndexOf('-')
|
||||
if (index > 0) return locale.slice(0, index)
|
||||
const index = locale.lastIndexOf('-');
|
||||
|
||||
if (index > 0) return locale.slice(0, index);
|
||||
|
||||
const { fallbackLocale } = getOptions();
|
||||
|
||||
const { fallbackLocale } = getOptions()
|
||||
if (fallbackLocale && !isRelatedLocale(locale, fallbackLocale)) {
|
||||
return fallbackLocale
|
||||
return fallbackLocale;
|
||||
}
|
||||
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getRelatedLocalesOf(locale: string): string[] {
|
||||
const locales = locale
|
||||
.split('-')
|
||||
.map((_, i, arr) => arr.slice(0, i + 1).join('-'))
|
||||
.map((_, i, arr) => arr.slice(0, i + 1).join('-'));
|
||||
|
||||
const { fallbackLocale } = getOptions();
|
||||
|
||||
const { fallbackLocale } = getOptions()
|
||||
if (fallbackLocale && !isRelatedLocale(locale, fallbackLocale)) {
|
||||
return locales.concat(getRelatedLocalesOf(fallbackLocale))
|
||||
return locales.concat(getRelatedLocalesOf(fallbackLocale));
|
||||
}
|
||||
return locales
|
||||
|
||||
return locales;
|
||||
}
|
||||
|
||||
export function getCurrentLocale() {
|
||||
return current
|
||||
return current;
|
||||
}
|
||||
|
||||
$locale.subscribe((newLocale: string) => {
|
||||
current = newLocale
|
||||
current = newLocale;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
document.documentElement.setAttribute('lang', newLocale)
|
||||
document.documentElement.setAttribute('lang', newLocale);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const localeSet = $locale.set;
|
||||
|
||||
const localeSet = $locale.set
|
||||
$locale.set = (newLocale: string): void | Promise<void> => {
|
||||
if (getClosestAvailableLocale(newLocale) && hasLocaleQueue(newLocale)) {
|
||||
const loadingDelay = getOptions().loadingDelay
|
||||
const { loadingDelay } = getOptions();
|
||||
|
||||
let loadingTimer: number
|
||||
let loadingTimer: number;
|
||||
|
||||
// if there's no current locale, we don't wait to set isLoading to true
|
||||
// because it would break pages when loading the initial locale
|
||||
|
@ -71,25 +75,29 @@ $locale.set = (newLocale: string): void | Promise<void> => {
|
|||
getCurrentLocale() != null &&
|
||||
loadingDelay
|
||||
) {
|
||||
loadingTimer = window.setTimeout(() => $isLoading.set(true), loadingDelay)
|
||||
loadingTimer = window.setTimeout(
|
||||
() => $isLoading.set(true),
|
||||
loadingDelay,
|
||||
);
|
||||
} else {
|
||||
$isLoading.set(true)
|
||||
$isLoading.set(true);
|
||||
}
|
||||
|
||||
return flush(newLocale)
|
||||
.then(() => {
|
||||
localeSet(newLocale)
|
||||
localeSet(newLocale);
|
||||
})
|
||||
.finally(() => {
|
||||
clearTimeout(loadingTimer)
|
||||
$isLoading.set(false)
|
||||
})
|
||||
clearTimeout(loadingTimer);
|
||||
$isLoading.set(false);
|
||||
});
|
||||
}
|
||||
return localeSet(newLocale)
|
||||
}
|
||||
|
||||
return localeSet(newLocale);
|
||||
};
|
||||
|
||||
// istanbul ignore next
|
||||
$locale.update = (fn: (locale: string) => void | Promise<void>) =>
|
||||
localeSet(fn(current))
|
||||
localeSet(fn(current));
|
||||
|
||||
export { $locale }
|
||||
export { $locale };
|
||||
|
|
|
@ -1,50 +1,50 @@
|
|||
import { Formats } from 'intl-messageformat'
|
||||
import { Formats } from 'intl-messageformat';
|
||||
|
||||
export interface DeepDictionary {
|
||||
[key: string]: DeepDictionary | string | string[]
|
||||
[key: string]: DeepDictionary | string | string[];
|
||||
}
|
||||
export type LocaleDictionary = Record<string, string>
|
||||
export type Dictionary = Record<string, LocaleDictionary>
|
||||
export type LocaleDictionary = Record<string, string>;
|
||||
export type Dictionary = Record<string, LocaleDictionary>;
|
||||
|
||||
export interface MessageObject {
|
||||
id?: string
|
||||
locale?: string
|
||||
format?: string
|
||||
default?: string
|
||||
values?: Record<string, string | number | Date>
|
||||
id?: string;
|
||||
locale?: string;
|
||||
format?: string;
|
||||
default?: string;
|
||||
values?: Record<string, string | number | Date>;
|
||||
}
|
||||
|
||||
export type MessageFormatter = (
|
||||
id: string | MessageObject,
|
||||
options?: MessageObject
|
||||
) => string
|
||||
options?: MessageObject,
|
||||
) => string;
|
||||
|
||||
export type TimeFormatter = (
|
||||
d: Date | number,
|
||||
options?: IntlFormatterOptions<Intl.DateTimeFormatOptions>
|
||||
) => string
|
||||
options?: IntlFormatterOptions<Intl.DateTimeFormatOptions>,
|
||||
) => string;
|
||||
|
||||
export type DateFormatter = (
|
||||
d: Date | number,
|
||||
options?: IntlFormatterOptions<Intl.DateTimeFormatOptions>
|
||||
) => string
|
||||
options?: IntlFormatterOptions<Intl.DateTimeFormatOptions>,
|
||||
) => string;
|
||||
|
||||
export type NumberFormatter = (
|
||||
d: number,
|
||||
options?: IntlFormatterOptions<Intl.NumberFormatOptions>
|
||||
) => string
|
||||
options?: IntlFormatterOptions<Intl.NumberFormatOptions>,
|
||||
) => string;
|
||||
|
||||
type IntlFormatterOptions<T> = T & {
|
||||
format?: string
|
||||
locale?: string
|
||||
}
|
||||
format?: string;
|
||||
locale?: string;
|
||||
};
|
||||
|
||||
export interface MemoizedIntlFormatter<T, U> {
|
||||
(options?: IntlFormatterOptions<U>): T
|
||||
(options?: IntlFormatterOptions<U>): T;
|
||||
}
|
||||
|
||||
export interface MessagesLoader {
|
||||
(): Promise<any>
|
||||
(): Promise<any>;
|
||||
}
|
||||
|
||||
export interface ConfigureOptions {
|
||||
|
|
|
@ -1,98 +1,124 @@
|
|||
// TODO: better tests. these are way too generic.
|
||||
|
||||
import { parse } from 'svelte/compiler'
|
||||
import { parse } from 'svelte/compiler';
|
||||
|
||||
import {
|
||||
collectFormatCalls,
|
||||
collectMessageDefinitions,
|
||||
collectMessages,
|
||||
extractMessages,
|
||||
} from '../../src/cli/extract'
|
||||
} from '../../src/cli/extract';
|
||||
|
||||
describe('collecting format calls', () => {
|
||||
test('returns nothing if there are no script tag', () => {
|
||||
const ast = parse(`<div>Hey</div>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
it('returns nothing if there are no script tag', () => {
|
||||
const ast = parse(`<div>Hey</div>`);
|
||||
const calls = collectFormatCalls(ast);
|
||||
|
||||
test('returns nothing if there are no imports', () => {
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('returns nothing if there are no imports', () => {
|
||||
const ast = parse(`<script>
|
||||
import Foo from 'foo';
|
||||
const $_ = () => 0; $_();
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
</script>`);
|
||||
|
||||
test('returns nothing if there are no format imports', () => {
|
||||
const calls = collectFormatCalls(ast);
|
||||
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('returns nothing if there are no format imports', () => {
|
||||
const ast = parse(
|
||||
`<script>
|
||||
import { init } from 'svelte-i18n';
|
||||
init({})
|
||||
</script>`
|
||||
)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
</script>`,
|
||||
);
|
||||
|
||||
test('collects all format calls in the instance script', () => {
|
||||
const calls = collectFormatCalls(ast);
|
||||
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('collects all format calls in the instance script', () => {
|
||||
const ast = parse(`<script>
|
||||
import { format, _ } from 'svelte-i18n'
|
||||
$format('foo')
|
||||
format('bar')
|
||||
let label = $_({id:'bar'})
|
||||
const a = { b: () => 0}
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(2)
|
||||
expect(calls[0]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[1]).toMatchObject({ type: 'CallExpression' })
|
||||
})
|
||||
</script>`);
|
||||
|
||||
test('collects all format calls with renamed imports', () => {
|
||||
const calls = collectFormatCalls(ast);
|
||||
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(calls[0]).toMatchObject({ type: 'CallExpression' });
|
||||
expect(calls[1]).toMatchObject({ type: 'CallExpression' });
|
||||
});
|
||||
|
||||
it('collects all format calls with renamed imports', () => {
|
||||
const ast = parse(`<script>
|
||||
import { format as _x, _ as intl, t as f } from 'svelte-i18n'
|
||||
$_x('foo')
|
||||
$intl({ id: 'bar' })
|
||||
$f({ id: 'bar' })
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(3)
|
||||
expect(calls[0]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[1]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[2]).toMatchObject({ type: 'CallExpression' })
|
||||
})
|
||||
})
|
||||
</script>`);
|
||||
|
||||
const calls = collectFormatCalls(ast);
|
||||
|
||||
expect(calls).toHaveLength(3);
|
||||
expect(calls[0]).toMatchObject({ type: 'CallExpression' });
|
||||
expect(calls[1]).toMatchObject({ type: 'CallExpression' });
|
||||
expect(calls[2]).toMatchObject({ type: 'CallExpression' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('collecting message definitions', () => {
|
||||
test('returns nothing if there are no imports from the library', () => {
|
||||
it('returns nothing if there are no imports from the library', () => {
|
||||
const ast = parse(
|
||||
`<script>
|
||||
import foo from 'foo';
|
||||
import { dictionary } from 'svelte-i18n';
|
||||
</script>`
|
||||
)
|
||||
expect(collectMessageDefinitions(ast)).toHaveLength(0)
|
||||
})
|
||||
</script>`,
|
||||
);
|
||||
|
||||
test('gets all message definition objects', () => {
|
||||
expect(collectMessageDefinitions(ast)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('gets all message definition objects', () => {
|
||||
const ast = parse(`<script>
|
||||
import { defineMessages } from 'svelte-i18n';
|
||||
defineMessages({ foo: { id: 'foo' }, bar: { id: 'bar' } })
|
||||
defineMessages({ baz: { id: 'baz' }, quix: { id: 'qux' } })
|
||||
</script>`)
|
||||
const definitions = collectMessageDefinitions(ast)
|
||||
expect(definitions).toHaveLength(4)
|
||||
expect(definitions[0]).toMatchObject({ type: 'ObjectExpression' })
|
||||
expect(definitions[1]).toMatchObject({ type: 'ObjectExpression' })
|
||||
expect(definitions[2]).toMatchObject({ type: 'ObjectExpression' })
|
||||
expect(definitions[3]).toMatchObject({ type: 'ObjectExpression' })
|
||||
})
|
||||
})
|
||||
</script>`);
|
||||
|
||||
const definitions = collectMessageDefinitions(ast);
|
||||
|
||||
expect(definitions).toHaveLength(4);
|
||||
expect(definitions[0]).toMatchObject({ type: 'ObjectExpression' });
|
||||
expect(definitions[1]).toMatchObject({ type: 'ObjectExpression' });
|
||||
expect(definitions[2]).toMatchObject({ type: 'ObjectExpression' });
|
||||
expect(definitions[3]).toMatchObject({ type: 'ObjectExpression' });
|
||||
});
|
||||
|
||||
it('throws an error if an spread is found', () => {
|
||||
const ast = parse(`<script>
|
||||
import { defineMessages } from 'svelte-i18n';
|
||||
const potato = { foo: { id: 'foo' }, bar: { id: 'bar' } }
|
||||
defineMessages({ ...potato })
|
||||
</script>`);
|
||||
|
||||
expect(() =>
|
||||
collectMessageDefinitions(ast),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Found invalid 'SpreadElement' at L4:23"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collecting messages', () => {
|
||||
test('collects all messages in both instance and html ASTs', () => {
|
||||
it('collects all messages in both instance and html ASTs', () => {
|
||||
const markup = `
|
||||
<script>
|
||||
import { _, defineMessages } from 'svelte-i18n';
|
||||
|
@ -108,11 +134,11 @@ describe('collecting messages', () => {
|
|||
|
||||
<div>{$_('msg_1')}</div>
|
||||
<div>{$_({id: 'msg_2'})}</div>
|
||||
<div>{$_('msg_3', { default: 'Message'})}</div>`
|
||||
<div>{$_('msg_3', { default: 'Message'})}</div>`;
|
||||
|
||||
const messages = collectMessages(markup)
|
||||
const messages = collectMessages(markup);
|
||||
|
||||
expect(messages).toHaveLength(7)
|
||||
expect(messages).toHaveLength(7);
|
||||
expect(messages).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ meta: { id: 'foo' } }),
|
||||
|
@ -126,25 +152,27 @@ describe('collecting messages', () => {
|
|||
expect.objectContaining({
|
||||
meta: { id: 'enabled', default: 'Enabled' },
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('messages extraction', () => {
|
||||
test('returns a object built based on all found message paths', () => {
|
||||
it('returns a object built based on all found message paths', () => {
|
||||
const markup = `<script>
|
||||
import { _ } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<h1>{$_.title('title')}</h1>
|
||||
<h2>{$_({ id: 'subtitle'})}</h2>
|
||||
`
|
||||
const dict = extractMessages(markup)
|
||||
expect(dict).toMatchObject({ title: '', subtitle: '' })
|
||||
})
|
||||
`;
|
||||
|
||||
test('creates deep nested properties', () => {
|
||||
const dict = extractMessages(markup);
|
||||
|
||||
expect(dict).toMatchObject({ title: '', subtitle: '' });
|
||||
});
|
||||
|
||||
it('creates deep nested properties', () => {
|
||||
const markup = `
|
||||
<script>import { _ } from 'svelte-i18n';</script>
|
||||
|
||||
|
@ -155,15 +183,17 @@ describe('messages extraction', () => {
|
|||
<li>{$_('list.1')}</li>
|
||||
<li>{$_('list.2')}</li>
|
||||
</ul>
|
||||
`
|
||||
const dict = extractMessages(markup)
|
||||
`;
|
||||
|
||||
const dict = extractMessages(markup);
|
||||
|
||||
expect(dict).toMatchObject({
|
||||
home: { page: { title: '', subtitle: '' } },
|
||||
list: ['', '', ''],
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
test('creates a shallow dictionary', () => {
|
||||
it('creates a shallow dictionary', () => {
|
||||
const markup = `
|
||||
<script>import { _ } from 'svelte-i18n';</script>
|
||||
|
||||
|
@ -174,18 +204,20 @@ describe('messages extraction', () => {
|
|||
<li>{$_('list.1')}</li>
|
||||
<li>{$_('list.2')}</li>
|
||||
</ul>
|
||||
`
|
||||
const dict = extractMessages(markup, { shallow: true })
|
||||
`;
|
||||
|
||||
const dict = extractMessages(markup, { shallow: true });
|
||||
|
||||
expect(dict).toMatchObject({
|
||||
'home.page.title': '',
|
||||
'home.page.subtitle': '',
|
||||
'list.0': '',
|
||||
'list.1': '',
|
||||
'list.2': '',
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
test('allow to pass a initial dictionary and only append non-existing props', () => {
|
||||
it('allow to pass a initial dictionary and only append non-existing props', () => {
|
||||
const markup = `
|
||||
<script>import { _ } from 'svelte-i18n';</script>
|
||||
|
||||
|
@ -196,7 +228,8 @@ describe('messages extraction', () => {
|
|||
<li>{$_('list.1')}</li>
|
||||
<li>{$_('list.2')}</li>
|
||||
</ul>
|
||||
`
|
||||
`;
|
||||
|
||||
const dict = extractMessages(markup, {
|
||||
overwrite: false,
|
||||
accumulator: {
|
||||
|
@ -206,7 +239,8 @@ describe('messages extraction', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
expect(dict).toMatchObject({
|
||||
home: {
|
||||
page: {
|
||||
|
@ -215,26 +249,28 @@ describe('messages extraction', () => {
|
|||
},
|
||||
},
|
||||
list: ['', '', ''],
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
test('allow to pass a initial dictionary and only append shallow non-existing props', () => {
|
||||
it('allow to pass a initial dictionary and only append shallow non-existing props', () => {
|
||||
const markup = `
|
||||
<script>import { _ } from 'svelte-i18n';</script>
|
||||
|
||||
<h1>{$_.title('home.page.title')}</h1>
|
||||
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
|
||||
`
|
||||
`;
|
||||
|
||||
const dict = extractMessages(markup, {
|
||||
overwrite: false,
|
||||
shallow: true,
|
||||
accumulator: {
|
||||
'home.page.title': 'Page title',
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
expect(dict).toMatchObject({
|
||||
'home.page.title': 'Page title',
|
||||
'home.page.subtitle': '',
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue