@@ -47,7 +47,7 @@ export const Home = component$(() => {
export default component$(() => {
return (
/**
- * Add Home translation (only available in child components)
+ * Add Home translations (only available in child components)
*/
@@ -56,8 +56,8 @@ export default component$(() => {
});
export const head: DocumentHead = {
- title: 'head.home.title',
- meta: [{ name: 'description', content: 'head.home.description' }]
+ title: 'runtime.head.home.title',
+ meta: [{ name: 'description', content: 'runtime.head.home.description' }]
};
// E.g. SSG
diff --git a/src/app/routes/[...lang]/page/index.tsx b/src/app/routes/[...lang]/page/index.tsx
index 689724e..17d48db 100644
--- a/src/app/routes/[...lang]/page/index.tsx
+++ b/src/app/routes/[...lang]/page/index.tsx
@@ -18,7 +18,7 @@ export const Page = component$(() => {
export default component$(() => {
return (
/**
- * Add Page translation (only available in child components)
+ * Add Page translations (only available in child components)
*/
@@ -27,8 +27,8 @@ export default component$(() => {
});
export const head: DocumentHead = {
- title: 'head.page.title',
- meta: [{ name: 'description', content: 'head.page.description' }]
+ title: 'runtime.head.page.title',
+ meta: [{ name: 'description', content: 'runtime.head.page.description' }]
};
// E.g. SSG
diff --git a/src/app/speak-config.ts b/src/app/speak-config.ts
index 646ac8d..7f13b81 100644
--- a/src/app/speak-config.ts
+++ b/src/app/speak-config.ts
@@ -76,7 +76,8 @@ export const storeLocale$: StoreLocaleFn = $((locale: SpeakLocale) => {
url.pathname = `/${locale.lang}${url.pathname}`;
}
- window.history.pushState({}, '', url);
+ // E.g. Just replace the state: no back or forward on language change
+ window.history.replaceState({}, '', url);
}
});
diff --git a/tools/core/cli-parser.ts b/tools/core/cli-parser.ts
new file mode 100644
index 0000000..48c6726
--- /dev/null
+++ b/tools/core/cli-parser.ts
@@ -0,0 +1,12 @@
+/**
+ * Parse a cli argument to { key: value }
+ */
+export function parseArgument(arg: string): { key: string, value: any } {
+ const property = arg.split('=');
+ if (property.length === 2 && property[0].startsWith('--')) {
+ const key = property[0].slice(2);
+ const value = /,/.test(property[1]) ? property[1].split(',') : property[1];
+ return { key, value };
+ }
+ return { key: 'error', value: `- wrong option: "${property[0]}"` };
+}
diff --git a/tools/core/format.ts b/tools/core/format.ts
new file mode 100644
index 0000000..493e137
--- /dev/null
+++ b/tools/core/format.ts
@@ -0,0 +1,28 @@
+export function toJsonString(target: { [key: string]: any }): string {
+ return JSON.stringify(target, replacer, 2);
+}
+
+export function minDepth(target: { [key: string]: any }): number {
+ return typeof target === 'object' && Object.keys(target).length > 0 ?
+ 1 + Math.min(1, ...Object.values(target).map(o => minDepth(o)))
+ : 0
+}
+
+export function sortTarget(target: { [key: string]: any }) {
+ return Object.keys(target).sort().reduce(
+ (out: any, key: string) => {
+ if (typeof target[key] === 'object')
+ out[key] = sortTarget(target[key]);
+ else
+ out[key] = target[key];
+ return out;
+ }, {}
+ );
+}
+
+/**
+ * Remove escaped sequences
+ */
+function replacer(key: string, value: any) {
+ return typeof value === 'string' ? value.replace(/\\/g, '') : value;
+}
diff --git a/tools/core/merge.ts b/tools/core/merge.ts
new file mode 100644
index 0000000..ee5ce70
--- /dev/null
+++ b/tools/core/merge.ts
@@ -0,0 +1,24 @@
+/**
+ * https://github.com/lukeed/dset
+ */
+export function deepSet(target: { [key: string]: any }, keys: string[], val: string) {
+ let i = 0;
+ const len = keys.length;
+ while (i < len) {
+ const key = keys[i++];
+ target[key] = (i === len) ? val : typeof target[key] === 'object' ? target[key] : {};
+ target = target[key];
+ }
+}
+
+export function deepMerge(target: { [key: string]: any }, source: { [key: string]: any }) {
+ if (typeof target === 'object' && typeof source === 'object') {
+ for (const key of Object.keys(source)) {
+ if (!target[key] || typeof source[key] !== 'object')
+ target[key] = source[key];
+ else
+ deepMerge(target[key], source[key]);
+ }
+ }
+ return target;
+}
diff --git a/tools/core/parser.ts b/tools/core/parser.ts
index e93f812..3cedf9d 100644
--- a/tools/core/parser.ts
+++ b/tools/core/parser.ts
@@ -310,3 +310,18 @@ export function parseSequenceExpressions(code: string, alias: string): CallExpre
return sequenceExpressions;
}
+
+/**
+ * Get $translate alias
+ */
+export function getTranslateAlias(code: string): string {
+ let translateAlias = code.match(/(?<=\$translate as).*?(?=,|\})/s)?.[0]?.trim() || '$translate';
+ // Escape special characters / Assert position at a word boundary
+ translateAlias = translateAlias.startsWith('$') ? `\\${translateAlias}` : `\\b${translateAlias}`;
+ return translateAlias;
+}
+
+export function parseJson(target: { [key: string]: any }, source: string): { [key: string]: any } {
+ target = { ...target, ...JSON.parse(source) };
+ return target;
+}
diff --git a/tools/extract.md b/tools/extract.md
new file mode 100644
index 0000000..343e424
--- /dev/null
+++ b/tools/extract.md
@@ -0,0 +1,68 @@
+# Qwik Speak Extract
+
+> Extract translations directly from the components
+
+## Usage
+### Command
+#### Get the code ready
+Optionally, you can use a default value for the keys. The syntax is `key@@[default value]`:
+```html
+
{t('app.title@@Qwik Speak'}
+
{t('home.greeting@@Hi! I am {{name}}', { name: 'Qwik Speak' })}
+
+```
+When you use a default value, it will be used as initial value for the key in every translation.
+
+> Note. A key will not be extracted when a function argument is a variable (dynamic).
+
+#### Naming conventions
+If you use scoped translations, the first property will be used as filename:
+```html
+
{t('app.text)}
+
{t('home.greeting)}
+```
+will generate two files for each language:
+```
+public/i18n
+│
+└───en-US
+ app.json
+ home.json
+```
+But if you don't always use scoped translations, only one file for each language will be generated, called `app.json`
+
+#### Configuration
+Add the command in `package.json`, and provide at least the supported languages:
+```json
+"scripts": {
+ "qwik-speak-extract": "qwik-speak-extract --supportedLangs=en-US,it-IT"
+}
+```
+Available options:
+- `basePath` The base path. Default to `'./'`
+- `sourceFilesPath` Path to files to search for translations. Default to `'src'`
+- `assetsPath` Path to translation files: `[basePath]/[assetsPath]/[lang]/*.json`. Default to `'public/i18n'`
+- `format` The format of the translation files. Default to `'json'`
+- `supportedLangs` Supported langs. Required
+- `keySeparator` Separator of nested keys. Default is `'.'`
+- `keyValueSeparator` Key-value separator. Default is `'@@'`
+
+> Note. Currently, only `json` is supported as format
+
+#### Running
+```shell
+npm run qwik-speak-extract
+```
+
+#### Updating
+If you add new translations in the components, or a new language, they will be merged into the existing files without losing the translations already made.
+
+### Using it programmatically
+Rather than using the command, you can invoke `qwikSpeakExtract` function:
+```typescript
+import { qwikSpeakExtract } from 'qwik-speak/extract';
+
+await qwikSpeakExtract({
+ supportedLangs: ['en-US', 'it-IT']
+});
+```
diff --git a/tools/extract/cli.js b/tools/extract/cli.js
deleted file mode 100644
index da8a453..0000000
--- a/tools/extract/cli.js
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env node
-
-import { extract } from './index.mjs';
-
-extract();
diff --git a/tools/extract/cli.ts b/tools/extract/cli.ts
new file mode 100644
index 0000000..c6d2042
--- /dev/null
+++ b/tools/extract/cli.ts
@@ -0,0 +1,78 @@
+import type { QwikSpeakExtractOptions } from './types';
+import { parseArgument } from '../core/cli-parser';
+import { qwikSpeakExtract } from './index';
+
+const assertType = (value: any, type: string): boolean => {
+ if (type === value) return true;
+ if (type === 'array' && Array.isArray(value)) return true;
+ if (type === 'string' && typeof (value) === 'string') return true;
+ return false;
+};
+
+const wrongOption = (key: string, value: any): string => `- option "${key}": wrong value ${JSON.stringify(value)}`;
+const missingOption = (name: string): string => `- missing option: "${name}"`;
+
+const args = process.argv.slice(2);
+
+const options: Partial = {};
+
+const errors: string[] = [];
+
+// Parse arguments
+for (const arg of args) {
+ const { key, value } = parseArgument(arg);
+ switch (key) {
+ case 'basePath':
+ if (assertType(value, 'string')) options.basePath = value;
+ else errors.push(wrongOption(key, value));
+ break;
+ case 'sourceFilesPath':
+ if (assertType(value, 'string')) options.sourceFilesPath = value;
+ else errors.push(wrongOption(key, value));
+ break;
+ case 'assetsPath':
+ if (assertType(value, 'string')) options.assetsPath = value;
+ else errors.push(wrongOption(key, value));
+ break;
+ case 'format':
+ if (assertType(value, 'json')) options.format = value;
+ else errors.push(wrongOption(key, value));
+ break;
+ case 'supportedLangs':
+ if (assertType(value, 'array')) options.supportedLangs = value;
+ else errors.push(wrongOption(key, value));
+ break;
+ case 'keySeparator':
+ if (assertType(value, 'string')) options.keySeparator = value;
+ else errors.push(wrongOption(key, value));
+ break;
+ case 'keyValueSeparator':
+ if (assertType(value, 'string')) options.keyValueSeparator = value;
+ else errors.push(wrongOption(key, value));
+ break;
+ case 'error':
+ errors.push(value);
+ break;
+ default:
+ errors.push(`- unknown option: "${key}"`);
+ }
+}
+
+// Required options
+if (!options.supportedLangs) errors.push(missingOption('supportedLangs'));
+
+// Log errors
+if (errors.length > 0) {
+ console.log('\x1b[36m%s\x1b[0m', 'Qwik Speak Extract options errors:');
+ for (const error of errors) {
+ console.log('\x1b[33m%s\x1b[0m', error);
+ }
+
+ process.exitCode = 1; // Exit process
+}
+
+// Process
+console.log('\x1b[36m%s\x1b[0m', 'Qwik Speak Extract');
+console.log('\x1b[32m%s\x1b[0m', 'extracting translation...');
+
+qwikSpeakExtract(options as QwikSpeakExtractOptions);
diff --git a/tools/extract/extract.ts b/tools/extract/extract.ts
deleted file mode 100644
index 0fc61bb..0000000
--- a/tools/extract/extract.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export async function extract() {
- const args = process.argv;
-
- // TODO
-}
diff --git a/tools/extract/index.ts b/tools/extract/index.ts
index b96ed90..5f15e3e 100644
--- a/tools/extract/index.ts
+++ b/tools/extract/index.ts
@@ -1 +1,215 @@
-export { extract } from './extract';
+import { readdir, readFile, writeFile } from 'fs/promises';
+import { existsSync, mkdirSync } from 'fs';
+import { extname, join, normalize } from 'path';
+
+import type { QwikSpeakExtractOptions, Translation } from './types';
+import { getTranslateAlias, parseJson, parseSequenceExpressions } from '../core/parser';
+import { deepMerge, deepSet } from '../core/merge';
+import { minDepth, sortTarget, toJsonString } from '../core/format';
+
+/**
+ * Extract translations from source files
+ */
+export async function qwikSpeakExtract(options: QwikSpeakExtractOptions) {
+ // Resolve options
+ const resolvedOptions: Required = {
+ ...options,
+ basePath: options.basePath ?? './',
+ sourceFilesPath: options.sourceFilesPath ?? 'src',
+ assetsPath: options.assetsPath ?? 'public/i18n',
+ format: options.format ?? 'json',
+ keySeparator: options.keySeparator ?? '.',
+ keyValueSeparator: options.keyValueSeparator ?? '@@',
+ }
+
+ // Logs
+ const stats = new Map();
+
+ const baseSources = normalize(`${resolvedOptions.basePath}/${resolvedOptions.sourceFilesPath}`);
+
+ // Source files
+ const sourceFiles: string[] = [];
+ // Translation data
+ const translation: Translation = Object.fromEntries(resolvedOptions.supportedLangs.map(value => [value, {}]));
+
+ /**
+ * Read source files recursively
+ */
+ const readSourceFiles = async (sourceFilesPath: string) => {
+ const files = await readdir(sourceFilesPath, { withFileTypes: true });
+ for (const file of files) {
+ const filePath = join(sourceFilesPath, file.name);
+ const ext = extname(file.name);
+ if (file.isDirectory()) {
+ await readSourceFiles(filePath);
+ } else if (/\.js|\.ts|\.jsx|\.tsx/.test(ext)) {
+ sourceFiles.push(filePath);
+ }
+ }
+ };
+
+ /**
+ * Parse source file to return keys
+ */
+ const parseSourceFile = async (file: string): Promise => {
+ const keys: string[] = [];
+
+ const code = await readFile(normalize(`${resolvedOptions.basePath}/${file}`), 'utf8');
+
+ if (/\$translate/.test(code)) {
+ const alias = getTranslateAlias(code);
+ // Parse sequence
+ const sequence = parseSequenceExpressions(code, alias);
+ for (const expr of sequence) {
+ const args = expr.arguments;
+
+ if (args?.[0]?.value) {
+ if (args[0].type === 'Identifier') {
+ stats.set('dynamic', (stats.get('dynamic') ?? 0) + 1);
+ continue;
+ }
+ if (args[0].type === 'Literal') {
+ if (args[0].value !== 'key' && /\${.*}/.test(args[0].value)) {
+ stats.set('dynamic', (stats.get('dynamic') ?? 0) + 1);
+ continue;
+ }
+ }
+ if (args[1]?.type === 'Identifier' || args[1]?.type === 'CallExpression' ||
+ args[2]?.type === 'Identifier' || args[2]?.type === 'CallExpression' ||
+ args[3]?.type === 'Identifier' || args[3]?.type === 'CallExpression') {
+ stats.set('dynamic', (stats.get('dynamic') ?? 0) + 1);
+ continue;
+ }
+
+ keys.push(args[0].value);
+ stats.set('keys', (stats.get('keys') ?? 0) + 1);
+ }
+ }
+ }
+
+ return keys;
+ };
+
+ /**
+ * Read, deep merge & sort translation data
+ */
+ const readAssets = async () => {
+ await Promise.all(resolvedOptions.supportedLangs.map(async lang => {
+ const baseAssets = normalize(`${resolvedOptions.basePath}/${resolvedOptions.assetsPath}/${lang}`);
+
+ if (!existsSync(baseAssets)) return;
+
+ const files = await readdir(baseAssets);
+
+ if (files.length > 0) {
+ const ext = extname(files[0]);
+ let data: Translation = {};
+
+ const tasks = files.map(filename => readFile(`${baseAssets}/${filename}`, 'utf8'));
+ const sources = await Promise.all(tasks);
+
+ for (const source of sources) {
+ if (source) {
+ switch (ext) {
+ case '.json':
+ data = parseJson(data, source);
+ break;
+ }
+ }
+ }
+
+ deepMerge(translation[lang], data);
+
+ // Sort by key
+ translation[lang] = sortTarget(translation[lang]);
+ }
+ }));
+ };
+
+ /**
+ * Write translation data
+ *
+ * Naming convention of keys:
+ * min depth > 1: filenames = each top-level property name
+ * min depth = 1: filename = 'app'
+ */
+ const writeAssets = async () => {
+ for (const lang of resolvedOptions.supportedLangs) {
+ const baseAssets = normalize(`${resolvedOptions.basePath}/${resolvedOptions.assetsPath}/${lang}`);
+
+ if (!existsSync(baseAssets)) {
+ mkdirSync(baseAssets, { recursive: true });
+ }
+
+ if (minDepth(translation[lang]) > 1) {
+ for (const topLevelProperty of Object.keys(translation[lang])) {
+ let data: string;
+ switch (resolvedOptions.format) {
+ case 'json':
+ // Computed property name
+ data = toJsonString({ [topLevelProperty]: translation[lang][topLevelProperty] });
+ break;
+ }
+ const file = normalize(`${baseAssets}/${topLevelProperty}.${resolvedOptions.format}`);
+ await writeFile(file, data);
+ console.log(file);
+ }
+ } else {
+ let data: string;
+ switch (resolvedOptions.format) {
+ case 'json':
+ data = toJsonString(translation[lang]);
+ break;
+ }
+
+ const file = normalize(`${baseAssets}/app.${resolvedOptions.format}`);
+ await writeFile(file, data);
+ console.log(file);
+ }
+ }
+ };
+
+ /**
+ * Start pipeline
+ */
+ await readSourceFiles(baseSources);
+
+ const tasks = sourceFiles.map(file => parseSourceFile(file));
+ const sources = await Promise.all(tasks);
+
+ let keys: string[] = [];
+ for (const source of sources) {
+ keys = keys.concat(source);
+ }
+
+ // Deep set
+ for (let key of keys) {
+ let defaultValue: string | undefined = undefined;
+
+ [key, defaultValue] = key.split(resolvedOptions.keyValueSeparator);
+
+ for (const lang of resolvedOptions.supportedLangs) {
+ deepSet(translation[lang], key.split(resolvedOptions.keySeparator), defaultValue || '');
+ }
+ }
+
+ // Read, deep merge & sort
+ await readAssets();
+
+ // Write
+ await writeAssets();
+
+ // Log
+ for (const [key, value] of stats) {
+ switch (key) {
+ case 'keys':
+ console.log('\x1b[32m%s\x1b[0m', `extracted keys: ${value}`);
+ break;
+ case 'dynamic':
+ console.log('\x1b[32m%s\x1b[0m', `skipped keys due to dynamic params: ${value}`);
+ break;
+ }
+ }
+}
+
+export type { QwikSpeakExtractOptions };
diff --git a/tools/extract/types.ts b/tools/extract/types.ts
new file mode 100644
index 0000000..aafac70
--- /dev/null
+++ b/tools/extract/types.ts
@@ -0,0 +1,38 @@
+/**
+ * Qwik Speak Extract Options
+ */
+export interface QwikSpeakExtractOptions {
+ /**
+ * The base path. Default to './'
+ */
+ basePath?: string;
+ /**
+ * Path to files to search for translations. Default to 'src'
+ */
+ sourceFilesPath?: string;
+ /**
+ * Path to translation files: [basePath]/[assetsPath]/[lang]/*.json. Default to 'public/i18n'
+ */
+ assetsPath?: string;
+ /**
+ * The format of the translation files. Default to 'json'
+ */
+ format?: 'json';
+ /**
+ * Supported langs. Required
+ */
+ supportedLangs: string[];
+ /**
+ * Separator of nested keys. Default is '.'
+ */
+ keySeparator?: string;
+ /**
+ * Key-value separator. Default is '@@'
+ */
+ keyValueSeparator?: string;
+}
+
+/**
+ * Translation data
+ */
+export type Translation = { [key: string]: any };
diff --git a/tools/inline.md b/tools/inline.md
index bbede58..e753178 100644
--- a/tools/inline.md
+++ b/tools/inline.md
@@ -25,7 +25,7 @@ export default defineConfig(() => {
qwikVite(),
qwikSpeakInline({
basePath: './',
- assetsPath: './public/i18n',
+ assetsPath: 'public/i18n',
supportedLangs: ['en-US', 'it-IT'],
defaultLang: 'en-US'
}),
@@ -63,7 +63,7 @@ When there are translations with dynamic keys or params, you can manage them at
}
});
```
-Likewise, you can also create lazy loaded runtime files for the different pages.
+Likewise, you can also create scoped runtime files for the different pages.
> Note. The `plural` function must be handled as a dynamic translation
diff --git a/tools/inline/plugin.ts b/tools/inline/plugin.ts
index 0628b2c..edb3bcc 100644
--- a/tools/inline/plugin.ts
+++ b/tools/inline/plugin.ts
@@ -1,10 +1,10 @@
import type { Plugin } from 'vite';
import { readFile, readdir } from 'fs/promises';
import { createWriteStream } from 'fs';
-import path from 'path';
+import { extname, normalize } from 'path';
import type { QwikSpeakInlineOptions, Translation } from './types';
-import type { Argument, Property } from '../core/parser';
+import { Argument, getTranslateAlias, parseJson, Property } from '../core/parser';
import { parseSequenceExpressions } from '../core/parser';
// Logs
@@ -59,12 +59,12 @@ export function qwikSpeakInline(options: QwikSpeakInlineOptions): Plugin {
async buildStart() {
// For all langs
await Promise.all(resolvedOptions.supportedLangs.map(async lang => {
- const baseDir = path.normalize(`${resolvedOptions.basePath}${resolvedOptions.assetsPath}/${lang}`);
+ const baseDir = normalize(`${resolvedOptions.basePath}/${resolvedOptions.assetsPath}/${lang}`);
// For all files
const files = await readdir(baseDir);
if (files.length > 0) {
- const ext = path.extname(files[0]);
+ const ext = extname(files[0]);
let data: Translation = {};
const tasks = files.map(filename => readFile(`${baseDir}/${filename}`, 'utf8'));
@@ -74,7 +74,7 @@ export function qwikSpeakInline(options: QwikSpeakInlineOptions): Plugin {
if (source) {
switch (ext) {
case '.json':
- data = await parseJson(data, source);
+ data = parseJson(data, source);
break;
}
}
@@ -117,17 +117,12 @@ export function qwikSpeakInline(options: QwikSpeakInlineOptions): Plugin {
return plugin;
}
-export async function parseJson(target: Translation, source: string): Promise {
- target = { ...target, ...JSON.parse(source) };
- return target;
-}
-
export function inline(
code: string,
translation: Translation,
opts: Required
): string | null {
- const alias = getAlias(code);
+ const alias = getTranslateAlias(code);
// Parse sequence
const sequence = parseSequenceExpressions(code, alias);
@@ -219,13 +214,6 @@ export function inline(
return code;
}
-export function getAlias(code: string): string {
- let translateAlias = code.match(/(?<=\$translate as).*?(?=,|\})/s)?.[0]?.trim() || '$translate';
- // Escape special characters / Assert position at a word boundary
- translateAlias = translateAlias.startsWith('$') ? `\\${translateAlias}` : `\\b${translateAlias}`;
- return translateAlias;
-}
-
export function multilingual(lang: string | undefined, supportedLangs: string[]): string | undefined {
if (!lang) return undefined;
return supportedLangs.find(x => x === lang);
diff --git a/tools/tests/extract.test.ts b/tools/tests/extract.test.ts
new file mode 100644
index 0000000..b32831b
--- /dev/null
+++ b/tools/tests/extract.test.ts
@@ -0,0 +1,48 @@
+import fs from 'fs/promises';
+import { normalize } from 'path';
+
+import { qwikSpeakExtract } from '../extract/index';
+import { mockAsset, mockSource } from './mock';
+
+jest.mock('fs/promises');
+
+describe('extract', () => {
+ beforeEach(() => {
+ // Reset mocks
+ jest.resetAllMocks();
+ });
+
+ test('extract json', async () => {
+ fs.readdir = jest.fn()
+ .mockImplementationOnce(() => [{ name: 'home.tsx', isDirectory: () => false }])
+ .mockImplementationOnce(() => ['home.json']);
+ fs.readFile = jest.fn()
+ .mockImplementationOnce(() => mockSource)
+ .mockImplementationOnce(() => mockAsset);
+ fs.writeFile = jest.fn();
+
+ await qwikSpeakExtract({
+ supportedLangs: ['en-US']
+ });
+
+ expect(fs.writeFile).toHaveBeenCalledTimes(2);
+ expect(fs.writeFile).toHaveBeenNthCalledWith(1, normalize('public/i18n/en-US/app.json'), `{
+ "app": {
+ "subtitle": "Translate your Qwik apps into any language",
+ "title": "Qwik Speak"
+ }
+}`);
+ expect(fs.writeFile).toHaveBeenNthCalledWith(2, normalize('public/i18n/en-US/home.json'), `{
+ "home": {
+ "dates": "Dates & relative time",
+ "greeting": "Hi! I am {{name}}",
+ "increment": "Increment",
+ "numbers": "Numbers & currencies",
+ "params": "",
+ "plural": "Plural",
+ "tags": "Html tags",
+ "text": "Internationalization (i18n) library to translate texts, dates and numbers in Qwik apps"
+ }
+}`);
+ });
+});
diff --git a/tools/tests/format.test.ts b/tools/tests/format.test.ts
new file mode 100644
index 0000000..1979b17
--- /dev/null
+++ b/tools/tests/format.test.ts
@@ -0,0 +1,20 @@
+import { minDepth, sortTarget } from '../core/format';
+
+describe('format', () => {
+ test('minDepth', () => {
+ let target = {};
+ let depth = minDepth(target);
+ expect(depth).toBe(0);
+ target = { key1: { subkey1: 'Subkey1' }, key2: 'Key2' };
+ depth = minDepth(target);
+ expect(depth).toBe(1);
+ target = { key1: { subkey1: 'Subkey1' }, key2: { subkey2: 'Subkey2' } };
+ depth = minDepth(target);
+ expect(depth).toBe(2);
+ });
+ test('sortTarget', () => {
+ let target = { b: { b: 'B', a: 'A' }, a: 'A' };
+ target = sortTarget(target);
+ expect(target).toEqual({ a: 'A', b: { a: 'A', b: 'B' } });
+ });
+});
diff --git a/tools/tests/inline.test.ts b/tools/tests/inline.test.ts
index 5d835fa..76921cc 100644
--- a/tools/tests/inline.test.ts
+++ b/tools/tests/inline.test.ts
@@ -1,23 +1,7 @@
-import { getKey, getValue, qwikSpeakInline, transpileFn, getAlias, addLang } from '../inline/plugin';
+import { getKey, getValue, qwikSpeakInline, transpileFn, addLang } from '../inline/plugin';
import { inlinedCode, mockCode } from './mock';
describe('inline', () => {
- test('getAlias', () => {
- let alias = getAlias(`import {
- $translate as t,
- plural as p,
- formatDate as fd,
- formatNumber as fn,
- relativeTime as rt,
- Speak,
- useSpeakLocale
- } from 'qwik-speak';`);
- expect(alias).toBe('\\bt');
- alias = getAlias("import { $translate as t } from 'qwik-speak';");
- expect(alias).toBe('\\bt');
- alias = getAlias("import { $translate } from 'qwik-speak';");
- expect(alias).toBe('\\$translate');
- });
test('getKey', () => {
let key = getKey('key1', '@@');
expect(key).toBe('key1');
diff --git a/tools/tests/merge.test.ts b/tools/tests/merge.test.ts
new file mode 100644
index 0000000..ed07cd4
--- /dev/null
+++ b/tools/tests/merge.test.ts
@@ -0,0 +1,15 @@
+import { deepMerge, deepSet } from '../core/merge';
+
+describe('merge', () => {
+ test('deepSet', () => {
+ const target = { key1: { subkey1: 'Subkey1' } };
+ deepSet(target, ['key1', 'subkey2'], 'Subkey2');
+ expect(target).toEqual({ key1: { subkey1: 'Subkey1', subkey2: 'Subkey2' } });
+ });
+ test('deepMerge', () => {
+ const target = { key1: { subkey1: 'Subkey1' } };
+ const source = { key1: { subkey1: 'NewSubkey1', subkey2: 'Subkey2' } };
+ deepMerge(target, source);
+ expect(target).toEqual({ key1: { subkey1: 'NewSubkey1', subkey2: 'Subkey2' } });
+ });
+});
diff --git a/tools/tests/mock.ts b/tools/tests/mock.ts
index b7505a9..42753d0 100644
--- a/tools/tests/mock.ts
+++ b/tools/tests/mock.ts
@@ -159,3 +159,86 @@ export const s_xJBzwgVGKaQ = ()=>{
]
});
};`;
+
+export const mockSource = `import { component$, useStore } from '@builder.io/qwik';
+import { DocumentHead, StaticGenerateHandler } from '@builder.io/qwik-city';
+import {
+ $translate as t,
+ plural as p,
+ formatDate as fd,
+ formatNumber as fn,
+ relativeTime as rt,
+ Speak,
+ useSpeakLocale
+} from 'qwik-speak';
+
+import { config } from '../../speak-config';
+
+export const Home = component$(() => {
+ const units = useSpeakLocale().units!;
+
+ const state = useStore({ count: 0 });
+
+ return (
+ <>
+
{t('app.title@@Qwik Speak')}
+
{t('app.subtitle@@Translate your Qwik apps into any language')}