MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 使用 Context 实现国际化方案

2021-01-074.7k 阅读

理解 React Context

Context 是什么

在 React 应用中,Context 是一种用于在组件树中共享数据的方式,而无需通过层层传递 props 的繁琐过程。它允许我们创建一个全局的数据存储,让多个组件可以直接从中读取数据,而不必在组件层级中手动传递数据。

想象一个多层嵌套的组件结构,顶层组件有一些数据,而深层嵌套的组件需要使用这些数据。如果不使用 Context,就需要将数据通过中间的每一层组件以 props 的形式传递下去,这在大型应用中会变得非常繁琐且难以维护。Context 则提供了一种更简洁的方式来解决这个问题。

Context 的工作原理

React Context 的核心概念包括创建 Context 对象、提供数据的 Provider 组件和消费数据的 Consumer 组件。

首先,通过 React.createContext() 方法创建一个 Context 对象。这个对象包含了 ProviderConsumer 两个属性。

const MyContext = React.createContext();

Provider 组件用于在组件树的某个层级提供数据。任何嵌套在 Provider 组件内的组件,只要它们订阅了这个 Context,都可以访问到 Provider 提供的数据。

<MyContext.Provider value={/* 你想要共享的数据 */}>
  {/* 子组件 */}
</MyContext.Provider>

Consumer 组件用于订阅 Context 并消费其中的数据。当 Context 的值发生变化时,使用 Consumer 的组件会重新渲染。

<MyContext.Consumer>
  {value => (
    {/* 使用 value 渲染组件 */}
  )}
</MyContext.Consumer>

国际化的需求与挑战

为什么需要国际化

随着互联网应用的全球化发展,让应用支持多种语言变得至关重要。不同地区的用户希望以自己熟悉的语言使用应用,这不仅提升了用户体验,也扩大了应用的受众范围。

例如,一个电商应用如果想要在全球范围内推广,就需要支持多种语言,以便不同国家和地区的用户能够轻松购物。国际化不仅仅是简单的文字翻译,还涉及到日期、时间、数字格式等多种文化相关的内容。

传统国际化方案的挑战

在 React 应用中实现国际化,传统的方法可能是通过在组件间传递语言相关的 props 来切换语言。然而,这种方法在大型应用中会面临诸多问题。

首先,随着组件层级的加深,传递 props 变得非常繁琐。想象一个有十几层嵌套的组件结构,为了让最底层的组件能够切换语言,需要在每一层都传递语言相关的 props,这大大增加了代码的复杂度和维护成本。

其次,不同组件可能需要不同的语言资源,而通过 props 传递可能无法很好地组织和管理这些资源。例如,一个页面可能有多个组件,每个组件可能需要不同的文本翻译,通过 props 传递可能导致代码冗余且难以管理。

使用 Context 实现国际化方案

创建国际化 Context

首先,我们需要创建一个用于国际化的 Context。这个 Context 将负责存储当前应用使用的语言以及语言相关的资源。

import React from'react';

const I18nContext = React.createContext();

export default I18nContext;

提供语言资源

接下来,我们需要一个组件来提供语言资源。这个组件将作为 Provider,包裹整个应用或应用的某个部分,以便子组件能够访问到语言资源。

import React from'react';
import I18nContext from './I18nContext';

const languageResources = {
  en: {
    greeting: 'Hello',
    goodbye: 'Goodbye'
  },
  zh: {
    greeting: '你好',
    goodbye: '再见'
  }
};

const I18nProvider = ({ children, initialLanguage = 'en' }) => {
  const [language, setLanguage] = React.useState(initialLanguage);
  const t = (key) => languageResources[language][key];

  return (
    <I18nContext.Provider value={{ language, t }}>
      {children}
    </I18nContext.Provider>
  );
};

export default I18nProvider;

在上面的代码中,I18nProvider 组件接收 childreninitialLanguage 属性。languageResources 对象存储了不同语言的文本资源。useState 钩子用于管理当前使用的语言,t 函数用于根据当前语言和传入的键获取对应的翻译文本。

消费语言资源

在组件中消费语言资源非常简单。我们只需要使用 I18nContext.Consumer 来订阅 Context 并获取其中的语言资源。

import React from'react';
import I18nContext from './I18nContext';

const GreetingComponent = () => {
  return (
    <I18nContext.Consumer>
      {({ t }) => (
        <div>
          {t('greeting')}
        </div>
      )}
    </I18nContext.Consumer>
  );
};

export default GreetingComponent;

GreetingComponent 中,通过 I18nContext.Consumer 获取到 t 函数,然后使用 t('greeting') 来获取当前语言下的问候语。

切换语言

为了实现语言切换功能,我们可以在 I18nProvider 中添加一个函数来更新当前语言。

import React from'react';
import I18nContext from './I18nContext';

const languageResources = {
  en: {
    greeting: 'Hello',
    goodbye: 'Goodbye'
  },
  zh: {
    greeting: '你好',
    goodbye: '再见'
  }
};

const I18nProvider = ({ children, initialLanguage = 'en' }) => {
  const [language, setLanguage] = React.useState(initialLanguage);
  const t = (key) => languageResources[language][key];

  const changeLanguage = (newLanguage) => {
    setLanguage(newLanguage);
  };

  return (
    <I18nContext.Provider value={{ language, t, changeLanguage }}>
      {children}
    </I18nContext.Provider>
  );
};

export default I18nProvider;

然后在需要切换语言的组件中,通过 I18nContext.Consumer 获取 changeLanguage 函数并调用它。

import React from'react';
import I18nContext from './I18nContext';

const LanguageSwitcher = () => {
  return (
    <I18nContext.Consumer>
      {({ changeLanguage }) => (
        <div>
          <button onClick={() => changeLanguage('en')}>English</button>
          <button onClick={() => changeLanguage('zh')}>中文</button>
        </div>
      )}
    </I18nContext.Consumer>
  );
};

export default LanguageSwitcher;

处理复杂的语言资源结构

在实际应用中,语言资源可能会非常复杂,包含多个层级和不同类型的数据。例如,可能有与特定模块相关的翻译,或者包含图片、音频等资源的国际化配置。

我们可以通过更复杂的数据结构来组织语言资源。比如,我们可以将语言资源按照模块进行划分。

const languageResources = {
  en: {
    userModule: {
      welcome: 'Welcome, {username}',
      profile: 'Profile'
    },
    productModule: {
      productList: 'Product List',
      productDetails: 'Product Details'
    }
  },
  zh: {
    userModule: {
      welcome: '欢迎, {username}',
      profile: '个人资料'
    },
    productModule: {
      productList: '产品列表',
      productDetails: '产品详情'
    }
  }
};

在消费这些资源时,我们需要相应地调整 t 函数。

const t = (module, key) => languageResources[language][module][key];

然后在组件中使用时,需要传入模块和键。

<I18nContext.Consumer>
  {({ t }) => (
    <div>
      {t('userModule', 'welcome')}
    </div>
  )}
</I18nContext.Consumer>

处理动态数据替换

在一些翻译文本中,可能需要动态替换某些数据。例如,在欢迎语中需要替换用户名。

我们可以使用模板字符串和字符串替换的方法来实现。

const t = (module, key, data = {}) => {
  let text = languageResources[language][module][key];
  Object.entries(data).forEach(([placeholder, value]) => {
    text = text.replace(`{${placeholder}}`, value);
  });
  return text;
};

在组件中使用时,传入替换的数据。

<I18nContext.Consumer>
  {({ t }) => (
    <div>
      {t('userModule', 'welcome', { username: 'John' })}
    </div>
  )}
</I18nContext.Consumer>

与 React Router 结合实现多语言路由

在很多应用中,我们希望根据语言切换路由。例如,在英文环境下访问 /products,在中文环境下访问 /产品

我们可以结合 React Router 来实现这一点。

首先,我们需要在路由配置中定义不同语言下的路径。

import { BrowserRouter as Router, Routes, Route } from'react-router-dom';
import Home from './Home';
import Products from './Products';

const enRoutes = [
  { path: '/', element: <Home /> },
  { path: '/products', element: <Products /> }
];

const zhRoutes = [
  { path: '/', element: <Home /> },
  { path: '/产品', element: <Products /> }
];

const RouterComponent = () => {
  return (
    <I18nContext.Consumer>
      {({ language }) => {
        const routes = language === 'en'? enRoutes : zhRoutes;
        return (
          <Router>
            <Routes>
              {routes.map((route, index) => (
                <Route key={index} {...route} />
              ))}
            </Routes>
          </Router>
        );
      }}
    </I18nContext.Consumer>
  );
};

export default RouterComponent;

在上面的代码中,根据当前语言选择不同的路由配置。这样,当用户切换语言时,路由也会相应地改变。

优化性能

在使用 Context 实现国际化时,由于 Context 的变化会导致所有订阅的组件重新渲染,可能会影响性能。

我们可以使用 React.memo 来优化组件性能。对于那些只依赖 Context 中的语言资源且在数据没有变化时不需要重新渲染的组件,可以使用 React.memo 进行包裹。

import React from'react';
import I18nContext from './I18nContext';

const GreetingComponent = () => {
  return (
    <I18nContext.Consumer>
      {({ t }) => (
        <div>
          {t('greeting')}
        </div>
      )}
    </I18nContext.Consumer>
  );
};

export default React.memo(GreetingComponent);

React.memo 会在组件 props 没有变化时阻止组件重新渲染,这里虽然没有显式的 props,但 Context 的变化也会被视为 props 的变化,通过 React.memo 可以避免不必要的重新渲染。

与第三方国际化库结合

虽然使用 Context 可以实现一个简单的国际化方案,但在实际项目中,可能还需要与一些成熟的第三方国际化库结合使用,以获得更强大的功能。

例如,react - i18next 是一个非常流行的 React 国际化库。它提供了更丰富的功能,如复数形式处理、嵌套翻译等。

我们可以将 react - i18next 与我们的 Context 方案结合。首先,安装 react - i18next

npm install react - i18next i18next

然后,在 I18nProvider 中初始化 i18next

import React from'react';
import I18nContext from './I18nContext';
import i18n from 'i18next';
import { initReactI18next } from'react - i18next';

const languageResources = {
  en: {
    greeting: 'Hello',
    goodbye: 'Goodbye'
  },
  zh: {
    greeting: '你好',
    goodbye: '再见'
  }
};

i18n.use(initReactI18next).init({
  resources: {
    en: {
      translation: languageResources.en
    },
    zh: {
      translation: languageResources.zh
    }
  },
  lng: 'en',
  fallbackLng: 'en',
  interpolation: {
    escapeValue: false
  }
});

const I18nProvider = ({ children, initialLanguage = 'en' }) => {
  const [language, setLanguage] = React.useState(initialLanguage);

  const changeLanguage = (newLanguage) => {
    setLanguage(newLanguage);
    i18n.changeLanguage(newLanguage);
  };

  return (
    <I18nContext.Provider value={{ language, changeLanguage }}>
      {children}
    </I18nContext.Provider>
  );
};

export default I18nProvider;

在组件中,我们可以使用 react - i18nextuseTranslation 钩子来获取翻译函数。

import React from'react';
import { useTranslation } from'react - i18next';

const GreetingComponent = () => {
  const { t } = useTranslation();
  return (
    <div>
      {t('greeting')}
    </div>
  );
};

export default GreetingComponent;

这样,我们既利用了 Context 的数据共享优势,又借助了 react - i18next 的强大功能。

处理日期、时间和数字格式

国际化不仅仅是文本翻译,还包括日期、时间和数字格式的处理。不同的语言和地区有不同的日期、时间和数字表示方式。

对于日期和时间格式,我们可以使用 Intl.DateTimeFormat

import React from'react';
import I18nContext from './I18nContext';

const DateComponent = () => {
  return (
    <I18nContext.Consumer>
      {({ language }) => {
        const date = new Date();
        const options = {
          year: 'numeric',
          month: 'long',
          day: 'numeric'
        };
        return (
          <div>
            {new Intl.DateTimeFormat(language, options).format(date)}
          </div>
        );
      }}
    </I18nContext.Consumer>
  );
};

export default DateComponent;

对于数字格式,我们可以使用 Intl.NumberFormat

import React from'react';
import I18nContext from './I18nContext';

const NumberComponent = () => {
  return (
    <I18nContext.Consumer>
      {({ language }) => {
        const number = 1234.56;
        const options = {
          style: 'decimal',
          minimumFractionDigits: 2
        };
        return (
          <div>
            {new Intl.NumberFormat(language, options).format(number)}
          </div>
        );
      }}
    </I18nContext.Consumer>
  );
};

export default NumberComponent;

通过这种方式,我们可以根据当前语言和地区显示合适的日期、时间和数字格式。

测试国际化功能

在开发国际化应用时,测试是非常重要的。我们需要确保不同语言下的文本翻译正确,日期、时间和数字格式显示正确,以及语言切换功能正常。

对于文本翻译的测试,我们可以使用 Jest 等测试框架。首先,安装 @testing - library/react

npm install @testing - library/react

然后,编写测试用例。

import React from'react';
import { render, screen } from '@testing - library/react';
import GreetingComponent from './GreetingComponent';
import I18nProvider from './I18nProvider';

describe('GreetingComponent', () => {
  test('should display correct greeting in English', () => {
    render(
      <I18nProvider initialLanguage="en">
        <GreetingComponent />
      </I18nProvider>
    );
    expect(screen.getByText('Hello')).toBeInTheDocument();
  });

  test('should display correct greeting in Chinese', () => {
    render(
      <I18nProvider initialLanguage="zh">
        <GreetingComponent />
      </I18nProvider>
    );
    expect(screen.getByText('你好')).toBeInTheDocument();
  });
});

对于日期、时间和数字格式的测试,我们可以使用 Intl.DateTimeFormatIntl.NumberFormat 的静态方法来获取格式化字符串,并与预期结果进行比较。

import React from'react';
import { render, screen } from '@testing - library/react';
import DateComponent from './DateComponent';
import I18nProvider from './I18nProvider';

describe('DateComponent', () => {
  test('should display correct date format in English', () => {
    render(
      <I18nProvider initialLanguage="en">
        <DateComponent />
      </I18nProvider>
    );
    const date = new Date();
    const options = {
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    };
    const expected = new Intl.DateTimeFormat('en', options).format(date);
    expect(screen.getByText(expected)).toBeInTheDocument();
  });

  test('should display correct date format in Chinese', () => {
    render(
      <I18nProvider initialLanguage="zh">
        <DateComponent />
      </I18nProvider>
    );
    const date = new Date();
    const options = {
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    };
    const expected = new Intl.DateTimeFormat('zh', options).format(date);
    expect(screen.getByText(expected)).toBeInTheDocument();
  });
});

通过这些测试用例,可以确保国际化功能在不同语言下的正确性。

部署和上线

在将国际化应用部署上线时,需要注意一些事项。

首先,确保所有的语言资源都被正确打包。如果使用 webpack 等打包工具,可以配置相应的插件来处理语言资源文件。例如,i18next - webpack - loader 可以帮助处理 i18next 的语言资源。

其次,考虑服务器端渲染(SSR)或静态站点生成(SSG)时的国际化支持。对于 SSR,需要在服务器端根据用户请求的语言设置正确的语言环境,并将相应的语言资源传递给客户端。对于 SSG,可以生成不同语言版本的静态页面。

另外,在部署后,需要监控和收集用户反馈,以便及时发现和修复可能存在的国际化问题,如翻译不准确、格式显示异常等。

通过以上详细的步骤和方法,我们可以在 React 应用中使用 Context 实现一个功能完备且可扩展的国际化方案,满足不同应用场景下的国际化需求。无论是小型应用还是大型企业级项目,这种方案都能够提供良好的用户体验和高效的开发维护流程。