Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add next-intl framework #934

Merged
merged 9 commits into from
Aug 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/by-frameworks/next-intl/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"rules": {
"import/named": "off",
"react/react-in-jsx-scope": "off"
}
}
4 changes: 4 additions & 0 deletions examples/by-frameworks/next-intl/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/node_modules
/.next/
.DS_Store
tsconfig.tsbuildinfo
4 changes: 4 additions & 0 deletions examples/by-frameworks/next-intl/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"i18n-ally.localesPaths": "messages",
"i18n-ally.enabledFrameworks": ["next-intl"]
}
3 changes: 3 additions & 0 deletions examples/by-frameworks/next-intl/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Use type safe message keys with `next-intl`
type Messages = typeof import('./messages/en.json')
declare interface IntlMessages extends Messages {}
9 changes: 9 additions & 0 deletions examples/by-frameworks/next-intl/messages/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"IndexPage": {
"description": "Eine Beschreibung",
"title": "next-intl Beispiel"
},
"Test": {
"title": "Test"
}
}
9 changes: 9 additions & 0 deletions examples/by-frameworks/next-intl/messages/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"IndexPage": {
"description": "Some description",
"title": "next-intl example"
},
"Test": {
"title": "Test"
}
}
5 changes: 5 additions & 0 deletions examples/by-frameworks/next-intl/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
22 changes: 22 additions & 0 deletions examples/by-frameworks/next-intl/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Binary file not shown.
34 changes: 34 additions & 0 deletions examples/by-frameworks/next-intl/src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { notFound } from 'next/navigation'
import { NextIntlClientProvider } from 'next-intl'
import { ReactNode } from 'react'

type Props = {
children: ReactNode
params: {locale: string}
}

export default async function LocaleLayout({
children,
params: { locale },
}: Props) {
let messages
try {
messages = (await import(`../../../messages/${locale}.json`)).default
}
catch (error) {
notFound()
}

return (
<html lang={locale}>
<head>
<title>next-intl</title>
</head>
<body>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
)
}
30 changes: 30 additions & 0 deletions examples/by-frameworks/next-intl/src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client'

import { useTranslations } from 'next-intl'

export default function IndexPage() {
const t = useTranslations('IndexPage')

t('title')
t.rich('title')
t.raw('title')

return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
<Test1 />
<Test2 />
</div>
)
}

function Test1() {
const t = useTranslations('Test')
return <p>{t('title')}</p>
}

function Test2() {
const t = useTranslations()
return <p>{t('Test.title')}</p>
}
11 changes: 11 additions & 0 deletions examples/by-frameworks/next-intl/src/middleware.tsx
Original file line number Diff line number Diff line change
@@ -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|.*\\..*).*)'],
}
37 changes: 37 additions & 0 deletions examples/by-frameworks/next-intl/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,8 @@
"lingui",
"jekyll",
"fluent-vue",
"fluent-vue-sfc"
"fluent-vue-sfc",
"next-intl"
]
},
"description": "%config.enabled_frameworks%"
Expand Down
2 changes: 2 additions & 0 deletions src/frameworks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ShopifyI18nextFramework from './i18next-shopify'
import VSCodeFramework from './vscode'
import NgxTranslateFramework from './ngx-translate'
Expand Down Expand Up @@ -49,6 +50,7 @@ export const frameworks: Framework[] = [
new I18nextFramework(),
new ShopifyI18nextFramework(),
new ReactI18nextFramework(),
new NextIntlFramework(),
new I18nTagFramework(),
new FluentVueFramework(),
new PhpJoomlaFramework(),
Expand Down
112 changes: 112 additions & 0 deletions src/frameworks/next-intl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { TextDocument } from 'vscode'
import { Framework, ScopeRange } from './base'
import { LanguageId } from '~/utils'
import { RewriteKeySource, RewriteKeyContext, KeyStyle } from '~/core'

class NextIntlFramework extends Framework {
id = 'next-intl'
display = 'next-intl'
namespaceDelimiter = '.'
perferredKeystyle?: KeyStyle = 'nested'

namespaceDelimiters = ['.']
namespaceDelimitersRegex = /[\.]/g

detection = {
packageJSON: [
'next-intl',
],
}

languageIds: LanguageId[] = [
'javascript',
'typescript',
'javascriptreact',
'typescriptreact',
'ejs',
]

usageMatchRegex = [
// Basic usage
'[^\\w\\d]t\\([\'"`]({key})[\'"`]',

// Rich text
'[^\\w\\d]t\.rich\\([\'"`]({key})[\'"`]',

// Raw text
'[^\\w\\d]t\.raw\\([\'"`]({key})[\'"`]',
]

refactorTemplates(keypath: string) {
// 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 [
...keypaths.map(cur =>
`{t('${cur}')}`,
),
...keypaths.map(cur =>
`t('${cur}')`,
),
]
Copy link
Contributor Author

@amannn amannn May 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to consider an existing namespace in this function, to automatically remove the namespace from the suggestion?

Also if the component uses a given namespace, it would be great if that would show up as a default when entering a key. Is this possible?

If not, I guess this is good enough!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespaces were hard to implement with the current architecture. Let's get the framework running as is now

}

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