Skip to content

Commit

Permalink
Merge pull request #3039 from opral/lorissigrist/parjs-179-paraglide-…
Browse files Browse the repository at this point in the history
…next-support-statically-typed-links

`Paraglide-Next` support `typedRoutes`
  • Loading branch information
LorisSigrist authored Aug 2, 2024
2 parents a8cba7a + f2c4b4d commit 94c2298
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 157 deletions.
19 changes: 19 additions & 0 deletions .changeset/dry-jobs-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@inlang/paraglide-next": minor
---

[Typed routes](https://nextjs.org/docs/app/api-reference/next-config-js/typedRoutes) are now supported. This adds typesafety to functions that expect an internal link.

- `<Link>`s now have typesafe `href` attributes
- `useRouter` now has has typesafe path arguments

```ts
import { paraglide } from "@inlang/paraglide-next/plugin"

export default paraglide({
experimental: {
typedRoutes: true, // enable this
},
paraglide: { ... },
})
```
Original file line number Diff line number Diff line change
Expand Up @@ -34,50 +34,49 @@ const strategy = DetectionStrategy<AvailableLanguageTag>()

> Manual Language switches only work if JS is enabled when using this strategy.

### Custom Strategy

The beatuy of the `RoutingStrategy` interface is that you can easily create your own routing strategy. All you need to do is implement the following functions:

```ts
const MyStrategy: RoutingStrategy<AvailableLanguageTag> = {
/**
* This function is called inside the middleware to determine the language for the current request.
*
* It's also OK to say you don't know & return undefined. In that case the Language Cookie will be used,
* This function is called inside the middleware to determine the language for the current request.
*
* It's also OK to say you don't know & return undefined. In that case the Language Cookie will be used,
* or Language negotiation if no cookie is present.
*/
resolveLocale(request: NextRequest) : AvailableLanguageTag | undefined

/**
* Returns the canonical pathname based on the localised pathname and it's language.
*
* The canonical pathname is the pathname you would need to get to the page you want
*
* The canonical pathname is the pathname you would need to get to the page you want
* in the `app/` directory if there weren't any i18n routing.
*
*
* @example /de/ueber-uns + de -> /about
*
*
*/
getCanonicalPath(
localisedPath: `/${string}`,
localisedPath: `/${string}`,
locale: AvailableLanguageTag
): `/${string}`


/**
* Returns the localized URL that can be used to navigate to the given path in the given language.
* It's a URL & not just a pathname so you can add query params and use other domains.
*
* It's a URL & not just a pathname so you can add query params and use other domains.
*
* For some strategies you might need to return different URLs based on if it's a language switch or not.
*
*
* @example /about + de -> /de/ueber-uns
*/
getLocalisedUrl(
canonicalPath: `/${string}`,
targetLocale: AvailableLanguageTag,
canonicalPath: `/${string}`,
targetLocale: AvailableLanguageTag,
isLanugageSwitch: boolean
) : import("url").UrlObject
}
```

To get some inspiration you might want to read the source-code of the built-in strategies.
To get some inspiration you might want to read the source-code of the built-in strategies.
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ export default paraglide({

basePath: "/base",
output: "standalone",
experimental: {
typedRoutes: true,
},
})
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Link } from "@/lib/i18n"
import * as m from "@/paraglide/messages.js"

export default function Home() {
return (
<>
<Link href="/about">About</Link>
<h1>{m.paraglide_and_next_app_router()}</h1>
<p>{m.this_app_was_localised_with_paraglide()}</p>
<p>{m.switch_languages_in_top_right()}</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client"
import { availableLanguageTags, AvailableLanguageTag, languageTag } from "@/paraglide/runtime"
import { usePathname, useRouter } from "@/lib/i18n"
import { Route } from "next"

export function SelectLanguage() {
const pathname = usePathname()
const pathname = usePathname() as Route
const router = useRouter()

const labels: Record<AvailableLanguageTag, string> = {
Expand Down
1 change: 1 addition & 0 deletions inlang/source-code/paraglide/paraglide-next/plugins.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ if (typeof version !== "string") {
export const plugins = [
typescript({
tsconfig: "./tsconfig.json",
exclude: ["**/node_modules/**/*"],
}),
cjs(),
resolve({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,38 @@ import {
} from "$paraglide/runtime.js"
import { addBasePath, basePath } from "../utils/basePath"
import NextLink from "next/link"
import React from "react"
import { RoutingStrategy } from "../routing-strategy/interface"
import React, { ComponentProps } from "react"
import { createLocaliseHref } from "../localiseHref"
import { serializeCookie } from "../utils/cookie"
import { LANG_COOKIE } from "../constants"
import { rsc } from "rsc-env"
import { DEV } from "../env"
import type { RoutingStrategy } from "../routing-strategy/interface"

type LocalisedLink<T extends string> = (
props: Omit<Parameters<typeof import("next/link").default>[0], "locale"> & { locale?: T }
export type LocalizedLink<T extends string> = (
props: LocalizedLinkProps<T>
) => ReturnType<typeof import("next/link").default>

type LocalizedLinkProps<T extends string> = Omit<
ComponentProps<typeof import("next/link").default>,
"locale"
> & { locale?: T }

/**
* Creates a link component that localises the href based on the current language.
* @param languageTag A function that returns the current language tag.
*/
export function createLink<T extends string>(strategy: RoutingStrategy<T>): LocalisedLink<T> {
export function createLink<T extends string>(strategy: RoutingStrategy<T>) {
const localiseHref = createLocaliseHref(strategy)

return React.forwardRef<
HTMLAnchorElement,
Omit<Parameters<typeof NextLink>[0], "locale"> & { locale?: T }
>((props, ref): ReturnType<typeof NextLink> => {
const Link: LocalizedLink<T> = React.forwardRef((props, ref) => {
const currentLanguageTag = languageTag() as T

if (DEV && props.locale && !isAvailableLanguageTag(props.locale)) {
const disjunctionFormatter = new Intl.ListFormat("en", { style: "long", type: "disjunction" })
const disjunctionFormatter = new Intl.ListFormat("en", {
style: "long",
type: "disjunction",
})
const availableLanguageTagsString = disjunctionFormatter.format(
availableLanguageTags.map((tag) => `"${tag}"`)
)
Expand Down Expand Up @@ -69,4 +74,6 @@ export function createLink<T extends string>(strategy: RoutingStrategy<T>): Loca
/>
)
})

return Link
}
Loading

0 comments on commit 94c2298

Please sign in to comment.