Skip to content

Commit

Permalink
fix(unplugin-vue-i18n): support js and ts resource formats (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
kazupon committed Mar 8, 2023
1 parent 14ae6f9 commit 84348c1
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 99 deletions.
11 changes: 11 additions & 0 deletions packages/unplugin-vue-i18n/README.md
Expand Up @@ -273,10 +273,21 @@ This plugin will automatically select and bundle `petite-vue-i18n` build accordi
- json5
- yaml
- yml
- js
- ts
```

Note `json` resources matches this option, it will be handled **before the internal json plugin of bundler, and will not be processed afterwards**, else the option doesn't match, the bundler side will handle.

Note that `js` and `ts` resources are limited to **simple export (`export default`) as locale messages object only**, such as programmatically dynamic resource construction is not guaranteed to work currently.

```js
export default {
hello: 'Hello, {name}!',
// ...
}
```

If nothing is specified for this option, i.e. `undefined`, nothing is done to the resource in the above format.

### `runtimeOnly`
Expand Down
14 changes: 7 additions & 7 deletions packages/unplugin-vue-i18n/package.json
Expand Up @@ -26,18 +26,18 @@
}
},
"dependencies": {
"@intlify/bundle-utils": "^4.0.0",
"@intlify/bundle-utils": "^5.0.0",
"@intlify/shared": "next",
"@rollup/pluginutils": "^4.2.0",
"@vue/compiler-sfc": "^3.2.45",
"debug": "^4.3.1",
"fast-glob": "^3.2.5",
"@rollup/pluginutils": "^5.0.2",
"@vue/compiler-sfc": "^3.2.47",
"debug": "^4.3.3",
"fast-glob": "^3.2.12",
"js-yaml": "^4.1.0",
"json5": "^2.2.0",
"json5": "^2.2.3",
"pathe": "^1.0.0",
"picocolors": "^1.0.0",
"source-map": "0.6.1",
"unplugin": "^1.0.0"
"unplugin": "^1.1.0"
},
"devDependencies": {
"mlly": "^1.0.0",
Expand Down
98 changes: 91 additions & 7 deletions packages/unplugin-vue-i18n/src/index.ts
@@ -1,4 +1,4 @@
import { createUnplugin } from 'unplugin'
import { createUnplugin, TransformResult } from 'unplugin'
import { normalize, parse as parsePath } from 'pathe'
import createDebug from 'debug'
import fg from 'fast-glob'
Expand All @@ -16,6 +16,7 @@ import { createFilter } from '@rollup/pluginutils'
import {
generateJSON,
generateYAML,
generateJavaScript,
checkInstallPackage,
checkVueI18nBridgeInstallPackage,
getVueI18nVersion
Expand All @@ -26,7 +27,12 @@ import { parseVueRequest, VueQuery } from './query'
import { createBridgeCodeGenerator } from './legacy'
import { getRaw, warn, error, raiseError } from './utils'

import type { UnpluginContextMeta, UnpluginOptions } from 'unplugin'
import type {
UnpluginContextMeta,
UnpluginOptions,
SourceMapCompact
} from 'unplugin'
import type { SourceMapInput } from 'rollup'
import type { PluginOptions } from './types'
import type { CodeGenOptions, DevEnv } from '@intlify/bundle-utils'

Expand Down Expand Up @@ -247,6 +253,76 @@ export const unplugin = createUnplugin<PluginOptions>((options = {}, meta) => {
return orgTransform!.apply(this, [code, id])
}
}

/**
* typescript transform handling
*
* NOTE:
* Typescript resources are handled using the already existing `vite:esbuild` plugin.
*/
const esbuildPlugin = config.plugins.find(
p => p.name === 'vite:esbuild'
)
if (esbuildPlugin) {
const orgTransform = esbuildPlugin.transform // backup @rollup/plugin-json
// @ts-ignore
esbuildPlugin.transform = async function (code: string, id: string) {
const result = (await orgTransform!.apply(this, [
code,
id
])) as TransformResult
if (result == null) {
return result
}

const { filename, query } = parseVueRequest(id)
if (!query.vue && filter(id) && /\.[c|m]?ts$/.test(id)) {
const [_code, inSourceMap]: [string, RawSourceMap | undefined] =
isString(result)
? [result, undefined]
: [result.code, result.map as RawSourceMap]

let langInfo = defaultSFCLang
langInfo = parsePath(filename)
.ext as Required<PluginOptions>['defaultSFCLang']

const generate = getGenerator(langInfo)
const parseOptions = getOptions(
filename,
isProduction,
query as Record<string, unknown>,
sourceMap,
{
inSourceMap,
isGlobal: globalSFCScope,
useClassComponent,
bridge,
exportESM: esm,
forceStringify
}
) as CodeGenOptions
debug('parseOptions', parseOptions)

const { code: generatedCode, map } = generate(
_code,
parseOptions,
bridge ? createBridgeCodeGenerator(_code, query) : undefined
)
debug('generated code', generatedCode)
console.log('generated code', generatedCode)
debug('sourcemap', map, sourceMap)

if (_code === generatedCode) return

return {
code: generatedCode,
map: (sourceMap ? map : { mappings: '' }) as any // eslint-disable-line @typescript-eslint/no-explicit-any
}
} else {
return result
}
}
}
},

async handleHotUpdate({ file, server }) {
Expand Down Expand Up @@ -412,14 +488,11 @@ export const unplugin = createUnplugin<PluginOptions>((options = {}, meta) => {
let inSourceMap: RawSourceMap | undefined

if (!query.vue) {
if (/\.(json5?|ya?ml)$/.test(id) && filter(id)) {
if (/\.(json5?|ya?ml|[c|m]?js)$/.test(id) && filter(id)) {
langInfo = parsePath(filename)
.ext as Required<PluginOptions>['defaultSFCLang']

const generate = /\.?json5?/.test(langInfo)
? generateJSON
: generateYAML

const generate = getGenerator(langInfo)
const parseOptions = getOptions(
filename,
isProduction,
Expand Down Expand Up @@ -517,6 +590,17 @@ export const unplugin = createUnplugin<PluginOptions>((options = {}, meta) => {
} as UnpluginOptions
})

function getGenerator(ext: string, defaultGen = generateJSON) {
// prettier-ignore
return /\.?json5?$/.test(ext)
? generateJSON
: /\.ya?ml$/.test(ext)
? generateYAML
: /\.([c|m]?js|[c|m]?ts)$/.test(ext)
? generateJavaScript
: defaultGen
}

function normalizeConfigResolveAlias(
resolve: any, // eslint-disable-line @typescript-eslint/no-explicit-any
framework: UnpluginContextMeta['framework']
Expand Down
4 changes: 4 additions & 0 deletions packages/unplugin-vue-i18n/test/fixtures/locales/en-GB.ts
@@ -0,0 +1,4 @@
export default {
// comment
message: "@.caml:{'no apples'} | {0} apple | {n} apples"
} as Record<string, any>
4 changes: 4 additions & 0 deletions packages/unplugin-vue-i18n/test/fixtures/locales/en-KK.mjs
@@ -0,0 +1,4 @@
export default {
// comment
message: "@.caml:{'no apples'} | {0} apple | {n} apples"
}
4 changes: 4 additions & 0 deletions packages/unplugin-vue-i18n/test/fixtures/locales/en-US.js
@@ -0,0 +1,4 @@
export default {
// comment
message: "@.caml:{'no apples'} | {0} apple | {n} apples"
}
2 changes: 1 addition & 1 deletion packages/unplugin-vue-i18n/test/utils.ts
Expand Up @@ -31,7 +31,7 @@ export async function bundleVite(
const input = (options.input as string) || './fixtures/entry.ts'
const target = (options.target as string) || './fixtures'
const include = (options.include as string[]) || [
resolve(__dirname, './fixtures/**')
resolve(__dirname, './fixtures/locales/**')
]
const silent = isBoolean(options.silent)
? options.silent === false
Expand Down
14 changes: 14 additions & 0 deletions packages/unplugin-vue-i18n/test/vite/resource-compilation.test.ts
Expand Up @@ -34,3 +34,17 @@ test('yml resource', async () => {
// expect(fn.source).toEqual(`@.caml:{'no apples'} | {0} apple | {n} apples`)
expect(fn(createMessageContext({ named: { n: 3 } }))).toEqual(`3 apples`)
})

test('js resource', async () => {
const { module } = await bundleAndRun('en-KK.mjs', bundleVite, options)
const fn = module.message
// expect(fn.source).toEqual(`@.caml:{'no apples'} | {0} apple | {n} apples`)
expect(fn(createMessageContext({ named: { n: 3 } }))).toEqual(`3 apples`)
})

test('ts resource', async () => {
const { module } = await bundleAndRun('en-GB.ts', bundleVite, options)
const fn = module.message
// expect(fn.source).toEqual(`@.caml:{'no apples'} | {0} apple | {n} apples`)
expect(fn(createMessageContext({ named: { n: 3 } }))).toEqual(`3 apples`)
})

0 comments on commit 84348c1

Please sign in to comment.