Skip to content

Commit adbe0fe

Browse files
kazuponota-meshi
andauthored
feat: support localePattern option for locale structured with directory (#270)
* feat: support `localePattern` option for locale structured with directory * fix: support string value * Replace judgment of localeKey with common method Co-authored-by: Yosuke Ota <[email protected]>
1 parent 324bce8 commit adbe0fe

File tree

17 files changed

+211
-21
lines changed

17 files changed

+211
-21
lines changed

docs/started.md

+23-5
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,25 @@ module.export = {
4444
// or
4545
// localeDir: {
4646
// pattern: './path/to/locales/*.{json,json5,yaml,yml}', // extension is glob formatting!
47-
// localeKey: 'file' // or 'key'
47+
// localeKey: 'file' // or 'path' or 'key'
4848
// }
4949
// or
5050
// localeDir: [
5151
// {
52+
// // 'file' case
5253
// pattern: './path/to/locales1/*.{json,json5,yaml,yml}',
53-
// localeKey: 'file' // or 'key'
54+
// localeKey: 'file'
5455
// },
5556
// {
57+
// // 'path' case
5658
// pattern: './path/to/locales2/*.{json,json5,yaml,yml}',
57-
// localeKey: 'file' // or 'key'
59+
// localePattern: /^.*\/(?<locale>[A-Za-z0-9-_]+)\/.*\.(json5?|ya?ml)$/,
60+
// localeKey: 'path'
61+
// },
62+
// {
63+
// // 'key' case
64+
// pattern: './path/to/locales3/*.{json,json5,yaml,yml}',
65+
// localeKey: 'key'
5866
// },
5967
// ]
6068

@@ -71,15 +79,25 @@ See [the rule list](../rules/)
7179
### `settings['vue-i18n']`
7280

7381
- `localeDir` ... You can specify a string or an object or an array.
82+
7483
- String option ... A glob for specifying files that store localization messages of project.
7584
- Object option
7685
- `pattern` (`string`) ... A glob for specifying files that store localization messages of project.
77-
- `localeKey` (`'file' | 'key'`) ... Specifies how to determine the locale for localization messages.
86+
- `localeKey` (`'file' | 'path' | 'key'`) ... Specifies how to determine the locale for localization messages.
7887
- `'file'` ... Determine the locale name from the filename. The resource file should only contain messages for that locale. Use this option if you use `vue-cli-plugin-i18n`. This option is also used when String option is specified.
88+
- `'path'` ... Determine the locale name from the path. In this case, the locale must be had structured with your rule on the path. It can be captured with the regular expression named capture. The resource file should only contain messages for that locale.
7989
- `'key'` ... Determine the locale name from the root key name of the file contents. The value of that key should only contain messages for that locale. Used when the resource file is in the format given to the `messages` option of the `VueI18n` constructor option.
80-
- Array option ... An array of String option and Object option. Useful if you have multiple locale directories.
90+
- `localePattern` ... Specifies how to determine pattern the locale for localization messages. This option means, when `localeKey` is `'path'`, you will need to capture the locale using a regular expression. You need to use the locale capture as a named capture `?<locale>`, so it’s be able to capture from the path of the locale resources. If you omit it, it will be captured from the resource path with the same regular expression pattern as `vue-cli-plugin-i18n`.
91+
92+
- Array option ... An array of String option and Object option. Useful if you have multiple locale directories.
8193
- `messageSyntaxVersion` (Optional) ... Specify the version of `vue-i18n` you are using. If not specified, the message will be parsed twice. Also, some rules require this setting.
8294

95+
::: warn NOTE
96+
97+
The `localePattern` options does not support SFC i18n custom blocks (`src` attribute), only for resources of files to import when specified in VueI18n's `messages` options (VueI18n v9 and later, `messages` specified in `createI18n`) for resources of files to import.
98+
99+
:::
100+
83101
### Running ESLint from command line
84102

85103
If you want to run `eslint` from command line, make sure you include the `.vue`, `.json`, `.json5`, `.yaml` and `.yml` extension using [the `--ext` option](https://eslint.org/docs/user-guide/configuring#specifying-file-extensions-to-lint) or a glob pattern because ESLint targets only `.js` files by default.

lib/rules/key-format-style.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ function create(context: RuleContext): RuleListener {
6363
upper?: KeyStack
6464
}
6565
let keyStack: KeyStack = {
66-
inLocale: targetLocaleMessage.localeKey === 'file'
66+
inLocale: targetLocaleMessage.isResolvedLocaleByFileName()
6767
}
6868
return {
6969
JSONProperty(node: JSONAST.JSONProperty) {
@@ -110,7 +110,7 @@ function create(context: RuleContext): RuleListener {
110110
upper?: KeyStack
111111
}
112112
let keyStack: KeyStack = {
113-
inLocale: targetLocaleMessage.localeKey === 'file'
113+
inLocale: targetLocaleMessage.isResolvedLocaleByFileName()
114114
}
115115
function withinKey(node: YAMLAST.YAMLNode) {
116116
for (const keyNode of yamlKeyNodes) {

lib/rules/no-duplicate-keys-in-locale.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function create(context: RuleContext): RuleListener {
4747
targetLocaleMessage: LocaleMessage,
4848
otherLocaleMessages: LocaleMessage[]
4949
): PathStack {
50-
if (targetLocaleMessage.localeKey === 'file') {
50+
if (targetLocaleMessage.isResolvedLocaleByFileName()) {
5151
const locale = targetLocaleMessage.locales[0]
5252
return createInitLocalePathStack(locale, otherLocaleMessages)
5353
} else {

lib/rules/no-missing-keys-in-other-locales.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ function create(context: RuleContext): RuleListener {
115115
}[]
116116
}
117117
let keyStack: KeyStack
118-
if (targetLocaleMessage.localeKey === 'file') {
118+
if (targetLocaleMessage.isResolvedLocaleByFileName()) {
119119
const locale = targetLocaleMessage.locales[0]
120120
keyStack = {
121121
locale,

lib/types/settings.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
* How to determine the locale for localization messages.
33
* - `'file'` ... Determine the locale name from the filename. The resource file should only contain messages for that locale.
44
* Use this option if you use `vue-cli-plugin-i18n`. This method is also used when String option is specified.
5-
* - `'key'` ... Determine the locale name from the root key name of the file contents. The value of that key should only contain messages for that locale.
5+
* - `'path'` ... Determine the locale name from the path of resource. In this case, the locale must be had structured with your rule on the path.
6+
* It can be captured with the regular expression named capture. The resource file should only contain messages for that locale.
7+
* - `'key'` ... Determine the locale name from the root key name of the file contents. The value of that key should only contain messages for that locale.
68
* Used when the resource file is in the format given to the `messages` option of the `VueI18n` constructor option.
79
*/
8-
export type LocaleKeyType = 'file' | 'key'
10+
export type LocaleKeyType = 'file' | 'path' | 'key'
911
/**
1012
* Type of `settings['vue-i18n'].localeDir`
1113
*/
@@ -29,4 +31,12 @@ export interface SettingsVueI18nLocaleDirObject {
2931
* Specifies how to determine the locale for localization messages.
3032
*/
3133
localeKey: LocaleKeyType
34+
/**
35+
* Specifies how to determine pattern the locale for localization messages.
36+
*
37+
* This option means, when `localeKey` is `'path'`, you will need to capture the locale using a regular expression.
38+
* You need to use the locale capture as a named capture `?<locale>`, so it’s be able to capture from the path of the locale resources.
39+
* If you omit it, it will be captured from the resource path with the same regular expression pattern as `vue-cli-plugin-i18n`.
40+
*/
41+
localePattern?: string | RegExp
3242
}

lib/utils/casing.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ const checkersMap = {
111111
/**
112112
* Convert text to camelCase
113113
*/
114-
export function camelCase(str: string) {
114+
export function camelCase(str: string): string {
115115
if (isPascalCase(str)) {
116116
return str.charAt(0).toLowerCase() + str.slice(1)
117117
}

lib/utils/index.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { getCwd } from './get-cwd'
2929
interface LocaleFiles {
3030
files: string[]
3131
localeKey: LocaleKeyType
32+
localePattern?: string | RegExp
3233
}
3334
const UNEXPECTED_ERROR_LOCATION = { line: 1, column: 0 }
3435
/**
@@ -112,15 +113,17 @@ function loadLocaleMessages(
112113
): FileLocaleMessage[] {
113114
const results: FileLocaleMessage[] = []
114115
const checkDupeMap: { [file: string]: LocaleKeyType[] } = {}
115-
for (const { files, localeKey } of localeFilesList) {
116+
for (const { files, localeKey, localePattern } of localeFilesList) {
116117
for (const file of files) {
117118
const localeKeys = checkDupeMap[file] || (checkDupeMap[file] = [])
118119
if (localeKeys.includes(localeKey)) {
119120
continue
120121
}
121122
localeKeys.push(localeKey)
122123
const fullpath = resolve(cwd, file)
123-
results.push(new FileLocaleMessage({ fullpath, localeKey }))
124+
results.push(
125+
new FileLocaleMessage({ fullpath, localeKey, localePattern })
126+
)
124127
}
125128
}
126129
return results
@@ -225,7 +228,8 @@ class LocaleDirLocaleMessagesCache {
225228
} else {
226229
return {
227230
files: targetFilesLoader.get(localeDir.pattern, cwd),
228-
localeKey: String(localeDir.localeKey ?? 'file') as LocaleKeyType
231+
localeKey: String(localeDir.localeKey ?? 'file') as LocaleKeyType,
232+
localePattern: localeDir.localePattern
229233
}
230234
}
231235
}

lib/utils/locale-messages.ts

+48-6
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,50 @@ import JSON5 from 'json5'
2020
import yaml from 'js-yaml'
2121
import { joinPath, parsePath } from './key-path'
2222

23+
// see https://github.com/kazupon/vue-cli-plugin-i18n/blob/e9519235a454db52fdafcd0517ce6607821ef0b4/generator/templates/js/src/i18n.js#L10
24+
const DEFAULT_LOCALE_PATTERN = '[A-Za-z0-9-_]+'
25+
const DEFAULT_LOCALE_FIELNAME_REGEX = new RegExp(
26+
`(${DEFAULT_LOCALE_PATTERN})\.`,
27+
'i'
28+
)
29+
const DEFAULT_LOCALE_CAPTURE_REGEX = new RegExp(
30+
`^.*\/(?<locale>${DEFAULT_LOCALE_PATTERN})\.(json5?|ya?ml)$`,
31+
'i'
32+
)
33+
2334
/**
2435
* The localization message class
2536
*/
2637
export abstract class LocaleMessage {
2738
public readonly fullpath: string
2839
public readonly localeKey: LocaleKeyType
2940
public readonly file: string
41+
public readonly localePattern: RegExp
3042
private _locales: string[] | undefined
3143
/**
3244
* @param {object} arg
3345
* @param {string} arg.fullpath Absolute path.
3446
* @param {string[]} [arg.locales] The locales.
3547
* @param {LocaleKeyType} arg.localeKey Specifies how to determine the locale for localization messages.
48+
* @param {RegExp} args.localePattern Specifies how to determin the regular expression pattern for how to get the locale.
3649
*/
3750
constructor({
3851
fullpath,
3952
locales,
40-
localeKey
53+
localeKey,
54+
localePattern
4155
}: {
4256
fullpath: string
4357
locales?: string[]
4458
localeKey: LocaleKeyType
59+
localePattern?: string | RegExp
4560
}) {
4661
this.fullpath = fullpath
4762
/** @type {LocaleKeyType} Specifies how to determine the locale for localization messages. */
4863
this.localeKey = localeKey
4964
/** @type {string} The localization messages file name. */
5065
this.file = fullpath.replace(/^.*(\\|\/|:)/, '')
66+
this.localePattern = this.getLocalePatternWithRegex(localePattern)
5167

5268
this._locales = locales
5369
}
@@ -57,6 +73,20 @@ export abstract class LocaleMessage {
5773
*/
5874
abstract getMessagesInternal(): I18nLocaleMessageDictionary
5975

76+
/**
77+
* Get locale pattern with regular expression
78+
*/
79+
getLocalePatternWithRegex(localePattern?: string | RegExp): RegExp {
80+
// prettier-ignore
81+
return localePattern != null
82+
? typeof localePattern === 'string'
83+
? new RegExp(localePattern, 'i')
84+
: Object.prototype.toString.call(localePattern) === '[object RegExp]'
85+
? localePattern
86+
: DEFAULT_LOCALE_CAPTURE_REGEX
87+
: DEFAULT_LOCALE_CAPTURE_REGEX
88+
}
89+
6090
/**
6191
* @returns {object} The localization messages object.
6292
*/
@@ -71,20 +101,28 @@ export abstract class LocaleMessage {
71101
return this._locales
72102
}
73103
if (this.localeKey === 'file') {
74-
// see https://github.com/kazupon/vue-cli-plugin-i18n/blob/e9519235a454db52fdafcd0517ce6607821ef0b4/generator/templates/js/src/i18n.js#L10
75-
const matched = this.file.match(/([A-Za-z0-9-_]+)\./i)
104+
const matched = this.file.match(DEFAULT_LOCALE_FIELNAME_REGEX)
76105
return (this._locales = [(matched && matched[1]) || this.file])
106+
} else if (this.localeKey === 'path') {
107+
const matched = this.fullpath.match(this.localePattern)
108+
return (this._locales = [
109+
(matched && matched.groups?.locale) || this.fullpath
110+
])
77111
} else if (this.localeKey === 'key') {
78112
return (this._locales = Object.keys(this.messages))
79113
}
80114
return (this._locales = [])
81115
}
82116

117+
isResolvedLocaleByFileName() {
118+
return this.localeKey === 'file' || this.localeKey === 'path'
119+
}
120+
83121
/**
84122
* Gets messages for the given locale.
85123
*/
86124
getMessagesFromLocale(locale: string): I18nLocaleMessageDictionary {
87-
if (this.localeKey === 'file') {
125+
if (this.isResolvedLocaleByFileName()) {
88126
if (!this.locales.includes(locale)) {
89127
return {}
90128
}
@@ -158,20 +196,24 @@ export class FileLocaleMessage extends LocaleMessage {
158196
* @param {string} arg.fullpath Absolute path.
159197
* @param {string[]} [arg.locales] The locales.
160198
* @param {LocaleKeyType} arg.localeKey Specifies how to determine the locale for localization messages.
199+
* @param {string | RegExp} args.localePattern Specifies how to determin the regular expression pattern for how to get the locale.
161200
*/
162201
constructor({
163202
fullpath,
164203
locales,
165-
localeKey
204+
localeKey,
205+
localePattern
166206
}: {
167207
fullpath: string
168208
locales?: string[]
169209
localeKey: LocaleKeyType
210+
localePattern?: string | RegExp
170211
}) {
171212
super({
172213
fullpath,
173214
locales,
174-
localeKey
215+
localeKey,
216+
localePattern
175217
})
176218
this._resource = new ResourceLoader(fullpath, fileName => {
177219
const ext = extname(fileName).toLowerCase()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"hello": "hello world",
3+
"messages": {
4+
"hello": "hi DIO!",
5+
"link": "@:messages.hello"
6+
},
7+
"hello_dio": "hello underscore DIO!",
8+
"hello {name}": "hello {name}!",
9+
"term": "I accept xxx {0}."
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
hello: "ハローワールド"
2+
messages:
3+
hello: "こんにちは、DIO!"
4+
link: "@:messages.hello"
5+
hello_dio: "こんにちは、アンダースコア DIO!"
6+
"hello {name}": "こんにちは、{name}!"
7+
term: "私は xxx の{0}に同意します。"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<template>
2+
<div id="app">
3+
<p v-t="'hello_dio'">{{ $t('messages.link') }}</p>
4+
<i18n path="term" tag="label" for="tos">
5+
<a :href="url" target="_blank">{{ $t('tos') }}</a>
6+
</i18n>
7+
</div>
8+
</template>
9+
10+
<script>
11+
export default {
12+
name: 'App',
13+
created() {
14+
this.$i18n.t('hello {name}', { name: 'DIO' })
15+
}
16+
}
17+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const $t = () => {}
2+
$t('hello')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello: world
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"hello": "world"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
en: {
3+
hello: "world"
4+
},
5+
ja: {
6+
hello: "ワールド"
7+
}
8+
}

tests/lib/rules/no-unused-keys.ts

+17
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,19 @@ new RuleTester({
146146
src: '.'
147147
}
148148
]
149+
}),
150+
...getTestCasesFromFixtures({
151+
cwd: join(cwdRoot, './valid/path-locales'),
152+
localeDir: {
153+
pattern: `./locales/**/*.{json,yaml,yml}`,
154+
localeKey: 'path',
155+
localePattern: /^.*\/(?<locale>[A-Za-z0-9-_]+)\/.*\.(json5?|ya?ml)$/
156+
},
157+
options: [
158+
{
159+
src: '.'
160+
}
161+
]
149162
})
150163
]
151164
: []),
@@ -1021,6 +1034,10 @@ ${' '.repeat(6)}
10211034
'constructor-option-format/src/main.js': true,
10221035
'multiple-locales/src/App.vue': true,
10231036
'multiple-locales/src/main.js': true,
1037+
'path-locales/locales/en/message.json': true,
1038+
'path-locales/locales/ja/message.yaml': true,
1039+
'path-locales/src/App.vue': true,
1040+
'path-locales/src/main.js': true,
10241041
'vue-cli-format/src/App.vue': true,
10251042
'vue-cli-format/src/main.js': true,
10261043
'constructor-option-format/locales/index.json': {

0 commit comments

Comments
 (0)