|
| 1 | +import { TextDocument } from 'vscode' |
| 2 | +import { Framework, ScopeRange } from './base' |
| 3 | +import { LanguageId } from '~/utils' |
| 4 | +import { RewriteKeySource, RewriteKeyContext, KeyStyle } from '~/core' |
| 5 | + |
| 6 | +class NextIntlFramework extends Framework { |
| 7 | + id = 'next-intl' |
| 8 | + display = 'next-intl' |
| 9 | + namespaceDelimiter = '.' |
| 10 | + perferredKeystyle?: KeyStyle = 'nested' |
| 11 | + |
| 12 | + namespaceDelimiters = ['.'] |
| 13 | + namespaceDelimitersRegex = /[\.]/g |
| 14 | + |
| 15 | + detection = { |
| 16 | + packageJSON: [ |
| 17 | + 'next-intl', |
| 18 | + ], |
| 19 | + } |
| 20 | + |
| 21 | + languageIds: LanguageId[] = [ |
| 22 | + 'javascript', |
| 23 | + 'typescript', |
| 24 | + 'javascriptreact', |
| 25 | + 'typescriptreact', |
| 26 | + 'ejs', |
| 27 | + ] |
| 28 | + |
| 29 | + usageMatchRegex = [ |
| 30 | + // Basic usage |
| 31 | + '[^\\w\\d]t\\([\'"`]({key})[\'"`]', |
| 32 | + |
| 33 | + // Rich text |
| 34 | + '[^\\w\\d]t\.rich\\([\'"`]({key})[\'"`]', |
| 35 | + |
| 36 | + // Raw text |
| 37 | + '[^\\w\\d]t\.raw\\([\'"`]({key})[\'"`]', |
| 38 | + ] |
| 39 | + |
| 40 | + refactorTemplates(keypath: string) { |
| 41 | + // Ideally we'd automatically consider the namespace here. Since this |
| 42 | + // doesn't seem to be possible though, we'll generate all permutations for |
| 43 | + // the `keypath`. E.g. `one.two.three` will generate `three`, `two.three`, |
| 44 | + // `one.two.three`. |
| 45 | + |
| 46 | + const keypaths = keypath.split('.').map((cur, index, parts) => { |
| 47 | + return parts.slice(parts.length - index - 1).join('.') |
| 48 | + }) |
| 49 | + return [ |
| 50 | + ...keypaths.map(cur => |
| 51 | + `{t('${cur}')}`, |
| 52 | + ), |
| 53 | + ...keypaths.map(cur => |
| 54 | + `t('${cur}')`, |
| 55 | + ), |
| 56 | + ] |
| 57 | + } |
| 58 | + |
| 59 | + rewriteKeys(key: string, source: RewriteKeySource, context: RewriteKeyContext = {}) { |
| 60 | + const dottedKey = key.split(this.namespaceDelimitersRegex).join('.') |
| 61 | + |
| 62 | + // When the namespace is explicitly set, ignore the current namespace scope |
| 63 | + if ( |
| 64 | + this.namespaceDelimiters.some(delimiter => key.includes(delimiter)) |
| 65 | + && context.namespace |
| 66 | + && dottedKey.startsWith(context.namespace.split(this.namespaceDelimitersRegex).join('.')) |
| 67 | + ) { |
| 68 | + // +1 for the an extra `.` |
| 69 | + key = key.slice(context.namespace.length + 1) |
| 70 | + } |
| 71 | + |
| 72 | + return dottedKey |
| 73 | + } |
| 74 | + |
| 75 | + getScopeRange(document: TextDocument): ScopeRange[] | undefined { |
| 76 | + if (!this.languageIds.includes(document.languageId as any)) |
| 77 | + return |
| 78 | + |
| 79 | + const ranges: ScopeRange[] = [] |
| 80 | + const text = document.getText() |
| 81 | + |
| 82 | + // Find matches of `useTranslations`, later occurences will override |
| 83 | + // previous ones (this allows for multiple components with different |
| 84 | + // namespaces in the same file). |
| 85 | + const regex = /useTranslations\(\s*(['"`](.*?)['"`])?/g |
| 86 | + let prevGlobalScope = false |
| 87 | + for (const match of text.matchAll(regex)) { |
| 88 | + if (typeof match.index !== 'number') |
| 89 | + continue |
| 90 | + |
| 91 | + const namespace = match[2] |
| 92 | + |
| 93 | + // End previous scope |
| 94 | + if (prevGlobalScope) |
| 95 | + ranges[ranges.length - 1].end = match.index |
| 96 | + |
| 97 | + // Start a new scope if a namespace is provided |
| 98 | + if (namespace) { |
| 99 | + prevGlobalScope = true |
| 100 | + ranges.push({ |
| 101 | + start: match.index, |
| 102 | + end: text.length, |
| 103 | + namespace, |
| 104 | + }) |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + return ranges |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +export default NextIntlFramework |
0 commit comments