Skip to content

Commit

Permalink
Add support for includeFilenameInKey to address intlify#336
Browse files Browse the repository at this point in the history
  • Loading branch information
lee committed Jan 27, 2023
1 parent 3bb2a43 commit 3f6eb9b
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 11 deletions.
8 changes: 8 additions & 0 deletions docs/started.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ module.export = {
// pattern: './path/to/locales3/*.{json,json5,yaml,yml}',
// localeKey: 'key'
// },
// {
// // 'path' case - including filenames in the key
// pattern: './path/to/locales4/*.{json,json5,yaml,yml}',
// localePattern: /^.*\/(?<locale>[A-Za-z0-9-_]+)\/.*\.(json5?|ya?ml)$/,
// localeKey: 'path',
// includeFilenameInKey: true
// },
// ]

// Specify the version of `vue-i18n` you are using.
Expand All @@ -87,6 +94,7 @@ See [the rule list](../rules/)
- `'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.
- `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`.
- `includeFilenameInKey` ... Specifies if the filename (without the extension) should be considered as part of the message keys. This is only valid when localeKey is set to 'path'. For example, the key 'title' in the file 'common.json' would be considered to have key 'common.title' if this flag is set to true.
- 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.

Expand Down
19 changes: 15 additions & 4 deletions lib/rules/no-missing-keys-in-other-locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import type {
import type { LocaleMessage, LocaleMessages } from '../utils/locale-messages'
import { joinPath } from '../utils/key-path'
import { createRule } from '../utils/rule'
import { getBasename } from '../utils/path-utils'
const debug = debugBuilder(
'eslint-plugin-vue-i18n:no-missing-keys-in-other-locales'
)

function create(context: RuleContext): RuleListener {
const filename = context.getFilename()
const basename = getBasename(filename)
const ignoreLocales: string[] = context.options[0]?.ignoreLocales || []

function reportMissing(
Expand Down Expand Up @@ -90,11 +92,20 @@ function create(context: RuleContext): RuleListener {
return localeMessages.locales
.filter(locale => !ignores.has(locale))
.map(locale => {
const dictList = localeMessages.localeMessages
.filter(lm =>
lm.includeFilenameInKey ? lm.basename === basename : true
)
.map(lm => {
const messages = lm.getMessagesFromLocale(locale)
return lm.includeFilenameInKey
? ((messages[basename] || {}) as I18nLocaleMessageDictionary)
: messages
})

return {
locale,
dictList: localeMessages.localeMessages.map(lm =>
lm.getMessagesFromLocale(locale)
)
dictList
}
})
}
Expand All @@ -121,7 +132,7 @@ function create(context: RuleContext): RuleListener {
keyStack = {
locale,
otherLocaleMessages: getOtherLocaleMessages(locale),
keyPath: []
keyPath: targetLocaleMessage.includeFilenameInKey ? [basename] : []
}
} else {
keyStack = {
Expand Down
7 changes: 7 additions & 0 deletions lib/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,11 @@ export interface SettingsVueI18nLocaleDirObject {
* 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
/**
* Specifies if the filename (without the extension) should be considered as part of the message keys.
*
* This is only valid when localeKey is set to 'path'.
* For example, the key 'title' in the file 'common.json' would be considered to have key 'common.title'.
*/
includeFilenameInKey?: boolean
}
18 changes: 15 additions & 3 deletions lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface LocaleFiles {
files: string[]
localeKey: LocaleKeyType
localePattern?: string | RegExp
includeFilenameInKey?: boolean
}
const UNEXPECTED_ERROR_LOCATION = { line: 1, column: 0 }
/**
Expand Down Expand Up @@ -129,7 +130,12 @@ function loadLocaleMessages(
): FileLocaleMessage[] {
const results: FileLocaleMessage[] = []
const checkDupeMap: { [file: string]: LocaleKeyType[] } = {}
for (const { files, localeKey, localePattern } of localeFilesList) {
for (const {
files,
localeKey,
localePattern,
includeFilenameInKey
} of localeFilesList) {
for (const file of files) {
const localeKeys = checkDupeMap[file] || (checkDupeMap[file] = [])
if (localeKeys.includes(localeKey)) {
Expand All @@ -138,7 +144,12 @@ function loadLocaleMessages(
localeKeys.push(localeKey)
const fullpath = resolve(cwd, file)
results.push(
new FileLocaleMessage({ fullpath, localeKey, localePattern })
new FileLocaleMessage({
fullpath,
localeKey,
localePattern,
includeFilenameInKey
})
)
}
}
Expand Down Expand Up @@ -245,7 +256,8 @@ class LocaleDirLocaleMessagesCache {
return {
files: targetFilesLoader.get(localeDir.pattern, cwd),
localeKey: String(localeDir.localeKey ?? 'file') as LocaleKeyType,
localePattern: localeDir.localePattern
localePattern: localeDir.localePattern,
includeFilenameInKey: localeDir.includeFilenameInKey
}
}
}
Expand Down
21 changes: 17 additions & 4 deletions lib/utils/locale-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ResourceLoader } from './resource-loader'
import JSON5 from 'json5'
import yaml from 'js-yaml'
import { joinPath, parsePath } from './key-path'
import { getBasename } from './path-utils'

// 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-_]+'
Expand All @@ -38,32 +39,39 @@ export abstract class LocaleMessage {
public readonly fullpath: string
public readonly localeKey: LocaleKeyType
public readonly file: string
public readonly basename: string
public readonly localePattern: RegExp
public readonly includeFilenameInKey: boolean
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.
* @param {Boolean} args.includeFilenameInKey Specifies if the filename should be included in the key for messages.
*/
constructor({
fullpath,
locales,
localeKey,
localePattern
localePattern,
includeFilenameInKey
}: {
fullpath: string
locales?: string[]
localeKey: LocaleKeyType
localePattern?: string | RegExp
includeFilenameInKey?: boolean
}) {
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.basename = getBasename(fullpath)
this.localePattern = this.getLocalePatternWithRegex(localePattern)
this.includeFilenameInKey = includeFilenameInKey || false

this._locales = locales
}
Expand Down Expand Up @@ -197,23 +205,27 @@ export class FileLocaleMessage extends LocaleMessage {
* @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.
* @param {Boolean} args.includeFilenameInKey Specifies if the filename should be included in the key for messages.
*/
constructor({
fullpath,
locales,
localeKey,
localePattern
localePattern,
includeFilenameInKey
}: {
fullpath: string
locales?: string[]
localeKey: LocaleKeyType
localePattern?: string | RegExp
includeFilenameInKey?: boolean
}) {
super({
fullpath,
locales,
localeKey,
localePattern
localePattern,
includeFilenameInKey
})
this._resource = new ResourceLoader(fullpath, fileName => {
const ext = extname(fileName).toLowerCase()
Expand All @@ -230,7 +242,8 @@ export class FileLocaleMessage extends LocaleMessage {
}

getMessagesInternal(): I18nLocaleMessageDictionary {
return this._resource.getResource()
const resource = this._resource.getResource()
return this.includeFilenameInKey ? { [this.basename]: resource } : resource
}
}

Expand Down
8 changes: 8 additions & 0 deletions lib/utils/path-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,11 @@ export function getRelativePath(filepath: string, baseDir: string): string {
}
return absolutePath.replace(/^\//, '')
}

export function getBasename(filepath: string): string {
return filepath
.replace(/^.*(\\|\/|:)/, '')
.split('.')
.slice(0, -1)
.join('.')
}
19 changes: 19 additions & 0 deletions tests/lib/utils/locale-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,23 @@ describe('FileLocaleMessage', () => {
assert.deepStrictEqual(messages.locales, ['en', 'ja'])
})
})

describe('localeKey: "path" with includeFilenameInKey = true', () => {
it('messages returned should be keyed by the filename', () => {
const testFilePath = path.resolve(
__dirname,
'../../fixtures/utils/locale-messages/locales/en/message.json'
)
const messages = new FileLocaleMessage({
fullpath: testFilePath,
localeKey: 'path',
localePattern: /^.*\/(?<locale>[A-Za-z0-9-_]+)\/.*\.(json5?|ya?ml)$/,
includeFilenameInKey: true
})
assert.deepStrictEqual(Object.keys(messages.messages), ['message'])
assert.deepStrictEqual(Object.keys(messages.messages['message'] || {}), [
'hello'
])
})
})
})
19 changes: 19 additions & 0 deletions tests/lib/utils/path-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @author Yosuke Ota
*/
import assert from 'assert'
import { getBasename } from '../../../lib/utils/path-utils'

describe('getBasename', () => {
it('return the filename without the extension', () => {
assert.strictEqual(
getBasename('~/some/clever/path/to/common.json'),
'common'
)

assert.strictEqual(
getBasename('~/some/clever/path/to/dotted.file.json'),
'dotted.file'
)
})
})

0 comments on commit 3f6eb9b

Please sign in to comment.