Skip to content

Commit 6440f8c

Browse files
enhance NextIntl integration to handle multiple scopes within one file
1 parent 4c504c9 commit 6440f8c

File tree

2 files changed

+39
-26
lines changed

2 files changed

+39
-26
lines changed

src/frameworks/next-intl.ts

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import { TextDocument } from 'vscode'
22
import { Framework, ScopeRange } from './base'
3+
import { KeyStyle, RewriteKeyContext, RewriteKeySource } from '~/core'
34
import { LanguageId } from '~/utils'
4-
import { RewriteKeySource, RewriteKeyContext, KeyStyle } from '~/core'
5+
6+
export interface NextIntlScopeRange extends ScopeRange {
7+
functionName?: string
8+
}
9+
10+
export const isNextIntlScopeRange = (scope: ScopeRange): scope is NextIntlScopeRange => {
11+
return (scope as NextIntlScopeRange).functionName !== undefined
12+
}
513

614
class NextIntlFramework extends Framework {
715
id = 'next-intl'
@@ -27,17 +35,18 @@ class NextIntlFramework extends Framework {
2735
]
2836

2937
usageMatchRegex = [
38+
// Match: t, tSpecific, tFoo (capture the full variable name to use in scope detection)
3039
// Basic usage
31-
'[^\\w\\d]t\\s*\\(\\s*[\'"`]({key})[\'"`]',
40+
'[^\\w\\d](t(?:[A-Z]\\w*)?)\\s*\\(\\s*[\'"`]({key})[\'"`]',
3241

3342
// Rich text
34-
'[^\\w\\d]t\\s*\.rich\\s*\\(\\s*[\'"`]({key})[\'"`]',
43+
'[^\\w\\d](t(?:[A-Z]\\w*)?)\\s*\\.rich\\s*\\(\\s*[\'"`]({key})[\'"`]',
3544

3645
// Markup text
37-
'[^\\w\\d]t\\s*\.markup\\s*\\(\\s*[\'"`]({key})[\'"`]',
46+
'[^\\w\\d](t(?:[A-Z]\\w*)?)\\s*\\.markup\\s*\\(\\s*[\'"`]({key})[\'"`]',
3847

3948
// Raw text
40-
'[^\\w\\d]t\\s*\.raw\\s*\\(\\s*[\'"`]({key})[\'"`]',
49+
'[^\\w\\d](t(?:[A-Z]\\w*)?)\\s*\\.raw\\s*\\(\\s*[\'"`]({key})[\'"`]',
4150
]
4251

4352
refactorTemplates(keypath: string) {
@@ -75,36 +84,32 @@ class NextIntlFramework extends Framework {
7584
return dottedKey
7685
}
7786

78-
getScopeRange(document: TextDocument): ScopeRange[] | undefined {
87+
getScopeRange(document: TextDocument): NextIntlScopeRange[] | undefined {
7988
if (!this.languageIds.includes(document.languageId as any))
8089
return
8190

82-
const ranges: ScopeRange[] = []
91+
const ranges: NextIntlScopeRange[] = []
8392
const text = document.getText()
8493

85-
// Find matches of `useTranslations` and `getTranslations`. Later occurences will
86-
// override previous ones (this allows for multiple components with different
87-
// namespaces in the same file). Note that `getTranslations` can either be called
88-
// with a single string argument or an object with a `namespace` key.
89-
const regex = /(useTranslations\(\s*|getTranslations\(\s*|namespace:\s+)(['"`](.*?)['"`])?/g
90-
let prevGlobalScope = false
94+
// Find matches of `useTranslations` and `getTranslations` and extracts the variable names.
95+
// If there are multiple occurrences in the same file, there will be multiple, overlapping scopes.
96+
// During resolution, the variable name will be used to determine which scope the key belongs to, allowing multiple namespaces in the same file.
97+
const regex = /(?:const|let|var)\s+(t(?:[A-Z]\w*)?)\s*=\s*(?:await\s+)?(useTranslations|getTranslations)\s*\(\s*['"`](.*?)['"`]\)/g
98+
9199
for (const match of text.matchAll(regex)) {
92100
if (typeof match.index !== 'number')
93101
continue
94102

103+
const variableName = match[1]
95104
const namespace = match[3]
96105

97-
// End previous scope
98-
if (prevGlobalScope)
99-
ranges[ranges.length - 1].end = match.index
100-
101-
// Start a new scope if a namespace is provided
106+
// Add a new scope if a namespace is provided
102107
if (namespace) {
103-
prevGlobalScope = true
104108
ranges.push({
105109
start: match.index,
106110
end: text.length,
107111
namespace,
112+
functionName: variableName,
108113
})
109114
}
110115
}

src/utils/Regex.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,37 @@
11
import { sortBy } from 'lodash'
2-
import { QUOTE_SYMBOLS } from '../meta'
2+
import { Config, CurrentFile } from '~/core'
3+
import { isNextIntlScopeRange, NextIntlScopeRange } from '~/frameworks/next-intl'
4+
import i18n from '~/i18n'
5+
import { Log } from '.'
36
import { KeyInDocument, RewriteKeyContext } from '../core/types'
47
import { ScopeRange } from '../frameworks/base'
5-
import { Log } from '.'
6-
import i18n from '~/i18n'
7-
import { CurrentFile, Config } from '~/core'
8+
import { QUOTE_SYMBOLS } from '../meta'
89

910
export function handleRegexMatch(
1011
text: string,
1112
match: RegExpExecArray,
1213
dotEnding = false,
1314
rewriteContext?: RewriteKeyContext,
14-
scopes: ScopeRange[] = [],
15+
scopes: ScopeRange[] | NextIntlScopeRange[] = [],
1516
namespaceDelimiters = [':', '/'],
1617
defaultNamespace?: string,
1718
starts: number[] = [],
1819
): KeyInDocument | undefined {
1920
const matchString = match[0]
20-
let key = match[1]
21+
22+
let keyIndex = 1
23+
let nextIntlFunctionName = undefined
24+
if (scopes.some(s => isNextIntlScopeRange(s))) {
25+
keyIndex = 2
26+
nextIntlFunctionName = match[1]
27+
}
28+
let key = match[keyIndex]
2129
if (!key)
2230
return
2331

2432
const start = match.index + matchString.lastIndexOf(key)
2533
const end = start + key.length
26-
const scope = scopes.find(s => s.start <= start && s.end >= end)
34+
const scope = scopes.find(s => s.start <= start && s.end >= end && (!isNextIntlScopeRange(s) || s.functionName === nextIntlFunctionName))
2735
const quoted = QUOTE_SYMBOLS.includes(text[start - 1])
2836

2937
const namespace = scope?.namespace || defaultNamespace

0 commit comments

Comments
 (0)