From d859314d52ebf99a0d4add3a3ab9f8cfdd30e3c2 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Thu, 11 May 2023 15:28:50 +0200 Subject: [PATCH 1/8] WIP --- examples/by-frameworks/next-intl/.eslintrc | 6 + examples/by-frameworks/next-intl/.gitignore | 4 + examples/by-frameworks/next-intl/global.d.ts | 3 + .../by-frameworks/next-intl/messages/de.json | 5 + .../by-frameworks/next-intl/messages/en.json | 5 + .../by-frameworks/next-intl/next-env.d.ts | 5 + examples/by-frameworks/next-intl/package.json | 22 +++ .../next-intl/public/favicon.ico | Bin 0 -> 15086 bytes .../next-intl/src/app/[locale]/layout.tsx | 34 ++++ .../next-intl/src/app/[locale]/page.tsx | 13 ++ .../next-intl/src/middleware.tsx | 11 ++ .../by-frameworks/next-intl/tsconfig.json | 37 +++++ src/frameworks/next-intl.ts | 152 ++++++++++++++++++ 13 files changed, 297 insertions(+) create mode 100644 examples/by-frameworks/next-intl/.eslintrc create mode 100644 examples/by-frameworks/next-intl/.gitignore create mode 100644 examples/by-frameworks/next-intl/global.d.ts create mode 100644 examples/by-frameworks/next-intl/messages/de.json create mode 100644 examples/by-frameworks/next-intl/messages/en.json create mode 100644 examples/by-frameworks/next-intl/next-env.d.ts create mode 100644 examples/by-frameworks/next-intl/package.json create mode 100644 examples/by-frameworks/next-intl/public/favicon.ico create mode 100644 examples/by-frameworks/next-intl/src/app/[locale]/layout.tsx create mode 100644 examples/by-frameworks/next-intl/src/app/[locale]/page.tsx create mode 100644 examples/by-frameworks/next-intl/src/middleware.tsx create mode 100644 examples/by-frameworks/next-intl/tsconfig.json create mode 100644 src/frameworks/next-intl.ts diff --git a/examples/by-frameworks/next-intl/.eslintrc b/examples/by-frameworks/next-intl/.eslintrc new file mode 100644 index 00000000..7629f73f --- /dev/null +++ b/examples/by-frameworks/next-intl/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "import/named": "off", + "react/react-in-jsx-scope": "off" + } +} \ No newline at end of file diff --git a/examples/by-frameworks/next-intl/.gitignore b/examples/by-frameworks/next-intl/.gitignore new file mode 100644 index 00000000..04239e7d --- /dev/null +++ b/examples/by-frameworks/next-intl/.gitignore @@ -0,0 +1,4 @@ +/node_modules +/.next/ +.DS_Store +tsconfig.tsbuildinfo diff --git a/examples/by-frameworks/next-intl/global.d.ts b/examples/by-frameworks/next-intl/global.d.ts new file mode 100644 index 00000000..e069a70d --- /dev/null +++ b/examples/by-frameworks/next-intl/global.d.ts @@ -0,0 +1,3 @@ +// Use type safe message keys with `next-intl` +type Messages = typeof import('./messages/en.json') +declare interface IntlMessages extends Messages {} diff --git a/examples/by-frameworks/next-intl/messages/de.json b/examples/by-frameworks/next-intl/messages/de.json new file mode 100644 index 00000000..3341f48d --- /dev/null +++ b/examples/by-frameworks/next-intl/messages/de.json @@ -0,0 +1,5 @@ +{ + "IndexPage": { + "title": "next-intl Beispiel" + } +} diff --git a/examples/by-frameworks/next-intl/messages/en.json b/examples/by-frameworks/next-intl/messages/en.json new file mode 100644 index 00000000..af1a96ab --- /dev/null +++ b/examples/by-frameworks/next-intl/messages/en.json @@ -0,0 +1,5 @@ +{ + "IndexPage": { + "title": "next-intl example" + } +} diff --git a/examples/by-frameworks/next-intl/next-env.d.ts b/examples/by-frameworks/next-intl/next-env.d.ts new file mode 100644 index 00000000..4f11a03d --- /dev/null +++ b/examples/by-frameworks/next-intl/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/by-frameworks/next-intl/package.json b/examples/by-frameworks/next-intl/package.json new file mode 100644 index 00000000..5109dff1 --- /dev/null +++ b/examples/by-frameworks/next-intl/package.json @@ -0,0 +1,22 @@ +{ + "name": "next-intl", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "next dev", + "lint": "tsc", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "^13.4.0", + "next-intl": "^2.14.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/node": "^17.0.23", + "@types/react": "^18.2.5", + "typescript": "^5.0.0" + } +} diff --git a/examples/by-frameworks/next-intl/public/favicon.ico b/examples/by-frameworks/next-intl/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4ddd8fff7086c240ed21438ca7d96f1b8dae0bc0 GIT binary patch literal 15086 zcmdU$O=uNY6vrnYO%#n9m0G$<)C8)FRuGb41&a%z2%-zOrYkKQ7LVcV8tZ8{r_I(J}2Xwd3WZ$*OwePxifd}IsgB? z@7{AiX5x7@UY)mPi>I{JyV~w~9iHd4w)*$0Jnt9fty0SKPwPGJD;0Q8Wn|)gA|*d3 zZJyV>ZcgVi%JR2KUOJ_pNiRu%l#WaPk^bu|*W~exGL*#z+iFOiKxv{cq~A-QNoSnvZWVz$%y2 z$kB(=jC597D#v2>V1NZC*rkY~fpzbN^>mc9}aM-le$W`3Rbh-!*zlSeaS_(t57xidzf?h9!_wJ6XMpeG;F(2?%@^} zTZz5QCkusdm&t8;0X=2;v z=4NyA=FO7%Pfbmky?gg2#`ihHeJl-sYxshdXBjsq7mY z8_mUw7gO^e8yhp7ot>$$ea>(%X}z;`>sB*5I-05ur%#_Y#Iv;R=W{QKf7*DOB3>4% zC)`u_AKPw8<7vuxS%^Q}Q;%QEpEOB4Eg@cp>j3vu@iWMsYu9*MV!RCVhdXmXI6u|B zOFS(xUWR$WJ@NV>s=meIX&K^WkiT6&bc&Dj`YnpP&5w(xWr&wS{&xM=By8vPv*j6; zM#a-I#mgXnIKv%r-oL19&_}`Ra6BzzybR*m{R{jzNz1x_B%V%BPv_?4GR8~GAFgn= z!d{p?v3*{0N<1AJ8Zs|lyeMUT8O4LM2F@IU!kI)_e}g@H_L#xJL9=w}(kLEj^5Mu? zPa!Gm`4#bW_3G7WI7ZpTonKL(#c+POu291V^0w=Gv7X=BY8=qTLOVb2w&WaA-W4pg z_ZPfVNg|8+{gJIFb*3*>V?2NfcG7m2$vk0h`@i3x4*QtFGv;}uQrr+*nI9_oTi_dv zxOXgioLZQ(?e_y}6iZ{#ZwxL^&=9>|`hvL_vv zayCHQqFj^5GrQ-*#;)IWnNEy;<($6sycVvrp4Uj4(FrcKN zvwpkUcIKIsPTVT}NSgmnYgOv+w20N4r9IMbr9Vq2rL(?rO&-rELs@J}6~-g|aZs9- zPD^QmIHgQhiVe2-NF(pcs9(zWSaEo9&#=V@zLH3X3Xe$n+2j1XJi`aR@aZBuRp=+F zi|-_5@QLp@fnz1GEl+$$wFAG=4^msUQYjeW{!ptt`bniQ!+ya6(@K{v%63*JUtzrP zJy>8OjL&bR;WD}Vy1F`(R_p8QS z3)aiW1e0>+L40 z-*@!(_U7itD4r4d!TA^bRO^q1h6b~2*|K1rT(A3mOt7s~bNyvGRCE1lIqcL$qL`%e zF71v_e7gwkz@DKR`wz>H{y;x@CUvP_p)%OwBfQ>LmXi=0*h5xh{~07$vHiE{t1C-pXV!RjLzkh$YPT0;{WAHuhnKNhJu)lWgTA}x{_V=CVx2>(su;#M@ zA9w|M`q$Xc<8RBbW@GJ!#C#`=ZNB}iEh&4Pk&zMe=+Pt7-riman>=juzhBUvYv%ET z2M + + next-intl + + + + {children} + + + + ) +} diff --git a/examples/by-frameworks/next-intl/src/app/[locale]/page.tsx b/examples/by-frameworks/next-intl/src/app/[locale]/page.tsx new file mode 100644 index 00000000..5ec52084 --- /dev/null +++ b/examples/by-frameworks/next-intl/src/app/[locale]/page.tsx @@ -0,0 +1,13 @@ +'use client' + +import { useTranslations } from 'next-intl' + +export default function IndexPage() { + const t = useTranslations('IndexPage') + + return ( +
+

{t('title')}

+
+ ) +} diff --git a/examples/by-frameworks/next-intl/src/middleware.tsx b/examples/by-frameworks/next-intl/src/middleware.tsx new file mode 100644 index 00000000..23f8881a --- /dev/null +++ b/examples/by-frameworks/next-intl/src/middleware.tsx @@ -0,0 +1,11 @@ +import createMiddleware from 'next-intl/middleware' + +export default createMiddleware({ + locales: ['en', 'de'], + defaultLocale: 'en', +}) + +export const config = { + // Skip all paths that should not be internationalized + matcher: ['/((?!api|_next|.*\\..*).*)'], +} diff --git a/examples/by-frameworks/next-intl/tsconfig.json b/examples/by-frameworks/next-intl/tsconfig.json new file mode 100644 index 00000000..2132fd18 --- /dev/null +++ b/examples/by-frameworks/next-intl/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "strict": false, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/src/frameworks/next-intl.ts b/src/frameworks/next-intl.ts new file mode 100644 index 00000000..efe88f83 --- /dev/null +++ b/src/frameworks/next-intl.ts @@ -0,0 +1,152 @@ +import { TextDocument } from 'vscode' +import { Framework, ScopeRange } from './base' +import { LanguageId } from '~/utils' +import { RewriteKeySource, RewriteKeyContext } from '~/core' + +class NextIntlFramework extends Framework { + id = 'next-intl' + display = 'next-intl' + namespaceDelimiter = '.' + + namespaceDelimiters = ['.'] + namespaceDelimitersRegex = /[\.]/g + + detection = { + packageJSON: [ + 'next-intl', + ], + } + + languageIds: LanguageId[] = [ + 'javascript', + 'typescript', + 'javascriptreact', + 'typescriptreact', + 'ejs', + ] + + // for visualize the regex, you can use https://regexper.com/ + usageMatchRegex = [ + '\\Wt\\({key})', + // - "[^\\w\\d]t\\(['\"`]({key})['\"`]" + + // todo: rich / raw / values + // '\\Wt(\.(rich|raw)?)?\\(\\s*[\'"`]({key})[\'"`]', + ] + + // derivedKeyRules = [ + // '{key}_plural', + // '{key}_0', + // '{key}_1', + // '{key}_2', + // '{key}_3', + // '{key}_4', + // '{key}_5', + // '{key}_6', + // '{key}_7', + // '{key}_8', + // '{key}_9', + // // support v4 format as well as v3 + // '{key}_zero', + // '{key}_one', + // '{key}_two', + // '{key}_few', + // '{key}_many', + // '{key}_other', + // ] + + refactorTemplates(keypath: string) { + return [ + `{t('${keypath}')}`, + `t('${keypath}')`, + ] + } + + // rewriteKeys(key: string, source: RewriteKeySource, context: RewriteKeyContext = {}) { + // const dottedKey = key.split(this.namespaceDelimitersRegex).join('.') + + // // when explicitly set the namespace, ignore current namespace scope + // if ( + // this.namespaceDelimiters.some(d => key.includes(d)) + // && context.namespace + // && dottedKey.startsWith(context.namespace.split(this.namespaceDelimitersRegex).join('.')) + // ) + // // +1 for the an extra `.` + // key = key.slice(context.namespace.length + 1) + + // // replace colons + // return dottedKey + // } + + // useTranslation + // https://react.i18next.com/latest/usetranslation-hook#loading-namespaces + // getScopeRange(document: TextDocument): ScopeRange[] | undefined { + // if (!this.languageIds.includes(document.languageId as any)) + // return + + // const ranges: ScopeRange[] = [] + // const text = document.getText() + + // // Add smaller local scope overrides first + // // Namespaced prefixed keys already handled by rewriteKeys + + // // t('foo', { ns: 'ns1' }) + // const regT = /\Wt\([^)]*?ns:\s*['"`](\w+)['"`]/g + + // for (const match of text.matchAll(regT)) { + // if (typeof match.index !== 'number') + // continue + + // if (match[1]) { + // ranges.push({ + // start: match.index, + // end: match.index + match[0].length, + // namespace: match[1], + // }) + // } + // } + + // // + // const regTrans = /\Wi18nKey=(?:(?!\/Trans>|\/>)[\S\s])*?ns=\s*['"`](.+?)['"`]/g + + // for (const match of text.matchAll(regTrans)) { + // if (typeof match.index !== 'number') + // continue + + // if (match[1]) { + // ranges.push({ + // start: match.index, + // end: match.index + match[0].length, + // namespace: match[1], + // }) + // } + // } + + // // Add first namespace as a global scope resetting on each occurrence + // // useTranslation(ns1) and useTranslation(['ns1', ...]) + // const regUse = /useTranslation\(\s*\[?\s*['"`](.*?)['"`]/g + // let prevGlobalScope = false + // for (const match of text.matchAll(regUse)) { + // if (typeof match.index !== 'number') + // continue + + // // end previous scope + // if (prevGlobalScope) + // ranges[ranges.length - 1].end = match.index + + // // start a new scope if namespace is provided + // if (match[1]) { + // prevGlobalScope = true + // ranges.push({ + // start: match.index, + // end: text.length, + // namespace: match[1], + // }) + // } + // } + + // return ranges + // } +} + +export default NextIntlFramework From d597f28858e32bb33c10b0e8e8fe541ec0e6c679 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Thu, 11 May 2023 15:34:29 +0200 Subject: [PATCH 2/8] Load framework --- src/frameworks/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts index f58395ca..17b9d40e 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -6,6 +6,7 @@ import FluentVueFramework from './fluent-vue' import ReactFramework from './react-intl' import I18nextFramework from './i18next' import ReactI18nextFramework from './react-i18next' +import NextIntlFramework from './next-intl' import VSCodeFramework from './vscode' import NgxTranslateFramework from './ngx-translate' import I18nTagFramework from './i18n-tag' @@ -47,6 +48,7 @@ export const frameworks: Framework[] = [ new EmberFramework(), new I18nextFramework(), new ReactI18nextFramework(), + new NextIntlFramework(), new I18nTagFramework(), new FluentVueFramework(), new PhpJoomlaFramework(), From 6b987da9cbe1c8f9147282f207d8e3583e6ed5b9 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Thu, 11 May 2023 15:37:54 +0200 Subject: [PATCH 3/8] Add VSCode setting --- examples/by-frameworks/next-intl/.vscode/settings.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 examples/by-frameworks/next-intl/.vscode/settings.json diff --git a/examples/by-frameworks/next-intl/.vscode/settings.json b/examples/by-frameworks/next-intl/.vscode/settings.json new file mode 100644 index 00000000..8b877ef5 --- /dev/null +++ b/examples/by-frameworks/next-intl/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "i18n-ally.localesPaths": "messages", + "i18n-ally.enabledFrameworks": [ + "next-intl" + ], + "i18n-ally.keystyle": "nested" +} From c530ed3da0fcff32a736f49a982bf306edaf7b6a Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Mon, 22 May 2023 11:43:41 +0200 Subject: [PATCH 4/8] Basic PoC --- .../by-frameworks/next-intl/messages/de.json | 4 + .../by-frameworks/next-intl/messages/en.json | 4 + .../next-intl/src/app/[locale]/page.tsx | 17 ++ package.json | 3 +- src/frameworks/next-intl.ts | 175 +++++++----------- 5 files changed, 91 insertions(+), 112 deletions(-) diff --git a/examples/by-frameworks/next-intl/messages/de.json b/examples/by-frameworks/next-intl/messages/de.json index 3341f48d..1e3138ca 100644 --- a/examples/by-frameworks/next-intl/messages/de.json +++ b/examples/by-frameworks/next-intl/messages/de.json @@ -1,5 +1,9 @@ { "IndexPage": { + "description": "Eine Beschreibung", "title": "next-intl Beispiel" + }, + "Test": { + "title": "Test" } } diff --git a/examples/by-frameworks/next-intl/messages/en.json b/examples/by-frameworks/next-intl/messages/en.json index af1a96ab..648f7ccb 100644 --- a/examples/by-frameworks/next-intl/messages/en.json +++ b/examples/by-frameworks/next-intl/messages/en.json @@ -1,5 +1,9 @@ { "IndexPage": { + "description": "Some description", "title": "next-intl example" + }, + "Test": { + "title": "Test" } } diff --git a/examples/by-frameworks/next-intl/src/app/[locale]/page.tsx b/examples/by-frameworks/next-intl/src/app/[locale]/page.tsx index 5ec52084..5f067b33 100644 --- a/examples/by-frameworks/next-intl/src/app/[locale]/page.tsx +++ b/examples/by-frameworks/next-intl/src/app/[locale]/page.tsx @@ -5,9 +5,26 @@ import { useTranslations } from 'next-intl' export default function IndexPage() { const t = useTranslations('IndexPage') + t('title') + t.rich('title') + t.raw('title') + return (

{t('title')}

+

{t('description')}

+ +
) } + +function Test1() { + const t = useTranslations('Test') + return <>{t('title')} +} + +function Test2() { + const t = useTranslations() + return <>{t('Test.title')} +} diff --git a/package.json b/package.json index aeb1e1bd..54322807 100644 --- a/package.json +++ b/package.json @@ -831,7 +831,8 @@ "lingui", "jekyll", "fluent-vue", - "fluent-vue-sfc" + "fluent-vue-sfc", + "next-intl" ] }, "description": "%config.enabled_frameworks%" diff --git a/src/frameworks/next-intl.ts b/src/frameworks/next-intl.ts index efe88f83..156849fb 100644 --- a/src/frameworks/next-intl.ts +++ b/src/frameworks/next-intl.ts @@ -25,128 +25,81 @@ class NextIntlFramework extends Framework { 'ejs', ] - // for visualize the regex, you can use https://regexper.com/ usageMatchRegex = [ - '\\Wt\\({key})', - // - "[^\\w\\d]t\\(['\"`]({key})['\"`]" + // Basic usage + '[^\\w\\d]t\\([\'"`]({key})[\'"`]', - // todo: rich / raw / values - // '\\Wt(\.(rich|raw)?)?\\(\\s*[\'"`]({key})[\'"`]', - ] + // Rich text + '[^\\w\\d]t\.rich\\([\'"`]({key})[\'"`]', - // derivedKeyRules = [ - // '{key}_plural', - // '{key}_0', - // '{key}_1', - // '{key}_2', - // '{key}_3', - // '{key}_4', - // '{key}_5', - // '{key}_6', - // '{key}_7', - // '{key}_8', - // '{key}_9', - // // support v4 format as well as v3 - // '{key}_zero', - // '{key}_one', - // '{key}_two', - // '{key}_few', - // '{key}_many', - // '{key}_other', - // ] + // Raw text + '[^\\w\\d]t\.raw\\([\'"`]({key})[\'"`]', + ] refactorTemplates(keypath: string) { + // Provide options where the last segment is the keypath. + // Ideally we'd automatically consider the namespace here. + const lastSegment = keypath.split('.').pop() + return [ `{t('${keypath}')}`, + `{t('${lastSegment}')}`, `t('${keypath}')`, + `t('${lastSegment}')`, ] } - // rewriteKeys(key: string, source: RewriteKeySource, context: RewriteKeyContext = {}) { - // const dottedKey = key.split(this.namespaceDelimitersRegex).join('.') - - // // when explicitly set the namespace, ignore current namespace scope - // if ( - // this.namespaceDelimiters.some(d => key.includes(d)) - // && context.namespace - // && dottedKey.startsWith(context.namespace.split(this.namespaceDelimitersRegex).join('.')) - // ) - // // +1 for the an extra `.` - // key = key.slice(context.namespace.length + 1) - - // // replace colons - // return dottedKey - // } - - // useTranslation - // https://react.i18next.com/latest/usetranslation-hook#loading-namespaces - // getScopeRange(document: TextDocument): ScopeRange[] | undefined { - // if (!this.languageIds.includes(document.languageId as any)) - // return - - // const ranges: ScopeRange[] = [] - // const text = document.getText() - - // // Add smaller local scope overrides first - // // Namespaced prefixed keys already handled by rewriteKeys - - // // t('foo', { ns: 'ns1' }) - // const regT = /\Wt\([^)]*?ns:\s*['"`](\w+)['"`]/g - - // for (const match of text.matchAll(regT)) { - // if (typeof match.index !== 'number') - // continue - - // if (match[1]) { - // ranges.push({ - // start: match.index, - // end: match.index + match[0].length, - // namespace: match[1], - // }) - // } - // } - - // // - // const regTrans = /\Wi18nKey=(?:(?!\/Trans>|\/>)[\S\s])*?ns=\s*['"`](.+?)['"`]/g - - // for (const match of text.matchAll(regTrans)) { - // if (typeof match.index !== 'number') - // continue - - // if (match[1]) { - // ranges.push({ - // start: match.index, - // end: match.index + match[0].length, - // namespace: match[1], - // }) - // } - // } - - // // Add first namespace as a global scope resetting on each occurrence - // // useTranslation(ns1) and useTranslation(['ns1', ...]) - // const regUse = /useTranslation\(\s*\[?\s*['"`](.*?)['"`]/g - // let prevGlobalScope = false - // for (const match of text.matchAll(regUse)) { - // if (typeof match.index !== 'number') - // continue - - // // end previous scope - // if (prevGlobalScope) - // ranges[ranges.length - 1].end = match.index - - // // start a new scope if namespace is provided - // if (match[1]) { - // prevGlobalScope = true - // ranges.push({ - // start: match.index, - // end: text.length, - // namespace: match[1], - // }) - // } - // } - - // return ranges - // } + rewriteKeys(key: string, source: RewriteKeySource, context: RewriteKeyContext = {}) { + const dottedKey = key.split(this.namespaceDelimitersRegex).join('.') + + // When the namespace is explicitly set, ignore the current namespace scope + if ( + this.namespaceDelimiters.some(delimiter => key.includes(delimiter)) + && context.namespace + && dottedKey.startsWith(context.namespace.split(this.namespaceDelimitersRegex).join('.')) + ) { + // +1 for the an extra `.` + key = key.slice(context.namespace.length + 1) + } + + return dottedKey + } + + getScopeRange(document: TextDocument): ScopeRange[] | undefined { + if (!this.languageIds.includes(document.languageId as any)) + return + + const ranges: ScopeRange[] = [] + const text = document.getText() + + // Find matches of `useTranslations`, later occurences will override + // previous ones (this allows for multiple components with different + // namespaces in the same file). + const regex = /useTranslations\(\s*(['"`](.*?)['"`])?/g + let prevGlobalScope = false + for (const match of text.matchAll(regex)) { + if (typeof match.index !== 'number') + continue + + const namespace = match[2] + + // End previous scope + if (prevGlobalScope) + ranges[ranges.length - 1].end = match.index + + // Start a new scope if a namespace is provided + if (namespace) { + prevGlobalScope = true + ranges.push({ + start: match.index, + end: text.length, + namespace, + }) + } + } + + return ranges + } } export default NextIntlFramework From 84da18168cefdadfaf9a1681a12dc324a479a2f1 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Mon, 22 May 2023 11:45:17 +0200 Subject: [PATCH 5/8] Cleanup --- examples/by-frameworks/next-intl/.vscode/settings.json | 4 +--- examples/by-frameworks/next-intl/src/app/[locale]/page.tsx | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/by-frameworks/next-intl/.vscode/settings.json b/examples/by-frameworks/next-intl/.vscode/settings.json index 8b877ef5..a936f677 100644 --- a/examples/by-frameworks/next-intl/.vscode/settings.json +++ b/examples/by-frameworks/next-intl/.vscode/settings.json @@ -1,7 +1,5 @@ { "i18n-ally.localesPaths": "messages", - "i18n-ally.enabledFrameworks": [ - "next-intl" - ], + "i18n-ally.enabledFrameworks": ["next-intl"], "i18n-ally.keystyle": "nested" } diff --git a/examples/by-frameworks/next-intl/src/app/[locale]/page.tsx b/examples/by-frameworks/next-intl/src/app/[locale]/page.tsx index 5f067b33..2b76a084 100644 --- a/examples/by-frameworks/next-intl/src/app/[locale]/page.tsx +++ b/examples/by-frameworks/next-intl/src/app/[locale]/page.tsx @@ -21,10 +21,10 @@ export default function IndexPage() { function Test1() { const t = useTranslations('Test') - return <>{t('title')} + return

{t('title')}

} function Test2() { const t = useTranslations() - return <>{t('Test.title')} + return

{t('Test.title')}

} From c1bbb77fbcfc0c7cbd2ee25c83c74f5aace652d2 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Mon, 22 May 2023 11:57:27 +0200 Subject: [PATCH 6/8] Add preferred keystyle --- src/frameworks/next-intl.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/frameworks/next-intl.ts b/src/frameworks/next-intl.ts index 156849fb..7a621d3d 100644 --- a/src/frameworks/next-intl.ts +++ b/src/frameworks/next-intl.ts @@ -1,12 +1,13 @@ import { TextDocument } from 'vscode' import { Framework, ScopeRange } from './base' import { LanguageId } from '~/utils' -import { RewriteKeySource, RewriteKeyContext } from '~/core' +import { RewriteKeySource, RewriteKeyContext, KeyStyle } from '~/core' class NextIntlFramework extends Framework { id = 'next-intl' display = 'next-intl' namespaceDelimiter = '.' + perferredKeystyle?: KeyStyle = 'nested' namespaceDelimiters = ['.'] namespaceDelimitersRegex = /[\.]/g From bffb52573479744c5166c2c6b42d2151a0cb7923 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Mon, 22 May 2023 12:08:11 +0200 Subject: [PATCH 7/8] Clean up --- examples/by-frameworks/next-intl/.vscode/settings.json | 3 +-- src/frameworks/next-intl.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/by-frameworks/next-intl/.vscode/settings.json b/examples/by-frameworks/next-intl/.vscode/settings.json index a936f677..7bd4e6fd 100644 --- a/examples/by-frameworks/next-intl/.vscode/settings.json +++ b/examples/by-frameworks/next-intl/.vscode/settings.json @@ -1,5 +1,4 @@ { "i18n-ally.localesPaths": "messages", - "i18n-ally.enabledFrameworks": ["next-intl"], - "i18n-ally.keystyle": "nested" + "i18n-ally.enabledFrameworks": ["next-intl"] } diff --git a/src/frameworks/next-intl.ts b/src/frameworks/next-intl.ts index 7a621d3d..4c3a61b9 100644 --- a/src/frameworks/next-intl.ts +++ b/src/frameworks/next-intl.ts @@ -43,10 +43,10 @@ class NextIntlFramework extends Framework { const lastSegment = keypath.split('.').pop() return [ - `{t('${keypath}')}`, `{t('${lastSegment}')}`, - `t('${keypath}')`, + `{t('${keypath}')}`, `t('${lastSegment}')`, + `t('${keypath}')`, ] } From 767c70e82622049c38854193e99d48d929842d37 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Tue, 23 May 2023 15:52:44 +0200 Subject: [PATCH 8/8] All permutations for keypaths --- src/frameworks/next-intl.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/frameworks/next-intl.ts b/src/frameworks/next-intl.ts index 4c3a61b9..879bb39e 100644 --- a/src/frameworks/next-intl.ts +++ b/src/frameworks/next-intl.ts @@ -38,15 +38,21 @@ class NextIntlFramework extends Framework { ] refactorTemplates(keypath: string) { - // Provide options where the last segment is the keypath. - // Ideally we'd automatically consider the namespace here. - const lastSegment = keypath.split('.').pop() - + // Ideally we'd automatically consider the namespace here. Since this + // doesn't seem to be possible though, we'll generate all permutations for + // the `keypath`. E.g. `one.two.three` will generate `three`, `two.three`, + // `one.two.three`. + + const keypaths = keypath.split('.').map((cur, index, parts) => { + return parts.slice(parts.length - index - 1).join('.') + }) return [ - `{t('${lastSegment}')}`, - `{t('${keypath}')}`, - `t('${lastSegment}')`, - `t('${keypath}')`, + ...keypaths.map(cur => + `{t('${cur}')}`, + ), + ...keypaths.map(cur => + `t('${cur}')`, + ), ] }