国际化功能
官网不一定是给一个国家的人看的,可能公司或是团队的业务是针对多个地区的,语言不应该成为价值观传输的阻碍,所以如果是多地区业务线的公司,实现多语言也是很必要的。
安装相关依赖包:
npm install i18next next-i18next react-i18next
next-i18next 包提供了 appWithTranslation 一个高阶组件(HOC),需要用这个高阶组件包装整个应用程序。
pages/_app.tsx
import type { AppProps, AppContext } from 'next/app';
import App from 'next/app';
import Head from 'next/head';
import axios from 'axios';
import ThemeContextProvider from '@/stores/theme';
import UserAgentProvider from '@/stores/userAgent';
import { LOCALDOMAIN, getIsMobile } from '@/utils';
import type { ILayoutProps } from '@/components/layout';
import { appWithTranslation } from 'next-i18next';
import Layout from '@/components/layout';
import '@/styles/globals.css'
const MyApp = (data: AppProps & ILayoutProps & { isMobile: boolean }) => {
const {
Component, pageProps, navbarData, footerData, isMobile
} = data;
return (
<div>
<Head>
<title>{`A Demo for 官网开发实战 (${
isMobile ? "移动端" : "pc 端"
})`}</title>
<meta
name="description"
content="A Demo for 官网开发实战"
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<ThemeContextProvider>
<UserAgentProvider>
<Layout navbarData={navbarData} footerData={footerData}>
<Component {...pageProps} />
</Layout>
</UserAgentProvider>
</ThemeContextProvider>
</div>
)
}
MyApp.getInitialProps = async (context: AppContext) => {
const pageProps = await App.getInitialProps(context);
const { data = {} } = await axios.get(`${LOCALDOMAIN}/api/layout`)
return {
...pageProps,
...data,
isMobile: getIsMobile(context),
}
}
export default appWithTranslation(MyApp);
现在为 next-i18next 创建一个配置文件,在项目根目录下创建文件 next-i18next.config.js 并添加如下配置。
module.exports = {
i18n: {
defaultLocale: 'zh-CN',
locales: ['en_US', 'zh-CN'],
},
ns: ['header', 'main', 'footer', 'common']
}
- locales: 包含网站上需要的语言环境的数组。
- defaultLocale: 要显示的默认语言环境。
现在将创建的 i18next 配置导入到 next.config.js 中。
/** @type {import('next').NextConfig} */
const path = require('path');
const semi = require('@douyinfe/semi-next').default({});
const { i18n } = require('./next-i18next.config');
const nextConfig = semi({
reactStrictMode: true,
swcMinify: true,
i18n,
images: {
domains: ['127.0.0.1'],
},
webpack: (config) => {
config.resolve.alias = {
...config.resolve.alias,
'@': path.resolve(__dirname),
};
return config;
}
});
module.exports = nextConfig
现在开始在应用程序中添加语言环境,在 public 目录下新建 locales 目录。
public/locales/en_US/main.json
{
"IpadStyle": "Currently Ipad style",
"PCStyle": "Currently it is PC style",
"MobileStyle": "Currently in mobile style"
}
public/locales/zh_CN/main.json
{
"IpadStyle": "当前是 Ipad 端样式",
"PCStyle": "当前是 pc 端样式",
"MobileStyle": "当前是移动端样式"
}
类似于主题化注入,针对语言也先来定义一套注入器(Context),通过缓存的方式统一管理,然后进行全局的注入。
constants/enum
export enum Language {
ch = "zh-CN",
en = "en_US",
}
stores/language.tsx
import {createContext, FC, useEffect, useState} from 'react';
import {Language} from '@/constants/enum';
interface ILanguageContextProps {
language: Language;
setLanguage: (language: Language) => void;
}
interface ILanguageContextProviderProps {
children: JSX.Element;
}
export const LanguageContext = createContext<ILanguageContextProps>({} as ILanguageContextProps);
const LanguageContextProvider: FC<ILanguageContextProviderProps> = ({children}) => {
const [language, setLanguage] = useState<Language>(Language.ch);
useEffect(() => {
const checkLanguage = () => {
const item = localStorage.getItem('language') as Language || Language.ch;
setLanguage(item);
document.getElementsByTagName('html')[0].lang = item;
}
// 初始化先执行一遍
checkLanguage();
// 监听浏览器缓存事件
window.addEventListener('storage', checkLanguage);
return (): void => {
// 解绑
window.removeEventListener('storage', checkLanguage);
}
}, []);
return (
<LanguageContext.Provider value={{
language,
setLanguage: (currentLanguage) => {
setLanguage(currentLanguage);
localStorage.setItem('language', currentLanguage);
document.getElementsByTagName('html')[0].lang = currentLanguage;
}
}}>
{children}
</LanguageContext.Provider>
)
}
export default LanguageContextProvider;
导入 serverSideTranslations,在 getServerSideProps 中进行道具传递。
pages/index.tsx
import {useContext, useEffect, useRef, useState} from 'react';
import axios from 'axios';
import type {NextPage} from 'next';
import {Pagination} from '@douyinfe/semi-ui';
import classNames from 'classnames';
import {ThemeContext} from '@/stores/theme';
import {useTranslation} from 'next-i18next';
import {serverSideTranslations} from 'next-i18next/serverSideTranslations';
import styles from '@/styles/Home.module.scss';
import {LOCALDOMAIN} from "@/utils";
import {IArticleIntroduction} from "@/pages/api/articleIntroduction";
import {LanguageContext} from "@/stores/language";
import {useRouter} from "next/router";
interface IHomeProps {
title: string;
description: string;
articles: {
total: number;
list: {
label: string;
info: string;
link: string;
}[];
};
}
const Home: NextPage<IHomeProps> = ({
title, description, articles
}) => {
const { i18n } = useTranslation();
const router = useRouter();
const { locale } = router;
const [content, setContent] = useState(articles);
const mainRef = useRef<HTMLDivElement>(null);
const { theme } = useContext(ThemeContext);
const { language } = useContext(LanguageContext);
useEffect(() => {
mainRef.current?.classList.remove(styles.withAnimation);
window.requestAnimationFrame(() => {
mainRef.current?.classList.add(styles.withAnimation);
});
}, [theme]);
useEffect(() => {
i18n?.changeLanguage(locale);
console.warn(locale)
}, [language, locale])
return (
<div className={styles.container}>
<main className={classNames([styles.main, styles.withAnimation])} ref={mainRef}>
<h1 className={styles.title}>{title}</h1>
<p className={styles.description}>{description}</p>
<div className={styles.grid}>
{
content?.list?.map((item, index) => {
return (
<div key={index} className={styles.card} onClick={(): void => {
window.open(
item?.link,
"blank",
"noopener=yes,noreferrer=yes"
);
}}>
<h2>{item?.label}</h2>
<p>{item?.info}</p>
</div>
)
})
}
</div>
<div className={styles.paginationArea}>
<Pagination total={content?.total} pageSize={6} onPageChange={pageNo => {
axios.post(`${LOCALDOMAIN}/api/articleIntroduction`, {
pageNo,
pageSize: 6,
}).then(({ data: {
total,
list: listData,
}}) => {
setContent({
list: listData?.map((item: IArticleIntroduction) => ({
label: item.label,
info: item.info,
link: `${LOCALDOMAIN}/article/${item.articleId}`,
})),
total,
})
})
}} />
</div>
</main>
</div>
)
}
export const getServerSideProps = async ({ locale }: { locale: string }) => {
const {
data: {
title, description,
}
} = await axios.get(`${LOCALDOMAIN}/api/home`);
const {
data: {
list: listData, total,
}} = await axios.post(`${LOCALDOMAIN}/api/articleIntroduction`, {
pageNo: 1,
pageSize: 6,
})
return {
props: {
...(await serverSideTranslations(locale, ['common', 'footer', 'header', 'main'])),
title,
description,
articles: {
total,
list: listData?.map((item: IArticleIntroduction) => ({
label: item.label,
info: item.info,
link: `${LOCALDOMAIN}/article/${item.articleId}`,
}))
},
}
};
}
export default Home;
在 pages/_document.tsx 中进行交互前注入:
import Document, {Html, Head, Main, NextScript, DocumentContext} from 'next/document'
import Script from 'next/script';
import {Language} from '@/constants/enum';
const MyDocument = () => {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
<Script id="theme-script" strategy="beforeInteractive">
{
`const theme = localStorage.getItem('theme') || 'light';
localStorage.setItem('theme', theme);
document.getElementsByTagName('html')[0].dataset.theme = theme;
const language = localStorage.getItem('language') || 'zh-CN';
localStorage.setItem('language', language);
document.getElementsByTagName('html')[0].lang = language;
`
}
</Script>
</body>
</Html>
)
}
export const getServerSideProps = async (context: DocumentContext & {locale: string}) => {
const initialProps = await Document.getInitialProps(context);
return { ...initialProps, locale: context?.locale || Language.ch };
}
export default MyDocument;
修改导航组件,添加语言环境切换器。
components/NavBar/index.tsx
import {FC, useContext, useEffect} from 'react';
import Link from "next/link";
import {useTranslation} from 'next-i18next';
import {ThemeContext} from '@/stores/theme';
import {UserAgentContext} from '@/stores/userAgent';
import {Environment, Language, Themes} from '@/constants/enum';
import styles from './index.module.scss';
import {LanguageContext} from "@/stores/language";
import {useRouter} from "next/router";
export interface INavBarProps {}
const NavBar: FC<INavBarProps> = ({}) => {
const { t } = useTranslation('main');
const router = useRouter();
const { locales, locale: activeLocale } = router;
const otherLocales = locales?.filter(
(locale) => locale !== activeLocale && locale !== "default"
);
const { setTheme } = useContext(ThemeContext);
const { setLanguage } = useContext(LanguageContext);
const { userAgent } = useContext(UserAgentContext);
useEffect(() => {
setLanguage(router.locale as Language);
}, [router.locale]);
return (
<div className={styles.navBar}>
<a href="http://localhost:3000/">
<div className={styles.logoIcon} />
</a>
<div className={styles.themeArea}>
{userAgent === Environment.pc && (
<span className={styles.text}>{t('PCStyle')}</span>
)}
{userAgent === Environment.ipad && (
<span className={styles.text}>{t('IpadStyle')}</span>
)}
{userAgent === Environment.mobile && (
<span className={styles.text}>{t('MobileStyle')}</span>
)}
</div>
{otherLocales?.map((locale) => {
const { pathname, query, asPath } = router;
return (
<span key={locale}>
<Link href={{ pathname, query }} as={asPath} locale={locale}>
{locale}
</Link>
</span>
);
})}
<div className={styles.themeIcon} onClick={(): void => {
setTheme(localStorage.getItem('theme') === Themes.light ? Themes.dark : Themes.light);
}}/>
</div>
)
}
export default NavBar;
在这里获得了 i18next 配置文件中提到的语言环境,然后映射每个区域设置项目并单击每个将链接如下:
<Link href={{ pathname, query }} as={asPath} locale={locale}>
上面的链接会将应用程序的区域设置 URL 更改为选择的相应区域设置。
useTranslation 从 next-i18next 包中导入钩子。
import { useTranslation } from "next-i18next";
现在可以使用一个函数来获取在 locales 目录 t() 中的 locale 文件中添加的语言字符串。
例如,下面的代码将从选择的相应语言环境(en_US 或 zh_CN)中获取字符串。
const { t } = useTranslation();
return (
<>
<span className={styles.text}>{t('MobileStyle')}</span>
</>
);
实现效果:
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论