diff --git a/jest.config.js b/jest.config.js index 19e6dc374..cbc6b350b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -71,6 +71,7 @@ module.exports = { "/packages/format-csv", "/packages/message-utils", "/packages/extractor-vue", + "/packages/vue", ], }, ], diff --git a/packages/extractor-vue/src/vue-extractor.ts b/packages/extractor-vue/src/vue-extractor.ts index 155ec8d84..7419e9acb 100644 --- a/packages/extractor-vue/src/vue-extractor.ts +++ b/packages/extractor-vue/src/vue-extractor.ts @@ -27,7 +27,17 @@ export const vueExtractor: ExtractorType = { filename, inMap: descriptor.template.map, id: filename, + compilerOptions: { + nodeTransforms: [ + // will be called for each ast "node" + // we want to run our test on the 1st real node + (node, context) => { + // context. + console.log(node, context) + }, + ], + isTS: isTsBlock(descriptor.script) || isTsBlock(descriptor.scriptSetup), }, diff --git a/packages/vue/README.md b/packages/vue/README.md new file mode 100644 index 000000000..dcc28f7dc --- /dev/null +++ b/packages/vue/README.md @@ -0,0 +1,34 @@ +[![License][badge-license]][license] +[![Version][badge-version]][package] +[![Downloads][badge-downloads]][package] + +# @lingui/vue + +> vue components for internationalization + +`@lingui/vue` is part of [LinguiJS][linguijs]. See the [documentation][documentation] for all information, tutorials and examples. + +## Installation + +```sh +npm install --save @lingui/vue +# yarn add @lingui/vue +``` + +## Usage + +See the [tutorial][tutorial] or [reference][reference] documentation. + +## License + +[MIT][license] + +[license]: https://github.com/lingui/js-lingui/blob/main/LICENSE +[linguijs]: https://github.com/lingui/js-lingui +[documentation]: https://lingui.dev +[tutorial]: https://lingui.dev/tutorials/vue +[reference]: https://lingui.dev/ref/vue +[package]: https://www.npmjs.com/package/@lingui/vue +[badge-downloads]: https://img.shields.io/npm/dw/@lingui/vue.svg +[badge-version]: https://img.shields.io/npm/v/@lingui/vue.svg +[badge-license]: https://img.shields.io/npm/l/@lingui/vue.svg diff --git a/packages/vue/package.json b/packages/vue/package.json new file mode 100644 index 000000000..5683d11ca --- /dev/null +++ b/packages/vue/package.json @@ -0,0 +1,116 @@ +{ + "name": "@lingui/vue", + "publishConfig": { + "access": "public" + }, + "version": "5.0.0-next.0", + "sideEffects": false, + "description": "Vue components & tools for translations", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "author": { + "name": "Jérôme Steunou", + "email": "jerome.steunou@gmail.com" + }, + "license": "MIT", + "keywords": [ + "vue", + "component", + "i18n", + "internationalization", + "i9n", + "translation", + "icu", + "messageformat", + "multilingual", + "localization", + "l10n" + ], + "scripts": { + "build": "rimraf ./dist && unbuild", + "stub": "unbuild --stub" + }, + "repository": { + "type": "git", + "url": "https://github.com/lingui/js-lingui.git" + }, + "bugs": { + "url": "https://github.com/lingui/js-lingui/issues" + }, + "engines": { + "node": ">=16.0.0" + }, + "exports": { + ".": { + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + }, + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + } + }, + "./compiler": { + "require": { + "types": "./dist/compiler/index.d.cts", + "default": "./dist/compiler/index.cjs" + }, + "import": { + "types": "./dist/compiler/index.d.mts", + "default": "./dist/compiler/index.mjs" + } + }, + "./extractor": { + "require": { + "types": "./dist/extractor/index.d.cts", + "default": "./dist/extractor/index.cjs" + }, + "import": { + "types": "./dist/extractor/index.d.mts", + "default": "./dist/extractor/index.mjs" + } + }, + "./vite-plugin": { + "require": { + "types": "./dist/vite-plugin/index.d.cts", + "default": "./dist/vite-plugin/index.cjs" + }, + "import": { + "types": "./dist/vite-plugin/index.d.mts", + "default": "./dist/vite-plugin/index.mjs" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "LICENSE", + "README.md", + "dist/" + ], + "dependencies": { + "@babel/core": "^7.20.12", + "@babel/generator": "^7.20.12", + "@babel/types": "^7.20.12", + "@lingui/babel-plugin-lingui-macro": "5.0.0-next.0", + "@lingui/cli": "5.0.0-next.0", + "@lingui/core": "5.0.0-next.0", + "@lingui/message-utils": "5.0.0-next.0", + "@lingui/vite-plugin": "5.0.0-next.0", + "@vue/compiler-core": "^3.3.4", + "@vue/compiler-sfc": "^3.3.4", + "vue": "^3.3.4" + }, + "peerDependencies": { + "@vitejs/plugin-vue": "*", + "vite": "^3 || ^4 || ^5.0.9" + }, + "devDependencies": { + "@lingui/conf": "5.0.0-next.0", + "@types/babel__core": "^7.20.5", + "@vitejs/plugin-vue": "^5.0.5", + "unbuild": "2.0.0", + "vite": "4.1.4" + } +} diff --git a/packages/vue/src/common/Trans.ts b/packages/vue/src/common/Trans.ts new file mode 100644 index 000000000..2a6340bd4 --- /dev/null +++ b/packages/vue/src/common/Trans.ts @@ -0,0 +1,202 @@ +import { + type ElementNode, + findProp, + TemplateChildNode, + SimpleExpressionNode, +} from "@vue/compiler-core" + +import { + isAttributeNode, + isElementNode, + isInterpolationNode, + isSimpleExpressionNode, + isTextNode, +} from "./predicates" +import { + TextToken, + Token, + JsMacroName, + ElementToken, + ArgToken, + isArgDecorator, + tokenizeArg, + MacroJsContext, +} from "@lingui/babel-plugin-lingui-macro/ast" +import * as t from "@babel/types" +import { + tokenizeAsChoiceComponentOrUndefined, + tokenizeAsLinguiTemplateLiteralOrUndefined, +} from "../compiler/transformVt" + +export const makeCounter = + (index = 0) => + () => + index++ + +export type MacroTransVueContext = { + // Positional expressions counter (e.g. for placeholders `Hello {0}, today is {1}`) + getExpressionIndex: () => number + getElementIndex: () => number + stripNonEssentialProps: boolean + isLinguiIdentifier: (node: t.Identifier, macro: JsMacroName) => boolean +} + +export function createMacroTransVueContext( + isLinguiIdentifier: MacroTransVueContext["isLinguiIdentifier"], + stripNonEssentialProps: boolean +): MacroTransVueContext { + return { + getExpressionIndex: makeCounter(), + getElementIndex: makeCounter(), + isLinguiIdentifier, + stripNonEssentialProps, + } +} + +function tokenizeText(value: string): TextToken { + return { + type: "text", + value, + } +} + +/** + * Receive a Vue element which is none of the Lingui macro, and returns Tokens for + * element itself and all of it's children + * + * ``` + * Hello world! + * ^^^^^^^^^^^^^^^^^^^^^^^ + * ``` + */ +function tokenizeElementNode( + node: ElementNode, + ctx: MacroTransVueContext +): ElementToken { + // !!! Important: Calculate element index before traversing children. + // That way outside elements are numbered before inner elements. (...and it looks pretty). + const name = ctx.getElementIndex() + + return { + type: "element", + name, + value: node, + children: node.children + .flatMap((child) => tokenizeChildren(child, ctx)) + .filter(Boolean) as Token[], + } +} + +/** + * Take `Hello {{name}}` and returns Tokens + */ +export function tokenizeTrans(node: ElementNode, ctx: MacroTransVueContext) { + return node.children + .flatMap((child) => tokenizeChildren(child, ctx)) + .filter(Boolean) as Token[] +} + +/** + * Process children of Elements and returns Tokens + */ +function tokenizeChildren( + node: TemplateChildNode, + ctx: MacroTransVueContext +): Token[] { + // Hello Username + // ^^^^^^ + if (isTextNode(node)) { + return [tokenizeText(node.content)] + } + + // Hello Username + // ^^^^^^^^^^^^^^^^^^^^^^^^^ + // goes into recursion for inner Element Nodes + if (isElementNode(node)) { + return [tokenizeElementNode(node, ctx)] + } + + // Hello {{ username }} + // ^^^^^^^^^^^^^^ + if (isInterpolationNode(node) && isSimpleExpressionNode(node.content)) { + return tokenizeSimpleExpressionNode(node.content, ctx) + } + + return [] +} + +/** + * Take an expression from Vue interpolation and returns Tokens + * + * ``` + * Hello {{ username }} + * ^^^^^^^^^^^^^ + * ``` + * + * Supported expressions: + * + * - {{ username }} -> {username} + * - {{ user.name }} -> {0} + * - {{ plural(...) }} | {{ select(...) }} -> {count, plural, one {...} other {...}} + * - {{ arg(username) }} -> username + */ +function tokenizeSimpleExpressionNode( + node: SimpleExpressionNode, + ctx: MacroTransVueContext +): Token[] { + if (node.ast) { + const vtTokens = tokenizeVt(node.ast, ctx) + + if (vtTokens) { + return vtTokens + } + + if (t.isCallExpression(node.ast) && isArgDecorator(node.ast, ctx)) { + return [tokenizeArg(node.ast, ctx)] + } + + return [ + { + type: "arg", + name: String(ctx.getExpressionIndex()), + value: node.ast as t.Expression, + } satisfies ArgToken, + ] + } + + // For simple interpolation with identifier + // only Vue doesn't populate an `.ast` + return [ + { + type: "arg", + name: node.content, + value: t.identifier(node.content), + } satisfies ArgToken, + ] +} + +function tokenizeVt(node: t.Node, ctx: MacroJsContext) { + for (const fn of [ + tokenizeAsChoiceComponentOrUndefined, + tokenizeAsLinguiTemplateLiteralOrUndefined, + ]) { + const tokens = fn(node, ctx) + + if (tokens) { + return tokens + } + } + + return +} + +export function getTextProp( + node: ElementNode, + propName: string +): string | undefined { + const prop = findProp(node, propName, undefined, false) + if (isAttributeNode(prop) && prop.value?.content) { + return prop.value.content + } + return undefined +} diff --git a/packages/vue/src/common/predicates.ts b/packages/vue/src/common/predicates.ts new file mode 100644 index 000000000..9165dae74 --- /dev/null +++ b/packages/vue/src/common/predicates.ts @@ -0,0 +1,49 @@ +import { + type AttributeNode, + type Node as BaseNode, + type DirectiveNode, + type ElementNode, + type InterpolationNode, + type SimpleExpressionNode, + type TextNode, + NodeTypes, +} from "@vue/compiler-core" + +import { TAGS } from "./types" + +// +export function isBaseNode(node: unknown): node is BaseNode { + return Boolean(node && typeof node === "object" && "type" in node) +} + +export function isElementNode(node: unknown): node is ElementNode { + return isBaseNode(node) && node.type === NodeTypes.ELEMENT +} + +export function isTextNode(node: unknown): node is TextNode { + return isBaseNode(node) && node.type === NodeTypes.TEXT +} + +export function isSimpleExpressionNode( + node: unknown +): node is SimpleExpressionNode { + return isBaseNode(node) && node.type === NodeTypes.SIMPLE_EXPRESSION +} + +export function isInterpolationNode(node: unknown): node is InterpolationNode { + return isBaseNode(node) && node.type === NodeTypes.INTERPOLATION +} + +export function isAttributeNode(node: unknown): node is AttributeNode { + return isBaseNode(node) && node.type === NodeTypes.ATTRIBUTE +} + +export function isDirectiveNode( + node: BaseNode | undefined +): node is DirectiveNode { + return node?.type === NodeTypes.DIRECTIVE +} + +export function isTrans(node: ElementNode): boolean { + return Boolean(node.tag === TAGS.Trans && node.children?.length) +} diff --git a/packages/vue/src/common/types.ts b/packages/vue/src/common/types.ts new file mode 100644 index 000000000..1bf889383 --- /dev/null +++ b/packages/vue/src/common/types.ts @@ -0,0 +1,3 @@ +export const TAGS = { + Trans: "Trans", +} as const diff --git a/packages/vue/src/compiler/index.ts b/packages/vue/src/compiler/index.ts new file mode 100644 index 000000000..6f1f27660 --- /dev/null +++ b/packages/vue/src/compiler/index.ts @@ -0,0 +1 @@ +export { transformer } from "./transformer" diff --git a/packages/vue/src/compiler/transformTrans.ts b/packages/vue/src/compiler/transformTrans.ts new file mode 100644 index 000000000..c1f53149d --- /dev/null +++ b/packages/vue/src/compiler/transformTrans.ts @@ -0,0 +1,188 @@ +import { + createSimpleExpression, + type ElementNode, + NodeTypes, + ConstantTypes, + ElementTypes, + SourceLocation, + type TemplateNode, +} from "@vue/compiler-core" +import { + createMacroTransVueContext, + tokenizeTrans, + getTextProp, +} from "../common/Trans" +import { + ICUMessageFormat, + createMessageDescriptor, + JsMacroName, +} from "@lingui/babel-plugin-lingui-macro/ast" +import generate from "@babel/generator" +import { ObjectExpression } from "@babel/types" + +function wrapInTemplateSlotNode( + index: number, + child: ElementNode +): TemplateNode { + const loc = child.loc + return { + type: NodeTypes.ELEMENT, + ns: 0, + tag: "template", + tagType: ElementTypes.TEMPLATE, + props: [ + { + type: NodeTypes.DIRECTIVE, + name: "slot", + exp: createSimpleExpression("{children}", false, loc, 0), + arg: createSimpleExpression(String(index), false, loc, 3), + modifiers: [], + loc, + }, + ], + isSelfClosing: false, + children: [child], + codegenNode: undefined, + loc, + } +} + +export function createInnerSlotNode(sourceChild: ElementNode): ElementNode { + if (sourceChild.isSelfClosing) return sourceChild + const loc = sourceChild.loc + // no need for a deep copy + return { + ...sourceChild, + children: [ + { + type: NodeTypes.ELEMENT, + ns: 0, + tag: "component", + tagType: ElementTypes.COMPONENT, + props: [ + { + type: NodeTypes.DIRECTIVE, + name: "bind", + exp: createSimpleExpression("children", false, loc, 0), + arg: createSimpleExpression("is", true, loc, 3), + modifiers: [], + loc, + }, + ], + isSelfClosing: false, + children: [], + loc, + codegenNode: undefined, + }, + ], + } +} + +function transformElement( + node: ElementNode, + descriptor: ObjectExpression, + loc: SourceLocation, + elements?: Record | undefined +) { + const simpleExp = createSimpleExpression( + generate(descriptor).code, + false, + loc, + ConstantTypes.NOT_CONSTANT + ) + + node.props = [ + { + type: NodeTypes.DIRECTIVE, + name: "bind", + exp: simpleExp, + arg: undefined, + modifiers: [], + loc, + }, + ] + + node.isSelfClosing = true + + if (elements) { + node.children = Object.keys(elements).map((key) => { + return wrapInTemplateSlotNode( + Number(key), + createInnerSlotNode(elements[key]! as any) + ) + }) + } else { + node.children = [] + } + + node.tagType = ElementTypes.COMPONENT +} + +// from SFC to Babel +function convertLoc( + loc: SourceLocation +): Parameters[1] { + return { + start: { ...loc.start, index: loc.start.offset }, + end: { ...loc.end, index: loc.end.offset }, + filename: loc.source, + identifierName: null, + } +} + +export function transformTrans( + node: ElementNode, + options: { + stripNonEssentialProps?: boolean + } +) { + const loc = node.loc + const tokens = tokenizeTrans( + node, + createMacroTransVueContext((identifier, macro) => { + if (macro === JsMacroName.t) { + return identifier.name === "vt" + } + + return identifier.name === macro + }, !!options.stripNonEssentialProps) + ) + + const messageFormat = new ICUMessageFormat() + const { message, values, elements } = messageFormat.fromTokens(tokens) + + const id = getTextProp(node, "id") + const context = getTextProp(node, "context") + const comment = getTextProp(node, "comment") + + const descriptor = createMessageDescriptor( + { message, values }, + convertLoc(loc), + false, + { + ...(id + ? { + id: { + text: id, + }, + } + : {}), + ...(context + ? { + context: { + text: context, + }, + } + : {}), + ...(comment + ? { + comment: { + text: comment, + }, + } + : {}), + } + ) + + transformElement(node, descriptor, loc, elements) +} diff --git a/packages/vue/src/compiler/transformVt.ts b/packages/vue/src/compiler/transformVt.ts new file mode 100644 index 000000000..b225aee48 --- /dev/null +++ b/packages/vue/src/compiler/transformVt.ts @@ -0,0 +1,127 @@ +import { + createSimpleExpression, + SourceLocation, + ConstantTypes, + ExpressionNode, +} from "@vue/compiler-core" +import * as t from "@babel/types" +import generate from "@babel/generator" +import { + JsMacroName, + createMessageDescriptorFromTokens, + isLinguiIdentifier, + tokenizeTemplateLiteral, + createMacroJsContext, + isChoiceMethod, + tokenizeChoiceComponent, + processDescriptor, + MacroJsContext, +} from "@lingui/babel-plugin-lingui-macro/ast" + +const RUNTIME_VT_SYMBOL = "vt._" + +function createLinguiSimpleExpression( + messageDescriptor: t.ObjectExpression, + oldLoc: SourceLocation +) { + const ast = t.callExpression(t.identifier(RUNTIME_VT_SYMBOL), [ + messageDescriptor, + ]) + + return createSimpleExpression( + generate(ast).code, + false, + oldLoc, + ConstantTypes.NOT_CONSTANT + ) +} + +function createVueMacroContext(options: { stripNonEssentialProps?: boolean }) { + return createMacroJsContext((identifier, macro) => { + if (macro === JsMacroName.t) { + return identifier.name === "vt" + } + + return identifier.name === macro + }, !!options?.stripNonEssentialProps) +} + +export function tokenizeAsChoiceComponentOrUndefined( + node: t.Node, + ctx: MacroJsContext +) { + // plural / select / selectOrdinal + if (t.isCallExpression(node) && isChoiceMethod(node, ctx)) { + return [tokenizeChoiceComponent(node, isChoiceMethod(node, ctx)!, ctx)] + } + + return +} + +export function tokenizeAsLinguiTemplateLiteralOrUndefined( + node: t.Node, + ctx: MacroJsContext +) { + // t`Hello!` + if ( + t.isTaggedTemplateExpression(node) && + isLinguiIdentifier(node.tag, JsMacroName.t, ctx) + ) { + return tokenizeTemplateLiteral(node, ctx) + } + return +} + +export function transformVt( + vueNode: ExpressionNode, + options: { + stripNonEssentialProps?: boolean + } +) { + const ctx = createVueMacroContext(options) + if (!vueNode.ast) { + return vueNode + } + + const node = vueNode.ast + + // plural / select / selectOrdinal + const choiceCmpTokens = tokenizeAsChoiceComponentOrUndefined(node, ctx) + + if (choiceCmpTokens) { + const messageDescriptor = createMessageDescriptorFromTokens( + choiceCmpTokens, + node.loc!, + ctx.stripNonEssentialProps + ) + + return createLinguiSimpleExpression(messageDescriptor, vueNode.loc!) + } + + // t(...) + if ( + t.isCallExpression(node) && + isLinguiIdentifier(node.callee, JsMacroName.t, ctx) + ) { + const messageDescriptor = processDescriptor( + node.arguments[0] as t.ObjectExpression, + ctx + ) + + return createLinguiSimpleExpression(messageDescriptor, vueNode.loc!) + } + + // t`Hello!` + const tplLiteralTokens = tokenizeAsLinguiTemplateLiteralOrUndefined(node, ctx) + if (tplLiteralTokens) { + const messageDescriptor = createMessageDescriptorFromTokens( + tplLiteralTokens, + node.loc!, + ctx.stripNonEssentialProps + ) + + return createLinguiSimpleExpression(messageDescriptor, vueNode.loc!) + } + + return vueNode +} diff --git a/packages/vue/src/compiler/transformer.test.ts b/packages/vue/src/compiler/transformer.test.ts new file mode 100644 index 000000000..0dbb5843c --- /dev/null +++ b/packages/vue/src/compiler/transformer.test.ts @@ -0,0 +1,428 @@ +import { run } from "../test/utils" + +describe("transformTrans", () => { + it("should transform a Trans component to runtime Trans call with id & message", () => { + const { code } = run(`This is some random content`) + + expect(code).toMatchInlineSnapshot(` + import { normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createBlock as _createBlock } from "vue" + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createBlock($setup["Trans"], _normalizeProps(_guardReactiveProps(/*i18n*/ + { + id: "cr8mms", + message: "This is some random content" + })), null, 16 /* FULL_PROPS */)) + } + `) + }) + + it("should transform a Trans component to runtime Trans call with id & message & context", () => { + const { code } = run( + ` + right + ` + ) + + expect(code).toMatchInlineSnapshot(` + import { normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createBlock as _createBlock } from "vue" + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createBlock($setup["Trans"], _normalizeProps(_guardReactiveProps(/*i18n*/ + { + id: "d1wX4r", + message: "right", + context: "direction" + })), null, 16 /* FULL_PROPS */)) + } + `) + }) + + it("should normalize whitespaces", () => { + const { code } = run( + ` + + right + + ` + ) + + expect(code).toMatchInlineSnapshot(` + import { normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createBlock as _createBlock } from "vue" + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createBlock($setup["Trans"], _normalizeProps(_guardReactiveProps(/*i18n*/ + { + id: "lpXHCY", + message: " right " + })), null, 16 /* FULL_PROPS */)) + } + `) + }) + + it("should transform with given id", () => { + const { code } = run( + ` + This is some random content + ` + ) + + expect(code).toMatchInlineSnapshot(` + import { normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createBlock as _createBlock } from "vue" + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createBlock($setup["Trans"], _normalizeProps(_guardReactiveProps(/*i18n*/ + { + id: "random.content", + message: "This is some random content" + })), null, 16 /* FULL_PROPS */)) + } + `) + }) + + it("should transform with values when present", () => { + const { code } = run( + ` + Hello {{ name }} welcome to {{ town }} you are now a {{ persona }}! + ` + ) + + expect(code).toMatchInlineSnapshot(` + import { normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createBlock as _createBlock } from "vue" + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createBlock($setup["Trans"], _normalizeProps(_guardReactiveProps(/*i18n*/ + { + id: "cc6wEV", + message: "Hello {name} welcome to {town} you are now a {persona}!", + values: { + name: _ctx.name, + town: _ctx.town, + persona: _ctx.persona + } + })), null, 16 /* FULL_PROPS */)) + } + `) + }) + + it("should transform with placeholders when inner tags", () => { + const { code } = run( + ` + Hello {{ name }} welcome to {{ town }}
you are now a {{ persona }}!
+ ` + ) + + expect(code).toMatchInlineSnapshot(` + import { resolveDynamicComponent as _resolveDynamicComponent, openBlock as _openBlock, createBlock as _createBlock, createElementVNode as _createElementVNode, normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, withCtx as _withCtx } from "vue" + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createBlock($setup["Trans"], _normalizeProps(_guardReactiveProps(/*i18n*/ + { + id: "r0tHqI", + message: "Hello <0>{name} welcome to {town} <1/> <2>you are now <3><4>a {persona}!", + values: { + name: _ctx.name, + town: _ctx.town, + persona: _ctx.persona + } + })), { + [0]: _withCtx(({children}) => [ + _createElementVNode("em", null, [ + (_openBlock(), _createBlock(_resolveDynamicComponent(children))) + ]) + ]), + [1]: _withCtx(({children}) => _cache[0] || (_cache[0] = [ + _createElementVNode("br", null, null, -1 /* HOISTED */) + ])), + [2]: _withCtx(({children}) => [ + _createElementVNode("span", null, [ + (_openBlock(), _createBlock(_resolveDynamicComponent(children))) + ]) + ]), + [3]: _withCtx(({children}) => [ + _createElementVNode("em", null, [ + (_openBlock(), _createBlock(_resolveDynamicComponent(children))) + ]) + ]), + [4]: _withCtx(({children}) => [ + _createElementVNode("i", null, [ + (_openBlock(), _createBlock(_resolveDynamicComponent(children))) + ]) + ]), + _: 2 /* DYNAMIC */ + }, 1040 /* FULL_PROPS, DYNAMIC_SLOTS */)) + } + `) + }) + + it("should handle simple quotes", () => { + const { code } = run( + ` + John's car is red + ` + ) + + expect(code).toMatchInlineSnapshot(` + import { normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createBlock as _createBlock } from "vue" + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createBlock($setup["Trans"], _normalizeProps(_guardReactiveProps(/*i18n*/ + { + id: "H3I1xb", + message: "John's car is red" + })), null, 16 /* FULL_PROPS */)) + } + `) + }) + + it("should handle double quotes", () => { + const { code } = run( + ` + This car is "red" + ` + ) + + expect(code).toMatchInlineSnapshot(` + import { normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createBlock as _createBlock } from "vue" + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createBlock($setup["Trans"], _normalizeProps(_guardReactiveProps(/*i18n*/ + { + id: "HP8WZU", + message: "This car is \\"red\\"" + })), null, 16 /* FULL_PROPS */)) + } + `) + }) + + it("should handle mixed quotes", () => { + const { code } = run( + ` + John's car is "red" + ` + ) + + expect(code).toMatchInlineSnapshot(` + import { normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createBlock as _createBlock } from "vue" + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createBlock($setup["Trans"], _normalizeProps(_guardReactiveProps(/*i18n*/ + { + id: "dp/IGY", + message: "John's car is \\"red\\"" + })), null, 16 /* FULL_PROPS */)) + } + `) + }) + + it("should handle complex interpolation", () => { + const { code } = run( + ` + Hello {{ user.name }} + ` + ) + + expect(code).toMatchInlineSnapshot(` + import { normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createBlock as _createBlock } from "vue" + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createBlock($setup["Trans"], _normalizeProps(_guardReactiveProps(/*i18n*/ + { + id: "Y7riaK", + message: "Hello {0}", + values: { + 0: _ctx.user.name + } + })), null, 16 /* FULL_PROPS */)) + } + `) + }) + + it("should support Plural as icu", () => { + const { code } = run( + ` +{ {{ arg(count) }}, plural, one {# book} other {# books} } + ` + ) + + expect(code).toMatchInlineSnapshot(` + import { resolveDynamicComponent as _resolveDynamicComponent, openBlock as _openBlock, createBlock as _createBlock, createElementVNode as _createElementVNode, normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, withCtx as _withCtx } from "vue" + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createBlock($setup["Trans"], _normalizeProps(_guardReactiveProps(/*i18n*/ + { + id: "fMz2/F", + message: "{ count, plural, one {# <0>book} other {# <1>books} }", + values: { + count: _ctx.count + } + })), { + [0]: _withCtx(({children}) => [ + _createElementVNode("em", null, [ + (_openBlock(), _createBlock(_resolveDynamicComponent(children))) + ]) + ]), + [1]: _withCtx(({children}) => [ + _createElementVNode("strong", null, [ + (_openBlock(), _createBlock(_resolveDynamicComponent(children))) + ]) + ]), + _: 2 /* DYNAMIC */ + }, 1040 /* FULL_PROPS, DYNAMIC_SLOTS */)) + } + `) + }) + + it("should support js plural inside of Trans", () => { + const { code } = run( + ` + + You have {{plural(count, {one: "# book", other: "# books"})}} + + ` + ) + + expect(code).toMatchInlineSnapshot(` + import { normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createBlock as _createBlock } from "vue" + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createBlock($setup["Trans"], _normalizeProps(_guardReactiveProps(/*i18n*/ + { + id: "8GTfFt", + message: " You have {count, plural, one {# book} other {# books}}", + values: { + count: _ctx.count + } + })), null, 16 /* FULL_PROPS */)) + } + `) + }) + + it("should handle multiple complex interpolation", () => { + const { code } = run( + ` + Hello {{ user.name }}! Do you love {{ isCatPerson ? "cat" : "dogs" }}? + ` + ) + + expect(code).toMatchInlineSnapshot(` + import { normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, openBlock as _openBlock, createBlock as _createBlock } from "vue" + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createBlock($setup["Trans"], _normalizeProps(_guardReactiveProps(/*i18n*/ + { + id: "EtDOPn", + message: "Hello {0}! Do you love {1}?", + values: { + 0: _ctx.user.name, + 1: _ctx.isCatPerson ? "cat" : "dogs" + } + })), null, 16 /* FULL_PROPS */)) + } + `) + }) +}) + +describe("transform vt", () => { + it("should transform t macro in props", () => { + const { code } = run('') + + expect(code).toMatchInlineSnapshot(` + import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" + + const _hoisted_1 = ["title"] + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createElementBlock("img", { + title: _ctx.vt._( + /*i18n*/ + { + id: "OVaF9k", + message: "Hello {name}", + values: { + name: _ctx.name + } + }), + src: "" + }, null, 8 /* PROPS */, _hoisted_1)) + } + `) + }) + + it("should transform t macro in interpolation", () => { + const { code } = run("
{{ vt`Hello ${name}` }}
") + + expect(code).toMatchInlineSnapshot(` + import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createElementBlock("div", null, _toDisplayString(_ctx.vt._( + /*i18n*/ + { + id: "OVaF9k", + message: "Hello {name}", + values: { + name: _ctx.name + } + })), 1 /* TEXT */)) + } + `) + }) + + it("should transform t macro with message descriptor", () => { + const { code } = run( + '' + ) + + expect(code).toMatchInlineSnapshot(` + import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" + + const _hoisted_1 = ["title"] + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createElementBlock("img", { + title: _ctx.vt._( + /*i18n*/ + { + id: "16G7ph", + message: "Hello", + context: \`i am a context\` + }), + src: "" + }, null, 8 /* PROPS */, _hoisted_1)) + } + `) + }) + + it("should support plural", () => { + const { code } = run( + `` + ) + + expect(code).toMatchInlineSnapshot(` + import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" + + const _hoisted_1 = ["title"] + + export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createElementBlock("img", { + title: _ctx.vt._( + /*i18n*/ + { + id: "CF5t+7", + message: "{0, plural, offset:1 =0 {No books} =1 {1 book} other {# books}}", + values: { + 0: _ctx.users.length + } + }), + src: "" + }, null, 8 /* PROPS */, _hoisted_1)) + } + `) + }) +}) diff --git a/packages/vue/src/compiler/transformer.ts b/packages/vue/src/compiler/transformer.ts new file mode 100644 index 000000000..a0ebecfd6 --- /dev/null +++ b/packages/vue/src/compiler/transformer.ts @@ -0,0 +1,63 @@ +import { type NodeTransform, NodeTypes } from "@vue/compiler-core" + +import { + isTrans, + isElementNode, + isDirectiveNode, + isInterpolationNode, +} from "../common/predicates" +import { transformVt } from "./transformVt" +import { transformTrans } from "./transformTrans" + +export type LinguiTransformerOptions = { + stripNonEssentialProps?: boolean +} + +/** + * Here is an entry point transformer. + * We need our template transformer to operate on user authored code + * before any other Vue transformer process it. + * + * So this transformer unshifts a real transformer to the transformers array. + * + */ +export const transformer = + (options: LinguiTransformerOptions = {}): NodeTransform => + (node, context) => { + if ( + node.type === NodeTypes.ROOT && + !context.nodeTransforms.find( + (t) => (t as any).__name === "linguiTransform" + ) + ) { + context.nodeTransforms.unshift(templateTransformer(options)) + } + } + +/** + * Actual transformer expanding macro calls. + */ +const templateTransformer = ( + options: LinguiTransformerOptions +): NodeTransform => { + const transformer: NodeTransform = (node) => { + if (isElementNode(node)) { + if (isTrans(node)) { + transformTrans(node, options) + } + + for (const prop of node.props) { + if (isDirectiveNode(prop) && prop.exp) { + prop.exp = transformVt(prop.exp, options) + } + } + } + + if (isInterpolationNode(node)) { + node.content = transformVt(node.content, options) + } + } + + ;(transformer as any).__name = "linguiTransform" + return transformer +} diff --git a/packages/vue/src/components/Trans.ts b/packages/vue/src/components/Trans.ts new file mode 100644 index 000000000..c7b415942 --- /dev/null +++ b/packages/vue/src/components/Trans.ts @@ -0,0 +1,34 @@ +import { defineComponent, type MaybeRef, type PropType, unref } from "vue" + +import { useI18n } from "../plugins/lingui" +import { formatElements } from "./format" + +// + +type Values = Record + +// eslint-disable-next-line import/no-default-export +export default defineComponent({ + props: { + id: { type: String, required: false }, + values: { type: Object as PropType, required: false }, + message: { type: String, required: false }, + context: { type: String, required: false }, + }, + setup(props, ctx) { + return () => { + if (!props.id) return "" + const i18n = useI18n() + const unrefValues = Object.fromEntries( + Object.entries(props.values || {}).map(([key, value]) => [ + key, + unref(value), + ]) + ) + const translation = i18n.t(props.id, unrefValues, { + message: props.message || "fallback message", + }) + return formatElements(translation, { ...ctx.slots }) + } + }, +}) diff --git a/packages/vue/src/components/format.ts b/packages/vue/src/components/format.ts new file mode 100644 index 000000000..f44dfde66 --- /dev/null +++ b/packages/vue/src/components/format.ts @@ -0,0 +1,86 @@ +import { h, type Slot } from "vue" + +// + +// match paired and unpaired tags +const tagRe = /<([a-zA-Z0-9]+)>(.*?)<\/\1>|<([a-zA-Z0-9]+)\/>/u +const nlRe = /(?:\r\n|\r|\n)/gu + +type Element = ReturnType | ReturnType +type Node = Element | string | undefined + +/* + * `getElements` - return array of element indices and element children + * + * `parts` is array of [pairedIndex, children, unpairedIndex, textAfter, ...] + * where: + * - `pairedIndex` is index of paired element (undef for unpaired) + * - `children` are children of paired element (undef for unpaired) + * - `unpairedIndex` is index of unpaired element (undef for paired) + * - `textAfter` is string after all elements (empty string, if there's nothing) + * + * `parts` length is always a multiple of 4 + * + * Returns: Array<[elementIndex, children, after]> + */ +function getElements( + parts: string[] +): Array { + if (!parts.length) return [] + + const [paired, children, unpaired, after] = parts.slice(0, 4) + + const triple = [paired || unpaired, children || "", after] as const + return [triple].concat(getElements(parts.slice(4, parts.length))) +} + +/** + * `formatElements` - parse string and return tree of vue elements + * + * `value` is string to be formatted with Paired or (unpaired) + * placeholders. `elements` is a array of vue slots which indexes + * correspond to element indexes in formatted string + */ +export function formatElements( + value: string, + elements: { [key: string]: Slot | undefined } = {} +): Array { + const parts = value.replace(nlRe, "").split(tagRe) + + // no inline elements, return + if (parts.length === 1) return [value] + + const tree: Array = [] + + const before = parts.shift() + if (before) tree.push(before) + + for (const [index, children, after] of getElements(parts)) { + const slot = typeof index !== "undefined" ? elements[index] : undefined + let element: Element + + if (!slot) { + console.error( + `Can't use slot at index '${index}' as it is not declared in the original translation` + ) + // ignore problematic element but push its children and elements after it + element = h("span", children) + } else { + // slots display props with text interpolation + // only way to do recursive thing is to give a component that will render + // our subchildren / subslot + const childrenInComponent = { + setup: () => () => formatElements(children, elements), + } + element = slot({ + children: childrenInComponent, + }) + } + + tree.push(element) + + if (after) tree.push(after) + } + + return tree +} diff --git a/packages/vue/src/components/vt.ts b/packages/vue/src/components/vt.ts new file mode 100644 index 000000000..fc50587d1 --- /dev/null +++ b/packages/vue/src/components/vt.ts @@ -0,0 +1,20 @@ +import { type MessageDescriptor } from "@lingui/core" + +import { useI18n } from "../plugins/lingui" +import { type MacroMessageDescriptor } from "@lingui/core/macro" + +export function vt(descriptor: MacroMessageDescriptor): string +export function vt( + literals: TemplateStringsArray, + ...placeholders: any[] +): string +export function vt(...args: any[]): any {} + +/** + * Internal runtime used by Vue macro + * @internal + */ +;(vt as any)._ = (descriptor: MessageDescriptor) => { + const i18n = useI18n() + return i18n._(descriptor) +} diff --git a/packages/vue/src/extractor/__snapshots__/extractor.test.ts.snap b/packages/vue/src/extractor/__snapshots__/extractor.test.ts.snap new file mode 100644 index 000000000..2b8e60a3a --- /dev/null +++ b/packages/vue/src/extractor/__snapshots__/extractor.test.ts.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`vue extractor should extract message from functional component 1`] = ` +[ + { + comment: undefined, + context: undefined, + id: Render function message, + message: undefined, + origin: [ + functional.vue, + 10, + 33, + ], + placeholders: {}, + }, +] +`; + +exports[`vue extractor should extract message from vue file 1`] = ` +[ + { + comment: undefined, + context: undefined, + id: Setup message, + message: undefined, + origin: [ + test.vue.ts, + 4, + 0, + ], + placeholders: {}, + }, + { + comment: undefined, + context: undefined, + id: Script message, + message: undefined, + origin: [ + test.vue, + 19, + 20, + ], + placeholders: {}, + }, + { + comment: undefined, + context: undefined, + id: custom.id, + message: My message, + origin: [ + test.vue, + 27, + 11, + ], + placeholders: {}, + }, + { + comment: Message comment, + context: undefined, + id: my.message, + message: My descriptor message, + origin: [ + test.vue, + 29, + 10, + ], + placeholders: {}, + }, + { + comment: undefined, + context: undefined, + id: id used as message, + message: undefined, + origin: [ + test.vue, + 35, + 5, + ], + placeholders: {}, + }, + { + comment: undefined, + context: undefined, + id: My message without ID and context, + message: undefined, + origin: [ + test.vue, + 36, + 11, + ], + placeholders: {}, + }, +] +`; + +exports[`vue extractor should support macro expansion 1`] = ` +[ + { + comment: undefined, + context: undefined, + id: W3aHmM, + message: Hello from core macro, + origin: [ + macro.vue.ts, + 5, + 0, + ], + placeholders: {}, + }, + { + comment: undefined, + context: undefined, + id: QBEpY5, + message: Simple Trans, + origin: [ + , + null, + null, + ], + placeholders: {}, + }, + { + comment: undefined, + context: another, + id: ZZxZGw, + message: Simple Trans, + origin: [ + , + null, + null, + ], + placeholders: {}, + }, + { + comment: this is a comment, + context: undefined, + id: 24lTfo, + message: Trans with Comment, + origin: [ + , + null, + null, + ], + placeholders: {}, + }, + { + comment: undefined, + context: undefined, + id: some.id, + message: Trans With Id, + origin: [ + , + null, + null, + ], + placeholders: {}, + }, + { + comment: undefined, + context: undefined, + id: 3TftrZ, + message: Hello <0>{name} welcome to {town} <1/><2>you are now <3><4>a {persona}!, + origin: [ + , + null, + null, + ], + placeholders: { + name: _ctx.name, + persona: _ctx.persona, + town: _ctx.town, + }, + }, + { + comment: undefined, + context: undefined, + id: OVaF9k, + message: Hello {name}, + origin: [ + , + null, + null, + ], + placeholders: { + name: _ctx.name, + }, + }, + { + comment: undefined, + context: undefined, + id: esnaQO, + message: {count, plural, one {# book} other {# books}}, + origin: [ + , + null, + null, + ], + placeholders: { + count: _ctx.count, + }, + }, +] +`; diff --git a/packages/vue/src/extractor/extractor.test.ts b/packages/vue/src/extractor/extractor.test.ts new file mode 100644 index 000000000..3b9b488c8 --- /dev/null +++ b/packages/vue/src/extractor/extractor.test.ts @@ -0,0 +1,108 @@ +import { makeConfig } from "@lingui/conf" +import fs from "fs" +import path from "path" +import { vueExtractor } from "./extractor" +// eslint-disable-next-line import/no-extraneous-dependencies +import type { ExtractedMessage } from "@lingui/babel-plugin-extract-messages" + +function normalizePath(entries: ExtractedMessage[]): ExtractedMessage[] { + return entries.map((entry) => { + const [filename, lineNumber, column] = entry.origin! + const projectRoot = process.cwd() + + return { + ...entry, + origin: [path.relative(projectRoot, filename ?? ""), lineNumber, column], + } + }) +} + +describe("vue extractor", () => { + const linguiConfig = makeConfig({ + locales: ["en", "nb"], + sourceLocale: "en", + rootDir: ".", + catalogs: [ + { + path: "/{locale}", + include: [""], + exclude: [], + }, + ], + extractorParserOptions: { + tsExperimentalDecorators: false, + flow: false, + }, + }) + + it("should ignore non vue files in extractor", async () => { + const match = vueExtractor.match("test.js") + + expect(match).toBeFalsy() + }) + + it("should extract message from vue file", async () => { + const filePath = path.resolve(__dirname, "fixtures/test.vue") + const code = fs.readFileSync(filePath, "utf-8") + + let messages: ExtractedMessage[] = [] + + await vueExtractor.extract( + "test.vue", + code, + (res) => { + messages.push(res) + }, + { + linguiConfig, + } + ) + + messages = normalizePath(messages) + + expect(messages).toMatchSnapshot() + }) + it("should support macro expansion", async () => { + const filePath = path.resolve(__dirname, "fixtures/macro.vue") + const code = fs.readFileSync(filePath, "utf-8") + + let messages: ExtractedMessage[] = [] + + await vueExtractor.extract( + "macro.vue", + code, + (res) => { + messages.push(res) + }, + { + linguiConfig, + } + ) + + messages = normalizePath(messages) + + expect(messages).toMatchSnapshot() + }) + + it("should extract message from functional component", async () => { + const filePath = path.resolve(__dirname, "fixtures/functional.vue") + const code = fs.readFileSync(filePath, "utf-8") + + let messages: ExtractedMessage[] = [] + + await vueExtractor.extract( + "functional.vue", + code, + (res) => { + messages.push(res) + }, + { + linguiConfig, + } + ) + + messages = normalizePath(messages) + + expect(messages).toMatchSnapshot() + }) +}) diff --git a/packages/vue/src/extractor/extractor.ts b/packages/vue/src/extractor/extractor.ts new file mode 100644 index 000000000..718f2a90a --- /dev/null +++ b/packages/vue/src/extractor/extractor.ts @@ -0,0 +1,86 @@ +import { extractor as defaultExtractor } from "@lingui/cli/api" +import { + compileTemplate, + parse, + type SFCBlock, + type SFCTemplateCompileResults, +} from "@vue/compiler-sfc" +import { transformer } from "../compiler" + +// + +type RawSourceMap = SFCTemplateCompileResults["map"] + +function isFirstIsString( + arr: [string | undefined, RawSourceMap | undefined, boolean] +): arr is [string, RawSourceMap | undefined, boolean] { + return typeof arr[0] === "string" +} + +type ExtractorType = typeof defaultExtractor + +export const vueExtractor: ExtractorType = { + match(filename) { + return filename.endsWith(".vue") + }, + async extract(filename, code, onMessageExtracted, ctx) { + const { descriptor } = parse(code, { + sourceMap: true, + filename, + ignoreEmpty: true, + }) + const isTsBlock = (block: SFCBlock | null | undefined) => + block?.lang === "ts" + const compiledTemplate = + descriptor.template && + compileTemplate({ + source: descriptor.template.content, + filename, + inMap: descriptor.template.map, + id: filename, + compilerOptions: { + comments: true, + isTS: + isTsBlock(descriptor.script) || isTsBlock(descriptor.scriptSetup), + nodeTransforms: [transformer()], + }, + }) + + const targets = [ + [ + descriptor.script?.content, + descriptor.script?.map, + isTsBlock(descriptor.script), + ], + [ + descriptor.scriptSetup?.content, + descriptor.scriptSetup?.map, + isTsBlock(descriptor.scriptSetup), + ], + [ + compiledTemplate?.code, + compiledTemplate?.map, + isTsBlock(descriptor.script) || isTsBlock(descriptor.scriptSetup), + ], + ] satisfies [string | undefined, RawSourceMap | undefined, boolean][] + + // early return to please TypeScript + if (!ctx) return + + await Promise.all( + targets + .filter<[string, RawSourceMap | undefined, boolean]>(isFirstIsString) + .map(([source, map, isTs]) => + defaultExtractor.extract( + filename + (isTs ? ".ts" : ""), + source, + onMessageExtracted, + { + ...ctx, + sourceMaps: map, + } + ) + ) + ) + }, +} diff --git a/packages/vue/src/extractor/fixtures/functional.vue b/packages/vue/src/extractor/fixtures/functional.vue new file mode 100644 index 000000000..2a1b390a0 --- /dev/null +++ b/packages/vue/src/extractor/fixtures/functional.vue @@ -0,0 +1,12 @@ + diff --git a/packages/vue/src/extractor/fixtures/macro.vue b/packages/vue/src/extractor/fixtures/macro.vue new file mode 100644 index 000000000..e6c6bb8cb --- /dev/null +++ b/packages/vue/src/extractor/fixtures/macro.vue @@ -0,0 +1,32 @@ + + + diff --git a/packages/vue/src/extractor/fixtures/test.vue b/packages/vue/src/extractor/fixtures/test.vue new file mode 100644 index 000000000..7db3d1ff4 --- /dev/null +++ b/packages/vue/src/extractor/fixtures/test.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/packages/vue/src/extractor/index.ts b/packages/vue/src/extractor/index.ts new file mode 100644 index 000000000..51731cf0a --- /dev/null +++ b/packages/vue/src/extractor/index.ts @@ -0,0 +1 @@ +export { vueExtractor } from "./extractor" diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts new file mode 100644 index 000000000..257ffb173 --- /dev/null +++ b/packages/vue/src/index.ts @@ -0,0 +1,3 @@ +export { default as Trans } from "./components/Trans" +export { vt } from "./components/vt" +export { linguiPlugin, useI18n } from "./plugins/lingui" diff --git a/packages/vue/src/plugins/lingui.ts b/packages/vue/src/plugins/lingui.ts new file mode 100644 index 000000000..f2a03911d --- /dev/null +++ b/packages/vue/src/plugins/lingui.ts @@ -0,0 +1,25 @@ +import { type I18n } from "@lingui/core" +import { inject, type Plugin } from "vue" + +type LinguiPluginOptions = { + i18n: I18n +} + +export const linguiPlugin: Plugin = { + install(app, options) { + app.provide("i18n", options.i18n) + options.i18n.on("change", () => { + // TODO: make components re-render + }) + }, +} + +export function useI18n(): I18n { + const innerI18n = inject("i18n") + if (!innerI18n) { + throw new Error( + "Should provider an i18n instance 1st; use the LinguiPlugin." + ) + } + return innerI18n +} diff --git a/packages/vue/src/test/utils.ts b/packages/vue/src/test/utils.ts new file mode 100644 index 000000000..59376c84d --- /dev/null +++ b/packages/vue/src/test/utils.ts @@ -0,0 +1,30 @@ +import { compileTemplate } from "vue/compiler-sfc" +import { transformer } from "../compiler" +import { parse } from "@vue/compiler-sfc" +import { BindingTypes } from "@vue/compiler-core" + +export function run(source: string) { + const code = source.trim().startsWith("${source}` + + const { descriptor } = parse(code, { + sourceMap: true, + filename: "App.vue", + ignoreEmpty: true, + }) + + return compileTemplate({ + filename: "App.vue", + id: "app", + source: descriptor.template?.src!, + ast: descriptor.template?.ast, + + compilerOptions: { + bindingMetadata: { + Trans: BindingTypes.SETUP_MAYBE_REF, + }, + nodeTransforms: [transformer()], + }, + }) +} diff --git a/packages/vue/src/vite-plugin/core-macros-plugin.ts b/packages/vue/src/vite-plugin/core-macros-plugin.ts new file mode 100644 index 000000000..2b48e13fd --- /dev/null +++ b/packages/vue/src/vite-plugin/core-macros-plugin.ts @@ -0,0 +1,36 @@ +import { type Plugin, type TransformResult } from "vite" +import * as babel from "@babel/core" + +const sourceRegex = /\.(:?[j|t]sx?|vue)$/u + +// make babel macros works in vite +export function linguiCoreMacros(): Plugin { + return { + name: "vite-plugin-vue-lingui-babel-macro", + async transform( + source: string, + filename: string + ): Promise { + if (filename.includes("node_modules")) { + return undefined + } + + if (!sourceRegex.test(filename)) { + return undefined + } + + const result = await babel.transformAsync(source, { + filename, + plugins: ["@lingui/babel-plugin-lingui-macro"], + babelrc: false, + configFile: false, + sourceMaps: true, + }) + + return { + code: result?.code, + map: result?.map, + } as TransformResult + }, + } as const +} diff --git a/packages/vue/src/vite-plugin/index.ts b/packages/vue/src/vite-plugin/index.ts new file mode 100644 index 000000000..c640b2176 --- /dev/null +++ b/packages/vue/src/vite-plugin/index.ts @@ -0,0 +1,59 @@ +import { PluginOption } from "vite" +import { lingui } from "@lingui/vite-plugin" +import { linguiCoreMacros } from "./core-macros-plugin" +import type { Api } from "@vitejs/plugin-vue" +import { transformer } from "../compiler" + +type LinguiConfigOpts = { + cwd?: string + configPath?: string + skipValidation?: boolean +} + +type Options = { + suppressRegisteringBabelMacro?: boolean + linguiConfigOptions?: LinguiConfigOpts + isProduction?: boolean +} + +export function vueLingui(options: Options = {}): PluginOption[] { + options = { + suppressRegisteringBabelMacro: false, + isProduction: process.env.NODE_ENV === "production", + ...options, + } + + return [ + lingui(options.linguiConfigOptions), + !options.suppressRegisteringBabelMacro ? linguiCoreMacros() : false, + { + name: "vite-plugin-lingui-vue-transform", + enforce: "pre", + configResolved(config) { + const vitePlugin = config.plugins.find( + (plugin) => plugin.name === "vite:vue" + ) + + if (!vitePlugin) { + throw new Error( + "Lingui Vue Plugin: Vite plugin is not found in your configuration. Please install it and to your Vite config" + ) + } + + const api = vitePlugin.api as Api + + // register Lingui template transformer + api.options.template = { + ...api.options.template, + compilerOptions: { + ...api.options.template?.compilerOptions, + nodeTransforms: [ + transformer({ stripNonEssentialProps: options.isProduction }), + ...(api.options.template?.compilerOptions?.nodeTransforms || []), + ], + }, + } + }, + }, + ] +} diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json new file mode 100644 index 000000000..2ada66de6 --- /dev/null +++ b/packages/vue/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "strict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + } +} diff --git a/tsconfig.json b/tsconfig.json index 976d100e0..35270188f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,10 +24,12 @@ "@lingui/conf": ["./packages/conf/src"], "@lingui/babel-plugin-lingui-macro": ["./packages/babel-plugin-lingui-macro/src/index.ts"], "@lingui/babel-plugin-lingui-macro/macro": ["./packages/babel-plugin-lingui-macro/src/macro.ts"], + "@lingui/babel-plugin-lingui-macro/ast": ["./packages/babel-plugin-lingui-macro/src/ast.ts"], "@lingui/macro": ["./packages/macro"], "@lingui/format-po": ["./packages/format-po/src/po.ts"], "@lingui/format-json": ["./packages/format-json/src/json.ts"], - "@lingui/extractor-vue": ["./packages/extractor-vue/src"] + "@lingui/extractor-vue": ["./packages/extractor-vue/src"], + "@lingui/vue": ["./packages/vue/src"] } }, "exclude": [ diff --git a/yarn.lock b/yarn.lock index f1708300d..18de4b436 100644 --- a/yarn.lock +++ b/yarn.lock @@ -215,6 +215,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.20.12": + version: 7.25.6 + resolution: "@babel/generator@npm:7.25.6" + dependencies: + "@babel/types": ^7.25.6 + "@jridgewell/gen-mapping": ^0.3.5 + "@jridgewell/trace-mapping": ^0.3.25 + jsesc: ^2.5.1 + checksum: b55975cd664f5602304d868bb34f4ee3bed6f5c7ce8132cd92ff27a46a53a119def28a182d91992e86f75db904f63094a81247703c4dc96e4db0c03fd04bcd68 + languageName: node + linkType: hard + "@babel/generator@npm:^7.20.7": version: 7.20.7 resolution: "@babel/generator@npm:7.20.7" @@ -754,6 +766,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-string-parser@npm:7.24.8" + checksum: 39b03c5119216883878655b149148dc4d2e284791e969b19467a9411fccaa33f7a713add98f4db5ed519535f70ad273cdadfd2eb54d47ebbdeac5083351328ce + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.18.6, @babel/helper-validator-identifier@npm:^7.19.1": version: 7.19.1 resolution: "@babel/helper-validator-identifier@npm:7.19.1" @@ -979,6 +998,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.25.3": + version: 7.25.6 + resolution: "@babel/parser@npm:7.25.6" + dependencies: + "@babel/types": ^7.25.6 + bin: + parser: ./bin/babel-parser.js + checksum: 85b237ded09ee43cc984493c35f3b1ff8a83e8dbbb8026b8132e692db6567acc5a1659ec928e4baa25499ddd840d7dae9dee3062be7108fe23ec5f94a8066b1e + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.18.6" @@ -2170,6 +2200,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.20.12, @babel/types@npm:^7.25.6": + version: 7.25.6 + resolution: "@babel/types@npm:7.25.6" + dependencies: + "@babel/helper-string-parser": ^7.24.8 + "@babel/helper-validator-identifier": ^7.24.7 + to-fast-properties: ^2.0.0 + checksum: 9b2f84ff3f874ad05b0b9bf06862c56f478b65781801f82296b4cc01bee39e79c20a7c0a06959fed0ee582c8267e1cb21638318655c5e070b0287242a844d1c9 + languageName: node + linkType: hard + "@babel/types@npm:^7.21.0, @babel/types@npm:^7.21.2, @babel/types@npm:^7.8.3": version: 7.21.2 resolution: "@babel/types@npm:7.21.2" @@ -3442,6 +3483,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.5.0": + version: 1.5.0 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" + checksum: 05df4f2538b3b0f998ea4c1cd34574d0feba216fa5d4ccaef0187d12abf82eafe6021cec8b49f9bb4d90f2ba4582ccc581e72986a5fcf4176ae0cfeb04cf52ec + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.15, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.17 resolution: "@jridgewell/trace-mapping@npm:0.3.17" @@ -3494,7 +3542,7 @@ __metadata: languageName: node linkType: hard -"@lingui/babel-plugin-extract-messages@5.0.0-next.1, @lingui/babel-plugin-extract-messages@workspace:packages/babel-plugin-extract-messages": +"@lingui/babel-plugin-extract-messages@5.0.0-next.1, @lingui/babel-plugin-extract-messages@^5.0.0-next.0, @lingui/babel-plugin-extract-messages@workspace:packages/babel-plugin-extract-messages": version: 0.0.0-use.local resolution: "@lingui/babel-plugin-extract-messages@workspace:packages/babel-plugin-extract-messages" dependencies: @@ -3507,7 +3555,7 @@ __metadata: languageName: unknown linkType: soft -"@lingui/babel-plugin-lingui-macro@5.0.0-next.1, @lingui/babel-plugin-lingui-macro@workspace:*, @lingui/babel-plugin-lingui-macro@workspace:packages/babel-plugin-lingui-macro": +"@lingui/babel-plugin-lingui-macro@5.0.0-next.1, @lingui/babel-plugin-lingui-macro@^5.0.0-next.0, @lingui/babel-plugin-lingui-macro@workspace:*, @lingui/babel-plugin-lingui-macro@workspace:packages/babel-plugin-lingui-macro": version: 0.0.0-use.local resolution: "@lingui/babel-plugin-lingui-macro@workspace:packages/babel-plugin-lingui-macro" dependencies: @@ -3530,7 +3578,26 @@ __metadata: languageName: unknown linkType: soft -"@lingui/cli@5.0.0-next.1, @lingui/cli@workspace:*, @lingui/cli@workspace:packages/cli": +"@lingui/babel-plugin-lingui-macro@npm:5.0.0-next.0": + version: 5.0.0-next.0 + resolution: "@lingui/babel-plugin-lingui-macro@npm:5.0.0-next.0" + dependencies: + "@babel/core": ^7.20.12 + "@babel/runtime": ^7.20.13 + "@babel/types": ^7.20.7 + "@lingui/conf": ^5.0.0-next.0 + "@lingui/core": ^5.0.0-next.0 + "@lingui/message-utils": ^5.0.0-next.0 + peerDependencies: + babel-plugin-macros: 2 || 3 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + checksum: ef25544b5ec69d1cd6c4bf4b97657aefa50fec22da2c26eebe8b153cc619b0d562022d324582bb5462d0147e4a6b0d05f833ae2962c69d270ae1ec7d9ab697f6 + languageName: node + linkType: hard + +"@lingui/cli@5.0.0-next.1, @lingui/cli@^5.0.0-next.0, @lingui/cli@workspace:*, @lingui/cli@workspace:packages/cli": version: 0.0.0-use.local resolution: "@lingui/cli@workspace:packages/cli" dependencies: @@ -3575,7 +3642,47 @@ __metadata: languageName: unknown linkType: soft -"@lingui/conf@5.0.0-next.1, @lingui/conf@workspace:packages/conf": +"@lingui/cli@npm:5.0.0-next.0": + version: 5.0.0-next.0 + resolution: "@lingui/cli@npm:5.0.0-next.0" + dependencies: + "@babel/core": ^7.21.0 + "@babel/generator": ^7.21.1 + "@babel/parser": ^7.21.2 + "@babel/runtime": ^7.21.0 + "@babel/types": ^7.21.2 + "@lingui/babel-plugin-extract-messages": ^5.0.0-next.0 + "@lingui/babel-plugin-lingui-macro": ^5.0.0-next.0 + "@lingui/conf": ^5.0.0-next.0 + "@lingui/core": ^5.0.0-next.0 + "@lingui/format-po": ^5.0.0-next.0 + "@lingui/message-utils": ^5.0.0-next.0 + babel-plugin-macros: ^3.0.1 + chalk: ^4.1.0 + chokidar: 3.5.1 + cli-table: 0.3.6 + commander: ^10.0.0 + convert-source-map: ^2.0.0 + date-fns: ^3.6.0 + esbuild: ^0.21.5 + glob: ^7.1.4 + inquirer: ^7.3.3 + micromatch: 4.0.2 + normalize-path: ^3.0.0 + ora: ^5.1.0 + pathe: ^1.1.0 + pkg-up: ^3.1.0 + pofile: ^1.1.4 + pseudolocale: ^2.0.0 + ramda: ^0.27.1 + source-map: ^0.8.0-beta.0 + bin: + lingui: dist/lingui.js + checksum: 9d230bc1dfb05824dd7bf5b6aca818484646e522bf04f5340af15040bba73a2a73b218a4dc68bf803ce4d764f1b5f6fb8ea096f153b6d4b143155b995afbcb66 + languageName: node + linkType: hard + +"@lingui/conf@5.0.0-next.1, @lingui/conf@^5.0.0-next.0, @lingui/conf@workspace:packages/conf": version: 0.0.0-use.local resolution: "@lingui/conf@workspace:packages/conf" dependencies: @@ -3591,7 +3698,21 @@ __metadata: languageName: unknown linkType: soft -"@lingui/core@5.0.0-next.1, @lingui/core@workspace:*, @lingui/core@workspace:^, @lingui/core@workspace:packages/core": +"@lingui/conf@npm:5.0.0-next.0": + version: 5.0.0-next.0 + resolution: "@lingui/conf@npm:5.0.0-next.0" + dependencies: + "@babel/runtime": ^7.20.13 + chalk: ^4.1.0 + cosmiconfig: ^8.0.0 + jest-validate: ^29.4.3 + jiti: ^1.17.1 + lodash.get: ^4.4.2 + checksum: e711b15c711519a4c4a398fb48317fa8a741224f64eeedd15773e7417d81df3d3486714a15e541b2207884e9acc2dda7c2e6b3b69a3c07c8e48851a464aa0df0 + languageName: node + linkType: hard + +"@lingui/core@5.0.0-next.1, @lingui/core@^5.0.0-next.0, @lingui/core@workspace:*, @lingui/core@workspace:^, @lingui/core@workspace:packages/core": version: 0.0.0-use.local resolution: "@lingui/core@workspace:packages/core" dependencies: @@ -3622,6 +3743,25 @@ __metadata: languageName: node linkType: hard +"@lingui/core@npm:5.0.0-next.0": + version: 5.0.0-next.0 + resolution: "@lingui/core@npm:5.0.0-next.0" + dependencies: + "@babel/runtime": ^7.20.13 + "@lingui/message-utils": ^5.0.0-next.0 + unraw: ^3.0.0 + peerDependencies: + "@lingui/babel-plugin-lingui-macro": 5.0.0-next.0 + babel-plugin-macros: 2 || 3 + peerDependenciesMeta: + "@lingui/babel-plugin-lingui-macro": + optional: true + babel-plugin-macros: + optional: true + checksum: d8855075a10b3f64d72a2d7a5cd7fc95108934385b5351ae694a3522c2db8710a376fcc2221cf8db229029b5679ad123d31db77a4f79b5b2aa6e8129816f59af + languageName: node + linkType: hard + "@lingui/detect-locale@workspace:packages/detect-locale": version: 0.0.0-use.local resolution: "@lingui/detect-locale@workspace:packages/detect-locale" @@ -3683,7 +3823,7 @@ __metadata: languageName: unknown linkType: soft -"@lingui/format-po@5.0.0-next.1, @lingui/format-po@workspace:packages/format-po": +"@lingui/format-po@5.0.0-next.1, @lingui/format-po@^5.0.0-next.0, @lingui/format-po@workspace:packages/format-po": version: 0.0.0-use.local resolution: "@lingui/format-po@workspace:packages/format-po" dependencies: @@ -3736,7 +3876,7 @@ __metadata: languageName: unknown linkType: soft -"@lingui/message-utils@5.0.0-next.1, @lingui/message-utils@workspace:packages/message-utils": +"@lingui/message-utils@5.0.0-next.1, @lingui/message-utils@^5.0.0-next.0, @lingui/message-utils@workspace:packages/message-utils": version: 0.0.0-use.local resolution: "@lingui/message-utils@workspace:packages/message-utils" dependencies: @@ -3756,6 +3896,16 @@ __metadata: languageName: node linkType: hard +"@lingui/message-utils@npm:5.0.0-next.0": + version: 5.0.0-next.0 + resolution: "@lingui/message-utils@npm:5.0.0-next.0" + dependencies: + "@messageformat/parser": ^5.0.0 + js-sha256: ^0.10.1 + checksum: ef88a68b91dd33c621e04b95a9549a04ad167016de40e676b7318cc18d855dc8938c4d21194f050b10bfce4e91aa38d54ae5a95bd7d82858cff624e9a7dbc972 + languageName: node + linkType: hard + "@lingui/react@5.0.0-next.1, @lingui/react@workspace:*, @lingui/react@workspace:packages/react": version: 0.0.0-use.local resolution: "@lingui/react@workspace:packages/react" @@ -3794,6 +3944,18 @@ __metadata: languageName: unknown linkType: soft +"@lingui/vite-plugin@npm:5.0.0-next.0": + version: 5.0.0-next.0 + resolution: "@lingui/vite-plugin@npm:5.0.0-next.0" + dependencies: + "@lingui/cli": ^5.0.0-next.0 + "@lingui/conf": ^5.0.0-next.0 + peerDependencies: + vite: ^3 || ^4 || ^5.0.9 + checksum: f7cef1ad460b65aeacd5040e4d8d8c9231b1e8f84c872b810ba55a6bb62c934077ca3c617337024c36e1f112a7c498fd80c3755e8bd299d93595554ed65ed19f + languageName: node + linkType: hard + "@lingui/vite-plugin@workspace:packages/vite-plugin": version: 0.0.0-use.local resolution: "@lingui/vite-plugin@workspace:packages/vite-plugin" @@ -3810,6 +3972,32 @@ __metadata: languageName: unknown linkType: soft +"@lingui/vue@workspace:packages/vue": + version: 0.0.0-use.local + resolution: "@lingui/vue@workspace:packages/vue" + dependencies: + "@babel/core": ^7.20.12 + "@babel/generator": ^7.20.12 + "@babel/types": ^7.20.12 + "@lingui/babel-plugin-lingui-macro": 5.0.0-next.0 + "@lingui/cli": 5.0.0-next.0 + "@lingui/conf": 5.0.0-next.0 + "@lingui/core": 5.0.0-next.0 + "@lingui/message-utils": 5.0.0-next.0 + "@lingui/vite-plugin": 5.0.0-next.0 + "@types/babel__core": ^7.20.5 + "@vitejs/plugin-vue": ^5.0.5 + "@vue/compiler-core": ^3.3.4 + "@vue/compiler-sfc": ^3.3.4 + unbuild: 2.0.0 + vite: 4.1.4 + vue: ^3.3.4 + peerDependencies: + "@vitejs/plugin-vue": "*" + vite: ^3 || ^4 || ^5.0.9 + languageName: unknown + linkType: soft + "@messageformat/parser@npm:^5.0.0": version: 5.0.0 resolution: "@messageformat/parser@npm:5.0.0" @@ -4751,6 +4939,19 @@ __metadata: languageName: node linkType: hard +"@types/babel__core@npm:^7.20.5": + version: 7.20.5 + resolution: "@types/babel__core@npm:7.20.5" + dependencies: + "@babel/parser": ^7.20.7 + "@babel/types": ^7.20.7 + "@types/babel__generator": "*" + "@types/babel__template": "*" + "@types/babel__traverse": "*" + checksum: a3226f7930b635ee7a5e72c8d51a357e799d19cbf9d445710fa39ab13804f79ab1a54b72ea7d8e504659c7dfc50675db974b526142c754398d7413aa4bc30845 + languageName: node + linkType: hard + "@types/babel__generator@npm:*": version: 7.6.4 resolution: "@types/babel__generator@npm:7.6.4" @@ -5251,6 +5452,16 @@ __metadata: languageName: node linkType: hard +"@vitejs/plugin-vue@npm:^5.0.5": + version: 5.1.4 + resolution: "@vitejs/plugin-vue@npm:5.1.4" + peerDependencies: + vite: ^5.0.0 + vue: ^3.2.25 + checksum: 80ee2d749c84fc8c495647f7704b143b8670464e6ddc894171d7a28547073cce6055e47fb6ce596a2ae525ae1059a08499d0336cc5a27997c21368d4db2bb6be + languageName: node + linkType: hard + "@vue/compiler-core@npm:3.2.47": version: 3.2.47 resolution: "@vue/compiler-core@npm:3.2.47" @@ -5263,6 +5474,19 @@ __metadata: languageName: node linkType: hard +"@vue/compiler-core@npm:3.5.8, @vue/compiler-core@npm:^3.3.4": + version: 3.5.8 + resolution: "@vue/compiler-core@npm:3.5.8" + dependencies: + "@babel/parser": ^7.25.3 + "@vue/shared": 3.5.8 + entities: ^4.5.0 + estree-walker: ^2.0.2 + source-map-js: ^1.2.0 + checksum: 2e6a71f9efef6e3746ad210b336a03cf7098e874b8f0f3aadc5043a8bfaa6b37759d00680b09ee4e266832253202b8d0d62d8b7d5af00094ed2987a47903cdb9 + languageName: node + linkType: hard + "@vue/compiler-dom@npm:3.2.47": version: 3.2.47 resolution: "@vue/compiler-dom@npm:3.2.47" @@ -5273,6 +5497,33 @@ __metadata: languageName: node linkType: hard +"@vue/compiler-dom@npm:3.5.8": + version: 3.5.8 + resolution: "@vue/compiler-dom@npm:3.5.8" + dependencies: + "@vue/compiler-core": 3.5.8 + "@vue/shared": 3.5.8 + checksum: 8350175c19a7b70f1682ffbc6efba5bc4d359e09730874d4d9242d52fcf73b2026628b1bba6aac7ee909f237ec2d3cb897d6fb18a611f65e2e6491450ada0c4a + languageName: node + linkType: hard + +"@vue/compiler-sfc@npm:3.5.8, @vue/compiler-sfc@npm:^3.3.4": + version: 3.5.8 + resolution: "@vue/compiler-sfc@npm:3.5.8" + dependencies: + "@babel/parser": ^7.25.3 + "@vue/compiler-core": 3.5.8 + "@vue/compiler-dom": 3.5.8 + "@vue/compiler-ssr": 3.5.8 + "@vue/shared": 3.5.8 + estree-walker: ^2.0.2 + magic-string: ^0.30.11 + postcss: ^8.4.47 + source-map-js: ^1.2.0 + checksum: be89b02bb5d16fc46f7ff962b95b6152a52e08197360a1ae6eb9e5ac92b45bb9f140c3f5297ca2dcc7396f94d4e7eb39a66610ab3a72ab33516fe41dfd45531b + languageName: node + linkType: hard + "@vue/compiler-sfc@npm:^3.2.47": version: 3.2.47 resolution: "@vue/compiler-sfc@npm:3.2.47" @@ -5301,6 +5552,16 @@ __metadata: languageName: node linkType: hard +"@vue/compiler-ssr@npm:3.5.8": + version: 3.5.8 + resolution: "@vue/compiler-ssr@npm:3.5.8" + dependencies: + "@vue/compiler-dom": 3.5.8 + "@vue/shared": 3.5.8 + checksum: 965250915016709ba60347fbdd58dd3b44ffbafb5a178eb0c31fa35515e262e1a5bce32b6744600599e1387e8d9a335f10e23ebe69a79091f9e81ee122af9bde + languageName: node + linkType: hard + "@vue/reactivity-transform@npm:3.2.47": version: 3.2.47 resolution: "@vue/reactivity-transform@npm:3.2.47" @@ -5314,6 +5575,49 @@ __metadata: languageName: node linkType: hard +"@vue/reactivity@npm:3.5.8": + version: 3.5.8 + resolution: "@vue/reactivity@npm:3.5.8" + dependencies: + "@vue/shared": 3.5.8 + checksum: 4b6238a5d65254ad50d00fc7e4dbac722b0657d9d15e62df1b8f1db02c01f29cbc6c166814021d84ceb01b5538cb9849ec93a2edd55f6996e07538ffefe6bfee + languageName: node + linkType: hard + +"@vue/runtime-core@npm:3.5.8": + version: 3.5.8 + resolution: "@vue/runtime-core@npm:3.5.8" + dependencies: + "@vue/reactivity": 3.5.8 + "@vue/shared": 3.5.8 + checksum: 5b6255d0e67889210e352423963563ec5181fcc5cac1c45dea8bed938b059b71292cb161fcb0fa0e51fbfae0dd66bc642f8f5c15feb592a69993829e6a1af67b + languageName: node + linkType: hard + +"@vue/runtime-dom@npm:3.5.8": + version: 3.5.8 + resolution: "@vue/runtime-dom@npm:3.5.8" + dependencies: + "@vue/reactivity": 3.5.8 + "@vue/runtime-core": 3.5.8 + "@vue/shared": 3.5.8 + csstype: ^3.1.3 + checksum: dbe1e86f3ee10f9c9b318ff3668a4e0cde643ecb6ad57776f8ac7c7b6959d1708bedd18ca51408197f66b05bbd11642b24bb69b88539cd497340ea590899c6f2 + languageName: node + linkType: hard + +"@vue/server-renderer@npm:3.5.8": + version: 3.5.8 + resolution: "@vue/server-renderer@npm:3.5.8" + dependencies: + "@vue/compiler-ssr": 3.5.8 + "@vue/shared": 3.5.8 + peerDependencies: + vue: 3.5.8 + checksum: b7acf66152efeb0be4d3162003303096a6de59f2bb2eed44d387c5b6c1e236395c4353b4b2aca496af150cbef22383d3b4d195fd32dd595761d209b5249fd230 + languageName: node + linkType: hard + "@vue/shared@npm:3.2.47": version: 3.2.47 resolution: "@vue/shared@npm:3.2.47" @@ -5321,6 +5625,13 @@ __metadata: languageName: node linkType: hard +"@vue/shared@npm:3.5.8": + version: 3.5.8 + resolution: "@vue/shared@npm:3.5.8" + checksum: ea60091f7c9881b7aacbe4e821e817594f4ac77d2675447f94da03c3459b272d00266349aa7a8b182bdc2e426d1878f7a4d2ae83d14887932325dcb99557c991 + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.11.1": version: 1.11.1 resolution: "@webassemblyjs/ast@npm:1.11.1" @@ -6225,7 +6536,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.3, braces@npm:~3.0.2": +"braces@npm:^3.0.1, braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -6636,6 +6947,15 @@ __metadata: languageName: node linkType: hard +"cli-table@npm:0.3.6": + version: 0.3.6 + resolution: "cli-table@npm:0.3.6" + dependencies: + colors: 1.0.3 + checksum: b0cd08578c810240920438cc2b3ffb4b4f5106b29f3362707f1d8cfc0c0440ad2afb70b96e30ce37f72f0ffe1e844ae7341dde4df17d51ad345eb186a5903af2 + languageName: node + linkType: hard + "cli-table@npm:^0.3.11": version: 0.3.11 resolution: "cli-table@npm:0.3.11" @@ -7184,6 +7504,13 @@ __metadata: languageName: node linkType: hard +"csstype@npm:^3.1.3": + version: 3.1.3 + resolution: "csstype@npm:3.1.3" + checksum: 8db785cc92d259102725b3c694ec0c823f5619a84741b5c7991b8ad135dfaa66093038a1cc63e03361a6cd28d122be48f2106ae72334e067dd619a51f49eddf7 + languageName: node + linkType: hard + "dargs@npm:^7.0.0": version: 7.0.0 resolution: "dargs@npm:7.0.0" @@ -7642,6 +7969,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^4.5.0": + version: 4.5.0 + resolution: "entities@npm:4.5.0" + checksum: 853f8ebd5b425d350bffa97dd6958143179a5938352ccae092c62d1267c4e392a039be1bae7d51b6e4ffad25f51f9617531fedf5237f15df302ccfb452cbf2d7 + languageName: node + linkType: hard + "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -11767,6 +12101,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.11": + version: 0.30.11 + resolution: "magic-string@npm:0.30.11" + dependencies: + "@jridgewell/sourcemap-codec": ^1.5.0 + checksum: e041649453c9a3f31d2e731fc10e38604d50e20d3585cd48bc7713a6e2e1a3ad3012105929ca15750d59d0a3f1904405e4b95a23b7e69dc256db3c277a73a3ca + languageName: node + linkType: hard + "magic-string@npm:^0.30.3": version: 0.30.3 resolution: "magic-string@npm:0.30.3" @@ -11921,6 +12264,16 @@ __metadata: languageName: node linkType: hard +"micromatch@npm:4.0.2": + version: 4.0.2 + resolution: "micromatch@npm:4.0.2" + dependencies: + braces: ^3.0.1 + picomatch: ^2.0.5 + checksum: 39590a96d9ffad21f0afac044d0a5af4f33715a16fdd82c53a01c8f5ff6f70832a31b53e52972dac3deff8bf9f0bed0207d1c34e54ab3306a5e4c4efd5f7d249 + languageName: node + linkType: hard + "micromatch@npm:^4.0.4, micromatch@npm:^4.0.5, micromatch@npm:^4.0.7": version: 4.0.7 resolution: "micromatch@npm:4.0.7" @@ -12253,6 +12606,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.7": + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" + bin: + nanoid: bin/nanoid.cjs + checksum: d36c427e530713e4ac6567d488b489a36582ef89da1d6d4e3b87eded11eb10d7042a877958c6f104929809b2ab0bafa17652b076cdf84324aa75b30b722204f2 + languageName: node + linkType: hard + "nanospinner@npm:^1.1.0": version: 1.1.0 resolution: "nanospinner@npm:1.1.0" @@ -13391,7 +13753,14 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": +"picocolors@npm:^1.1.0": + version: 1.1.0 + resolution: "picocolors@npm:1.1.0" + checksum: a64d653d3a188119ff45781dfcdaeedd7625583f45280aea33fcb032c7a0d3959f2368f9b192ad5e8aade75b74dbd954ffe3106c158509a45e4c18ab379a2acd + languageName: node + linkType: hard + +"picomatch@npm:^2.0.4, picomatch@npm:^2.0.5, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf @@ -13505,6 +13874,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.4.47": + version: 8.4.47 + resolution: "postcss@npm:8.4.47" + dependencies: + nanoid: ^3.3.7 + picocolors: ^1.1.0 + source-map-js: ^1.2.1 + checksum: f78440a9d8f97431dd2ab1ab8e1de64f12f3eff38a3d8d4a33919b96c381046a314658d2de213a5fa5eb296b656de76a3ec269fdea27f16d5ab465b916a0f52c + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -14639,6 +15019,13 @@ __metadata: languageName: node linkType: hard +"source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": + version: 1.2.1 + resolution: "source-map-js@npm:1.2.1" + checksum: 4eb0cd997cdf228bc253bcaff9340afeb706176e64868ecd20efbe6efea931465f43955612346d6b7318789e5265bdc419bc7669c1cebe3db0eb255f57efa76b + languageName: node + linkType: hard + "source-map-support@npm:0.5.13": version: 0.5.13 resolution: "source-map-support@npm:0.5.13" @@ -15976,6 +16363,24 @@ __metadata: languageName: node linkType: hard +"vue@npm:^3.3.4": + version: 3.5.8 + resolution: "vue@npm:3.5.8" + dependencies: + "@vue/compiler-dom": 3.5.8 + "@vue/compiler-sfc": 3.5.8 + "@vue/runtime-dom": 3.5.8 + "@vue/server-renderer": 3.5.8 + "@vue/shared": 3.5.8 + peerDependencies: + typescript: "*" + peerDependenciesMeta: + typescript: + optional: true + checksum: 8f385681699b1a176240713dbabc3e9fe6490034fb14828fb2508aced04e5c1aaa700bebade8921d8f79734bfb5754ccf124a3e2b10fc83c800152404b32554e + languageName: node + linkType: hard + "w3c-hr-time@npm:^1.0.2": version: 1.0.2 resolution: "w3c-hr-time@npm:1.0.2"