将国际化添加到Next.js应用
#javascript #react #nextjs #i18n

介绍

我通常用英语编写UI副本。也没有其他语言。但这必须随着Railtrack而改变。这是一个用于跟踪欧洲火车旅行的网络应用程序。主要的重点是德语国家。因此,将UI转换为德语是有意义的。加上我是德语人士的事实,达成了交易。

我确定您知道通过URL定义语言的网站。因此,website.com/en/welcome将以英语显示欢迎页面,而website.com/de/welcome将以德语显示。 Next.js开箱即用。他们称其为internationalized routing。您定义了支持语言的列表。然后,Next.js自动检测其浏览器中设置的用户语言。然后,取决于开发人员(通常在库的帮助下)加载此语言的正确翻译。

这对网站非常有用。例如营销页面或博客。但是,在成熟的Web应用程序中,这并不理想。有两个原因:

  • 它膨胀了URL。 app.website.com/dashboard看起来比app.website.com/en/dashboard要干净很多3
  • 语言设置应在人口级别上持续存在。无论用户从哪个系统登录到哪个系统。这意味着我们可以单独依靠Accept-Language标头。

我们如何解决这个问题

确定用户语言的流程应该看起来像这样。按优先级排名:

  1. 如果可用,请使用用户在其设置中设置的语言
  2. 如果未设置语言,请使用默认浏览器语言

用于处理客户端上的翻译,我们将使用出色的next-intl库。它为我们提供了如何获取服务器上所需语言环境的翻译文件的完全自由。

语言选择设置

让用户选择他的首选语言完全取决于应用程序的细节。在RailTrack中,有一个设置屏幕,用户可以在其中更改语言。根据默认,设置将切换到他的浏览器语言。但是,一旦用户首次更改设置,我们将在数据库中存储该值,以便在以后的会话中持续。

Screenshot from Railtrack showing the language choice dialog in settings.

获取服务器上正确的语言环境

我们将需要几件事

  • 确定我们将使用哪个本地的逻辑
  • 代码加载当前语言环境的翻译文件并将其发送给客户端
  • 消费和显示客户的翻译

获取地方

这是决定我们将用于网站的逻辑。如果可用,则使用用户在设置中选择的语言。如果没有设置,则将使用浏览器语言。

这是由以下函数来处理的。我们将在我们的getServerSideProps中的一个时刻称呼它。

// src/utils/getLocale.ts

import parser from 'accept-language-parser';
import { getCookie, setCookie } from 'cookies-next';
import { GetServerSidePropsContext } from 'next';

import { prisma } from '@/server/db/client';
import { getUserFromContext } from '@/utils/serverUser';

export const LANG_COOKIE_KEY = 'i18n-lang';
const SUPPORTED_LANGS = ['en', 'de'];
const DEFAULT_LANG = 'en';

const getBrowserLanguage = (ctx: GetServerSidePropsContext) => {
  const languages = ctx.req.headers['accept-language'];

  // use english as the default
  return parser.pick(SUPPORTED_LANGS, languages ?? '', { loose: true }) ?? DEFAULT_LANG;
};

export const getLocale = async (ctx: GetServerSidePropsContext): Promise<string> => {
  // first check for actual cookie
  const cookieLanguage = getCookie(LANG_COOKIE_KEY, ctx);

  if (cookieLanguage === 'browser') {
    // then check for browser language
    return getBrowserLanguage(ctx);
  }

  // if we have the actual cookie with a valid language
  if (cookieLanguage && typeof cookieLanguage === 'string' && SUPPORTED_LANGS.includes(cookieLanguage)) {
    return cookieLanguage;
  }

  // Only check auth after cookie fails
  const user = await getUserFromContext(ctx);

  const isAuthenticated = user && user.role === 'authenticated';

  if (!isAuthenticated) {
    return DEFAULT_LANG;
  }

  // this means we haven't set the cookie
  // so now we fetch settings and check there.
  const settings = await prisma.settings.findUnique({ where: { userId: user.id } });

  // and if it's there, store in cookie and use it
  if (settings?.language) {
    setCookie(LANG_COOKIE_KEY, settings?.language, { ...ctx, sameSite: 'lax' });

    return settings?.language;
  }

  // if we also have nothing in the DB, just use the browser language

  // set the language selection to browser from now on
  // this prevents us from having to make a DB query each time
  setCookie(LANG_COOKIE_KEY, 'browser', { ...ctx, sameSite: 'lax' });

  return getBrowserLanguage(ctx);
};

我们大量使用cookie来缓存语言偏好。为什么?从每个页面上的数据库中获取用户设置将严重损害性能。

因此,我们缓存了cookie中数据库查询的结果。在下一页加载中,将从cookie提取所选语言,而无需再次运行数据库查询。

使用翻译

每种语言的翻译可以在简单的JSON文件中定义。

// src/locales/de.json

{
  "departure": "Von",
  "arrival": "Nach",
  "add": {
    "time": "Abfahrtszeit",
    "save": "Speichern"
  }
}

// src/locales/en.json

{
  "departure": "From",
  "arrival": "To",
  "add": {
    "time": "Departure time",
    "save": "Save"
  }
}

在您将使用它们的每个页面上的getServersideprops中获取翻译。

export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
  // calls the function we created above
  const locale = await getLocale(ctx);

  return {
    // loads the translation for the correct locale and sends it to the page as a prop
    props: { messages: (await import(`../locales/${locale}.json`)).default, locale },
  };
};

要在我们的组件中消耗此内容,我们需要将NEXT-INTL提供商添加到我们的_app.tsx文件中。不要忘记安装库:NPM安装Next-Intl。

import { AbstractIntlMessages, NextIntlProvider } from 'next-intl';
import type { AppType } from 'next/dist/shared/lib/utils';

const DEFAULT_LANG = 'en';

type Props = {
  messages: AbstractIntlMessages;
  locale: string;
};

const MyApp: AppType<Props> = ({ Component, pageProps }) => (
  <NextIntlProvider messages={pageProps.messages} locale={pageProps.locale ?? DEFAULT_LANG}>
    <Component {...pageProps} />
  </NextIntlProvider>
);

现在,使用客户端上的翻译就像添加useTranslations挂钩并调用所需键返回的函数一样简单。

const Journeys: NextPage = () => {
  const t = useTranslations();

  return (
    <Wrapper title={t('navigation.journeys')}>
      <JourneyList />
    </Wrapper>
  );
};

缓存无效

想象用户A将语言设置为设备A上的德语。然后,他在设备B上登录我们的cookie设置为英语。在这种情况下,上面的实现切勿再次查询数据库以检查cookie中设置的语言是否仍然是最新的。我们应该做一些减轻这种情况的事情:

  • 在每次登录时,请重新获取语言设置,并在需要时更新cookie。如果您在同一浏览器中与其他用户登录,这也很重要。因为该用户可能具有不同的语言设置。
  • 定期刷新cookie信息。通过在cookie上设置到期日期,或在加载页面后查询设置DB。

对于RailTrack,我选择了在每个标志并注册的饼干上无效的策略。在您执行登录并注册操作的任何地方添加以下片段。

import { LANG_COOKIE_KEY } from 'utils/getLocale';

// add this inside the login function handler
deleteCookie(LANG_COOKIE_KEY);

包起来

这是我为将国际化添加到我的Web应用程序中所做的。让我知道您喜欢我的解决方案。如果您有任何建议,请与您联系。

如果您想查看我所描述的现实世界示例,请在此处查看我的轨道库:https://github.com/noahflk/railtrack