Skip to content

Commit

Permalink
Merge pull request #2295 from opral/next-adapter-improve-api
Browse files Browse the repository at this point in the history
Next Adapter API Improvements
  • Loading branch information
LorisSigrist authored Feb 27, 2024
2 parents 294bfa4 + 42a6913 commit 23a5e50
Show file tree
Hide file tree
Showing 34 changed files with 991 additions and 91 deletions.
5 changes: 5 additions & 0 deletions .changeset/khaki-wolves-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inlang/paraglide-js-adapter-next": patch
---

fix: `middleware` no longer sets `Link` header on excluded pages
8 changes: 8 additions & 0 deletions .changeset/perfect-cameras-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@inlang/paraglide-js-adapter-next": patch
---

Simplify API of the `<ParaglideJS>` component used in the pages router.
You no longer need to pass the `runtime` and `router.locale` as props to the `<ParaglideJS>` component. Instead, you can just use the component without any props. It will automatically use the runtime and language tag from the context.

This change was enabled by the last-minute plugin changes that made it valuale to use in the pages router.
5 changes: 5 additions & 0 deletions .changeset/proud-icons-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inlang/paraglide-js-adapter-next": minor
---

feat: Support translated Pathnames
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,10 @@ Last, let's add the `<ParaglideJS>` component to your `_app.js` file. This will
// _app.js
import type { AppProps } from "next/app"
import { ParaglideJS } from "@inlang/paraglide-js-adapter-next/pages"
//This may not exist until you add your first message & start your dev server
import * as runtime from "@/paraglide/runtime.js"

export default function App({ Component, pageProps, router }: AppProps) {
export default function App({ Component, pageProps }: AppProps) {
return (
<ParaglideJS runtime={runtime} language={router.locale}>
<ParaglideJS>
<Component {...pageProps} />
</ParaglideJS>
)
Expand Down
77 changes: 73 additions & 4 deletions inlang/source-code/paraglide/paraglide-js-adapter-next/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,76 @@ function Component() {
}
```

#### Excluding certain routes from i18n

You can exclude certain routes from i18n using the `exclude` option on `createI18n`. You can either pass a string or a regex.

```ts
export const { ... } =
createI18n<AvailableLanguageTag>({
//array of routes to exclude
exclude: [
/^\/api(\/.*)?$/ //excludes all routes starting with /api
"/admin" //excludes /admin, but not /admin/anything - globs are not supported
],
})
```
Excluded routes won't be prefixed with the language tag & the middleware will not add `Link` headers to them.
> Tip: LLMs are really good at writing regexes.
#### Changing the default language
Usually the default language is the `sourceLanguageTag` you defined in your `project.inlang/settings.json`. If you want to change it, you can use the `defaultLanguage` option on `createI18n`.
```ts
export const { ... } =
createI18n<AvailableLanguageTag>({
defaultLanguage: "de"
})
```
This will change which langauge doesn't get a prefix in the URL.
#### Translated Pathnames
You can translate pathnames by adding a `pathname` option to `createI18n`. This allows you to define a different pathname for each language.
```ts
export const { ... } =
createI18n<AvailableLanguageTag>({
pathname: {
"/about": {
de: "/ueber-uns",
en: "/about"
}
}
})
```
An even better option is to use a message to manage the pathnames. This way you can change the pathnames without changing the code.
```json
// messages/en.json
{
"about_pathname": "/about"
}
// messages/de.json
{
"about_pathname": "/ueber-uns"
}
```
```ts
export const { ... } =
createI18n<AvailableLanguageTag>({
pathname: {
"/about": m.about_pathname //pass as reference
}
})
```
## (legacy) Setup With the Pages Router
The Pages router already comes with i18n support out of the box. You can read more about it in the[NextJS Pages router documentation](https://nextjs.org/docs/advanced-features/i18n-routing). Thanks to this, Paraglide doesn't need to provide it's own routing. All the Adapter does in the Pages router is react to the language change.
Expand All @@ -201,15 +271,14 @@ module.exports = {
This will have the effect that NextJS will automatically prefix all routes with the locale. For example, the route `/about` will become `/en/about` for the English locale and `/de/about` for the German locale. The only language that won't be prefixed is the default locale.
Now all that's left is to tell paraglide which language to use. To do that, wrap your `_app.js` file with the `ParaglideJS` component, pass it the current language and the paraglide runtime module.
Now all that's left is to tell paraglide which language to use. To do that, wrap your `_app.js` file with the `ParaglideJS` component.
```jsx
import { ParaglideJS } from "@inlang/paraglide-js-adapter-next/pages"
import * as runtime from "@/paraglide/runtime.js"

export default function App({ Component, pageProps, router }: AppProps) {
export default function App({ Component, pageProps }: AppProps) {
return (
<ParaglideJS runtime={runtime} language={router.locale}>
<ParaglideJS>
<Component {...pageProps} />
</ParaglideJS>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"currentLanguageTag": "Dä aktuell sprachtag isch \"{languageTag}\".",
"greeting": "Hoi {name}! Du häsch {count} Nachrichte.",
"about": "Über üs",
"on_the_client": "Ich bi uf em Client."
"on_the_client": "Ich bi uf em Client.",
"about_path": "/ueber-uns"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"currentLanguageTag": "Der aktuelle Sprachtag ist \"{languageTag}\".",
"greeting": "Hallo {name}! Du hast {count} Nachrichten.",
"about": "Über uns",
"on_the_client": "Ich bin auf dem Client."
"on_the_client": "Ich bin auf dem Client.",
"about_path": "/ueber-uns"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"currentLanguageTag": "The current language tag is \"{languageTag}\".",
"greeting": "Welcome {name}! You have {count} messages.",
"about": "About",
"on_the_client": "I'm on the client!"
"on_the_client": "I'm on the client!",
"about_path": "/about"
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { AvailableLanguageTag } from "@/paraglide/runtime"
import { createI18n } from "@inlang/paraglide-js-adapter-next"
import * as m from "@/paraglide/messages"

export const { Link, middleware, useRouter, usePathname, redirect, permanentRedirect } =
createI18n<AvailableLanguageTag>({
exclude: ["/not-translated"],
pathnames: {
"/about": m.about_path,
"/form": {
en: "/form",
de: "/formular",
"de-CH": "/formular",
},
},
})
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { AppProps } from "next/app"
import { ParaglideJS } from "@inlang/paraglide-js-adapter-next/pages"
import * as runtime from "@/paraglide/runtime.js"

export default function App({ Component, pageProps, router }: AppProps) {
export default function App({ Component, pageProps }: AppProps) {
return (
<ParaglideJS runtime={runtime} language={router.locale}>
<ParaglideJS>
<Component {...pageProps} />
</ParaglideJS>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe("<Link>", () => {
prefixStrategy({
availableLanguageTags,
defaultLanguage: sourceLanguageTag,
pathnames: {},
exclude: () => false,
})
)
Expand All @@ -44,6 +45,7 @@ describe("<Link>", () => {
prefixStrategy({
availableLanguageTags,
defaultLanguage: sourceLanguageTag,
pathnames: {},
exclude: () => false,
})
)
Expand All @@ -60,4 +62,59 @@ describe("<Link>", () => {
render(<Link href="/about" data-testid="english-link" />)
expect(screen.getByTestId("english-link").getAttribute("href")).toEqual("/about")
})

it("renders a link with searchParams", () => {
const Link = createLink(
() => languageTag(), //For some reason we can't pass languageTag as a reference directly
prefixStrategy({
availableLanguageTags,
defaultLanguage: sourceLanguageTag,
pathnames: {},
exclude: () => false,
})
)

setLanguageTag("de")
render(
<Link href="/about?param=1" data-testid="german-link">
{languageTag()}
</Link>
)
expect(screen.getByTestId("german-link").getAttribute("href")).toEqual("/de/about?param=1")

setLanguageTag("en")
render(<Link href="/about?param=1" data-testid="english-link" />)
expect(screen.getByTestId("english-link").getAttribute("href")).toEqual("/about?param=1")
})

it("localises hrefs with path translations and searchParams", () => {
const Link = createLink(
() => languageTag(), //For some reason we can't pass languageTag as a reference directly
prefixStrategy({
availableLanguageTags,
defaultLanguage: sourceLanguageTag,
pathnames: {
"/about": {
de: "/ueber-uns",
en: "/about",
},
},
exclude: () => false,
})
)

setLanguageTag("de")
render(
<Link href="/about?params=1" data-testid="german-link">
{languageTag()}
</Link>
)
expect(screen.getByTestId("german-link").getAttribute("href")).toEqual("/de/ueber-uns?params=1")

setLanguageTag("en")
render(<Link href="/about?param=1" data-testid="english-link" />)
expect(screen.getByTestId("english-link").getAttribute("href")).toEqual("/about?param=1")
})


})

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { ExcludeConfig } from "./exclude"
import { UserPathTranslations } from "./pathnames/types"

export type I18nOptions<T extends string> = {
/**
* A list of patterns that should not be localized.
*
* @example
* ```ts
* exclude: [/^\/api\//] // Exclude `/api/*` from localization
* ```
*
* @default []
*/
exclude?: ExcludeConfig

/**
* The translations for pathnames.
* They should **not** include the base path or the language tag.
*
* You can include parameters in the pathnames by using square brackets.
* If you are using a parameter, you must include it in all translations.
*
* @example
* ```ts
* pathnames: {
* "/about": {
* de: "/ueber-uns",
* en: "/about",
* fr: "/a-propos",
* },
* "/users/[slug]": {
* en: "/users/[slug]",
* // parameters don't have to be their own path-segment
* de: "/benutzer-[slug]",
* // parameters don't have to be in the same position
* fr: "/[slug]/utilisateurs",
* },
* //you can also use messages for pathnames (pass as reference)
* "/admin": m.admin_path
* }
* ```
*/
pathnames?: UserPathTranslations<T>

/**
* The default language to use when no language is set.
*
* @default sourceLanguageTag
*/
defaultLanguage?: T
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import { createNavigation, createRedirects } from "./navigation"
import { createExclude } from "./exclude"
import { createMiddleware } from "./middleware"
import { I18nOptions } from "./config"
import { resolvePathTranslations } from "./pathnames/resolvePathTranslations"

export function createI18n<T extends string = string>(options: I18nOptions<T> = {}) {
const exclude = createExclude(options.exclude ?? [])
const pathnames = resolvePathTranslations(options.pathnames ?? {}, availableLanguageTags as T[])

const strategy = prefixStrategy<T>({
availableLanguageTags,
availableLanguageTags: availableLanguageTags as readonly T[],
pathnames,
defaultLanguage: options.defaultLanguage ?? (sourceLanguageTag as T),
exclude,
})
Expand All @@ -24,7 +27,7 @@ export function createI18n<T extends string = string>(options: I18nOptions<T> =
const Link = createLink<T>(getLanguage, strategy)
const { usePathname, useRouter } = createNavigation<T>(getLanguage, strategy)
const { redirect, permanentRedirect } = createRedirects<T>(getLanguage, strategy)
const middleware = createMiddleware<T>(strategy)
const middleware = createMiddleware<T>(exclude, strategy)

return {
Link,
Expand Down
Loading

0 comments on commit 23a5e50

Please sign in to comment.