Skip to content

Commit

Permalink
feat: add new argument to useActualLocale to simplify integration a…
Browse files Browse the repository at this point in the history
…nd a new `resolveLocale` API
  • Loading branch information
nbouvrette committed Aug 14, 2022
1 parent d3d1063 commit 93b1c8e
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 134 deletions.
59 changes: 11 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ npm install next-multilingual
- A powerful `useMessages` hook that supports [ICU MessageFormat](https://unicode-org.github.io/icu/userguide/format_parse/messages/) and JSX injection out of the box.
- The ability to use localized URLs (e.g., `/en-us/contact-us` for U.S. English and `/fr-ca/nous-joindre` for Canadian French).
- All page URLs will use locale prefixes (related to this [discussion](https://github.com/vercel/next.js/discussions/18419)).
- Can easily be configured with smart language detection that dynamically renders the homepage, without using redirections.
- Can easily be configured with smart locale detection that dynamically renders the homepage, without using redirections.
- Automatically generate canonical and alternate links optimized for SEO.

## Before we start 💎
Expand Down Expand Up @@ -153,25 +153,18 @@ We need to create a [custom `App`](https://nextjs.org/docs/advanced-features/cus

```ts
import type { AppProps } from 'next/app'

import { setCookieLocale, useActualLocale } from 'next-multilingual'
import { useRouter } from 'next/router'
import { useActualLocale } from 'next-multilingual'

export default function MyApp({ Component, pageProps }: AppProps): JSX.Element {
// Forces Next.js to use the actual (proper) locale.
useActualLocale()
// The next two lines are optional, to enable smart locale detection on the homepage.
const router = useRouter()
// Persist locale on page load (only used on `/` when using smart locale detection).
setCookieLocale(router.locale)
useActualLocale() // Forces Next.js to use the actual (proper) locale.
return <Component {...pageProps} />
}
```

This basically does two things, as mentioned in the comments:

1. Inject the actual locale in Next.js' router since we need to use a "fake default locale".
2. (optional) Persist the actual locale in the cookie so we can reuse it when hitting the homepage without a locale (`/`).
2. By default, persist the actual locale in the cookie so we can reuse it when hitting the homepage without a locale (`/`). If you do not want to use `next-multilingual`'s locale detection you can use `useActualLocale(false)` instead.

### Create a custom `Document` (`_document.tsx`)

Expand Down Expand Up @@ -224,35 +217,23 @@ Now that everything has been configured, we can focus on using `next-multilingua

### Creating the homepage

> ⚠️ Note that while we recommend using smart language detection to dynamically render the homepage, this is completely optional. By using advanced configuration with `localeDetection: true`, you will restore the default Next.js behavior without the need of using `getServerSideProps`.
> ⚠️ Note that while we recommend using smart locale detection to dynamically render the homepage, this is completely optional. By using advanced configuration with `localeDetection: true`, you will restore the default Next.js behavior without the need of using `getServerSideProps`.

The homepage is a bit more complex than other pages, because we need to implement dynamic language detection (and display) for the following reason:
The homepage is a bit more complex than other pages, because we need to implement dynamic locale detection (and display) for the following reason:

- Redirecting on `/` can have a negative impact on SEO and is not the best user experience.
- `next-multilingual` comes with a `getPreferredLocale` API that offers smarter auto-detection than the default Next.js implementation.

You can find a full implementation in the [example](./example/pages/index.tsx), but here is a stripped down version:

```tsx
import {
getActualDefaultLocale,
getActualLocale,
getActualLocales,
getCookieLocale,
getPreferredLocale,
ResolvedLocaleServerSideProps,
useResolvedLocale,
} from 'next-multilingual'
import type { GetServerSideProps, NextPage } from 'next'
import { ResolvedLocaleServerSideProps, resolveLocale, useResolvedLocale } from 'next-multilingual'
import { getTitle, useMessages } from 'next-multilingual/messages'
import { useRouter } from 'next/router'
import Layout from '@/layout'
import type { GetServerSideProps, NextPage } from 'next'
const Home: NextPage<ResolvedLocaleServerSideProps> = ({ resolvedLocale }) => {
const router = useRouter()
// Force Next.js to use a locale that was resolved dynamically on the homepage.
useResolvedLocale(resolvedLocale)
Expand All @@ -271,27 +252,9 @@ export default Home
export const getServerSideProps: GetServerSideProps<ResolvedLocaleServerSideProps> = async (
nextPageContext
) => {
const { req, locale, locales, defaultLocale } = nextPageContext
const actualLocales = getActualLocales(locales, defaultLocale)
const actualDefaultLocale = getActualDefaultLocale(locales, defaultLocale)
const cookieLocale = getCookieLocale(nextPageContext, actualLocales)
let resolvedLocale = getActualLocale(locale, defaultLocale, locales)
// When Next.js tries to use the default locale, try to find a better one.
if (locale === defaultLocale) {
resolvedLocale = cookieLocale
? cookieLocale
: getPreferredLocale(
req.headers['accept-language'],
actualLocales,
actualDefaultLocale
).toLowerCase()
}
return {
props: {
resolvedLocale,
resolvedLocale: resolveLocale(nextPageContext),
},
}
}
Expand All @@ -306,8 +269,6 @@ In a nutshell, this is what is happening:
- The client overwrites the value on the router to make this dynamic across the application.
- The value is also stored back in the cookie to keep the selection consistent

You might have noticed the `getActual*` APIs. Theses API is part of a [set of "utility" APIs](./src/index.ts) that helps abstract some of the complexity that we configured in Next.js. These APIs are very useful, since we can no longer rely on the locales provided by Next.js. The main reason for this is that we set the default Next.js locale to `mul` (for multilingual) to allow us to do the dynamic detection on the homepage. These APIs are simple and more details are available in your IDE (JSDoc).

### Creating messages

Every time that you create a `tsx`, `ts`, `jsx` or `js` (compilable) file and that you need localized messages, you can simply create a message file in your supported locales that will only be usable by these files. Just like CSS modules, the idea is that you can have message files associated with another file's local scope. This has the benefit of making messages more modular and also avoids sharing messages across different contexts (more details in the [design decisions document](./docs/design-decisions.md) on why this is bad).
Expand Down Expand Up @@ -1061,6 +1022,8 @@ This allows a seamless experience across localized URLs when using simple parame

We also provided a [fully working example](./example/pages/dynamic-route-test/[id].tsx) for those who want to see it in action.

You might also have noticed the `getActual*` APIs. Theses API is part of a [set of "utility" APIs](./src/index.ts) that helps abstract some of the complexity that we configured in Next.js. These APIs are very useful, since we can no longer rely on the locales provided by Next.js. The main reason for this is that we set the default Next.js locale to `mul` (for multilingual) to allow us to do the dynamic detection on the homepage. These APIs are simple and more details are available in your IDE (JSDoc).

## Translation Process 🈺

Our ideal translation process is one where you send the modified files to your localization vendor (while working in a branch), and get back the translated files, with the correct locale in the filenames. Once you get the files back you basically submit them back in your branch which means localization becomes an integral part of the development process. Basically, the idea is:
Expand Down
10 changes: 2 additions & 8 deletions example/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import type { AppProps } from 'next/app'
import './_app.css'

import { setCookieLocale, useActualLocale } from 'next-multilingual'
import { useRouter } from 'next/router'
import { useActualLocale } from 'next-multilingual'

export default function MyApp({ Component, pageProps }: AppProps): JSX.Element {
// Forces Next.js to use the actual (proper) locale.
useActualLocale()
// The next two lines are optional, to enable smart locale detection on the homepage.
const router = useRouter()
// Persist locale on page load (only used on `/` when using smart locale detection).
setCookieLocale(router.locale)
useActualLocale() // Forces Next.js to use the actual (proper) locale.
return <Component {...pageProps} />
}
30 changes: 30 additions & 0 deletions example/pages/index copy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Layout from '@/layout'
import type { GetServerSideProps, NextPage } from 'next'
import { ResolvedLocaleServerSideProps, resolveLocale, useResolvedLocale } from 'next-multilingual'
import { getTitle, useMessages } from 'next-multilingual/messages'

const Home: NextPage<ResolvedLocaleServerSideProps> = ({ resolvedLocale }) => {
// Force Next.js to use a locale that was resolved dynamically on the homepage.
useResolvedLocale(resolvedLocale)

// Load the messages in the correct locale.
const messages = useMessages()

return (
<Layout title={getTitle(messages)}>
<h1>{messages.format('headline')}</h1>
</Layout>
)
}

export default Home

export const getServerSideProps: GetServerSideProps<ResolvedLocaleServerSideProps> = async (
nextPageContext
) => {
return {
props: {
resolvedLocale: resolveLocale(nextPageContext),
},
}
}
32 changes: 9 additions & 23 deletions example/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import Layout from '@/components/layout/Layout'
import type { GetServerSideProps, NextPage } from 'next'

import {
getActualDefaultLocale,
getActualLocale,
getActualLocales,
getCookieLocale,
getPreferredLocale,
normalizeLocale,
ResolvedLocaleServerSideProps,
resolveLocale,
useResolvedLocale,
useRouter,
} from 'next-multilingual'

import { getTitle, useMessages } from 'next-multilingual/messages'

import { useEffect, useRef, useState } from 'react'

import { useFruitsMessages } from '../messages/fruits/useFruitsMessages'

import { HelloApiSchema } from './api/hello'

import styles from './index.module.css'

const Home: NextPage<ResolvedLocaleServerSideProps> = ({ resolvedLocale }) => {
Expand Down Expand Up @@ -178,30 +182,12 @@ const Home: NextPage<ResolvedLocaleServerSideProps> = ({ resolvedLocale }) => {
export default Home

export const getServerSideProps: GetServerSideProps<ResolvedLocaleServerSideProps> = async (
nextPageContext
context
// eslint-disable-next-line @typescript-eslint/require-await
) => {
const { req, locale, locales, defaultLocale } = nextPageContext

const actualLocales = getActualLocales(locales, defaultLocale)
const actualDefaultLocale = getActualDefaultLocale(locales, defaultLocale)
const cookieLocale = getCookieLocale(nextPageContext, actualLocales)
let resolvedLocale = getActualLocale(locale, defaultLocale, locales)

// When Next.js tries to use the default locale, try to find a better one.
if (locale === defaultLocale) {
resolvedLocale =
cookieLocale ??
getPreferredLocale(
req.headers['accept-language'],
actualLocales,
actualDefaultLocale
).toLowerCase()
}

return {
props: {
resolvedLocale,
resolvedLocale: resolveLocale(context),
},
}
}
60 changes: 30 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,17 @@
"e2e-build-base-path": "cross-env CYPRESS_isProd=true BASE_PATH=/some-path CYPRESS_basePath=/some-path start-server-and-test start-example-build http://localhost:3000/some-path cypress",
"e2e-build-headless": "cross-env CYPRESS_isProd=true start-server-and-test start-example-build http://localhost:3000 cypress-headless",
"e2e-build-headless-base-path": "cross-env CYPRESS_isProd=true BASE_PATH=/some-path CYPRESS_basePath=/some-path start-server-and-test start-example-build http://localhost:3000/some-path cypress-headless",
"test": "npm run build & npm run e2e-headless && npm run e2e-headless-base-path && npm run e2e-build-headless && npm run e2e-build-headless-base-path"
"test": "npm run build-to-release & npm run e2e-headless && npm run e2e-headless-base-path && npm run e2e-build-headless && npm run e2e-build-headless-base-path"
},
"dependencies": {
"@babel/core": "^7.18.10",
"cheap-watch": "^1.0.4",
"colorette": "^2.0.19",
"intl-messageformat": "^10.1.1",
"messages-modules": "^1.1.2",
"messages-modules": "^1.1.3",
"nookies": "^2.5.2",
"properties-file": "^2.1.0",
"resolve-accept-language": "^1.1.15",
"properties-file": "^2.1.1",
"resolve-accept-language": "^1.1.16",
"webpack": "^5.74.0"
},
"devDependencies": {
Expand All @@ -127,7 +127,7 @@
"cypress-fail-on-console-error": "^3.2.1",
"cypress-wait-until": "^1.7.2",
"dotenv-cli": "^6.0.0",
"eslint": "^8.21.0",
"eslint": "^8.22.0",
"eslint-config-next": "v12.2.5",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-node": "^0.3.6",
Expand Down
Loading

0 comments on commit 93b1c8e

Please sign in to comment.