NextJs 14 (App router) i18n、data fetch

499 阅读3分钟

1. Data Fetch

1.1. Client Component

"use client"

export default async function page({ params: { locale } }: { params: { locale: Locale } }) {
  useEffect(() => {
    // get data
  }, [])
}

client component获取数据跟普通的获取没啥区别,值得注意的是 next默认是 server component,需要声明 use client

1.2. Server Component

Next 官方推荐使用 fetch,但是 axios 同样能用于服务端。

async function getData () {
  const { data } = await getMemberList()
  return data.data.list
}

export default async function page({ params: { locale } }: { params: { locale: Locale } }) {
  const list = await getData()
  return (
    <div className={styles.team}>
      <div className='imgWrapper'>
        <img src='/images/Team-3.jpg' alt='icon' />
      </div>
      <div className='alignwide'>
        <h1>我们的团队</h1>
        <Row gutter={[10, 10]} style={{width: '100%'}}>
          {
            list.map((item: any) => (
              <Col key={item.ID} md={8} sm={24} xs={24}>
                <Staff avatar={getImgUrl(item.avatar)} name={lt(locale, item.name)} title={lt(locale, item.title)} />
              </Col>
            ))
          }
        </Row>
      </div>
    </div>
  )
}

2. i18n

2.1. 安装依赖

npm install i18next react-i18next i18next-resources-to-backend next-i18n-router
  • react-i18nexti18next:i18n 基础包
  • i18next-resources-to-backend:加载资源到 server
  • next-i18n-router用于在 app router 中实现国际化路由和地区检测

2.2. 改造项目

  • /app/[locale]用于传递 locale 路由参数
  • /app/lib/i18n/config.tsi18n配置
  • /app/lib/i18n/index.ts翻译函数
  • /app/locales本地翻译资源
  • /middleware.ts这里通过 next-i18n-router国际化路由
  • /app/ui/TranslationsProvider/index.tsxi18n provider for client render

/app/lib/i18n/config.ts

export default {
  locales: ['en-US', 'zh-CN'],
  defaultLocale: 'en-US',
  prefixDefault: false
}

/app/lib/i18n/index.ts

import { createInstance, i18n } from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import { initReactI18next } from 'react-i18next/initReactI18next'
import i18nConfig from './config'

// Local i18n
export async function initTranslations(
  locale: string,
  namespaces: string[],
  i18nInstance?: i18n,
  resources?: any
) {
  i18nInstance = i18nInstance || createInstance();

  i18nInstance.use(initReactI18next);

  if (!resources) {
    i18nInstance.use(
      resourcesToBackend(
        (language: string, namespace: string) =>
          import(`@/locales/${language}/${namespace}.json`) // 读取翻译资源
      )
    );
  }

  await i18nInstance.init({
    lng: locale,
    resources,
    fallbackLng: i18nConfig.defaultLocale,
    supportedLngs: i18nConfig.locales,
    defaultNS: namespaces[0],
    fallbackNS: namespaces[0],
    ns: namespaces,
    preload: resources ? [] : i18nConfig.locales
  });

  return {
    i18n: i18nInstance,
    resources: i18nInstance.services.resourceStore.data,
    t: i18nInstance.t
  };
}

// BackEnd i18n
export type Locale = 'en-US' | 'zh-CN'
export type LocalizedText<T extends Locale> = {
  [key in T]: string;
}
export function getLocaleText<T extends Locale>(
  locale: T, text: LocalizedText<T>, tryonly?: boolean,
): string {
  if (!text[locale] || text[locale] === '' || text[locale] === '\n') {
    if (!tryonly) {
      for (const key in text) {
        if (text[key]) {
          return text[key];
        }
      }
    } else {
      return '';
    }
  }
  return text[locale];
}

export const lt = getLocaleText

/middleware.ts(根目录,不要搞错了)

import { i18nRouter } from 'next-i18n-router';
import i18nConfig from './app/lib/i18n/config';

export function middleware(request: any) {
  return i18nRouter(request, i18nConfig);
}

// applies this middleware only to files in the app directory
export const config = {
  matcher: '/((?!api|static|.*\..*|_next).*)'
};

/app/ui/TranslationsProvider/index.tsx

'use client'

import { I18nextProvider } from 'react-i18next'
import { initTranslations } from '@/lib/i18n'
import { createInstance } from 'i18next'

export default function TranslationsProvider({
  children,
  locale,
  namespaces,
  resources
}: {
  children: React.ReactNode,
  locale: string,
  namespaces: string[],
  resources: any
}) {
  const i18n = createInstance()
  initTranslations(locale, namespaces, i18n, resources)
  return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>
}

2.3. 使用

/app/[locale]/layout.tsx

type RootLayoutProps = Readonly<{
  children: React.ReactNode
  params: {
    locale: string
  }
}>
export default function RootLayout({
  children,
  params: {
    locale
  }
}: RootLayoutProps) {
  return (
    <html lang={locale}>
      <body className={inter.className}>
        <AntdRegistry>
          <Header locale={locale} />
          {children}
          <Footer />
        </AntdRegistry>
      </body>
    </html>
  );
}

/app/ui/Header.tsx

import { initTranslations } from '@/lib/i18n'
import LanguageChanger from '../LanguageChanger' // langChange component
import TranslationsProvider from '../TranslationsProvider' // CR component
import styles from './index.module.scss'
import Image from 'next/image'

const i18nNamespaces = ['home', 'common']
export default async function Header({ locale }: { locale: string }) {
  const { resources, t } = await initTranslations(locale, i18nNamespaces)

  return (
    <div className={styles.header}>
      <div className={styles.imgWrapper}>
        <a href='/'>
          <Image src='/images/logo.jpg' width={500} height={59} alt='logo' />
        </a>
      </div>
      <div className={styles.menuWrapper}>
        <ul className={styles.menu}>
          {
            menus.map((item) => (
              <li key={item.title} className={styles.item}>
                <a href={item.url} className={styles.txt}>{item.title}</a>
              </li>
            ))
          }
          <li>
            {/* Server render */}
            {t('title')}
          </li>
          <li>
            {/* Client Render */}
            <TranslationsProvider namespaces={i18nNamespaces} locale={locale} resources={resources}>
              <LanguageChanger />
            </TranslationsProvider>
          </li>
        </ul>
      </div>
    </div>
  )
}
  • async/await只能在 server component中使用,所以 i18n不能直接在 client render中使用。
  • client render需要在 server component的后代组件中使用。
  • 在包裹TranslationsProvider中的组件可以直接使用useTranslation

2.4. 切换语言

'use client'

import { useRouter } from 'next/navigation'
import { usePathname } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import i18nConfig from '../../lib/i18n/config'
import { Select } from 'antd'
import Cookies from 'js-cookie'

export default function LanguageChanger() {
  const { t, i18n } = useTranslation(['common'])
  const router = useRouter()
  const currentPathname = usePathname()
  const currentLocale = i18n.language

  const handleChange = (value: string) => {
    const newLocale = value

    // set cookie for next-i18n-router
    Cookies.set('NEXT_LOCALE', newLocale, { expires: 30 })

    // redirect to the new locale path
    if (
      currentLocale === i18nConfig.defaultLocale &&
      !i18nConfig.prefixDefault
    ) {
      router.push('/' + newLocale + currentPathname);
    } else {
      router.push(
        currentPathname.replace(`/${currentLocale}`, `/${newLocale}`)
      );
    }

    router.refresh();
  };

  const options = i18nConfig.locales.map((item) => ({
    label: t(item),
    value: item
  }))

  return (
    <Select
      defaultValue={currentLocale}
      style={{ width: 120 }}
      onChange={handleChange}
      options={options}
    />
  );
}
  • next-i18n-router使用NEXT_LOCALE存储 locale,那么我们可以借助它来设置区域。