react-admin: 国际化开发

376 阅读6分钟

react-admin是一个开箱即用的中大型后台管理系统,不仅只有前端解决方案,更是提供了基于nestjs的后端解决方案。

前端项目地址

后端项目地址

项目采用i18nextreact-i18next来做国际化,国际化资源使用本地静态文件。

初始化操作可参考react-i18next提供的示例

安装依赖

pnpm add i8next react-i18next
# if you'd like to detect user language and load translation
pnpm add i18next-http-backend i18next-browser-languagedetector

配置i18next

  • 新建src/utils/i18n.ts
// src/utils/i18n.ts
import { initReactI18next } from 'react-i18next';

import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend'; // fallback http load

import { PUBLIC_PATH } from './constants';

i18n
  .use(Backend)
  // 检测用户当前使用的语言
  // 文档: https://github.com/i18next/i18next-browser-languageDetector
  .use(LanguageDetector)
  // 注入 react-i18next 实例
  .use(initReactI18next)
  // 初始化 i18next
  // 配置参数的文档: https://www.i18next.com/overview/configuration-options
  .init({
    backend: {
      // for all available options read the backend's repository readme file
      loadPath: `${PUBLIC_PATH}/locales/{{lng}}/{{ns}}.json`,
    },
    // debug: true,
    fallbackLng: 'zh-CN',
    interpolation: {
      escapeValue: false,
    },
    react: {
      bindI18nStore: 'added', // this way, when the HttpBackend delivers new translations (thanks to refreshAndUpdateStore), the UI gets updated
    },
  });

export default i18n;
  • 新建public/locales/en/transtion.jsonpublic/locales/zh-CN/transtion.json

注意这里的静态资源文件命名格式是xxx/xxx/<language_code>/<namespace>.json

// public/locales/en/transition.json
{
 "language": {
    "zh-CN": "Chinese",
    "en": "English"
  },
  "notification": {
    "todo": "Todo",
    "all": "All",
    "clear": "Mark All As Read",
    "messageCenter": "Message Center"
  },
  "userCenter": {
    "aboutProject": "About Project",
    "profile": "Profile",
    "preferences": "Preferences",
    "lockScreen": "Lock Screen",
    "logout": "Logout"
  },
  "tabs": {
    "closeCurrent": "Close Current",
    "reload": "Reload",
    "pin": "Pin",
    "cancelPin": "Cancel Pin",
    "openNewWindow": "Open in New Window",
    "closeLeft": "Close Left Tabs",
    "closeRight": "Close Right Tabs",
    "closeOther": "Close Other Tabs",
    "closeAll": "Close All Tabs",
    "fullScreen": "FullScreen"
  }
  // ......
}
// public/locales/zh-CN/transition.json
{
 "language": {
    "zh-CN": "简体中文",
    "en": "English"
  },
  "notification": {
    "todo": "待办",
    "all": "全部",
    "clear": "全部标记为已读",
    "messageCenter": "消息中心"
  },
  "userCenter": {
    "aboutProject": "关于项目",
    "profile": "个人中心",
    "preferences": "偏好设置",
    "lockScreen": "锁定屏幕",
    "logout": "退出登录"
  },
  "tabs": {
    "closeCurrent": "关闭当前标签",
    "reload": "重新加载",
    "pin": "固定",
    "cancelPin": "取消固定",
    "openNewWindow": "在新窗口打开",
    "closeLeft": "关闭左侧标签",
    "closeRight": "关闭右侧标签",
    "closeOther": "关闭其他标签",
    "closeAll": "关闭全部标签",
    "fullScreen": "全屏浏览"
  }
  // ......
}
  • main.tsx中导入
// src/main.tsx
import React from 'react';

// ......
import '@/utils/i18n.ts';
// ......
ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);

使用翻译及语言切换组件

在初始化好了118next后,就可以在组件或者页面中使用了,官方提供了多种使用方法hooksHOCRender Props。大家可根据场景和自己喜欢的方式选择使用,本项目采用hooks的方式。具体使用方法可参考react-i18next或者下面这个语言切换组件中的使用方式。

  • 翻译hooks 这个hooks`使用起来很简单就是在需要翻译的地方调用一下对应的函数
// src/components/business/I18n
import { useTranslation } from 'react-i18next';

// ......
const I18n = () => {
 // 这里引入翻译函数
  const { t, i18n } = useTranslation();
 
  const items = [
    {
      key: 'zh-CN',
      icon: '',
      label: (
        <div className="flex items-center">
          <Icon
            icon="emojione-v1:flag-for-china"
            fontSize={20}
            wrapClassName="cursor-pointer mr-[4]"
          />
          {/*注意这行,就是使用翻译函数,翻译函数参数就是翻译文件(json)里的字段key,如果字段找不到key会原样输出,所以必须保证字段key是正确的*/}
          <span>{t('language.zh-CN')}</span>
        </div>
      ),
    },
    {
      key: 'en',
      label: (
        <div className="flex items-center">
          <Icon
            icon="twemoji:flag-us-outlying-islands"
            fontSize={20}
            wrapClassName="cursor-pointer mr-[4]"
          />
          {/*注意这行,就是使用翻译函数,翻译函数参数就是翻译文件(json)里的字段key,如果字段找不到key会原样输出,所以必须保证字段key是正确的*/}
          <span>{t('language.en')}</span>
        </div>
      ),
    },
  ];
   // ......
};

export default I18n;
  • 语言切换组件 语言切换组件主要是给用户手动切换语言的,切换时需要更新i18n里的语言值,以及项目存储的语言值。切换i18n的语言值是翻译的核心,i18n会根据这个值去读取语言文件里面的值,然后通过翻译函数展示出来。项目存储的语言值是为了在项目其他地方根据语言的切换使用不同的语言,比如antd的语言切换。具体如下:
// src/components/business/I18n
import { useTranslation } from 'react-i18next';

import { Dropdown } from 'antd';

import Icon from '@/components/RaIcon';

import useGlobalStore from '@/store';
const I18n = () => {
  const { t, i18n } = useTranslation();
  const changeAppLanguage = useGlobalStore((state) => state.changeAppLanguage);
  const items = [
    {
      key: 'zh-CN',
      icon: '',
      label: (
        <div className="flex items-center">
          <Icon
            icon="emojione-v1:flag-for-china"
            fontSize={20}
            wrapClassName="cursor-pointer mr-[4]"
          />
          <span>{t('language.zh-CN')}</span>
        </div>
      ),
    },
    {
      key: 'en',
      label: (
        <div className="flex items-center">
          <Icon
            icon="twemoji:flag-us-outlying-islands"
            fontSize={20}
            wrapClassName="cursor-pointer mr-[4]"
          />
          <span>{t('language.en')}</span>
        </div>
      ),
    },
  ];
  return (
    <Dropdown
      trigger={['click']}
      placement="bottom"
      menu={{
        items,
        selectable: true,
        onClick: ({ key }) => {
          // 点击语言item时更新i18n 和 项目的语言值
          i18n.changeLanguage(key);
          changeAppLanguage(key);
        },
      }}
      getPopupContainer={(triggerNode) =>
        (triggerNode?.parentNode as HTMLElement) || document.body
      }
      overlayStyle={{ width: 120 }}
    >
      <span className="flex-center leading-none p-[4] cursor-pointer bg-transparent rounded-full hover:bg-[var(--ant-color-bg-layout)] transition-all">
        <Icon icon="carbon:translate" fontSize={20} />
      </span>
    </Dropdown>
  );
};

export default I18n;

自动别浏览器的语言并切换到对应的语言

前面我们实现了手动切换语言的组件,为了更好地用户体验,当用户更换了浏览器语言能自动更新项目语言。所以我们需要写一个对应的hooks来自动识别浏览器语言。

  • useBrowserLanguage hooks 这个hooks主要是监听用户手动切换浏览器语言(chrome浏览器设置-首选语言-选择需要的语言并置顶),浏览器语言切换后更新项目语言
// src/hooks/useBrowserLanguage.ts
import { useEffect } from 'react';

import useGlobalStore from '@/store';

export const useBrowserLanguage = () => {
  const { changeAppLanguage } = useGlobalStore();

  // 获取navigator
  const navigator = window?.navigator;
  function handleLanguageChange() {
    if (navigator) {
      const lang = navigator.language;
      if (lang.includes('zh')) {
        changeAppLanguage('zh-CN');
      } else {
        // 目前只支持中英文,所以其他一律处理为英文
        changeAppLanguage('en');
      }
    }
  }
  useEffect(() => {
    // 监听languagechange事件,如果有变化,就更新存储的language值
    window.addEventListener('languagechange', handleLanguageChange);
    return () => {
      window.removeEventListener('languagechange', handleLanguageChange);
    };
  }, []);
};
  • useI18n hooks 在手动更改了浏览器语言后需要设置i18n的语言值,并且更新组件库的语言值
// src/hooks/useI18n.ts
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';

import enUS from 'antd/locale/en_US';
import zhCN from 'antd/locale/zh_CN';
import dayjs from 'dayjs';

import useGlobalStore from '@/store';

const useI18n = () => {
  const { i18n } = useTranslation();
  const { changeAppLanguage, appLanguage } = useGlobalStore();
  // 监听浏览器语言并设置i18n和项目语言
  useEffect(() => {
    const lang = navigator.language;
    i18n.changeLanguage(lang);
    changeAppLanguage(lang);
  }, [navigator.language]);
// 项目语言更新后更新dayjs的语言
  useEffect(() => {
    if (appLanguage === 'en') {
      dayjs.locale('en');
    } else {
      dayjs.locale('zh-cn');
    }
  }, [appLanguage]);
// 项目语言更新后更新antd语言
  const antdLanguage = useMemo(() => {
    if (appLanguage === 'en') return enUS;
    return zhCN;
  }, [appLanguage]);
  return { antdLanguage };
};

export default useI18n;
  • 使用useI18n hooksuseBrowserLanguage hooks
// src/App.tsx
import { RouterProvider } from 'react-router-dom';

import { ConfigProvider, App as AntApp, theme as antdTheme } from 'antd';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';


import { AppContext } from './AppContext';

import { useBrowserLanguage } from './hooks/useBrowserLanguage';

import useI18n from './hooks/useI18n';


import useGlobalStore from '@/store';

import 'antd/dist/reset.css';

dayjs.locale('zh-cn');

const App = () => {
    // 监听浏览器语言
  useBrowserLanguage();
  // 切换语言后设置antd的语言值
  const { antdLanguage } = useI18n();
  const { primaryColor } = useGlobalStore(
    useShallow((state) => ({
      primaryColor: state.primaryColor,
    })),
  );
  const theme = useTheme();
  return (
    <AppContext.Provider value={{ theme, appCssTokenKey: 'ra-css-var' }}>
      <ConfigProvider
        // 设置antd的语言值
        locale={antdLanguage}
        theme={{
          cssVar: { key: 'ra-css-var' },
          hashed: false,
          // ......
        }}
      >
        <AntApp className="w-full h-full">
          <RouterProvider router={router} />
        </AntApp>
        {/* <LockScreen /> */}
      </ConfigProvider>
    </AppContext.Provider>
  );
};

export default App;

总结

国际化开发总的来说还是很简单的,只需要根据react-i18next文档一步一步实现即可。因为目前项目体量不大,采用本地静态资源资源包,后续可能会增加动态资源包及资源包管理功能。