diff --git a/src/frameworks/next-intl.ts b/src/frameworks/next-intl.ts index edfa765e..9b95164c 100644 --- a/src/frameworks/next-intl.ts +++ b/src/frameworks/next-intl.ts @@ -1,7 +1,15 @@ import { TextDocument } from 'vscode' import { Framework, ScopeRange } from './base' +import { KeyStyle, RewriteKeyContext, RewriteKeySource } from '~/core' import { LanguageId } from '~/utils' -import { RewriteKeySource, RewriteKeyContext, KeyStyle } from '~/core' + +export interface NextIntlScopeRange extends ScopeRange { + functionName?: string +} + +export const isNextIntlScopeRange = (scope: ScopeRange): scope is NextIntlScopeRange => { + return (scope as NextIntlScopeRange).functionName !== undefined +} class NextIntlFramework extends Framework { id = 'next-intl' @@ -27,17 +35,18 @@ class NextIntlFramework extends Framework { ] usageMatchRegex = [ + // Match: t, tSpecific, tFoo (capture the full variable name to use in scope detection) // Basic usage - '[^\\w\\d]t\\s*\\(\\s*[\'"`]({key})[\'"`]', + '[^\\w\\d](t(?:[A-Z]\\w*)?)\\s*\\(\\s*[\'"`]({key})[\'"`]', // Rich text - '[^\\w\\d]t\\s*\.rich\\s*\\(\\s*[\'"`]({key})[\'"`]', + '[^\\w\\d](t(?:[A-Z]\\w*)?)\\s*\\.rich\\s*\\(\\s*[\'"`]({key})[\'"`]', // Markup text - '[^\\w\\d]t\\s*\.markup\\s*\\(\\s*[\'"`]({key})[\'"`]', + '[^\\w\\d](t(?:[A-Z]\\w*)?)\\s*\\.markup\\s*\\(\\s*[\'"`]({key})[\'"`]', // Raw text - '[^\\w\\d]t\\s*\.raw\\s*\\(\\s*[\'"`]({key})[\'"`]', + '[^\\w\\d](t(?:[A-Z]\\w*)?)\\s*\\.raw\\s*\\(\\s*[\'"`]({key})[\'"`]', ] refactorTemplates(keypath: string) { @@ -75,36 +84,32 @@ class NextIntlFramework extends Framework { return dottedKey } - getScopeRange(document: TextDocument): ScopeRange[] | undefined { + getScopeRange(document: TextDocument): NextIntlScopeRange[] | undefined { if (!this.languageIds.includes(document.languageId as any)) return - const ranges: ScopeRange[] = [] + const ranges: NextIntlScopeRange[] = [] const text = document.getText() - // Find matches of `useTranslations` and `getTranslations`. Later occurences will - // override previous ones (this allows for multiple components with different - // namespaces in the same file). Note that `getTranslations` can either be called - // with a single string argument or an object with a `namespace` key. - const regex = /(useTranslations\(\s*|getTranslations\(\s*|namespace:\s+)(['"`](.*?)['"`])?/g - let prevGlobalScope = false + // Find matches of `useTranslations` and `getTranslations` and extracts the variable names. + // If there are multiple occurrences in the same file, there will be multiple, overlapping scopes. + // During resolution, the variable name will be used to determine which scope the key belongs to, allowing multiple namespaces in the same file. + const regex = /(?:const|let|var)\s+(t(?:[A-Z]\w*)?)\s*=\s*(?:await\s+)?(useTranslations|getTranslations)\s*\(\s*['"`](.*?)['"`]\)/g + for (const match of text.matchAll(regex)) { if (typeof match.index !== 'number') continue + const variableName = match[1] const namespace = match[3] - // End previous scope - if (prevGlobalScope) - ranges[ranges.length - 1].end = match.index - - // Start a new scope if a namespace is provided + // Add a new scope if a namespace is provided if (namespace) { - prevGlobalScope = true ranges.push({ start: match.index, end: text.length, namespace, + functionName: variableName, }) } } diff --git a/src/utils/Regex.ts b/src/utils/Regex.ts index 0d4abd68..c590151c 100644 --- a/src/utils/Regex.ts +++ b/src/utils/Regex.ts @@ -1,29 +1,39 @@ import { sortBy } from 'lodash' -import { QUOTE_SYMBOLS } from '../meta' +import { Config, CurrentFile } from '~/core' +import { isNextIntlScopeRange, NextIntlScopeRange } from '~/frameworks/next-intl' +import i18n from '~/i18n' +import { Log } from '.' import { KeyInDocument, RewriteKeyContext } from '../core/types' import { ScopeRange } from '../frameworks/base' -import { Log } from '.' -import i18n from '~/i18n' -import { CurrentFile, Config } from '~/core' +import { QUOTE_SYMBOLS } from '../meta' export function handleRegexMatch( text: string, match: RegExpExecArray, dotEnding = false, rewriteContext?: RewriteKeyContext, - scopes: ScopeRange[] = [], + scopes: ScopeRange[] | NextIntlScopeRange[] = [], namespaceDelimiters = [':', '/'], defaultNamespace?: string, starts: number[] = [], ): KeyInDocument | undefined { const matchString = match[0] - let key = match[1] + + let keyIndex = 1 + let nextIntlFunctionName: string | undefined + const hasNextIntlScope = scopes.some(s => isNextIntlScopeRange(s)) + // Switch to NextIntl capture layout when the regex actually provided both groups + if (hasNextIntlScope && typeof match[2] !== 'undefined') { + keyIndex = 2 + nextIntlFunctionName = match[1] as string + } + let key = match[keyIndex] if (!key) return const start = match.index + matchString.lastIndexOf(key) const end = start + key.length - const scope = scopes.find(s => s.start <= start && s.end >= end) + const scope = scopes.find(s => s.start <= start && s.end >= end && (!isNextIntlScopeRange(s) || s.functionName === nextIntlFunctionName)) const quoted = QUOTE_SYMBOLS.includes(text[start - 1]) const namespace = scope?.namespace || defaultNamespace