diff --git a/.textlintrc.js b/.textlintrc.js index e0a4ad87b..cc626c6e4 100644 --- a/.textlintrc.js +++ b/.textlintrc.js @@ -12,6 +12,8 @@ module.exports = { 'max-doc-width': 360, 'unexpanded-acronym': { ignore_acronyms: [ + 'JIT', + 'AOT', 'UMD', 'NPM', 'CDN', diff --git a/benchmark/complex-jit-aot.mjs b/benchmark/complex-jit-aot.mjs new file mode 100644 index 000000000..50a105364 --- /dev/null +++ b/benchmark/complex-jit-aot.mjs @@ -0,0 +1,82 @@ +import { createCommonJS } from 'mlly' +import { baseCompile } from '@intlify/message-compiler' +import { + translate, + createCoreContext, + compile, + registerMessageCompiler, + clearCompileCache +} from '@intlify/core-base' +import { createI18n } from 'vue-i18n' +import { resolve, dirname } from 'pathe' +import { readJson } from './utils.mjs' + +const { require } = createCommonJS(import.meta.url) +const { Suite } = require('benchmark') + +function precompile(data) { + const keys = Object.keys(data) + keys.forEach(key => { + const { ast } = baseCompile(data[key], { jit: true, location: false }) + data[key] = ast + }) + return data +} + +async function main() { + const resources = await readJson( + resolve(dirname('.'), './benchmark/complex.json') + ) + const len = Object.keys(resources).length + + console.log(`complex pattern on ${len} resources (JIT + AOT):`) + console.log() + + registerMessageCompiler(compile) + const precompiledResources = precompile(resources) + + const ctx = createCoreContext({ + locale: 'en', + modifiers: { + caml: val => val + }, + messages: { + en: precompiledResources + } + }) + + const i18n = createI18n({ + legacy: false, + locale: 'en', + modifiers: { + caml: val => val + }, + messages: { + en: precompiledResources + } + }) + + new Suite('complex pattern') + .add(`resolve time with core`, () => { + translate(ctx, 'complex500', 2) + }) + .add(`resolve time on composition`, () => { + clearCompileCache() + i18n.global.t('complex500', 2) + }) + .add(`resolve time on composition with compile cache`, () => { + i18n.global.t('complex500', 2) + }) + .on('error', event => { + console.log(String(event.target)) + }) + .on('cycle', event => { + console.log(String(event.target)) + }) + .run() +} + +main().catch(err => { + console.error(err) + process.exit(1) +}) diff --git a/benchmark/complex-jit.mjs b/benchmark/complex-jit.mjs new file mode 100644 index 000000000..a46c9d2d7 --- /dev/null +++ b/benchmark/complex-jit.mjs @@ -0,0 +1,71 @@ +import { createCommonJS } from 'mlly' +import { + translate, + createCoreContext, + compile, + registerMessageCompiler, + clearCompileCache +} from '@intlify/core-base' +import { createI18n } from 'vue-i18n' +import { resolve, dirname } from 'pathe' +import { readJson } from './utils.mjs' + +const { require } = createCommonJS(import.meta.url) +const { Suite } = require('benchmark') + +async function main() { + const resources = await readJson( + resolve(dirname('.'), './benchmark/complex.json') + ) + const len = Object.keys(resources).length + + console.log(`complex pattern on ${len} resources (JIT):`) + console.log() + + registerMessageCompiler(compile) + + const ctx = createCoreContext({ + locale: 'en', + modifiers: { + caml: val => val + }, + messages: { + en: resources + } + }) + + const i18n = createI18n({ + legacy: false, + locale: 'en', + modifiers: { + caml: val => val + }, + messages: { + en: resources + } + }) + + new Suite('complex pattern') + .add(`resolve time with core`, () => { + translate(ctx, 'complex500', 2) + }) + .add(`resolve time on composition`, () => { + clearCompileCache() + i18n.global.t('complex500', 2) + }) + .add(`resolve time on composition with compile cache`, () => { + i18n.global.t('complex500', 2) + }) + .on('error', event => { + console.log(String(event.target)) + }) + .on('cycle', event => { + console.log(String(event.target)) + }) + .run() +} + +main().catch(err => { + console.error(err) + process.exit(1) +}) diff --git a/benchmark/complex.mjs b/benchmark/complex.mjs index a53b188fd..16583efb6 100644 --- a/benchmark/complex.mjs +++ b/benchmark/complex.mjs @@ -12,51 +12,45 @@ const { require } = createCommonJS(import.meta.url) const { Suite } = require('benchmark') async function main() { - const data = await readJson(resolve(dirname('.'), './benchmark/complex.json')) - const len = Object.keys(data).length + const resources = await readJson( + resolve(dirname('.'), './benchmark/complex.json') + ) + const len = Object.keys(resources).length - console.log(`complex pattern on ${len} resources:`) + console.log(`complex pattern on ${len} resources (AOT):`) console.log() - let i18n + const ctx = createCoreContext({ + locale: 'en', + modifiers: { + caml: val => val + }, + messages: { + en: resources + } + }) + + const i18n = createI18n({ + legacy: false, + locale: 'en', + modifiers: { + caml: val => val + }, + messages: { + en: resources + } + }) new Suite('complex pattern') .add(`resolve time with core`, () => { - const ctx = createCoreContext({ - locale: 'en', - modifiers: { - caml: val => val - }, - messages: { - en: data - } - }) - for (const [key] of Object.entries(data)) { - translate(ctx, key, 2) - } + translate(ctx, 'complex500', 2) }) .add(`resolve time on composition`, () => { clearCompileCache() - - i18n = createI18n({ - legacy: false, - locale: 'en', - modifiers: { - caml: val => val - }, - messages: { - en: data - } - }) - - for (const [key] of Object.entries(data)) { - i18n.global.t(key, 2) - } + i18n.global.t('complex500', 2) }) .add(`resolve time on composition with compile cache`, () => { - for (const [key] of Object.entries(data)) { - i18n.global.t(key, 2) - } + i18n.global.t('complex500', 2) }) .on('error', event => { console.log(String(event.target)) diff --git a/benchmark/index.mjs b/benchmark/index.mjs index 21ac71591..accf768ad 100644 --- a/benchmark/index.mjs +++ b/benchmark/index.mjs @@ -1,26 +1,38 @@ -import { exec } from 'child_process' -import { dirname } from 'pathe' +import { spawn } from 'child_process' function run(pattner) { return new Promise((resolve, reject) => { - exec( - `node ./benchmark/${pattner}.mjs`, - { cwd: dirname('.') }, - (error, stdout) => { - if (error) { - return reject(error) - } - console.log(stdout) + const child = spawn('node', [`./benchmark/${pattner}.mjs`], { + stdio: 'inherit' + }) + + child.once('error', err => { + reject(err) + }) + + child.once('exit', code => { + if (code !== 0) { + reject(new Error(`exit with code ${code}`)) + } else { resolve() } - ) + }) }) } ;(async () => { try { - for (const p of ['compile', 'simple', 'complex']) { + for (const p of [ + 'compile', + 'simple', + 'simple-jit', + 'simple-jit-aot', + 'complex', + 'complex-jit', + 'complex-jit-aot' + ]) { await run(p) + console.log() } } catch (e) { console.error(e) diff --git a/benchmark/simple-jit-aot.mjs b/benchmark/simple-jit-aot.mjs new file mode 100644 index 000000000..a9229bb37 --- /dev/null +++ b/benchmark/simple-jit-aot.mjs @@ -0,0 +1,75 @@ +import { createCommonJS } from 'mlly' +import { baseCompile } from '@intlify/message-compiler' +import { + translate, + createCoreContext, + compile, + registerMessageCompiler, + clearCompileCache +} from '@intlify/core-base' +import { createI18n } from 'vue-i18n' +import { resolve, dirname } from 'pathe' +import { readJson } from './utils.mjs' + +const { require } = createCommonJS(import.meta.url) +const { Suite } = require('benchmark') + +function precompile(data) { + const keys = Object.keys(data) + keys.forEach(key => { + const { ast } = baseCompile(data[key], { jit: true, location: false }) + data[key] = ast + }) + return data +} + +async function main() { + const resources = await readJson( + resolve(dirname('.'), './benchmark/simple.json') + ) + const len = Object.keys(resources).length + + console.log(`simple pattern on ${len} resources (JIT + AOT):`) + console.log() + + registerMessageCompiler(compile) + const precompiledResources = precompile(resources) + + const ctx = createCoreContext({ + locale: 'en', + messages: { + en: precompiledResources + } + }) + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { + en: precompiledResources + } + }) + + new Suite('simple pattern') + .add(`resolve time with core`, () => { + translate(ctx, 'hello500') + }) + .add(`resolve time on composition`, () => { + clearCompileCache() + i18n.global.t('hello500') + }) + .add(`resolve time on composition with compile cache`, () => { + i18n.global.t('hello500') + }) + .on('error', event => { + console.log(String(event.target)) + }) + .on('cycle', event => { + console.log(String(event.target)) + }) + .run() +} + +main().catch(err => { + console.error(err) + process.exit(1) +}) diff --git a/benchmark/simple-jit.mjs b/benchmark/simple-jit.mjs new file mode 100644 index 000000000..fad102285 --- /dev/null +++ b/benchmark/simple-jit.mjs @@ -0,0 +1,64 @@ +import { createCommonJS } from 'mlly' +import { + translate, + createCoreContext, + compile, + registerMessageCompiler, + clearCompileCache +} from '@intlify/core-base' +import { createI18n } from 'vue-i18n' +import { resolve, dirname } from 'pathe' +import { readJson } from './utils.mjs' + +const { require } = createCommonJS(import.meta.url) +const { Suite } = require('benchmark') + +async function main() { + const resources = await readJson( + resolve(dirname('.'), './benchmark/simple.json') + ) + const len = Object.keys(resources).length + + console.log(`simple pattern on ${len} resources (JIT):`) + console.log() + + registerMessageCompiler(compile) + + const ctx = createCoreContext({ + locale: 'en', + messages: { + en: resources + } + }) + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { + en: resources + } + }) + + new Suite('simple pattern') + .add(`resolve time with core`, () => { + translate(ctx, 'hello500') + }) + .add(`resolve time on composition`, () => { + clearCompileCache() + i18n.global.t('hello500') + }) + .add(`resolve time on composition with compile cache`, () => { + i18n.global.t('hello500') + }) + .on('error', event => { + console.log(String(event.target)) + }) + .on('cycle', event => { + console.log(String(event.target)) + }) + .run() +} + +main().catch(err => { + console.error(err) + process.exit(1) +}) diff --git a/benchmark/simple.mjs b/benchmark/simple.mjs index 04a31e789..231f60068 100644 --- a/benchmark/simple.mjs +++ b/benchmark/simple.mjs @@ -12,44 +12,38 @@ const { require } = createCommonJS(import.meta.url) const { Suite } = require('benchmark') async function main() { - const data = await readJson(resolve(dirname('.'), './benchmark/simple.json')) - const len = Object.keys(data).length + const resources = await readJson( + resolve(dirname('.'), './benchmark/simple.json') + ) + const len = Object.keys(resources).length - console.log(`simple pattern on ${len} resources:`) + console.log(`simple pattern on ${len} resources (AOT):`) console.log() - let i18n + const ctx = createCoreContext({ + locale: 'en', + messages: { + en: resources + } + }) + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { + en: resources + } + }) - new Suite('complex pattern') + new Suite('simple pattern') .add(`resolve time with core`, () => { - const ctx = createCoreContext({ - locale: 'en', - messages: { - en: data - } - }) - for (const [key] of Object.entries(data)) { - translate(ctx, key) - } + translate(ctx, 'hello500') }) .add(`resolve time on composition`, () => { clearCompileCache() - - i18n = createI18n({ - legacy: false, - locale: 'en', - messages: { - en: data - } - }) - for (const [key] of Object.entries(data)) { - i18n.global.t(key) - } + i18n.global.t('hello500') }) .add(`resolve time on composition with compile cache`, () => { - for (const [key] of Object.entries(data)) { - i18n.global.t(key) - } + i18n.global.t('hello500') }) .on('error', event => { console.log(String(event.target)) diff --git a/docs/guide/advanced/optimization.md b/docs/guide/advanced/optimization.md index 35e0a26a2..9f52f10e9 100644 --- a/docs/guide/advanced/optimization.md +++ b/docs/guide/advanced/optimization.md @@ -14,10 +14,21 @@ For bundler, it’s configured to bundle `vue-i18n.esm-bundler.js` with [`@intli IF [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) is enabled, `vue-i18n.esm-bundler.js` would not work with compiler due to `eval` statements. These statements violate the `default-src 'self'` header. Instead you need to use `vue-i18n.runtime.esm-bundler.js`. ::: +:::warning NOTICE +From v9.3, the CSP issue can be worked around by JIT compilation of the vue-i18n message compiler. See [JIT compilation for details](#jit-compilation). +::: + The use of this ES Module means that **all locale messages have to pre-compile to Message functions**. what this means it improves performance because vue-i18n just only execute Message functions, so no compilation. Also, the message compiler is not bundled, therefore **bundle size can be reduced** +:::warning NOTICE +If you are using the JIT compilation, all locale messages will not necessarily be compiled with the Message function. + +Also, since the message compiler is also bundled, the bundle size cannot be reduced. **This is a trade-off**. +::: + + ## How to configure We can configure these modules with module path using the module resolve alias feature (e.g. `resolve.alias` vite and webpack) of some bundler, but It takes time and effort. @@ -182,19 +193,49 @@ About options and features, see the detail [page](https://github.com/intlify/bun No need to do anything. [Quasar CLI](https://quasar.dev) takes care of the optimizations for you. -## Reduce bundle size with feature build flags + +## Feature build flags + +### Reduce bundle size with tree-shaking The `esm-bundler` builds now exposes global feature flags that can be overwritten at compile time: - `__VUE_I18N_FULL_INSTALL__` (enable/disable, in addition to vue-i18n APIs, components and directives all fully support installation: `true`) - `__VUE_I18N_LEGACY_API__` (enable/disable vue-i18n legacy style APIs support, default: `true`) -- `__INTLIFY_PROD_DEVTOOLS__` (enable/disable `@intlify/devtools` support in production, default: `false`) + +The build will work without configuring these flags, however it is **strongly recommended** to properly configure them in order to get proper tree shaking in the final bundle. + +About how to configure for bundler, see the [here](#configure-feature-flags-for-bundler). + +### JIT compilation + +:::tip Support Version +:new: 9.3+ +::: + +Before v9.3, vue-i18n message compiler precompiled locale messages like AOT. + +However, it had the following issues: + +- CSP issues: hard to work on service/web workers, edge-side runtimes of CDNs and etc. +- Back-end integration: hard to get messages from back-end such as database via API and localize them dynamically + +To solve these issues, JIT style compilation is supported message compiler. + +the each time localization is performed in an application using `$t` or `t` functions, message resources will be compiled on message compiler. + +You need to configure the following feature flag with `esm-bundler` build and bundler such as vite: + +- `__INTLIFY_JIT_COMPILATION__` (enable/disable message compiler for JIT style, default: `false`) :::warning NOTICE -`__INTLIFY_PROD_DEVTOOLS__` flag is experimental, and `@intlify/devtools` is WIP yet. +This feature is opted out as default, because compatibility with previous version before v9.3. ::: -The build will work without configuring these flags, however it is **strongly recommended** to properly configure them in order to get proper tree shaking in the final bundle. To configure these flags: +About how to configure for bundler, see the [here](#configure-feature-flags-for-bundler). + + +### Configure feature flags for bundler - webpack: use [DefinePlugin](https://webpack.js.org/plugins/define-plugin/) - Rollup: use [@rollup/plugin-replace](https://github.com/rollup/plugins/tree/master/packages/replace) @@ -211,6 +252,7 @@ Also, if you are using the Vue CLI, you can use the [officially provided plugin] The replacement value **must be boolean literals** and cannot be strings, otherwise the bundler/minifier will not be able to properly evaluate the conditions. ::: + ## Pre translations with extensions You can use pre-translation(server-side rendering) with vue-i18n-extensions package. diff --git a/package.json b/package.json index e3c5cd9bc..e98a997c1 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,11 @@ "scripts": { "benchmark": "node ./benchmark/index.mjs", "build": "node -r esbuild-register scripts/build.ts", - "build:explorer": "cd packages/format-explorer && vite build", + "build:explorer": "pnpm --filter @intlify/message-format-explorer build", "build:size": "pnpm build && run-p build:size-*", - "build:size-core": "cd packages/size-check-core && pnpm build", - "build:size-petite-vue-i18n": "cd packages/size-check-petite-vue-i18n && pnpm build", - "build:size-vue-i18n": "cd packages/size-check-vue-i18n && pnpm build", + "build:size-core": "pnpm --filter @intlify/size-check-core build", + "build:size-petite-vue-i18n": "pnpm --filter @intlify/size-check-petite-vue-i18n build", + "build:size-vue-i18n": "pnpm --filter @intlify/size-check-vue-i18n build", "build:sourcemap": "pnpm build --sourcemap", "build:type": "./scripts/build.sh", "build:typed": "pnpm build core-base vue-i18n-core --types", @@ -33,7 +33,10 @@ "coverage": "opener coverage/index.html", "dev": "node -r esbuild-register scripts/dev.ts", "dev:e2e": "cross-env TZ=UTC vitest -c ./vitest.e2e.config.ts", - "dev:explorer": "vite packages/format-explorer", + "dev:explorer": "pnpm --filter @intlify/message-format-explorer dev", + "dev:size-core": "pnpm --filter @intlify/size-check-core dev", + "dev:size-petite-vue-i18n": "pnpm --filter @intlify/size-check-petite-vue-i18n dev", + "dev:size-vue-i18n": "pnpm --filter @intlify/size-check-vue-i18n dev", "docs:apigen": "pnpm docs:apigen:core && pnpm docs:apigen:vue", "docs:apigen:core": "api-docs-gen ./temp/core-base.api.json -o ./docs/api -c ./docsgen.config.js -g noprefix -t ./tsdoc.json && mv ./docs/api/general.md ./docs/api/temp.md", "docs:apigen:vue": "api-docs-gen ./temp/vue-i18n-core.api.json -o ./docs/api -c ./docsgen.config.js -g noprefix -t ./tsdoc.json && tail -n +3 ./docs/api/temp.md >> ./docs/api/general.md && rm -rf ./docs/api/temp.md", @@ -53,8 +56,11 @@ "lint:secret": "npx secretlint \"**/*\"", "prebuild": "test \"$CI\" = true && npx pnpm install -r --store=node_modules/.pnpm-store || echo skipping pnpm install", "preinstall": "node ./scripts/preinstall.js", + "preview:explorer": "pnpm --filter @intlify/message-format-explorer preview", + "preview:size-core": "pnpm --filter @intlify/size-check-core preview", + "preview:size-petite-vue-i18n": "pnpm --filter @intlify/size-check-petite-vue-i18n preview", + "preview:size-vue-i18n": "pnpm --filter @intlify/size-check-vue-i18n preview", "release": "bumpp package.json packages/**/package.json --commit \"release: v\" --push --tag", - "serve:explorer": "cd packages/format-explorer && vite preview", "test": "npm-run-all lint test:cover test:type check-install test:e2e", "test:cover": "pnpm test:unit --coverage", "test:e2e": "cross-env TZ=UTC vitest run -c ./vitest.e2e.config.ts", @@ -170,7 +176,7 @@ "pnpm": { "overrides": { "vue": "3.3.4", - "vite": "^4.0.0" + "vite": "^4.3.9" } } } diff --git a/packages/core-base/src/compilation.ts b/packages/core-base/src/compilation.ts new file mode 100644 index 000000000..cf10ee1f3 --- /dev/null +++ b/packages/core-base/src/compilation.ts @@ -0,0 +1,155 @@ +import { warn, format, isBoolean, isString } from '@intlify/shared' +import { + baseCompile as baseCompileCore, + defaultOnError, + detectHtmlTag +} from '@intlify/message-compiler' +import { format as formatMessage } from './format' +import { CoreErrorCodes, createCoreError } from './errors' + +import type { + CompileOptions, + CompileError, + CompilerResult, + ResourceNode +} from '@intlify/message-compiler' +import type { MessageFunction, MessageFunctions } from './runtime' + +type CoreBaseCompileOptions = CompileOptions & { + warnHtmlMessage?: boolean +} + +const WARN_MESSAGE = `Detected HTML in '{source}' message. Recommend not using HTML messages to avoid XSS.` + +function checkHtmlMessage(source: string, warnHtmlMessage?: boolean): void { + if (warnHtmlMessage && detectHtmlTag(source)) { + warn(format(WARN_MESSAGE, { source })) + } +} + +const defaultOnCacheKey = (message: string): string => message +let compileCache: unknown = Object.create(null) + +export function clearCompileCache(): void { + compileCache = Object.create(null) +} + +function baseCompile( + message: string, + options: CoreBaseCompileOptions = {} +): CompilerResult & { detectError: boolean } { + // error detecting on compile + let detectError = false + const onError = options.onError || defaultOnError + options.onError = (err: CompileError): void => { + detectError = true + onError(err) + } + + // compile with mesasge-compiler + return { ...baseCompileCore(message, options), detectError } +} + +export function compileToFunction< + Message = string, + MessageSource extends string | ResourceNode = string +>( + message: MessageSource, + options: CoreBaseCompileOptions = {} +): MessageFunction { + if (!isString(message)) { + throw createCoreError(CoreErrorCodes.NOT_SUPPORT_AST) + } + + if (__RUNTIME__) { + __DEV__ && + warn( + `Runtime compilation is not supported in ${ + __BUNDLE_FILENAME__ || 'N/A' + }.` + ) + return (() => message) as MessageFunction + } else { + // check HTML message + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const warnHtmlMessage = isBoolean(options.warnHtmlMessage) + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + options.warnHtmlMessage + : true + __DEV__ && checkHtmlMessage(message, warnHtmlMessage) + + // check caches + const onCacheKey = options.onCacheKey || defaultOnCacheKey + const cacheKey = onCacheKey(message) + const cached = (compileCache as MessageFunctions)[cacheKey] + if (cached) { + return cached + } + + // compile + const { code, detectError } = baseCompile(message, options) + + // evaluate function + const msg = new Function(`return ${code}`)() as MessageFunction + + // if occurred compile error, don't cache + return !detectError + ? ((compileCache as MessageFunctions)[cacheKey] = msg) + : msg + } +} + +export function compile< + Message = string, + MessageSource extends string | ResourceNode = string +>( + message: MessageSource, + options: CoreBaseCompileOptions = {} +): MessageFunction { + if (isString(message)) { + // check HTML message + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const warnHtmlMessage = isBoolean(options.warnHtmlMessage) + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + options.warnHtmlMessage + : true + __DEV__ && checkHtmlMessage(message, warnHtmlMessage) + + // check caches + const onCacheKey = options.onCacheKey || defaultOnCacheKey + const cacheKey = onCacheKey(message) + const cached = (compileCache as MessageFunctions)[cacheKey] + if (cached) { + return cached + } + + // compile with JIT mode + const { ast, detectError } = baseCompile(message, { + ...options, + location: __DEV__, + jit: true + }) + + // compose message function from AST + const msg = formatMessage(ast) + + // if occurred compile error, don't cache + return !detectError + ? ((compileCache as MessageFunctions)[cacheKey] = msg) + : msg + } else { + // AST case (passed from bundler) + const cacheKey = (message as ResourceNode).cacheKey + if (cacheKey) { + const cached = (compileCache as MessageFunctions)[cacheKey] + if (cached) { + return cached + } + // compose message function from message (AST) + return ((compileCache as MessageFunctions)[cacheKey] = + formatMessage(message as ResourceNode)) + } else { + return formatMessage(message as ResourceNode) + } + } +} diff --git a/packages/core-base/src/compile.ts b/packages/core-base/src/compile.ts deleted file mode 100644 index 21f838e9f..000000000 --- a/packages/core-base/src/compile.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { warn, format, isBoolean } from '@intlify/shared' -import { - baseCompile, - defaultOnError, - detectHtmlTag -} from '@intlify/message-compiler' - -import type { CompileOptions, CompileError } from '@intlify/message-compiler' -import type { MessageFunction, MessageFunctions } from './runtime' - -const WARN_MESSAGE = `Detected HTML in '{source}' message. Recommend not using HTML messages to avoid XSS.` - -function checkHtmlMessage(source: string, warnHtmlMessage?: boolean): void { - if (warnHtmlMessage && detectHtmlTag(source)) { - warn(format(WARN_MESSAGE, { source })) - } -} - -const defaultOnCacheKey = (source: string): string => source -let compileCache: unknown = Object.create(null) - -export function clearCompileCache(): void { - compileCache = Object.create(null) -} - -export function compileToFunction( - source: string, - options: CompileOptions = {} -): MessageFunction { - if (__RUNTIME__) { - __DEV__ && - warn( - `Runtime compilation is not supported in ${ - __BUNDLE_FILENAME__ || 'N/A' - }.` - ) - return (() => source) as MessageFunction - } else { - // check HTML message - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const warnHtmlMessage = isBoolean((options as any).warnHtmlMessage) - ? // eslint-disable-next-line @typescript-eslint/no-explicit-any - (options as any).warnHtmlMessage - : true - __DEV__ && checkHtmlMessage(source, warnHtmlMessage) - - // check caches - const onCacheKey = options.onCacheKey || defaultOnCacheKey - const key = onCacheKey(source) - const cached = (compileCache as MessageFunctions)[key] - if (cached) { - return cached - } - - // compile error detecting - let occurred = false - const onError = options.onError || defaultOnError - options.onError = (err: CompileError): void => { - occurred = true - onError(err) - } - - // compile - const { code } = baseCompile(source, options) - - // evaluate function - const msg = new Function(`return ${code}`)() as MessageFunction - - // if occurred compile error, don't cache - return !occurred ? ((compileCache as MessageFunctions)[key] = msg) : msg - } -} diff --git a/packages/core-base/src/context.ts b/packages/core-base/src/context.ts index c847d00d7..e734e9a79 100644 --- a/packages/core-base/src/context.ts +++ b/packages/core-base/src/context.ts @@ -17,7 +17,7 @@ import { CoreWarnCodes, getWarnMessage } from './warnings' import { resolveWithKeyValue } from './resolver' import { fallbackWithSimple } from './fallbacker' -import type { CompileOptions } from '@intlify/message-compiler' +import type { CompileOptions, ResourceNode } from '@intlify/message-compiler' import type { VueDevToolsEmitter } from '@intlify/vue-devtools' import type { Path, MessageResolver } from './resolver' import type { @@ -28,6 +28,7 @@ import type { PluralizationRules, MessageProcessor, MessageFunction, + MessageFunctionReturn, MessageType } from './runtime' import type { @@ -95,12 +96,15 @@ export type CoreMissingHandler = ( /** @VueI18nGeneral */ export type PostTranslationHandler = ( - translated: MessageType, + translated: MessageFunctionReturn, key: string -) => MessageType +) => MessageFunctionReturn -export type MessageCompiler = ( - source: string, +export type MessageCompiler< + Message = string, + MessageSource extends string | ResourceNode = string +> = ( + message: MessageSource, options?: CompileOptions ) => MessageFunction @@ -167,7 +171,7 @@ export interface CoreOptions< processor?: MessageProcessor warnHtmlMessage?: boolean escapeParameter?: boolean - messageCompiler?: MessageCompiler + messageCompiler?: MessageCompiler messageResolver?: MessageResolver localeFallbacker?: LocaleFallbacker fallbackContext?: CoreContext @@ -205,7 +209,7 @@ export interface CoreTranslationContext { processor: MessageProcessor | null warnHtmlMessage: boolean escapeParameter: boolean - messageCompiler: MessageCompiler | null + messageCompiler: MessageCompiler | null messageResolver: MessageResolver } @@ -298,7 +302,7 @@ function getDefaultLinkedModifiers< let _compiler: unknown | null export function registerMessageCompiler( - compiler: MessageCompiler + compiler: MessageCompiler ): void { _compiler = compiler } diff --git a/packages/core-base/src/errors.ts b/packages/core-base/src/errors.ts index e52b2f715..3dd25b469 100644 --- a/packages/core-base/src/errors.ts +++ b/packages/core-base/src/errors.ts @@ -15,7 +15,8 @@ export const CoreErrorCodes = { INVALID_ARGUMENT: code, // 15 INVALID_DATE_ARGUMENT: inc(), // 16 INVALID_ISO_DATE_ARGUMENT: inc(), // 17 - __EXTEND_POINT__: inc() // 18 + NOT_SUPPORT_AST: inc(), // 18 + __EXTEND_POINT__: inc() // 19 } as const export type CoreErrorCodes = @@ -36,5 +37,6 @@ export const errorMessages: { [code: number]: string } = { 'The date provided is an invalid Date object.' + 'Make sure your Date represents a valid date.', [CoreErrorCodes.INVALID_ISO_DATE_ARGUMENT]: - 'The argument provided is not a valid ISO date string' + 'The argument provided is not a valid ISO date string', + [CoreErrorCodes.NOT_SUPPORT_AST]: 'Not support AST' } diff --git a/packages/core-base/src/format.ts b/packages/core-base/src/format.ts new file mode 100644 index 000000000..b257f6257 --- /dev/null +++ b/packages/core-base/src/format.ts @@ -0,0 +1,99 @@ +import { NodeTypes } from '@intlify/message-compiler' + +import type { + Node, + TextNode, + LiteralNode, + ListNode, + MessageNode, + NamedNode, + LinkedNode, + LinkedKeyNode, + LinkedModifierNode, + ResourceNode +} from '@intlify/message-compiler' +import type { + MessageContext, + MessageFunction, + MessageType, + MessageFunctionReturn +} from './runtime' + +export function format( + ast: ResourceNode +): MessageFunction { + const msg = (ctx: MessageContext): MessageFunctionReturn => + formatParts(ctx, ast) + // TODO: add meta data for vue-devtools debugging, such as `key`, `source` and `locale` + // TODO: optimization for static text message + return msg +} + +function formatParts( + ctx: MessageContext, + ast: ResourceNode +): MessageFunctionReturn { + if (ast.body.type === NodeTypes.Plural) { + return ctx.plural( + ast.body.cases.reduce( + (messages, c) => + [ + ...messages, + formatMessageParts(ctx, c) + ] as MessageFunctionReturn, + [] as MessageFunctionReturn + ) as Message[] + ) as MessageFunctionReturn + } else { + return formatMessageParts(ctx, ast.body) + } +} + +function formatMessageParts( + ctx: MessageContext, + node: MessageNode +): MessageFunctionReturn { + if (node.static) { + return ctx.type === 'text' + ? (node.static as MessageFunctionReturn) + : ctx.normalize([node.static] as MessageType[]) + } else { + const messages = node.items.reduce( + (acm, c) => [...acm, formatMessagePart(ctx, c)], + [] as MessageType[] + ) + return ctx.normalize(messages) as MessageFunctionReturn + } +} + +function formatMessagePart( + ctx: MessageContext, + node: Node +): MessageType { + switch (node.type) { + case NodeTypes.Text: + return (node as TextNode).value as MessageType + case NodeTypes.Literal: + return (node as LiteralNode).value as MessageType + case NodeTypes.Named: + return ctx.interpolate(ctx.named((node as NamedNode).key)) + case NodeTypes.List: + return ctx.interpolate(ctx.list((node as ListNode).index)) + case NodeTypes.Linked: + return ctx.linked( + formatMessagePart(ctx, (node as LinkedNode).key) as string, + (node as LinkedNode).modifier + ? (formatMessagePart(ctx, (node as LinkedNode).modifier!) as string) + : undefined, + ctx.type + ) + case NodeTypes.LinkedKey: + return (node as LinkedKeyNode).value as MessageType + case NodeTypes.LinkedModifier: + return (node as LinkedModifierNode).value as MessageType + default: + throw new Error( + `unhandled node type on format message part: ${node.type}` + ) + } +} diff --git a/packages/core-base/src/index.ts b/packages/core-base/src/index.ts index 2c141e85a..2869fea3a 100644 --- a/packages/core-base/src/index.ts +++ b/packages/core-base/src/index.ts @@ -1,4 +1,4 @@ -import { getGlobalThis } from '@intlify/shared' +import { initFeatureFlags } from './misc' export { CompileError, @@ -9,7 +9,7 @@ export * from './resolver' export * from './runtime' export * from './context' export * from './fallbacker' -export * from './compile' +export * from './compilation' export * from './translate' export * from './datetime' export * from './number' @@ -18,10 +18,6 @@ export { CoreError, CoreErrorCodes, createCoreError } from './errors' export * from './types' export * from './devtools' -// TODO: we could not exports for Node native ES Moudles yet... - if (__ESM_BUNDLER__ && !__TEST__) { - if (typeof __FEATURE_PROD_INTLIFY_DEVTOOLS__ !== 'boolean') { - getGlobalThis().__INTLIFY_PROD_DEVTOOLS__ = false - } + initFeatureFlags() } diff --git a/packages/core-base/src/misc.ts b/packages/core-base/src/misc.ts new file mode 100644 index 000000000..f64a2fc17 --- /dev/null +++ b/packages/core-base/src/misc.ts @@ -0,0 +1,15 @@ +import { getGlobalThis } from '@intlify/shared' + +/** + * This is only called in esm-bundler builds. + * istanbul-ignore-next + */ +export function initFeatureFlags(): void { + if (typeof __FEATURE_PROD_INTLIFY_DEVTOOLS__ !== 'boolean') { + getGlobalThis().__INTLIFY_PROD_DEVTOOLS__ = false + } + + if (typeof __FEATURE_JIT_COMPILATION__ !== 'boolean') { + getGlobalThis().__INTLIFY_JIT_COMPILATION__ = false + } +} diff --git a/packages/core-base/src/runtime.ts b/packages/core-base/src/runtime.ts index c267f67b2..133897259 100644 --- a/packages/core-base/src/runtime.ts +++ b/packages/core-base/src/runtime.ts @@ -36,11 +36,15 @@ export type MessageType = T extends string ? string : StringConvertable +export type MessageFunctionReturn = T extends string + ? MessageType + : MessageType[] + export type MessageFunctionCallable = ( ctx: MessageContext -) => MessageType +) => MessageFunctionReturn export type MessageFunctionInternal = { - (ctx: MessageContext): MessageType + (ctx: MessageContext): MessageFunctionReturn key?: string locale?: string source?: string @@ -54,8 +58,8 @@ export type MessageResolveFunction = ( ) => MessageFunction export type MessageNormalize = ( - values: MessageType[] -) => MessageType + values: MessageType[] +) => MessageFunctionReturn export type MessageInterpolate = (val: unknown) => MessageType export interface MessageProcessor { type?: string @@ -244,11 +248,12 @@ export function createMessageContext( type = arg2 || type } } - let msg = message(key)(ctx) - // The message in vnode resolved with linked are returned as an array by processor.nomalize - if (type === 'vnode' && isArray(msg) && modifier) { - msg = msg[0] - } + const ret = message(key)(ctx) + const msg = + // The message in vnode resolved with linked are returned as an array by processor.nomalize + type === 'vnode' && isArray(ret) && modifier + ? ret[0] + : (ret as MessageType) return modifier ? _modifier(modifier)(msg as T, type) : msg } diff --git a/packages/core-base/src/translate.ts b/packages/core-base/src/translate.ts index 78da45191..5e3d5cedf 100644 --- a/packages/core-base/src/translate.ts +++ b/packages/core-base/src/translate.ts @@ -29,7 +29,11 @@ import { CoreErrorCodes, createCoreError } from './errors' import { translateDevTools } from './devtools' import { VueDevToolsTimelineEvents } from '@intlify/vue-devtools' -import type { CompileOptions, CompileError } from '@intlify/message-compiler' +import type { + CompileOptions, + CompileError, + ResourceNode +} from '@intlify/message-compiler' import type { AdditionalPayloads } from '@intlify/devtools-if' import type { Path, PathValue } from './resolver' import type { @@ -37,9 +41,9 @@ import type { FallbackLocale, NamedValue, MessageFunction, + MessageFunctionReturn, MessageFunctionInternal, MessageContextOptions, - MessageType, MessageContext } from './runtime' import type { @@ -53,6 +57,9 @@ const NOOP_MESSAGE_FUNCTION = () => '' export const isMessageFunction = (val: unknown): val is MessageFunction => isFunction(val) +export const isMessageAST = (val: unknown): val is ResourceNode => + isObject(val) && val.type === 0 && 'body' in val + /** * # translate * @@ -167,7 +174,7 @@ export function translate< >( context: Context, key: Key | ResourceKeys | number | MessageFunction -): MessageType | number +): MessageFunctionReturn | number export function translate< Context extends CoreContext, @@ -180,7 +187,7 @@ export function translate< context: Context, key: Key | ResourceKeys | number | MessageFunction, plural: number -): MessageType | number +): MessageFunctionReturn | number export function translate< Context extends CoreContext, @@ -194,7 +201,7 @@ export function translate< key: Key | ResourceKeys | number | MessageFunction, plural: number, options: TranslateOptions -): MessageType | number +): MessageFunctionReturn | number export function translate< Context extends CoreContext, @@ -207,7 +214,7 @@ export function translate< context: Context, key: Key | ResourceKeys | number | MessageFunction, defaultMsg: string -): MessageType | number +): MessageFunctionReturn | number export function translate< Context extends CoreContext, @@ -221,7 +228,7 @@ export function translate< key: Key | ResourceKeys | number | MessageFunction, defaultMsg: string, options: TranslateOptions -): MessageType | number +): MessageFunctionReturn | number export function translate< Context extends CoreContext, @@ -234,7 +241,7 @@ export function translate< context: Context, key: Key | ResourceKeys | number | MessageFunction, list: unknown[] -): MessageType | number +): MessageFunctionReturn | number export function translate< Context extends CoreContext, @@ -248,7 +255,7 @@ export function translate< key: Key | ResourceKeys | number | MessageFunction, list: unknown[], plural: number -): MessageType | number +): MessageFunctionReturn | number export function translate< Context extends CoreContext, @@ -262,7 +269,7 @@ export function translate< key: Key | ResourceKeys | number | MessageFunction, list: unknown[], defaultMsg: string -): MessageType | number +): MessageFunctionReturn | number export function translate< Context extends CoreContext, @@ -276,7 +283,7 @@ export function translate< key: Key | ResourceKeys | number | MessageFunction, list: unknown[], options: TranslateOptions -): MessageType | number +): MessageFunctionReturn | number export function translate< Context extends CoreContext, @@ -289,7 +296,7 @@ export function translate< context: Context, key: Key | ResourceKeys | number | MessageFunction, named: NamedValue -): MessageType | number +): MessageFunctionReturn | number export function translate< Context extends CoreContext, @@ -303,7 +310,7 @@ export function translate< key: Key | ResourceKeys | number | MessageFunction, named: NamedValue, plural: number -): MessageType | number +): MessageFunctionReturn | number export function translate< Context extends CoreContext, @@ -317,7 +324,7 @@ export function translate< key: Key | ResourceKeys | number | MessageFunction, named: NamedValue, defaultMsg: string -): MessageType | number +): MessageFunctionReturn | number export function translate< Context extends CoreContext, @@ -331,13 +338,16 @@ export function translate< key: Key | ResourceKeys | number | MessageFunction, named: NamedValue, options: TranslateOptions -): MessageType | number +): MessageFunctionReturn | number // implementation of `translate` function export function translate< Context extends CoreContext, Message = string ->(context: Context, ...args: unknown[]): MessageType | number { +>( + context: Context, + ...args: unknown[] +): MessageFunctionReturn | number { const { fallbackFormat, postTranslation, @@ -408,7 +418,11 @@ export function translate< let cacheBaseKey = key if ( !resolvedMessage && - !(isString(format) || isMessageFunction(format)) + !( + isString(format) || + isMessageAST(format) || + isMessageFunction(format) + ) ) { if (enableDefaultMsg) { format = defaultMsgOrKey @@ -419,12 +433,17 @@ export function translate< // checking message format and target locale if ( !resolvedMessage && - (!(isString(format) || isMessageFunction(format)) || + (!( + isString(format) || + isMessageAST(format) || + isMessageFunction(format) + ) || !isString(targetLocale)) ) { - return unresolving ? NOT_REOSLVED : (key as MessageType) + return unresolving ? NOT_REOSLVED : (key as MessageFunctionReturn) } + // TODO: refactor if (__DEV__ && isString(format) && context.messageCompiler == null) { warn( `The message format compilation is not supported in this build. ` + @@ -432,7 +451,7 @@ export function translate< `You need to pre-compilation all message format. ` + `So translate function return '${key}'.` ) - return key as MessageType + return key as MessageFunctionReturn } // setup compile error detecting @@ -455,7 +474,7 @@ export function translate< // if occurred compile error, return the message format if (occurred) { - return format as MessageType + return format as MessageFunctionReturn } // evaluate message with context @@ -614,7 +633,10 @@ function resolveMessageFormat( } } - if (isString(format) || isFunction(format)) break + if (isString(format) || isMessageAST(format) || isMessageFunction(format)) { + break + } + const missingRet = handleMissing( context as any, // eslint-disable-line @typescript-eslint/no-explicit-any key, @@ -667,12 +689,12 @@ function compileMessageFormat( } const msg = messageCompiler( - format as string, + format as string | ResourceNode, getCompileOptions( context, targetLocale, cacheBaseKey, - format as string, + format as string | ResourceNode, warnHtmlMessage, errorDetector ) @@ -707,7 +729,7 @@ function evaluateMessage( context: CoreContext, msg: MessageFunction, msgCtx: MessageContext -): MessageType { +): MessageFunctionReturn { // for vue-devtools timeline event let start: number | null = null let startTag: string | undefined @@ -785,7 +807,7 @@ function getCompileOptions( context: CoreContext, locale: Locale, key: string, - source: string, + source: string | ResourceNode, warnHtmlMessage: boolean, errorDetector?: (err: CompileError) => void ): CompileOptions { @@ -794,18 +816,20 @@ function getCompileOptions( onError: (err: CompileError): void => { errorDetector && errorDetector(err) if (!__BRIDGE__ && __DEV__) { + const _source = getSourceForCodeFrame(source) const message = `Message compilation error: ${err.message}` const codeFrame = err.location && + _source && generateCodeFrame( - source, + _source, err.location.start.offset, err.location.end.offset ) const emitter = (context as unknown as CoreInternalContext).__v_emitter - if (emitter) { + if (emitter && _source) { emitter.emit(VueDevToolsTimelineEvents.COMPILE_ERROR, { - message: source, + message: _source, error: err.message, start: err.location && err.location.start.offset, end: err.location && err.location.end.offset, @@ -822,6 +846,17 @@ function getCompileOptions( } as CompileOptions } +function getSourceForCodeFrame( + source: string | ResourceNode +): string | undefined { + if (isString(source)) { + } else { + if (source.loc?.source) { + return source.loc.source + } + } +} + function getMessageContextOptions( context: CoreContext, locale: Locale, @@ -854,7 +889,7 @@ function getMessageContextOptions( val = resolveValue(message, key) } - if (isString(val)) { + if (isString(val) || isMessageAST(val)) { let occurred = false const errorDetector = () => { occurred = true diff --git a/packages/core-base/test/compilation.test.ts b/packages/core-base/test/compilation.test.ts new file mode 100644 index 000000000..8ec3cc365 --- /dev/null +++ b/packages/core-base/test/compilation.test.ts @@ -0,0 +1,56 @@ +import { baseCompile } from '@intlify/message-compiler' +import { + compileToFunction, + compile, + clearCompileCache +} from '../src/compilation' +import { createMessageContext as context } from '../src/runtime' + +beforeAll(() => { + clearCompileCache() +}) + +describe('compileToFunction', () => { + test('basic', () => { + const msg = compileToFunction('hello {name}!') + const ctx = context({ + named: { name: 'kazupon' } + }) + expect(msg(ctx)).toBe('hello kazupon!') + }) + + test('error', () => { + let occured = false + compileToFunction('hello {name!', { + onError: () => (occured = true) + }) + expect(occured).toBe(true) + }) +}) + +describe('compile', () => { + test('basic', () => { + const msg = compile('hello {name}!') + const ctx = context({ + named: { name: 'kazupon' } + }) + expect(msg(ctx)).toBe('hello kazupon!') + }) + + test('AST passing', () => { + const { ast } = baseCompile('hello {name}!') + const msg = compile(ast) + const ctx = context({ + named: { name: 'kazupon' } + }) + expect(msg(ctx)).toBe('hello kazupon!') + }) + + test('error', () => { + let occured = false + compile('hello {name!', { + onError: () => (occured = true) + }) + expect(occured).toBe(true) + }) +}) diff --git a/packages/core-base/test/datetime.test.ts b/packages/core-base/test/datetime.test.ts index 7bf6492ec..b7b8c64e2 100644 --- a/packages/core-base/test/datetime.test.ts +++ b/packages/core-base/test/datetime.test.ts @@ -27,7 +27,7 @@ import { registerMessageCompiler, registerLocaleFallbacker } from '../src/context' -import { compileToFunction } from '../src/compile' +import { compileToFunction } from '../src/compilation' import { fallbackWithLocaleChain } from '../src/fallbacker' import type { DateTimeFormats } from '../src/types' diff --git a/packages/core-base/test/devtools.test.ts b/packages/core-base/test/devtools.test.ts index 23625233b..7aa5ad464 100644 --- a/packages/core-base/test/devtools.test.ts +++ b/packages/core-base/test/devtools.test.ts @@ -1,6 +1,6 @@ import { createCoreContext, translate } from '../src/index' import { createEmitter } from '@intlify/shared' -import { compileToFunction } from '../src/compile' +import { compileToFunction } from '../src/compilation' import { IntlifyDevToolsHooks } from '@intlify/devtools-if' import { setDevToolsHook, getDevToolsHook } from '../src/devtools' diff --git a/packages/core-base/test/format.test.ts b/packages/core-base/test/format.test.ts new file mode 100644 index 000000000..193e87905 --- /dev/null +++ b/packages/core-base/test/format.test.ts @@ -0,0 +1,132 @@ +import { baseCompile as compile } from '@intlify/message-compiler' +import { format } from '../src/format' +import { createMessageContext as context } from '../src/runtime' + +describe('features', () => { + test('text: hello world', () => { + const { ast } = compile('hello world', { jit: true }) + const msg = format(ast) + const ctx = context() + expect(msg(ctx)).toBe('hello world') + }) + + test('named: hello {name} !', () => { + const { ast } = compile('hello {name} !', { jit: true }) + const msg = format(ast) + const ctx = context({ + named: { name: 'kazupon' } + }) + expect(msg(ctx)).toBe('hello kazupon !') + }) + + test('list: hello {0} !', () => { + const { ast } = compile('hello {0} !', { jit: true }) + const msg = format(ast) + const ctx = context({ + list: ['kazupon'] + }) + expect(msg(ctx)).toBe('hello kazupon !') + }) + + test("literal: hello {'kazupon'} !", () => { + const { ast } = compile("hello {'kazupon'} !", { jit: true }) + const msg = format(ast) + const ctx = context({}) + expect(msg(ctx)).toBe('hello kazupon !') + }) + + describe('linked', () => { + test('key: hello @:name !', () => { + const { ast } = compile('hello @:name !', { jit: true }) + const msg = format(ast) + const ctx = context({ + messages: { + name: () => 'kazupon' + } + }) + expect(msg(ctx)).toBe('hello kazupon !') + }) + + test('list: hello @:{0} !', () => { + const { ast } = compile('hello @:{0} !', { jit: true }) + const msg = format(ast) + const ctx = context({ + list: ['kazupon'], + messages: { + kazupon: () => 'kazupon' + } + }) + expect(msg(ctx)).toBe('hello kazupon !') + }) + + test('named: hello @:{name} !', () => { + const { ast } = compile('hello @:{name} !', { jit: true }) + const msg = format(ast) + const ctx = context({ + named: { name: 'kazupon' }, + messages: { + kazupon: () => 'kazupon' + } + }) + expect(msg(ctx)).toBe('hello kazupon !') + }) + + test("literal: hello @:{'kazupon'} !", () => { + const { ast } = compile("hello @:{'kazupon'} !", { jit: true }) + const msg = format(ast) + const ctx = context({ + messages: { + kazupon: () => 'kazupon' + } + }) + expect(msg(ctx)).toBe('hello kazupon !') + }) + + test('modifier: hello @.upper:{name} !', () => { + const { ast } = compile('hello @.upper:{name} !', { jit: true }) + const msg = format(ast) + const ctx = context({ + modifiers: { + upper: (val: string) => val.toUpperCase() + }, + named: { name: 'kazupon' }, + messages: { + kazupon: () => 'kazupon' + } + }) + expect(msg(ctx)).toBe('hello KAZUPON !') + }) + }) + + describe('plural', () => { + test('simple: no apples | one apple | too much apples', () => { + const { ast } = compile('no apples | one apple | too much apples', { + jit: true + }) + const msg = format(ast) + const ctx = context({ + pluralIndex: 1 + }) + expect(msg(ctx)).toBe('one apple') + }) + + test(`@.upper:{'no apples'} | {0} apple | {n} apples`, () => { + const { ast } = compile( + `@.upper:{'no apples'} | {0} apple | {n} apples`, + { jit: true } + ) + const msg = format(ast) + const ctx = context({ + pluralIndex: 2, + modifiers: { + upper: (val: string) => val.toUpperCase() + }, + list: [1], + named: { + n: 3 + } + }) + expect(msg(ctx)).toBe('3 apples') + }) + }) +}) diff --git a/packages/core-base/test/number.test.ts b/packages/core-base/test/number.test.ts index 62f36542a..7e437c8e9 100644 --- a/packages/core-base/test/number.test.ts +++ b/packages/core-base/test/number.test.ts @@ -27,7 +27,7 @@ import { registerMessageCompiler, registerLocaleFallbacker } from '../src/context' -import { compileToFunction } from '../src/compile' +import { compileToFunction } from '../src/compilation' import { fallbackWithLocaleChain } from '../src/fallbacker' import { NumberFormats } from '../src/types/index' diff --git a/packages/core-base/test/translate.test.ts b/packages/core-base/test/translate.test.ts index ba44f33b6..f900d1bf2 100644 --- a/packages/core-base/test/translate.test.ts +++ b/packages/core-base/test/translate.test.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/no-explicit-any */ +import { baseCompile } from '@intlify/message-compiler' + // utils import * as shared from '@intlify/shared' vi.mock('@intlify/shared', async () => { @@ -18,12 +20,12 @@ import { registerMessageResolver, registerLocaleFallbacker } from '../src/context' -import { compileToFunction } from '../src/compile' +import { compileToFunction, compile } from '../src/compilation' import { fallbackWithLocaleChain } from '../src/fallbacker' import { resolveValue } from '../src/resolver' import { createTextNode } from './helper' -import type { MessageContext } from '../src/runtime' +import type { MessageContext, MessageFunctionReturn } from '../src/runtime' import type { VNode } from './helper' import type { MessageType, MessageProcessor } from '../src/runtime' import type { PickupKeys } from '../src/types/utils' @@ -622,7 +624,10 @@ describe('context pluralRule option', () => { describe('context postTranslation option', () => { test('basic', () => { let key = '' - const postTranslation = (str: string, _key: string) => { + const postTranslation = ( + str: MessageFunctionReturn, + _key: string + ) => { key = _key return str.trim() } @@ -970,4 +975,21 @@ describe('processor', () => { }) }) +describe('AST passing', () => { + test('simple text', () => { + registerMessageCompiler(compile) + + const msg = 'hi kazupon !' + const { ast } = baseCompile(msg, { jit: true, location: false }) + + const ctx = context({ + locale: 'en', + messages: { + en: { hi: ast } + } + }) + expect(translate(ctx, 'hi')).toEqual('hi kazupon !') + }) +}) + /* eslint-enable @typescript-eslint/no-empty-function, @typescript-eslint/no-explicit-any */ diff --git a/packages/core/package.json b/packages/core/package.json index b1ec3ab75..4ad0b50e8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -28,11 +28,12 @@ "dist" ], "main": "index.js", - "module": "dist/core.runtime.mjs", + "module": "dist/core.mjs", "unpkg": "dist/core.global.js", "jsdelivr": "dist/core.global.js", "types": "dist/core.d.ts", "dependencies": { + "@intlify/shared": "workspace:*", "@intlify/core-base": "workspace:*" }, "engines": { @@ -53,7 +54,7 @@ "exports": { ".": { "types": "./dist/core.d.ts", - "import": "./dist/core.runtime.mjs", + "import": "./dist/core.mjs", "browser": "./dist/core.esm-browser.js", "node": { "import": { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 519703d99..48f252050 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,14 +1,24 @@ import { registerMessageCompiler, compileToFunction, + compile, registerMessageResolver, resolveValue, registerLocaleFallbacker, fallbackWithLocaleChain } from '@intlify/core-base' +import { initFeatureFlags } from '../../core-base/src/misc' + +if (__ESM_BUNDLER__ && !__TEST__) { + initFeatureFlags() +} // register message compiler at @intlify/core -registerMessageCompiler(compileToFunction) +if (!__FEATURE_JIT_COMPILATION__) { + registerMessageCompiler(compileToFunction) +} else { + registerMessageCompiler(compile) +} // register message resolver at @intlify/core registerMessageResolver(resolveValue) diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index 1d5c0737f..1a58cb183 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -4,6 +4,11 @@ import { registerLocaleFallbacker, fallbackWithLocaleChain } from '@intlify/core-base' +import { initFeatureFlags } from '../../core-base/src/misc' + +if (__ESM_BUNDLER__ && !__TEST__) { + initFeatureFlags() +} // register message resolver at @intlify/core registerMessageResolver(resolveValue) diff --git a/packages/format-explorer/package.json b/packages/format-explorer/package.json index 3595a4d0c..61604efba 100644 --- a/packages/format-explorer/package.json +++ b/packages/format-explorer/package.json @@ -5,21 +5,21 @@ "private": true, "scripts": { "dev": "vite", - "build": "vue-tsc --noEmit && vite build", - "serve": "vite preview", + "build": "vite build", + "preview": "vite preview", "clean": "yarn clean:dist && yarn clean:deps", "clean:dist": "rm -rf ./dist", "clean:deps": "rm -rf ./node_modules" }, "dependencies": { "vue": "^3.3.4", - "source-map": "^0.6.1", - "monaco-editor": "^0.39.0" + "source-map-js": "^1.0.2", + "monaco-editor": "^0.38.0" }, "devDependencies": { - "@vitejs/plugin-vue": "^4.2.0", + "@vitejs/plugin-vue": "^4.2.3", "@vue/compiler-sfc": "^3.3.4", "vite": "^4.3.3", - "vue-tsc": "^1.6.5" + "vue-tsc": "^1.8.1" } } diff --git a/packages/format-explorer/src/App.vue b/packages/format-explorer/src/App.vue index 7b208c94b..9ddec3eb6 100644 --- a/packages/format-explorer/src/App.vue +++ b/packages/format-explorer/src/App.vue @@ -1,11 +1,14 @@