Skip to content

Commit a8b1aca

Browse files
authored
feat: i18n custom block pre-compilation bundling (#93)
* feat: i18n custom block pre-compilation bundling * fix: add node v14
1 parent 29f04c5 commit a8b1aca

File tree

11 files changed

+1246
-724
lines changed

11 files changed

+1246
-724
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
strategy:
1515
matrix:
1616
os: [ubuntu-latest]
17-
node: [10, 12]
17+
node: [10, 12, 14]
1818
steps:
1919
- name: Checkout
2020
uses: actions/checkout@v2

example/webpack.config.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@ module.exports = {
3838
{
3939
resourceQuery: /blockType=i18n/,
4040
type: 'javascript/auto',
41-
use: [path.resolve(__dirname, '../lib/index.js')]
41+
use: [
42+
{
43+
loader: path.resolve(__dirname, '../lib/index.js'),
44+
options: {
45+
preCompile: true
46+
}
47+
}
48+
]
4249
}
4350
]
4451
},

package.json

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,28 @@
2424
}
2525
},
2626
"dependencies": {
27+
"flat": "^5.0.0",
2728
"js-yaml": "^3.13.1",
28-
"json5": "^2.1.1"
29+
"json5": "^2.1.1",
30+
"loader-utils": "^2.0.0",
31+
"vue-i18n": "^9.0.0-alpha.7"
2932
},
3033
"devDependencies": {
34+
"@types/flat": "^5.0.0",
3135
"@types/jest": "^25.0.0",
3236
"@types/js-yaml": "^3.12.1",
3337
"@types/jsdom": "^16.0.0",
3438
"@types/json5": "^0.0.30",
39+
"@types/loader-utils": "^1.1.3",
3540
"@types/memory-fs": "^0.3.2",
3641
"@types/node": "^13.1.4",
42+
"@types/prettier": "^2.0.0",
3743
"@types/webpack": "^4.41.1",
3844
"@types/webpack-merge": "^4.1.5",
3945
"@typescript-eslint/eslint-plugin": "^2.26.0",
4046
"@typescript-eslint/parser": "^2.26.0",
4147
"@typescript-eslint/typescript-estree": "^2.26.0",
42-
"@vue/compiler-sfc": "^3.0.0-alpha.11",
48+
"@vue/compiler-sfc": "^3.0.0-beta.4",
4349
"babel-loader": "^8.1.0",
4450
"eslint": "^6.8.0",
4551
"eslint-config-prettier": "^6.10.1",
@@ -52,14 +58,13 @@
5258
"lerna-changelog": "^1.0.0",
5359
"memory-fs": "^0.5.0",
5460
"opener": "^1.5.1",
55-
"prettier": "^2.0.4",
61+
"prettier": "^2.0.5",
5662
"puppeteer": "^2.1.1",
5763
"shipjs": "^0.18.0",
5864
"ts-jest": "^25.3.0",
5965
"typescript": "^3.8.3",
6066
"typescript-eslint-language-service": "^2.0.3",
61-
"vue": "^3.0.0-alpha.11",
62-
"vue-i18n": "^9.0.0-alpha.2",
67+
"vue": "^3.0.0-beta.6",
6368
"vue-loader": "^16.0.0-alpha.3",
6469
"webpack": "^4.42.1",
6570
"webpack-cli": "^3.3.11",
@@ -81,6 +86,9 @@
8186
],
8287
"license": "MIT",
8388
"main": "lib/index.js",
89+
"peerDependencies": {
90+
"vue": "^3.0.0-beta.6"
91+
},
8492
"repository": {
8593
"type": "git",
8694
"url": "git+https://github.com/intlify/vue-i18n-loader.git"
@@ -91,11 +99,11 @@
9199
"clean": "rm -rf ./coverage && rm -rf ./lib/*.js*",
92100
"coverage": "opener coverage/lcov-report/index.html",
93101
"example": "yarn build && webpack-dev-server --config example/webpack.config.js --inline --hot",
102+
"fix": "yarn lint:fix && yarn format:fix",
94103
"format": "prettier --config .prettierrc --ignore-path .prettierignore '**/*.{js,json,html}'",
95104
"format:fix": "yarn format --write",
96105
"lint": "eslint ./src ./test --ext .ts",
97106
"lint:fix": "yarn lint --fix",
98-
"fix": "yarn lint:fix && yarn format:fix",
99107
"release:prepare": "shipjs prepare",
100108
"release:trigger": "shipjs trigger",
101109
"test": "yarn lint && yarn test:cover && yarn test:e2e",

src/gen.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { ParsedUrlQuery } from 'querystring'
2+
import JSON5 from 'json5'
3+
import yaml from 'js-yaml'
4+
import { flatten } from 'flat'
5+
import prettier from 'prettier'
6+
import {
7+
baseCompile,
8+
LocaleMessages,
9+
Locale,
10+
CompileOptions,
11+
generateFormatCacheKey,
12+
friendlyJSONstringify
13+
} from 'vue-i18n'
14+
15+
export type VueI18nLoaderOptions = {
16+
preCompile?: boolean
17+
}
18+
19+
export function generateCode(
20+
source: string | Buffer,
21+
query: ParsedUrlQuery,
22+
options: VueI18nLoaderOptions
23+
): string {
24+
const data = convert(source, query.lang as string)
25+
let value = JSON.parse(data)
26+
27+
if (query.locale && typeof query.locale === 'string') {
28+
value = Object.assign({}, { [query.locale]: value })
29+
}
30+
31+
let code = ''
32+
const preCompile = !!options.preCompile
33+
34+
if (preCompile) {
35+
code += generateCompiledCode(value as LocaleMessages)
36+
code += `export default function (Component) {
37+
Component.__i18n = Component.__i18n || _getResource
38+
}\n`
39+
} else {
40+
value = friendlyJSONstringify(value)
41+
code += `export default function (Component) {
42+
Component.__i18n = Component.__i18n || []
43+
Component.__i18n.push('${value}')
44+
}\n`
45+
}
46+
47+
return prettier.format(code, { parser: 'babel' })
48+
}
49+
50+
function convert(source: string | Buffer, lang: string): string {
51+
const value = Buffer.isBuffer(source) ? source.toString() : source
52+
53+
switch (lang) {
54+
case 'yaml':
55+
case 'yml':
56+
const data = yaml.safeLoad(value)
57+
return JSON.stringify(data, undefined, '\t')
58+
case 'json5':
59+
return JSON.stringify(JSON5.parse(value))
60+
default:
61+
return value
62+
}
63+
}
64+
65+
function generateCompiledCode(messages: LocaleMessages): string {
66+
let code = ''
67+
code += `function _register(functions, pathkey, msg) {
68+
const path = JSON.stringify(pathkey)
69+
functions[path] = msg
70+
}
71+
`
72+
code += `const _getResource = () => {
73+
const functions = Object.create(null)
74+
`
75+
76+
const locales = Object.keys(messages) as Locale[]
77+
locales.forEach(locale => {
78+
const message = flatten(messages[locale]) as { [key: string]: string }
79+
const keys = Object.keys(message)
80+
keys.forEach(key => {
81+
const format = message[key]
82+
let occured = false
83+
const options = {
84+
mode: 'arrow',
85+
// TODO: source mapping !
86+
onError(err) {
87+
console.error(err)
88+
occured = true
89+
}
90+
} as CompileOptions
91+
const result = baseCompile(format, options)
92+
if (!occured) {
93+
code += ` _register(functions, ${generateFormatCacheKey(
94+
locale,
95+
key,
96+
format
97+
)}, ${result.code})\n`
98+
}
99+
})
100+
})
101+
102+
code += `
103+
return { functions }
104+
}
105+
`
106+
107+
return code
108+
}

src/index.ts

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
import webpack from 'webpack'
2-
import { ParsedUrlQuery, parse } from 'querystring'
2+
import loaderUtils from 'loader-utils'
3+
import { parse } from 'querystring'
34
import { RawSourceMap } from 'source-map'
4-
import JSON5 from 'json5'
5-
import yaml from 'js-yaml'
5+
import { generateCode, VueI18nLoaderOptions } from './gen'
66

77
const loader: webpack.loader.Loader = function (
88
source: string | Buffer,
99
sourceMap: RawSourceMap | undefined
1010
): void {
11+
const loaderContext = this // eslint-disable-line @typescript-eslint/no-this-alias
12+
const options = loaderUtils.getOptions(loaderContext) || {}
13+
1114
if (this.version && Number(this.version) >= 2) {
1215
try {
1316
this.cacheable && this.cacheable()
14-
this.callback(
15-
null,
16-
`export default ${generateCode(source, parse(this.resourceQuery))}`,
17-
sourceMap
18-
)
17+
const code = `${generateCode(
18+
source,
19+
parse(this.resourceQuery),
20+
options as VueI18nLoaderOptions
21+
)}`
22+
this.callback(null, code, sourceMap)
1923
} catch (err) {
2024
this.emitError(err.message)
2125
this.callback(err)
@@ -27,40 +31,4 @@ const loader: webpack.loader.Loader = function (
2731
}
2832
}
2933

30-
function generateCode(source: string | Buffer, query: ParsedUrlQuery): string {
31-
const data = convert(source, query.lang as string)
32-
let value = JSON.parse(data)
33-
34-
if (query.locale && typeof query.locale === 'string') {
35-
value = Object.assign({}, { [query.locale]: value })
36-
}
37-
38-
value = JSON.stringify(value)
39-
.replace(/\u2028/g, '\\u2028')
40-
.replace(/\u2029/g, '\\u2029')
41-
.replace(/\\/g, '\\\\')
42-
43-
let code = ''
44-
code += `function (Component) {
45-
Component.__i18n = Component.__i18n || []
46-
Component.__i18n.push('${value.replace(/\u0027/g, '\\u0027')}')
47-
}\n`
48-
return code
49-
}
50-
51-
function convert(source: string | Buffer, lang: string): string {
52-
const value = Buffer.isBuffer(source) ? source.toString() : source
53-
54-
switch (lang) {
55-
case 'yaml':
56-
case 'yml':
57-
const data = yaml.safeLoad(value)
58-
return JSON.stringify(data, undefined, '\t')
59-
case 'json5':
60-
return JSON.stringify(JSON5.parse(value))
61-
default:
62-
return value
63-
}
64-
}
65-
6634
export default loader

test/__snapshots__/index.test.ts.snap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ Array [
4646

4747
exports[`special characters 1`] = `
4848
Array [
49-
"{\\"en\\":{\\"hello\\":\\"hello\\\\ngreat\\\\t\\\\\\"world\\\\\\"\\"}}",
49+
"{\\"en\\":{\\"hello\\":\\"hello
50+
great \\"world\\"\\"}}",
5051
]
5152
`;
5253

test/fixtures/compile.vue

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<i18n>
2+
{
3+
"en": {
4+
"hello": "hello world!",
5+
"named": "hi, {name} !",
6+
"list": "hi, {0} !",
7+
"literal": "hi, { 'kazupon' } !",
8+
"linked": "hi, @:name !",
9+
"plural": "@.caml:{'no apples'} | {0} apple | {n} apples",
10+
"nest": {
11+
"lieteral": "hi, kazupon !"
12+
},
13+
"hi, {name} !": "hi hi!",
14+
"こんにちは": "こんにちは!",
15+
"single-quote": "I don't know!",
16+
"emoji": "😺",
17+
"unicode": "\u0041",
18+
"unicode-escape": "\\u0041",
19+
"backslash-single-quote": "\\'",
20+
"backslash-backslash": "\\\\"
21+
},
22+
"ja": {
23+
"hello": "こんにちは!"
24+
}
25+
}
26+
</i18n>

test/index.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { bundleAndRun } from './utils'
2+
import { MessageFunction, baseCompile } from 'vue-i18n'
3+
import prettier from 'prettier'
24

35
test('basic', async () => {
46
const { module } = await bundleAndRun('basic.vue')
@@ -44,3 +46,19 @@ test('json5', async () => {
4446
const { module } = await bundleAndRun('json5.vue')
4547
expect(module.__i18n).toMatchSnapshot()
4648
})
49+
50+
test('pre compile', async () => {
51+
const options: prettier.Options = { parser: 'babel' }
52+
const { module } = await bundleAndRun('compile.vue', {
53+
preCompile: true
54+
})
55+
const { functions } = module.__i18n()
56+
for (const [key, value] of Object.entries(functions)) {
57+
const msg = value as MessageFunction
58+
const data = JSON.parse(key)
59+
const result = baseCompile(data.s, { mode: 'arrow' })
60+
expect(prettier.format(msg.toString(), options)).toMatch(
61+
prettier.format(result.code, options)
62+
)
63+
}
64+
})

test/utils.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,19 @@ export function bundle(fixture: string, options = {}): Promise<BundleResolve> {
4141
{
4242
resourceQuery: /blockType=i18n/,
4343
type: 'javascript/auto',
44-
use: [path.resolve(__dirname, '../src/index.ts')]
44+
use: [
45+
{
46+
loader: path.resolve(__dirname, '../src/index.ts'),
47+
options
48+
}
49+
]
4550
}
4651
]
4752
},
4853
plugins: [new VueLoaderPlugin()]
4954
}
5055

51-
const config = merge({}, baseConfig, options)
56+
const config = merge({}, baseConfig)
5257
const compiler = webpack(config)
5358

5459
const mfs = new memoryfs() // eslint-disable-line

tsconfig.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"compilerOptions": {
33
/* Basic Options */
44
// "incremental": true, /* Enable incremental compilation */
5-
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
5+
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
66
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
77
// "lib": [], /* Specify library files to be included in the compilation. */
88
// "allowJs": true, /* Allow javascript files to be compiled. */
@@ -39,15 +39,16 @@
3939
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
4040

4141
/* Module Resolution Options */
42-
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
42+
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
4343
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
4444
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
4545
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
4646
// "typeRoots": [], /* List of folders to include type definitions from. */
47-
//"typeRoots": [
48-
// "./@types",
49-
// "./node_modules/@types"
50-
//],
47+
"typeRoots": [
48+
"./@types",
49+
"./node_modules/vue-i18n/dist",
50+
"./node_modules/@types"
51+
],
5152
// "types": [], /* Type declaration files to be included in compilation. */
5253
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
5354
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */

0 commit comments

Comments
 (0)