阐述前端国际化 (i18n) 和本地化 (l10n) 在 JavaScript 应用中的实现方案,例如动态加载语言包和日期格式化。

各位观众老爷,晚上好!我是今天的讲师,咱们今天聊聊前端国际化(i18n)和本地化(l10n),这俩兄弟在JavaScript应用里怎么玩儿。别怕,咱们不搞学术报告,就当是拉家常,保证你听完能上手。

开场白:啥是i18n和l10n?

简单说,i18n(Internationalization)就是让你的应用做好准备,能适应各种语言和文化。 它是一种架构,一种设计理念,把应用的代码和语言文字内容分离。而l10n(Localization)就是根据特定的语言和文化,把应用真正变成当地人习惯的样子。它是i18n的具体实现。

你可以把i18n想象成一个百变金刚的骨架,而l10n就是给这个骨架穿上不同国家地区的衣服和配饰。

第一部分:语言包,i18n的基石

要搞国际化,首先得有“语言包”。语言包就是一个JSON文件,里面放着各种语言对应的文本。 就像一个翻译字典,你的应用要显示什么文字,就去字典里查对应的翻译。

  • 语言包的结构

    // en.json
    {
      "greeting": "Hello, {name}!",
      "welcomeMessage": "Welcome to our website!",
      "product.name": "Awesome Product",
      "price": "Price: {price, number, currency}",
      "date": "Today is {today, date, ::yyyyMMdd}"
    }
    
    // zh-CN.json
    {
      "greeting": "你好,{name}!",
      "welcomeMessage": "欢迎来到我们的网站!",
      "product.name": "超棒的产品",
      "price": "价格:{price, number, currency}",
      "date": "今天是 {today, date, ::yyyyMMdd}"
    }

    这里面,greetingwelcomeMessage等等就是key,后面是对应语言的文本。注意,key的设计要统一规范,方便管理和维护。{name}{price}{today} 这些是占位符,后面我们会讲怎么替换。

  • 语言包的加载

    动态加载语言包是最佳实践。这样可以避免一次性加载所有语言,减少初始加载时间。你可以使用fetch或者XMLHttpRequest来加载JSON文件。

    async function loadLocale(locale) {
      try {
        const response = await fetch(`./locales/${locale}.json`);
        if (!response.ok) {
          throw new Error(`Failed to load locale ${locale}`);
        }
        const data = await response.json();
        return data;
      } catch (error) {
        console.error('Error loading locale:', error);
        return null;
      }
    }

    这个函数会根据传入的locale参数,加载对应的JSON文件。

第二部分:i18n库的选择与使用

自己手写i18n逻辑当然可以,但没必要。现在有很多优秀的i18n库,能帮你省不少事。 比较流行的库有:

  • i18next: 功能强大,支持多种框架,社区活跃。
  • FormatJS (React Intl): 由Yahoo维护,专门为React设计的。
  • Polymer i18n: 为Polymer框架设计的。

这里我们以 i18next 为例,演示一下怎么用。

  • 安装 i18next

    npm install i18next i18next-browser-languagedetector i18next-http-backend
  • 配置 i18next

    import i18next from 'i18next';
    import HttpApi from 'i18next-http-backend';
    import LanguageDetector from 'i18next-browser-languagedetector';
    
    i18next
      .use(HttpApi)
      .use(LanguageDetector)
      .init({
        fallbackLng: 'en', // 默认语言
        debug: true, // 开启debug模式,方便调试
        detection: {
          order: ['localStorage', 'cookie', 'navigator', 'htmlTag', 'path', 'subdomain'],
          lookupLocalStorage: 'i18nextLng',
          lookupCookie: 'i18next',
        },
        backend: {
          loadPath: '/locales/{{lng}}.json', // 语言包的路径
        },
      });
    
    export default i18next;

    这个配置做了几件事:

    • fallbackLng: 设置默认语言,如果找不到用户设置的语言,就用这个。
    • debug: 开启debug模式,方便在控制台查看i18next的运行情况。
    • detection: 设置语言检测的顺序。 这里尝试从localStorage、cookie、浏览器设置等地方获取用户设置的语言。
    • backend: 设置语言包的加载路径。
  • 使用 i18next

    import i18next from './i18n'; // 引入配置好的i18next
    import { useEffect, useState } from 'react';
    
    function MyComponent() {
      const [greeting, setGreeting] = useState('');
    
      useEffect(() => {
        i18next.on('languageChanged', (lng) => {
          setGreeting(i18next.t('greeting', { name: 'World' }));
        });
        setGreeting(i18next.t('greeting', { name: 'World' }));
      }, []);
    
      return (
        <div>
          <h1>{greeting}</h1>
          <p>{i18next.t('welcomeMessage')}</p>
        </div>
      );
    }
    
    export default MyComponent;

    这里我们用i18next.t()函数来获取翻译后的文本。 第一个参数是key,第二个参数是占位符的值。

第三部分:Pluralization(复数形式)

不同语言对复数的处理方式不一样。 比如英语,只有单数和复数两种形式。 但俄语有三种,阿拉伯语有六种! 所以,在i18n里,处理复数形式是个大问题。

  • i18next 的复数支持

    i18next 通过在key后面加上_zero_one_two_few_many_other来区分不同的复数形式。

    // en.json
    {
      "itemCount_zero": "No items",
      "itemCount_one": "One item",
      "itemCount_other": "{count} items"
    }
    
    // zh-CN.json
    {
      "itemCount_zero": "没有物品",
      "itemCount_one": "一个物品",
      "itemCount_other": "{count} 个物品"
    }
    import i18next from './i18n';
    
    function displayItemCount(count) {
      return i18next.t('itemCount', { count });
    }
    
    console.log(displayItemCount(0)); // No items (en) / 没有物品 (zh-CN)
    console.log(displayItemCount(1)); // One item (en) / 一个物品 (zh-CN)
    console.log(displayItemCount(5)); // 5 items (en) / 5 个物品 (zh-CN)

    i18next 会根据count的值,自动选择对应的复数形式。

第四部分:日期、时间和数字的格式化

不同国家/地区对日期、时间和数字的格式化方式也不一样。 比如美国用MM/DD/YYYY,欧洲用DD/MM/YYYY。 数字的小数点和千位分隔符也不一样。

  • JavaScript 内置的 Intl 对象

    JavaScript 提供了内置的 Intl 对象,专门用来处理国际化相关的问题。

    • Intl.DateTimeFormat: 格式化日期和时间。

      const now = new Date();
      
      const enUsFormatter = new Intl.DateTimeFormat('en-US', {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
      });
      
      const deDeFormatter = new Intl.DateTimeFormat('de-DE', {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
      });
      
      console.log(enUsFormatter.format(now)); // July 26, 2024
      console.log(deDeFormatter.format(now)); // 26. Juli 2024
    • Intl.NumberFormat: 格式化数字。

      const number = 1234567.89;
      
      const enUsFormatter = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'USD',
      });
      
      const deDeFormatter = new Intl.NumberFormat('de-DE', {
        style: 'currency',
        currency: 'EUR',
      });
      
      console.log(enUsFormatter.format(number)); // $1,234,567.89
      console.log(deDeFormatter.format(number)); // 1.234.567,89 €
  • 在 i18next 中使用 Intl 对象

    i18next 可以和 Intl 对象结合使用,实现更灵活的格式化。

    // en.json
    {
      "date": "Today is {{value, date}}",
      "price": "Price: {{value, number}}"
    }
    
    // js
    import i18next from './i18n';
    
    i18next.services.formatter.add('date', (value, lng, options) => {
      const formatter = new Intl.DateTimeFormat(lng, options);
      return formatter.format(value);
    });
    
    i18next.services.formatter.add('number', (value, lng, options) => {
      const formatter = new Intl.NumberFormat(lng, options);
      return formatter.format(value);
    });
    
    console.log(i18next.t('date', { value: new Date() }));
    console.log(i18next.t('price', { value: 1234.56 }));

    这里我们给 i18next 添加了两个自定义的formatter,分别用来格式化日期和数字。

第五部分:RTL(Right-to-Left)支持

有些语言是从右往左写的,比如阿拉伯语和希伯来语。 为了支持这些语言,你需要做一些额外的处理。

  • HTML 的 dir 属性

    给 HTML 元素添加 dir="rtl" 属性,可以改变文本的阅读方向。

    <div dir="rtl">
      This text will be displayed from right to left.
    </div>
  • CSS 的调整

    RTL 语言的布局和LTR语言是相反的。 你需要调整CSS样式,比如把float: left改成float: right

    /* LTR */
    .container {
      float: left;
    }
    
    /* RTL */
    [dir="rtl"] .container {
      float: right;
    }
  • i18next 的 RTL 支持

    i18next 可以根据当前语言,自动添加 dir 属性到 <html> 标签上。

    i18next.on('languageChanged', (lng) => {
      document.documentElement.setAttribute('dir', i18next.dir(lng));
    });

第六部分:其他注意事项

  • 翻译质量

    翻译质量是i18n的关键。 最好请专业的翻译人员来翻译,或者使用机器翻译后再人工校对。

  • 测试

    一定要对不同语言进行测试,确保应用在各种语言环境下都能正常工作。

  • 语言切换

    提供方便的语言切换功能,让用户可以自由选择自己喜欢的语言。

  • 持续集成

    把i18n集成到持续集成流程中,确保每次代码提交都能自动检查i18n相关的问题。

  • 处理特殊字符

    注意处理不同语言的特殊字符,比如中文的引号和英文的引号不一样。

  • 字符串连接

    尽量避免在代码中拼接字符串,因为不同语言的语序可能不一样。

    // 错误的做法
    const message = "You have " + count + " messages.";
    
    // 正确的做法
    const message = i18next.t('message', { count });
    // en.json
    {
      "message": "You have {{count}} messages."
    }
    
    // zh-CN.json
    {
      "message": "你有 {{count}} 条消息。"
    }

总结

国际化和本地化是个复杂但有趣的过程。 选好工具,规范流程,多做测试,你的应用就能走向世界啦! 希望今天的讲解对你有所帮助。 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注