六、Nextjs多语言与主题切换

本章节做一个 Nextjs 的补充知识:多语言与主题的切换

多语言

  1. 安装依赖:
1
npm install next-intl
  1. 创建目录结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
- app
- [locale]
- layout.tsx
- page.tsx
- i18n.ts
- next.config.mjs
- middleware.ts
- messages
- en.json
- zh.json
- components
- LanguageSwitch.tsx
  1. 添加对应的语言内容
1
2
3
4
5
6
7
8
9
10
11
12
13
// messages/en.json
{
"Index": {
"title": "Hello world!"
}
}

// message/zh.json
{
"Index": {
"title": "你好 世界!"
}
}
  1. next.config.mjs中使用该插件
1
2
3
4
5
6
7
8
import createNextIntlPlugin from 'next-intl/plugin';

const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {};

export default withNextIntl(nextConfig);

  1. i18n.ts中使用语言文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';

// Can be imported from a shared config
const locales = ['en', 'zh'];

export default getRequestConfig(async ({locale}) => {
// Validate that the incoming `locale` parameter is valid
if (!locales.includes(locale as any)) notFound();

return {
messages: (await import(`./messages/${locale}.json`)).default
};
});
  1. middleware.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
// A list of all locales that are supported
locales: ['en', 'zh'],

// Used when no locale matches
defaultLocale: 'en'
});

export const config = {
// Match only internationalized pathnames
matcher: ['/', '/(zh|en)/:path*']
};
  1. app/[locale]/layout.tsx提供语言信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server';

export default async function LocaleLayout({
children,
params: {locale}
}: {
children: React.ReactNode;
params: {locale: string};
}) {
// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages();

return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
  1. 在其他子路由中使用语言信息,比如app/[locale]/page.tsx
1
2
3
4
5
6
import {useTranslations} from 'next-intl';

export default function Index() {
const t = useTranslations('Index');
return <h1>{t('title')}</h1>;
}
  1. 创建 LanguageSwitch.tsx组件用于切换语言
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
'use client'
import { useLocale } from "next-intl"
import { usePathname, useRouter } from "next/navigation"
export default function LanguageSwitch() {
const localActive = useLocale()
const router = useRouter()
const pathname = usePathname()

const onChangeLanguage = () => {
const nextLocale = localActive === 'en' ? 'zh' : 'en'
const newPath = pathname.replace(/^\/(en|zh)/, `/${nextLocale}/`)
router.replace(newPath, {
scroll: false,
})
}
return (
<button
className="w-[2.5rem] h-[2.5rem] bg-opacity-80 flex items-center justify-center gap-1 transition-all"
onClick={onChangeLanguage}
>
<span className="text-sm hover:scale-[1.15] active:scale-105 transition-all">
{localActive === 'en' ? 'EN' : 'ZH'}
</span>
</button>
)
}

自行决定将组件在什么位置引入,可以在app/[locale]/layout.tsx中引入使用

主题

主题通过 tailwindCSS 实现即可。

  1. tailwind.config.ts 中添加:
1
darkMode: 'class',
  1. 创建 context 提供主题数据并提供 hook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// context/theme-context.tsx
'use client'

import { createContext, useContext, useEffect, useState } from "react"

type ThemeContextType = {
theme: string
toggleTheme: () => void
}

type ThemeContextProviderProp = {
children: React.ReactNode
}

const ThemeContext = createContext<ThemeContextType | null>(null)

export const ThemeContextProvider = ({ children }: ThemeContextProviderProp) => {
const [theme, setTheme] = useState('light')
const toggleTheme = () => {
if (theme === 'light') {
setTheme('dark')
window.localStorage.setItem('theme', 'dark')
document.documentElement.classList.add('dark')
} else {
setTheme('light')
window.localStorage.setItem('theme', 'light')
document.documentElement.classList.remove('dark')
}
}

useEffect(() => {
const localTheme = window.localStorage.getItem("theme")
if (localTheme) {
setTheme(localTheme)
if (localTheme === "dark") {
document.documentElement.classList.add("dark")
} else {
document.documentElement.classList.remove("dark")
}
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
setTheme("dark")
document.documentElement.classList.add("dark")
}
}, [])

return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within a ThemeContextProvider')
}
return context
}

自行决定在什么位置使用,推荐在 app/[locale]/layout.tsx 中包裹 children,对所有的子路由都提供主题信息

  1. 创建 ThemeSwitch.tsx 组件用来切换主题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
'use client'
import { useTheme } from "@/context/theme-context";
import { BsSun, BsMoon } from 'react-icons/bs'
export default function ThemeSwitch() {
const {theme, toggleTheme} = useTheme()
return (
<button
className="w-[2.5rem] h-[2.5rem] bg-opacity-80 flex items-center justify-center hover:scale-[1.15] active:scale-105 transition-all"
onClick={toggleTheme}
>
{theme === 'light' ? <BsSun /> : <BsMoon />}
</button>
)
}

这里用到了 react-icons 中的图标,可以自行选择使用什么来展示不同的主题模式

组件应用到什么位置自行控制,但是一定是要在 ThemeContextProvider 包裹中才能使用 useTheme

  1. 正常写样式即可
1
2
// example
<div className="bg-white text-black dark:bg-black dark:text-white"></div>
作者

胡兆磊

发布于

2024-07-14

更新于

2024-07-05

许可协议