diff --git a/examples/by-frameworks/react-i18next/.vscode/settings.json b/examples/by-frameworks/react-i18next/.vscode/settings.json index 1bc560e1..e9593c3e 100644 --- a/examples/by-frameworks/react-i18next/.vscode/settings.json +++ b/examples/by-frameworks/react-i18next/.vscode/settings.json @@ -4,10 +4,12 @@ "react-i18next" ], "i18n-ally.namespace": true, + "i18n-ally.defaultNamespace": "translation", "i18n-ally.pathMatcher": "{locale}/{namespaces}.json", "i18n-ally.keystyle": "nested", "i18n-ally.keysInUse": [ "description.part2_whatever" ], + "i18n-ally.keyPrefix": true // "i18n-ally.defaultNamespace": "translation" } diff --git a/examples/by-frameworks/react-i18next/public/locales/en/pages/home.json b/examples/by-frameworks/react-i18next/public/locales/en/pages/home.json index 117b3803..32dcd26f 100644 --- a/examples/by-frameworks/react-i18next/public/locales/en/pages/home.json +++ b/examples/by-frameworks/react-i18next/public/locales/en/pages/home.json @@ -1,3 +1,7 @@ { - "title": "Home" + "title": "Home", + "description": { + "part1": "To get started, edit <1>src/App.js and save to reload.", + "part2": "Switch language between english and german using buttons above." + } } diff --git a/examples/by-frameworks/react-i18next/src/App.jsx b/examples/by-frameworks/react-i18next/src/App.jsx index aa2f6e53..42ea4b43 100644 --- a/examples/by-frameworks/react-i18next/src/App.jsx +++ b/examples/by-frameworks/react-i18next/src/App.jsx @@ -100,3 +100,8 @@ function Page5() { ) } +function KeyPrefix() { + const { t } = useTranslation('pages/home', { keyPrefix: 'description' }); + + t('part1') +} \ No newline at end of file diff --git a/package.json b/package.json index 3ec6f99e..f4be0d29 100644 --- a/package.json +++ b/package.json @@ -1273,6 +1273,11 @@ "type": "string", "description": "%config.default_namespace%" }, + "i18n-ally.keyPrefix": { + "type": "boolean", + "description": "Enable keyPrefix parsing", + "default": false + }, "i18n-ally.derivedKeyRules": { "deprecationMessage": "Deprecated. Use \"i18n-ally.usage.derivedKeyRules\" instead." }, diff --git a/src/core/Config.ts b/src/core/Config.ts index 3eedd1c5..7b5c756b 100644 --- a/src/core/Config.ts +++ b/src/core/Config.ts @@ -22,6 +22,7 @@ export class Config { 'encoding', 'namespace', 'defaultNamespace', + 'keyPrefix', 'disablePathParsing', 'readonly', 'languageTagSystem', @@ -146,6 +147,10 @@ export class Config { return this.getConfig('defaultNamespace') } + static get enableKeyPrefix(): boolean | undefined { + return this.getConfig('keyPrefix') + } + static get enabledFrameworks(): string[] | undefined { let ids = this.getConfig('enabledFrameworks') if (!ids || !ids.length) @@ -180,7 +185,7 @@ export class Config { return this.getConfig('sortCompare') || 'binary' } - static get sortLocale(): string | undefined{ + static get sortLocale(): string | undefined { return this.getConfig('sortLocale') } diff --git a/src/core/KeyDetector.ts b/src/core/KeyDetector.ts index 288c188c..07b451b3 100644 --- a/src/core/KeyDetector.ts +++ b/src/core/KeyDetector.ts @@ -55,13 +55,17 @@ export class KeyDetector { return keyRange?.key } - static getScopedKey(document: TextDocument, position: Position) - { + static getScopedKey(document: TextDocument, position: Position) { const scopes = Global.enabledFrameworks.flatMap(f => f.getScopeRange(document) || []) - if (scopes.length > 0) - { + if (scopes.length > 0) { const offset = document.offsetAt(position) - return scopes.filter(s => s.start < offset && offset < s.end).map(s => s.namespace).join('.') + return scopes + .filter(s => s.start < offset && offset < s.end) + .map((s) => { + const key = [s.namespace, s.keyPrefix].filter(Boolean).join('.') + return CurrentFile.loader.rewriteKeys(key, 'reference', { namespace: s.namespace }) + }) + .join('.') } } diff --git a/src/editor/completion.ts b/src/editor/completion.ts index 96cbf48b..d199d17d 100644 --- a/src/editor/completion.ts +++ b/src/editor/completion.ts @@ -1,6 +1,6 @@ import { CompletionItemProvider, TextDocument, Position, CompletionItem, CompletionItemKind, languages } from 'vscode' import { ExtensionModule } from '~/modules' -import { Global, KeyDetector, Loader, CurrentFile, LocaleTree, LocaleNode } from '~/core' +import { Global, KeyDetector, Loader, CurrentFile, LocaleTree, LocaleNode, Config } from '~/core' class CompletionProvider implements CompletionItemProvider { public provideCompletionItems( @@ -16,17 +16,19 @@ class CompletionProvider implements CompletionItemProvider { if (key === undefined) return - const scopedKey = KeyDetector.getScopedKey(document, position) + let scopedKey = KeyDetector.getScopedKey(document, position) if (!key) { + scopedKey = scopedKey || Config.defaultNamespace + return Object .values(CurrentFile.loader.keys) + .filter(key => key.startsWith(scopedKey || '')) .map((key) => { let resolvedKey = key if (scopedKey) - { - resolvedKey = key.replace(`${scopedKey}.`, "") - } + resolvedKey = key.replace(`${scopedKey}.`, '') + const item = new CompletionItem(resolvedKey, CompletionItemKind.Text) item.detail = loader.getValueByKey(key) return item diff --git a/src/frameworks/base.ts b/src/frameworks/base.ts index 90951496..83e8d758 100644 --- a/src/frameworks/base.ts +++ b/src/frameworks/base.ts @@ -17,6 +17,7 @@ export interface ScopeRange { start: number end: number namespace: string + keyPrefix?: string } export abstract class Framework { diff --git a/src/frameworks/react-i18next.ts b/src/frameworks/react-i18next.ts index 12ccc293..bfa9e346 100644 --- a/src/frameworks/react-i18next.ts +++ b/src/frameworks/react-i18next.ts @@ -162,7 +162,7 @@ class ReactI18nextFramework extends Framework { // Add first namespace as a global scope resetting on each occurrence // useTranslation(ns1) and useTranslation(['ns1', ...]) - const regUse = /useTranslation\(\s*\[?\s*['"`](.*?)['"`]/g + const regUse = /useTranslation\(\s*\[?\s*['"`](?.*?)['"`](,\s*['"`][^"'`]*['"`])*\s*\]?\s*(?:,\s*\{[^}]*keyPrefix\s*:\s*['"`](?.*?)['"`][^}]*\})?\s*\)/g let prevGlobalScope = false for (const match of text.matchAll(regUse)) { if (typeof match.index !== 'number') @@ -173,12 +173,13 @@ class ReactI18nextFramework extends Framework { ranges[ranges.length - 1].end = match.index // start a new scope if namespace is provided - if (match[1]) { + if (match.groups?.translationKey) { prevGlobalScope = true ranges.push({ start: match.index, end: text.length, - namespace: match[1], + namespace: match.groups.translationKey, + keyPrefix: match.groups.keyPrefix, }) } } diff --git a/src/utils/Regex.ts b/src/utils/Regex.ts index 0d4abd68..978cd335 100644 --- a/src/utils/Regex.ts +++ b/src/utils/Regex.ts @@ -27,6 +27,7 @@ export function handleRegexMatch( const quoted = QUOTE_SYMBOLS.includes(text[start - 1]) const namespace = scope?.namespace || defaultNamespace + const keyPrefix = Config.enableKeyPrefix ? scope?.keyPrefix : undefined // prevent duplicated detection when multiple frameworks enables at the same time. if (starts.includes(start)) @@ -38,7 +39,7 @@ export function handleRegexMatch( const hasExplicitNamespace = namespaceDelimiters.some(delimiter => key.includes(delimiter)) if (!hasExplicitNamespace && namespace) - key = `${namespace}.${key}` + key = `${namespace}.${keyPrefix ? `${keyPrefix}.` : ''}${key}` if (dotEnding || !key.endsWith('.')) { key = CurrentFile.loader.rewriteKeys(key, 'reference', {