From a70ee9fd0480507204138eea7a8c1ed899d95825 Mon Sep 17 00:00:00 2001 From: Christoffer Jahren Date: Fri, 31 Mar 2023 10:54:11 +0200 Subject: [PATCH 1/3] feat: add new package with vue extractor --- jest.config.js | 2 +- lerna.json | 9 +- packages/extractor-vue/README.md | 47 +++++++ packages/extractor-vue/package.json | 41 +++++++ .../src/__snapshots__/extractor.test.ts.snap | 72 +++++++++++ packages/extractor-vue/src/extractor.test.ts | 64 ++++++++++ packages/extractor-vue/src/fixtures/test.vue | 35 ++++++ packages/extractor-vue/src/index.ts | 1 + packages/extractor-vue/src/vue-extractor.ts | 63 ++++++++++ tsconfig.json | 10 +- yarn.lock | 115 +++++++++++++++++- 11 files changed, 445 insertions(+), 14 deletions(-) create mode 100644 packages/extractor-vue/README.md create mode 100644 packages/extractor-vue/package.json create mode 100644 packages/extractor-vue/src/__snapshots__/extractor.test.ts.snap create mode 100644 packages/extractor-vue/src/extractor.test.ts create mode 100644 packages/extractor-vue/src/fixtures/test.vue create mode 100644 packages/extractor-vue/src/index.ts create mode 100644 packages/extractor-vue/src/vue-extractor.ts diff --git a/jest.config.js b/jest.config.js index 9febee3a7..4299de529 100644 --- a/jest.config.js +++ b/jest.config.js @@ -32,7 +32,6 @@ module.exports = { ".*.js.snap$", ], coverageReporters: ["lcov", "text"], - globalSetup: "./scripts/jest/setupTimezone.js", projects: [ { @@ -70,6 +69,7 @@ module.exports = { "/packages/format-json", "/packages/format-csv", "/packages/message-utils", + "/packages/extractor-vue", ], }, ], diff --git a/lerna.json b/lerna.json index aee16b325..b1aa4fb7c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,8 +1,6 @@ { "version": "4.0.0-next.7", - "packages": [ - "packages/*" - ], + "packages": ["packages/*"], "npmClient": "yarn", "useWorkspaces": true, "command": { @@ -19,10 +17,7 @@ ] }, "publish": { - "allowBranch": [ - "main", - "next" - ], + "allowBranch": ["main", "next"], "ignoreChanges": [ "**/CHANGELOG.md", "**/examples/*", diff --git a/packages/extractor-vue/README.md b/packages/extractor-vue/README.md new file mode 100644 index 000000000..0f07e3823 --- /dev/null +++ b/packages/extractor-vue/README.md @@ -0,0 +1,47 @@ +# Vue extractor + +This package contains a set of custom extractors that handles Vue files. It supports extracting messages from script and setup scripts as well as limited support for extracting messages from templates. + +## Installation + +```sh +npm install @lingui/extractor-vue +``` + +## Usage + +This custom extractor requires that you use typescript for your lingui configuration. + +```ts +import { vueExtractor } from "@lingui/extractor-vue" +import babel from "@lingui/cli/api/extractors/babel" + +const linguiConfig = { + locales: ["en", "nb"], + sourceLocale: "en", + catalogs: [ + { + path: "/src/{locale}", + include: ["/src"], + }, + ], + extractors: [babel, vueExtractor], +} +export default linguiConfig +``` + +## Vue template limitations + +This extractor assumes annotated `i18n` calls in Vue templates. + +The following examples will be extracted: + +- `i18n._(/*i18n*/ "Message")` +- `i18n.t(/*i18n*/ { id: "Message" })` +- `i18n.t(/*i18n*/ { message: "Message", id: "my.message", comment: "Comment" })` + +While the following examples wont: + +- `i18n._("Message")` +- `i18n.t({ id: "Message" })` +- `i18n.t({ message: "Message", id: "my.message", comment: "Comment" })` diff --git a/packages/extractor-vue/package.json b/packages/extractor-vue/package.json new file mode 100644 index 000000000..96fd7f751 --- /dev/null +++ b/packages/extractor-vue/package.json @@ -0,0 +1,41 @@ +{ + "name": "@lingui/extractor-vue", + "version": "4.0.0-next.5", + "description": "Custom vue extractor to be used with CLI tool", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "keywords": [ + "cli", + "i18n", + "internationalization", + "i10n", + "localization", + "i9n", + "translation", + "vue" + ], + "repository": "lingui/js-lingui", + "bugs": "https://github.com/lingui/js-lingui/issues", + "license": "MIT", + "author": { + "name": "Christoffer Jahren", + "email": "christoffer@jahren.it" + }, + "scripts": { + "build": "rimraf ./dist && unbuild", + "stub": "unbuild --stub" + }, + "engines": { + "node": ">=16.0.0" + }, + "dependencies": { + "@lingui/cli": "^4.0.0-next.5", + "@lingui/conf": "^4.0.0-next.5", + "@vue/compiler-sfc": "^3.2.47" + }, + "devDependencies": { + "@lingui/babel-plugin-extract-messages": "^4.0.0-next.5", + "unbuild": "^1.1.2" + } +} diff --git a/packages/extractor-vue/src/__snapshots__/extractor.test.ts.snap b/packages/extractor-vue/src/__snapshots__/extractor.test.ts.snap new file mode 100644 index 000000000..f145d8303 --- /dev/null +++ b/packages/extractor-vue/src/__snapshots__/extractor.test.ts.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`vue extractor should extract message from vue file 1`] = ` +[ + { + comment: undefined, + context: undefined, + id: Setup message, + message: undefined, + origin: [ + test.vue.ts, + 5, + 0, + ], + }, + { + comment: undefined, + context: undefined, + id: Script message, + message: undefined, + origin: [ + test.vue, + 16, + 20, + ], + }, + { + comment: undefined, + context: undefined, + id: custom.id, + message: My message, + origin: [ + test.vue, + 23, + 11, + ], + }, + { + comment: Message comment, + context: undefined, + id: my.message, + message: My descriptor message, + origin: [ + , + null, + null, + ], + }, + { + comment: undefined, + context: undefined, + id: id used as message, + message: undefined, + origin: [ + test.vue, + 33, + 11, + ], + }, + { + comment: undefined, + context: undefined, + id: My message without ID and context, + message: undefined, + origin: [ + test.vue, + 34, + 11, + ], + }, +] +`; diff --git a/packages/extractor-vue/src/extractor.test.ts b/packages/extractor-vue/src/extractor.test.ts new file mode 100644 index 000000000..e724a758b --- /dev/null +++ b/packages/extractor-vue/src/extractor.test.ts @@ -0,0 +1,64 @@ +import { makeConfig } from "@lingui/conf" +import fs from "fs" +import path from "path" +import { vueExtractor } from "." +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() + }) +}) diff --git a/packages/extractor-vue/src/fixtures/test.vue b/packages/extractor-vue/src/fixtures/test.vue new file mode 100644 index 000000000..3caaf4b45 --- /dev/null +++ b/packages/extractor-vue/src/fixtures/test.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/packages/extractor-vue/src/index.ts b/packages/extractor-vue/src/index.ts new file mode 100644 index 000000000..30af98fa4 --- /dev/null +++ b/packages/extractor-vue/src/index.ts @@ -0,0 +1 @@ +export { vueExtractor } from "./vue-extractor" diff --git a/packages/extractor-vue/src/vue-extractor.ts b/packages/extractor-vue/src/vue-extractor.ts new file mode 100644 index 000000000..aa1894581 --- /dev/null +++ b/packages/extractor-vue/src/vue-extractor.ts @@ -0,0 +1,63 @@ +import { parse, compileTemplate, SFCBlock } from "@vue/compiler-sfc" +import babel from "@lingui/cli/api/extractors/babel" +import type { ExtractorCtx, ExtractorType } from "@lingui/conf" + +export const vueExtractor: ExtractorType = { + match(filename: string) { + return filename.endsWith(".vue") + }, + async extract( + filename: string, + code: string, + onMessageExtracted, + ctx: ExtractorCtx + ) { + const { descriptor } = parse(code, { + sourceMap: true, + filename, + ignoreEmpty: true, + }) + + const compiledTemplate = compileTemplate({ + source: code, + filename, + id: filename, + }) + + const isTsBlock = (block: SFCBlock) => block?.attrs?.lang === "ts" + + const targets = [ + [ + descriptor.script?.content, + descriptor.script?.map, + isTsBlock(descriptor.script), + ], + [ + descriptor.scriptSetup?.content, + descriptor.scriptSetup?.map, + isTsBlock(descriptor.script), + ], + [ + compiledTemplate?.code, + compiledTemplate?.map, + isTsBlock(descriptor.script), + ], + ] as const + + await Promise.all( + targets + .filter(([source]) => Boolean(source)) + .map(([source, map, isTs]) => + babel.extract( + filename + (isTs ? ".ts" : ""), + source, + onMessageExtracted, + { + sourceMaps: map, + ...ctx, + } + ) + ) + ) + }, +} diff --git a/tsconfig.json b/tsconfig.json index 3a9192449..ee6e5f35a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,9 @@ "skipLibCheck": true, "target": "es2017", "paths": { - "@lingui/babel-plugin-extract-messages": ["./packages/babel-plugin-extract-messages/src"], + "@lingui/babel-plugin-extract-messages": [ + "./packages/babel-plugin-extract-messages/src" + ], "@lingui/core": ["./packages/core/src"], "@lingui/message-utils/*": ["./packages/message-utils/src/*"], "@lingui/cli/api": ["./packages/cli/src/api"], @@ -21,7 +23,11 @@ "@lingui/macro/node": ["./packages/macro/src/index.ts"], "@lingui/macro": ["./packages/macro/src"], "@lingui/format-po": ["./packages/format-po/src/po.ts"], - "@lingui/format-json": ["./packages/format-json/src/json.ts"] + "@lingui/format-json": ["./packages/format-json/src/json.ts"], + "@lingui/extractor-vue": ["./packages/extractor-vue/src"], + "@lingui/cli/api/extractors/babel": [ + "./packages/cli/src/api/extractors/babel" + ] } }, "exclude": [ diff --git a/yarn.lock b/yarn.lock index e898fea83..17ca0e09a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -439,6 +439,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.16.4": + version: 7.21.3 + resolution: "@babel/parser@npm:7.21.3" + bin: + parser: ./bin/babel-parser.js + checksum: a71e6456a1260c2a943736b56cc0acdf5f2a53c6c79e545f56618967e51f9b710d1d3359264e7c979313a7153741b1d95ad8860834cc2ab4ce4f428b13cc07be + languageName: node + linkType: hard + "@babel/parser@npm:^7.21.0, @babel/parser@npm:^7.21.2": version: 7.21.2 resolution: "@babel/parser@npm:7.21.2" @@ -2638,7 +2647,7 @@ __metadata: languageName: node linkType: hard -"@lingui/babel-plugin-extract-messages@4.0.0-next.7, @lingui/babel-plugin-extract-messages@workspace:packages/babel-plugin-extract-messages": +"@lingui/babel-plugin-extract-messages@4.0.0-next.7, @lingui/babel-plugin-extract-messages@^4.0.0-next.5, @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: @@ -2650,7 +2659,7 @@ __metadata: languageName: unknown linkType: soft -"@lingui/cli@4.0.0-next.7, @lingui/cli@workspace:*, @lingui/cli@workspace:packages/cli": +"@lingui/cli@4.0.0-next.7, @lingui/cli@^4.0.0-next.5, @lingui/cli@workspace:*, @lingui/cli@workspace:packages/cli": version: 0.0.0-use.local resolution: "@lingui/cli@workspace:packages/cli" dependencies: @@ -2695,7 +2704,7 @@ __metadata: languageName: unknown linkType: soft -"@lingui/conf@4.0.0-next.7, @lingui/conf@workspace:packages/conf": +"@lingui/conf@4.0.0-next.7, @lingui/conf@^4.0.0-next.5, @lingui/conf@workspace:packages/conf": version: 0.0.0-use.local resolution: "@lingui/conf@workspace:packages/conf" dependencies: @@ -2731,6 +2740,18 @@ __metadata: languageName: unknown linkType: soft +"@lingui/extractor-vue@workspace:packages/extractor-vue": + version: 0.0.0-use.local + resolution: "@lingui/extractor-vue@workspace:packages/extractor-vue" + dependencies: + "@lingui/babel-plugin-extract-messages": ^4.0.0-next.5 + "@lingui/cli": ^4.0.0-next.5 + "@lingui/conf": ^4.0.0-next.5 + "@vue/compiler-sfc": ^3.2.47 + unbuild: ^1.1.2 + languageName: unknown + linkType: soft + "@lingui/format-csv@workspace:packages/format-csv": version: 0.0.0-use.local resolution: "@lingui/format-csv@workspace:packages/format-csv" @@ -4299,6 +4320,76 @@ __metadata: languageName: node linkType: hard +"@vue/compiler-core@npm:3.2.47": + version: 3.2.47 + resolution: "@vue/compiler-core@npm:3.2.47" + dependencies: + "@babel/parser": ^7.16.4 + "@vue/shared": 3.2.47 + estree-walker: ^2.0.2 + source-map: ^0.6.1 + checksum: 9ccc2a0b897b59eea56ca4f92ed29c14cd1184f68532edf5fb0fe5cb2833bcf9e4836029effb6eb9a7c872e9e0350fafdcd96ff00c0b5b79e17ded0c068b5f84 + languageName: node + linkType: hard + +"@vue/compiler-dom@npm:3.2.47": + version: 3.2.47 + resolution: "@vue/compiler-dom@npm:3.2.47" + dependencies: + "@vue/compiler-core": 3.2.47 + "@vue/shared": 3.2.47 + checksum: 1eced735f865e6df0c2d7fa041f9f27996ff4c0d4baf5fad0f67e65e623215f4394c49bba337b78427c6e71f2cc2db12b19ec6b865b4c057c0a15ccedeb20752 + languageName: node + linkType: hard + +"@vue/compiler-sfc@npm:^3.2.47": + version: 3.2.47 + resolution: "@vue/compiler-sfc@npm:3.2.47" + dependencies: + "@babel/parser": ^7.16.4 + "@vue/compiler-core": 3.2.47 + "@vue/compiler-dom": 3.2.47 + "@vue/compiler-ssr": 3.2.47 + "@vue/reactivity-transform": 3.2.47 + "@vue/shared": 3.2.47 + estree-walker: ^2.0.2 + magic-string: ^0.25.7 + postcss: ^8.1.10 + source-map: ^0.6.1 + checksum: 4588a513310b9319a00adfdbe789cfe60d5ec19c51e8f2098152b9e81f54be170e16c40463f6b5e4c7ab79796fc31e2de93587a9dd1af136023fa03712b62e68 + languageName: node + linkType: hard + +"@vue/compiler-ssr@npm:3.2.47": + version: 3.2.47 + resolution: "@vue/compiler-ssr@npm:3.2.47" + dependencies: + "@vue/compiler-dom": 3.2.47 + "@vue/shared": 3.2.47 + checksum: 91bc6e46744d5405713c08d8e576971aa6d13a0cde84ec592d3221bf6ee228e49ce12233af8c18dc39723455b420df2951f3616ceb99758eb432485475fa7bc2 + languageName: node + linkType: hard + +"@vue/reactivity-transform@npm:3.2.47": + version: 3.2.47 + resolution: "@vue/reactivity-transform@npm:3.2.47" + dependencies: + "@babel/parser": ^7.16.4 + "@vue/compiler-core": 3.2.47 + "@vue/shared": 3.2.47 + estree-walker: ^2.0.2 + magic-string: ^0.25.7 + checksum: 6fe54374aa8c080c0c421e18134e84e723e2d3e53178cf084c1cd75bc8b1ffaaf07756801f3aa4e1e7ad1ba76356c28bbab4bc1b676159db8fc10f10f2cbd405 + languageName: node + linkType: hard + +"@vue/shared@npm:3.2.47": + version: 3.2.47 + resolution: "@vue/shared@npm:3.2.47" + checksum: 0aa711dc9160fa0e476e6e94eea4e019398adf2211352d0e4a672cfb6b65b104bbd5d234807d1c091107bdc0f5d818d0f12378987eb7861d39be3aa9f6cd6e3e + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.11.1": version: 1.11.1 resolution: "@webassemblyjs/ast@npm:1.11.1" @@ -10343,6 +10434,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.25.7": + version: 0.25.9 + resolution: "magic-string@npm:0.25.9" + dependencies: + sourcemap-codec: ^1.4.8 + checksum: 9a0e55a15c7303fc360f9572a71cffba1f61451bc92c5602b1206c9d17f492403bf96f946dfce7483e66822d6b74607262e24392e87b0ac27b786e69a40e9b1a + languageName: node + linkType: hard + "magic-string@npm:^0.27.0": version: 0.27.0 resolution: "magic-string@npm:0.27.0" @@ -12008,7 +12108,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.21": +"postcss@npm:^8.1.10, postcss@npm:^8.4.21": version: 8.4.21 resolution: "postcss@npm:8.4.21" dependencies: @@ -13180,6 +13280,13 @@ __metadata: languageName: node linkType: hard +"sourcemap-codec@npm:^1.4.8": + version: 1.4.8 + resolution: "sourcemap-codec@npm:1.4.8" + checksum: b57981c05611afef31605732b598ccf65124a9fcb03b833532659ac4d29ac0f7bfacbc0d6c5a28a03e84c7510e7e556d758d0bb57786e214660016fb94279316 + languageName: node + linkType: hard + "spdx-correct@npm:^3.0.0": version: 3.1.1 resolution: "spdx-correct@npm:3.1.1" From e23cbc00d545bf6aa1b0677571f927b2529c5166 Mon Sep 17 00:00:00 2001 From: Christoffer Jahren Date: Tue, 11 Apr 2023 13:17:38 +0200 Subject: [PATCH 2/3] feat: fix ts support in vue template + setup script --- .../src/__snapshots__/extractor.test.ts.snap | 10 +++++----- packages/extractor-vue/src/fixtures/test.vue | 9 +++++++-- packages/extractor-vue/src/vue-extractor.ts | 9 +++++---- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/extractor-vue/src/__snapshots__/extractor.test.ts.snap b/packages/extractor-vue/src/__snapshots__/extractor.test.ts.snap index f145d8303..5dc924ef2 100644 --- a/packages/extractor-vue/src/__snapshots__/extractor.test.ts.snap +++ b/packages/extractor-vue/src/__snapshots__/extractor.test.ts.snap @@ -9,7 +9,7 @@ exports[`vue extractor should extract message from vue file 1`] = ` message: undefined, origin: [ test.vue.ts, - 5, + 4, 0, ], }, @@ -20,7 +20,7 @@ exports[`vue extractor should extract message from vue file 1`] = ` message: undefined, origin: [ test.vue, - 16, + 19, 20, ], }, @@ -31,7 +31,7 @@ exports[`vue extractor should extract message from vue file 1`] = ` message: My message, origin: [ test.vue, - 23, + 28, 11, ], }, @@ -53,7 +53,7 @@ exports[`vue extractor should extract message from vue file 1`] = ` message: undefined, origin: [ test.vue, - 33, + 38, 11, ], }, @@ -64,7 +64,7 @@ exports[`vue extractor should extract message from vue file 1`] = ` message: undefined, origin: [ test.vue, - 34, + 39, 11, ], }, diff --git a/packages/extractor-vue/src/fixtures/test.vue b/packages/extractor-vue/src/fixtures/test.vue index 3caaf4b45..f89d08426 100644 --- a/packages/extractor-vue/src/fixtures/test.vue +++ b/packages/extractor-vue/src/fixtures/test.vue @@ -1,11 +1,14 @@ -