diff --git a/docs/started.md b/docs/started.md index 34b01b97..84ce79c8 100644 --- a/docs/started.md +++ b/docs/started.md @@ -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: /^.*\/(?[A-Za-z0-9-_]+)\/.*\.(json5?|ya?ml)$/, + // localeKey: 'path', + // includeFilenameInKey: true + // }, // ] // Specify the version of `vue-i18n` you are using. @@ -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 `?`, 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. diff --git a/lib/rules/no-missing-keys-in-other-locales.ts b/lib/rules/no-missing-keys-in-other-locales.ts index b876421d..862d78ac 100644 --- a/lib/rules/no-missing-keys-in-other-locales.ts +++ b/lib/rules/no-missing-keys-in-other-locales.ts @@ -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( @@ -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 } }) } @@ -121,7 +132,7 @@ function create(context: RuleContext): RuleListener { keyStack = { locale, otherLocaleMessages: getOtherLocaleMessages(locale), - keyPath: [] + keyPath: targetLocaleMessage.includeFilenameInKey ? [basename] : [] } } else { keyStack = { diff --git a/lib/types/settings.ts b/lib/types/settings.ts index 897f3dcc..2e33b4e7 100644 --- a/lib/types/settings.ts +++ b/lib/types/settings.ts @@ -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 } diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 71e32ec3..35a0e802 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -30,6 +30,7 @@ interface LocaleFiles { files: string[] localeKey: LocaleKeyType localePattern?: string | RegExp + includeFilenameInKey?: boolean } const UNEXPECTED_ERROR_LOCATION = { line: 1, column: 0 } /** @@ -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)) { @@ -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 + }) ) } } @@ -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 } } } diff --git a/lib/utils/locale-messages.ts b/lib/utils/locale-messages.ts index 443f4470..91437bb2 100644 --- a/lib/utils/locale-messages.ts +++ b/lib/utils/locale-messages.ts @@ -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-_]+' @@ -38,7 +39,9 @@ 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 @@ -46,24 +49,29 @@ export abstract class LocaleMessage { * @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 } @@ -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() @@ -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 } } diff --git a/lib/utils/path-utils.ts b/lib/utils/path-utils.ts index 8ffe7e38..6d2091b3 100644 --- a/lib/utils/path-utils.ts +++ b/lib/utils/path-utils.ts @@ -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('.') +} diff --git a/tests/lib/utils/locale-messages.ts b/tests/lib/utils/locale-messages.ts index 0a29dc85..81b0dd10 100644 --- a/tests/lib/utils/locale-messages.ts +++ b/tests/lib/utils/locale-messages.ts @@ -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: /^.*\/(?[A-Za-z0-9-_]+)\/.*\.(json5?|ya?ml)$/, + includeFilenameInKey: true + }) + assert.deepStrictEqual(Object.keys(messages.messages), ['message']) + assert.deepStrictEqual(Object.keys(messages.messages['message'] || {}), [ + 'hello' + ]) + }) + }) }) diff --git a/tests/lib/utils/path-utils.ts b/tests/lib/utils/path-utils.ts new file mode 100644 index 00000000..d9ade189 --- /dev/null +++ b/tests/lib/utils/path-utils.ts @@ -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' + ) + }) +})