chore: 🤖 update linter deps and format project

This commit is contained in:
Christian Kaisermann 2020-09-20 10:14:26 -03:00
parent 68e8c51a63
commit e659889f43
24 changed files with 3469 additions and 3084 deletions

View File

@ -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"
}
}

View File

@ -1,8 +1 @@
{
"semi": false,
"printWidth": 80,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"singleQuote": true
}
"@kiwi/prettier-config"

View File

@ -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",

View File

@ -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;
}

View File

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

View File

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

View File

@ -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: {} },
);
}

View File

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

View File

@ -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;
};
}

View File

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

View File

@ -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;
};

View File

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

View File

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

View File

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

View File

@ -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;
};

View File

@ -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 };

View File

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

View File

@ -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 };

View File

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

View File

@ -1,3 +1,3 @@
import { writable } from 'svelte/store'
import { writable } from 'svelte/store';
export const $isLoading = writable(false)
export const $isLoading = writable(false);

View File

@ -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 };

View File

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

View File

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

5384
yarn.lock

File diff suppressed because it is too large Load Diff