Skip to content

feat: i18n resource fully pre-compilation #141

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Dec 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module.exports = {
'object-curly-spacing': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/member-delimiter-style': 'off',
'@typescript-eslint/no-use-before-define': 'off'
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-non-null-assertion': 'off'
}
}
111 changes: 108 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
<br/>

## :star: Features
- i18n resource pre-compilation
- `i18n` custom block
- i18n resource definition
- i18n resource importing
- Locale of i18n resource definition
- Locale of i18n resource definition for global scope
- i18n resource formatting
- i18n resource optimaization


## :cd: Installation
Expand All @@ -39,9 +39,84 @@ $ npm i --save-dev @intlify/vue-i18n-loader@next
$ yarn add -D @intlify/vue-i18n-loader@next
```

## :rocket: i18n resource pre-compilation

### Why do we need to require the configuration?

Since vue-i18n@v9.0, The locale messages are handled with message compiler, which converts them to javascript functions after compiling. After compiling, message compiler converts them into javascript functions, which can improve the performance of the application.

However, with the message compiler, the javascript function conversion will not work in some environments (e.g. CSP). For this reason, vue-i18n@v9.0 and later offer a full version that includes compiler and runtime, and a runtime only version.

If you are using the runtime version, you will need to compile before importing locale messages by managing them in a file such as `.json`.

You can pre-commpile by configuring vue-i18n-loader as the webpack loader.

### Webpack configration

As an example, if your project has the locale messagess in `src/locales`, your webpack config will look like this:

```
├── dist
├── index.html
├── package.json
├── src
│   ├── App.vue
│   ├── locales
│   │   ├── en.json
│   │   └── ja.json
│   └── main.js
└── webpack.config.js
```

```js
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n' // import from runtime only
import App from './App.vue'

// import i18n resources
import en from './locale/en.json'
import ja from './locale/ja.json'

const i18n = createI18n({
locale: 'ja',
messages: {
en,
ja
}
})

const app = createApp(App)
app.use(i18n)
app.mount('#app')
```

In the case of the above project, you can use vue-i18n with webpack configuration to the following for runtime only:

```javascript
module.exports = {
module: {
rules: [
// ...
{
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
type: 'javascript/auto',
loader: '@intlify/vue-i18n-loader',
include: [ // Use `Rule.include` to specify the files of locale messages to be pre-compiled
path.resolve(__dirname, 'src/locales')
]
},
// ...
]
}
}
```

The above uses webpack's `Rule.include` to specify the i18n resources to be precompiled. You can also use [`Rule.exclude`](https://webpack.js.org/configuration/module/#ruleexclude) to set the target.


## :rocket: `i18n` custom block

the below example that`App.vue` have `i18n` custom block:
The below example that`App.vue` have `i18n` custom block:

### i18n resource definition

Expand Down Expand Up @@ -89,7 +164,7 @@ The locale messages defined at `i18n` custom blocks are **json format default**.

### i18n resource importing

you also can use `src` attribute:
You also can use `src` attribute:

```vue
<i18n src="./myLang.json"></i18n>
Expand Down Expand Up @@ -219,6 +294,36 @@ module.exports = {
}
```

## :rocket: loader options

### forceStringify

Whether pre-compile number and boolean values as message functions that return the string value, default `false`

```javascript
module.exports = {
module: {
rules: [
// ...
{
test: /\.(json5?|ya?ml)$/,
type: 'javascript/auto',
include: [path.resolve(__dirname, './src/locales')],
use: [
{
loader: '@intlify/vue-i18n-loader',
options: {
forceStringify: true
}
}
]
},
// ...
]
}
}
```

## :rocket: i18n resource optimization

You can optimize your localization performance with pre-compiling the i18n resources.
Expand Down
23 changes: 23 additions & 0 deletions e2e/composition.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
describe('composition', () => {
beforeAll(async () => {
await page.goto(`http://localhost:8080/composition/`)
})

test('initial rendering', async () => {
await expect(page).toMatch('言語')
await expect(page).toMatch('こんにちは、世界!')
await expect(page).toMatch('バナナが欲しい?')
await expect(page).toMatch('バナナ 0 個')
})

test('change locale', async () => {
await page.select('#app select', 'en')
await expect(page).toMatch('Language')
await expect(page).toMatch('hello, world!')
})

test('change banana select', async () => {
await page.select('#fruits select', '3')
await expect(page).toMatch('バナナ 3 個')
})
})
18 changes: 0 additions & 18 deletions e2e/example.test.js

This file was deleted.

16 changes: 16 additions & 0 deletions e2e/global.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
describe.skip(`global`, () => {
beforeAll(async () => {
await page.goto(`http://localhost:8080/global/`)
})

test('initial rendering', async () => {
await expect(page).toMatch('言語')
await expect(page).toMatch('こんにちは、世界!')
})

test('change locale', async () => {
await page.select('#app select', 'en')
await expect(page).toMatch('Language')
await expect(page).toMatch('hello, world!')
})
})
26 changes: 26 additions & 0 deletions e2e/legacy.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
describe('legacy', () => {
beforeAll(async () => {
await page.goto(`http://localhost:8080/legacy/`)
})

test('initial rendering', async () => {
await expect(page).toMatch('言語')
await expect(page).toMatch('こんにちは、世界!')
await expect(page).toMatch('バナナが欲しい?')
await expect(page).toMatch('バナナ 0 個')
})

test('change locale', async () => {
await page.select('#app select', 'en')
await expect(page).toMatch('Language')
await expect(page).toMatch('hello, world!')
await expect(page).toMatch('no bananas')
})

test('change banana select', async () => {
await page.select('#fruits select', '3')
await expect(page).toMatch('3 bananas')
await page.select('#app select', 'ja')
await expect(page).toMatch('バナナ 3 個')
})
})
8 changes: 7 additions & 1 deletion example/composable/App.vue → example/composition/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@
</select>
</form>
<p>{{ t('hello') }}</p>
<Banana />
</template>

<script>
import { useI18n } from 'vue-i18n'
import Banana from './Banana.vue'

export default {
name: 'App',
components: {
Banana
},
setup() {
const { t, locale } = useI18n({
locale: 'ja'
inheritLocale: true,
useScope: 'local'
})
return { t, locale }
}
Expand Down
26 changes: 26 additions & 0 deletions example/composition/Banana.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<template>
<form id="fruits">
<label>{{ t('select') }}</label>
<select v-model.number="select">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
</form>
<p>{{ t('fruits.banana', select, { n: select }) }}</p>
</template>

<script>
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'

export default {
name: 'Banana',
setup() {
const { t } = useI18n({ useScope: 'global' })
const select = ref(0)
return { t, select }
}
}
</script>
3 changes: 3 additions & 0 deletions example/composition/en.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
select: What do you want banana?
fruits:
banana: 'no bananas | {n} banana | {n} bananas'
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
<div id="app">
<App />
</div>
<script src="/dist/composable.js"></script>
<script src="/dist/composition.js"></script>
</body>
</html>
6 changes: 6 additions & 0 deletions example/composition/ja.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"select": "バナナが欲しい?",
"fruits": {
"banana": "バナナがない | バナナ {n} 個"
}
}
8 changes: 7 additions & 1 deletion example/composable/main.js → example/composition/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import App from './App.vue'

import ja from './ja.json'
import en from './en.yaml'

const i18n = createI18n({
legacy: false,
locale: 'ja',
messages: {}
messages: {
en,
ja
}
})

const app = createApp(App)
Expand Down
8 changes: 7 additions & 1 deletion example/legacy/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@
</select>
</form>
<p>{{ $t('hello') }}</p>
<Banana />
</template>

<script>
import Banana from './Banana.vue'

export default {
name: 'App'
name: 'App',
components: {
Banana
}
}
</script>

Expand Down
19 changes: 19 additions & 0 deletions example/legacy/Banana.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<template>
<form id="fruits">
<label>{{ $t('select') }}</label>
<select v-model.number="select">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
</form>
<p>{{ $tc('fruits.banana', select, { n: select }) }}</p>
</template>

<script>
export default {
name: 'Banana',
data: () => ({ select: 0 })
}
</script>
3 changes: 3 additions & 0 deletions example/legacy/en.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
select: What do you want banana?
fruits:
banana: 'no bananas | {n} banana | {n} bananas'
6 changes: 6 additions & 0 deletions example/legacy/ja.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"select": "バナナが欲しい?",
"fruits": {
"banana": "バナナがない | バナナ {n} 個"
}
}
8 changes: 7 additions & 1 deletion example/legacy/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import App from './App.vue'

import ja from './ja.json'
import en from './en.yaml'

const i18n = createI18n({
legacy: true,
locale: 'ja',
messages: {}
messages: {
en,
ja
}
})

const app = createApp(App)
Expand Down
Loading