Skip to content

feat: support localePattern option for locale structured with directory #270

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 3 commits into from
Nov 18, 2021
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
28 changes: 23 additions & 5 deletions docs/started.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,25 @@ module.export = {
// or
// localeDir: {
// pattern: './path/to/locales/*.{json,json5,yaml,yml}', // extension is glob formatting!
// localeKey: 'file' // or 'key'
// localeKey: 'file' // or 'path' or 'key'
// }
// or
// localeDir: [
// {
// // 'file' case
// pattern: './path/to/locales1/*.{json,json5,yaml,yml}',
// localeKey: 'file' // or 'key'
// localeKey: 'file'
// },
// {
// // 'path' case
// pattern: './path/to/locales2/*.{json,json5,yaml,yml}',
// localeKey: 'file' // or 'key'
// localePattern: /^.*\/(?<locale>[A-Za-z0-9-_]+)\/.*\.(json5?|ya?ml)$/,
// localeKey: 'path'
// },
// {
// // 'key' case
// pattern: './path/to/locales3/*.{json,json5,yaml,yml}',
// localeKey: 'key'
// },
// ]

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

- `localeDir` ... You can specify a string or an object or an array.

- String option ... A glob for specifying files that store localization messages of project.
- Object option
- `pattern` (`string`) ... A glob for specifying files that store localization messages of project.
- `localeKey` (`'file' | 'key'`) ... Specifies how to determine the locale for localization messages.
- `localeKey` (`'file' | 'path' | 'key'`) ... Specifies how to determine the locale for localization messages.
- `'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.
- `'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.
- `'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.
- Array option ... An array of String option and Object option. Useful if you have multiple locale directories.
- `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`.

- Array option ... An array of String option and Object option. Useful if you have multiple locale directories.
- `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.

::: warn NOTE

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.

:::

### Running ESLint from command line

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.
Expand Down
4 changes: 2 additions & 2 deletions lib/rules/key-format-style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function create(context: RuleContext): RuleListener {
upper?: KeyStack
}
let keyStack: KeyStack = {
inLocale: targetLocaleMessage.localeKey === 'file'
inLocale: targetLocaleMessage.isResolvedLocaleByFileName()
}
return {
JSONProperty(node: JSONAST.JSONProperty) {
Expand Down Expand Up @@ -110,7 +110,7 @@ function create(context: RuleContext): RuleListener {
upper?: KeyStack
}
let keyStack: KeyStack = {
inLocale: targetLocaleMessage.localeKey === 'file'
inLocale: targetLocaleMessage.isResolvedLocaleByFileName()
}
function withinKey(node: YAMLAST.YAMLNode) {
for (const keyNode of yamlKeyNodes) {
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/no-duplicate-keys-in-locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function create(context: RuleContext): RuleListener {
targetLocaleMessage: LocaleMessage,
otherLocaleMessages: LocaleMessage[]
): PathStack {
if (targetLocaleMessage.localeKey === 'file') {
if (targetLocaleMessage.isResolvedLocaleByFileName()) {
const locale = targetLocaleMessage.locales[0]
return createInitLocalePathStack(locale, otherLocaleMessages)
} else {
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/no-missing-keys-in-other-locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ function create(context: RuleContext): RuleListener {
}[]
}
let keyStack: KeyStack
if (targetLocaleMessage.localeKey === 'file') {
if (targetLocaleMessage.isResolvedLocaleByFileName()) {
const locale = targetLocaleMessage.locales[0]
keyStack = {
locale,
Expand Down
14 changes: 12 additions & 2 deletions lib/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
* How to determine the locale for localization messages.
* - `'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 method is also used when String option is specified.
* - `'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.
* - `'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.
* It can be captured with the regular expression named capture. The resource file should only contain messages for that locale.
* - `'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.
*/
export type LocaleKeyType = 'file' | 'key'
export type LocaleKeyType = 'file' | 'path' | 'key'
/**
* Type of `settings['vue-i18n'].localeDir`
*/
Expand All @@ -29,4 +31,12 @@ export interface SettingsVueI18nLocaleDirObject {
* Specifies how to determine the locale for localization messages.
*/
localeKey: LocaleKeyType
/**
* 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`.
*/
localePattern?: string | RegExp
}
2 changes: 1 addition & 1 deletion lib/utils/casing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const checkersMap = {
/**
* Convert text to camelCase
*/
export function camelCase(str: string) {
export function camelCase(str: string): string {
if (isPascalCase(str)) {
return str.charAt(0).toLowerCase() + str.slice(1)
}
Expand Down
10 changes: 7 additions & 3 deletions lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { getCwd } from './get-cwd'
interface LocaleFiles {
files: string[]
localeKey: LocaleKeyType
localePattern?: string | RegExp
}
const UNEXPECTED_ERROR_LOCATION = { line: 1, column: 0 }
/**
Expand Down Expand Up @@ -112,15 +113,17 @@ function loadLocaleMessages(
): FileLocaleMessage[] {
const results: FileLocaleMessage[] = []
const checkDupeMap: { [file: string]: LocaleKeyType[] } = {}
for (const { files, localeKey } of localeFilesList) {
for (const { files, localeKey, localePattern } of localeFilesList) {
for (const file of files) {
const localeKeys = checkDupeMap[file] || (checkDupeMap[file] = [])
if (localeKeys.includes(localeKey)) {
continue
}
localeKeys.push(localeKey)
const fullpath = resolve(cwd, file)
results.push(new FileLocaleMessage({ fullpath, localeKey }))
results.push(
new FileLocaleMessage({ fullpath, localeKey, localePattern })
)
}
}
return results
Expand Down Expand Up @@ -225,7 +228,8 @@ class LocaleDirLocaleMessagesCache {
} else {
return {
files: targetFilesLoader.get(localeDir.pattern, cwd),
localeKey: String(localeDir.localeKey ?? 'file') as LocaleKeyType
localeKey: String(localeDir.localeKey ?? 'file') as LocaleKeyType,
localePattern: localeDir.localePattern
}
}
}
Expand Down
54 changes: 48 additions & 6 deletions lib/utils/locale-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,50 @@ import JSON5 from 'json5'
import yaml from 'js-yaml'
import { joinPath, parsePath } from './key-path'

// see https://github.com/kazupon/vue-cli-plugin-i18n/blob/e9519235a454db52fdafcd0517ce6607821ef0b4/generator/templates/js/src/i18n.js#L10
const DEFAULT_LOCALE_PATTERN = '[A-Za-z0-9-_]+'
const DEFAULT_LOCALE_FIELNAME_REGEX = new RegExp(
`(${DEFAULT_LOCALE_PATTERN})\.`,
'i'
)
const DEFAULT_LOCALE_CAPTURE_REGEX = new RegExp(
`^.*\/(?<locale>${DEFAULT_LOCALE_PATTERN})\.(json5?|ya?ml)$`,
'i'
)

/**
* The localization message class
*/
export abstract class LocaleMessage {
public readonly fullpath: string
public readonly localeKey: LocaleKeyType
public readonly file: string
public readonly localePattern: RegExp
private _locales: string[] | undefined
/**
* @param {object} arg
* @param {string} arg.fullpath Absolute path.
* @param {string[]} [arg.locales] The locales.
* @param {LocaleKeyType} arg.localeKey Specifies how to determine the locale for localization messages.
* @param {RegExp} args.localePattern Specifies how to determin the regular expression pattern for how to get the locale.
*/
constructor({
fullpath,
locales,
localeKey
localeKey,
localePattern
}: {
fullpath: string
locales?: string[]
localeKey: LocaleKeyType
localePattern?: string | RegExp
}) {
this.fullpath = fullpath
/** @type {LocaleKeyType} Specifies how to determine the locale for localization messages. */
this.localeKey = localeKey
/** @type {string} The localization messages file name. */
this.file = fullpath.replace(/^.*(\\|\/|:)/, '')
this.localePattern = this.getLocalePatternWithRegex(localePattern)

this._locales = locales
}
Expand All @@ -57,6 +73,20 @@ export abstract class LocaleMessage {
*/
abstract getMessagesInternal(): I18nLocaleMessageDictionary

/**
* Get locale pattern with regular expression
*/
getLocalePatternWithRegex(localePattern?: string | RegExp): RegExp {
// prettier-ignore
return localePattern != null
? typeof localePattern === 'string'
? new RegExp(localePattern, 'i')
: Object.prototype.toString.call(localePattern) === '[object RegExp]'
? localePattern
: DEFAULT_LOCALE_CAPTURE_REGEX
: DEFAULT_LOCALE_CAPTURE_REGEX
}

/**
* @returns {object} The localization messages object.
*/
Expand All @@ -71,20 +101,28 @@ export abstract class LocaleMessage {
return this._locales
}
if (this.localeKey === 'file') {
// see https://github.com/kazupon/vue-cli-plugin-i18n/blob/e9519235a454db52fdafcd0517ce6607821ef0b4/generator/templates/js/src/i18n.js#L10
const matched = this.file.match(/([A-Za-z0-9-_]+)\./i)
const matched = this.file.match(DEFAULT_LOCALE_FIELNAME_REGEX)
return (this._locales = [(matched && matched[1]) || this.file])
} else if (this.localeKey === 'path') {
const matched = this.fullpath.match(this.localePattern)
return (this._locales = [
(matched && matched.groups?.locale) || this.fullpath
])
} else if (this.localeKey === 'key') {
return (this._locales = Object.keys(this.messages))
}
return (this._locales = [])
}

isResolvedLocaleByFileName() {
return this.localeKey === 'file' || this.localeKey === 'path'
}

/**
* Gets messages for the given locale.
*/
getMessagesFromLocale(locale: string): I18nLocaleMessageDictionary {
if (this.localeKey === 'file') {
if (this.isResolvedLocaleByFileName()) {
if (!this.locales.includes(locale)) {
return {}
}
Expand Down Expand Up @@ -158,20 +196,24 @@ export class FileLocaleMessage extends LocaleMessage {
* @param {string} arg.fullpath Absolute path.
* @param {string[]} [arg.locales] The locales.
* @param {LocaleKeyType} arg.localeKey Specifies how to determine the locale for localization messages.
* @param {string | RegExp} args.localePattern Specifies how to determin the regular expression pattern for how to get the locale.
*/
constructor({
fullpath,
locales,
localeKey
localeKey,
localePattern
}: {
fullpath: string
locales?: string[]
localeKey: LocaleKeyType
localePattern?: string | RegExp
}) {
super({
fullpath,
locales,
localeKey
localeKey,
localePattern
})
this._resource = new ResourceLoader(fullpath, fileName => {
const ext = extname(fileName).toLowerCase()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"hello": "hello world",
"messages": {
"hello": "hi DIO!",
"link": "@:messages.hello"
},
"hello_dio": "hello underscore DIO!",
"hello {name}": "hello {name}!",
"term": "I accept xxx {0}."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
hello: "ハローワールド"
messages:
hello: "こんにちは、DIO!"
link: "@:messages.hello"
hello_dio: "こんにちは、アンダースコア DIO!"
"hello {name}": "こんにちは、{name}!"
term: "私は xxx の{0}に同意します。"
17 changes: 17 additions & 0 deletions tests/fixtures/no-unused-keys/valid/path-locales/src/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<div id="app">
<p v-t="'hello_dio'">{{ $t('messages.link') }}</p>
<i18n path="term" tag="label" for="tos">
<a :href="url" target="_blank">{{ $t('tos') }}</a>
</i18n>
</div>
</template>

<script>
export default {
name: 'App',
created() {
this.$i18n.t('hello {name}', { name: 'DIO' })
}
}
</script>
2 changes: 2 additions & 0 deletions tests/fixtures/no-unused-keys/valid/path-locales/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const $t = () => {}
$t('hello')
1 change: 1 addition & 0 deletions tests/fixtures/utils/locale-messages/locales/en.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello: world
3 changes: 3 additions & 0 deletions tests/fixtures/utils/locale-messages/locales/en/message.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"hello": "world"
}
8 changes: 8 additions & 0 deletions tests/fixtures/utils/locale-messages/locales/message.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
en: {
hello: "world"
},
ja: {
hello: "ワールド"
}
}
17 changes: 17 additions & 0 deletions tests/lib/rules/no-unused-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,19 @@ new RuleTester({
src: '.'
}
]
}),
...getTestCasesFromFixtures({
cwd: join(cwdRoot, './valid/path-locales'),
localeDir: {
pattern: `./locales/**/*.{json,yaml,yml}`,
localeKey: 'path',
localePattern: /^.*\/(?<locale>[A-Za-z0-9-_]+)\/.*\.(json5?|ya?ml)$/
},
options: [
{
src: '.'
}
]
})
]
: []),
Expand Down Expand Up @@ -1021,6 +1034,10 @@ ${' '.repeat(6)}
'constructor-option-format/src/main.js': true,
'multiple-locales/src/App.vue': true,
'multiple-locales/src/main.js': true,
'path-locales/locales/en/message.json': true,
'path-locales/locales/ja/message.yaml': true,
'path-locales/src/App.vue': true,
'path-locales/src/main.js': true,
'vue-cli-format/src/App.vue': true,
'vue-cli-format/src/main.js': true,
'constructor-option-format/locales/index.json': {
Expand Down
Loading