react 国际化的实现代码示例


Posted in Javascript onSeptember 14, 2018

背景

楼主最近新接了一个项目,从0开始做,需要做多语言的国际化,今天搞了一下,基本达到了想要的效果, 在这里简单分享下:

一些探索

也说不上是探索吧,就Google了一波, 去gayHub 上找了一个比较成熟的库 react-i18next, 写了一些代码,现将过程分享一下, 附带详细代码,手把手教你实现国际化。

先睹为快

先看一下最后的成果:

// ...
import i18n from '@src/i18n';


// xxx component

 console.log('哈哈哈哈哈i18n来一发:', i18n.t('INVALID_ORDER'));

// ...
render() { 
 // ...
 <button> {i18n.t('INVALID_ORDER')} <button>
}

控制台中:

react 国际化的实现代码示例

对应json 中的信息:

react 国际化的实现代码示例

开始

原理

原理其实很简单: 字符串替换。

拉取远程的国际化json文件到本地,再根据语言做一个映射就可以了。

废话不多说, 来看代码吧。

先简单看一下目录结构:

react 国际化的实现代码示例

先看一下 config 里面的 相关代码:

env.js:

'use strict';

const fs = require('fs');
const path = require('path');
const paths = require('./paths');
const languages = require('./languages');

// Make sure that including paths.js after env.js will read .env variables.
delete require.cache[require.resolve('./paths')];

const NODE_ENV = process.env.NODE_ENV;
if (!NODE_ENV) {
 throw new Error(
  'The NODE_ENV environment variable is required but was not specified.'
 );
}

// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
var dotenvFiles = [
 `${paths.dotenv}.${NODE_ENV}.local`,
 `${paths.dotenv}.${NODE_ENV}`,
 // Don't include `.env.local` for `test` environment
 // since normally you expect tests to produce the same
 // results for everyone
 NODE_ENV !== 'test' && `${paths.dotenv}.local`,
 paths.dotenv,
].filter(Boolean);

// Load environment variables from .env* files. Suppress warnings using silent
// if this file is missing. dotenv will never modify any environment variables
// that have already been set. Variable expansion is supported in .env files.
// https://github.com/motdotla/dotenv
// https://github.com/motdotla/dotenv-expand
dotenvFiles.forEach(dotenvFile => {
 if (fs.existsSync(dotenvFile)) {
  require('dotenv-expand')(
   require('dotenv').config({
    path: dotenvFile,
   })
  );
 }
});

// We support resolving modules according to `NODE_PATH`.
// This lets you use absolute paths in imports inside large monorepos:
// https://github.com/facebookincubator/create-react-app/issues/253.
// It works similar to `NODE_PATH` in Node itself:
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims.
// https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421
// We also resolve them to make sure all tools using them work consistently.
const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
 .split(path.delimiter)
 .filter(folder => folder && !path.isAbsolute(folder))
 .map(folder => path.resolve(appDirectory, folder))
 .join(path.delimiter);

// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in Webpack configuration.
const REACT_APP = /^REACT_APP_/i;

function getClientEnvironment(publicUrl) {
 const raw = Object.keys(process.env)
  .filter(key => REACT_APP.test(key))
  .reduce(
   (env, key) => {
    env[key] = process.env[key];
    return env;
   },
   {
    // Useful for determining whether we're running in production mode.
    // Most importantly, it switches React into the correct mode.
    NODE_ENV: process.env.NODE_ENV || 'development',
    // Useful for resolving the correct path to static assets in `public`.
    // For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
    // This should only be used as an escape hatch. Normally you would put
    // images into the `src` and `import` them in code to get their paths.
    PUBLIC_URL: publicUrl,
    LANGUAGE: {
     resources: languages.resources,
     defaultLng: languages.defaultLng
    },
    COUNTRY: process.env.COUNTRY
   }
  );
 // Stringify all values so we can feed into Webpack DefinePlugin
 const stringified = {
  'process.env': Object.keys(raw).reduce((env, key) => {
   env[key] = JSON.stringify(raw[key]);
   return env;
  }, {}),
 };

 return { raw, stringified };
}

module.exports = getClientEnvironment;

主要看lannguage 相关的代码就好了, 其他的都create-react-app 的相关配置, 不用管。

再看下 language.js 里面的逻辑:

const path = require('path');
const paths = require('./paths');
const localesHash = require('../i18n/localesHash');
const resourcesHash = require('../i18n/resourcesHash');

const COUNTRY = process.env.COUNTRY || 'sg';
const country = (COUNTRY).toUpperCase();
const defaultLng = localesHash[country][0];

const langs = [
 'en',
 'id'
];

const prefixLangs = [];
const entries = {};

for (let i = 0, len = langs.length; i < len; i++) {
 const prefixLang = `dict_${langs[i]}`
 prefixLangs.push(prefixLang)
 entries[prefixLang] = path.resolve(paths.appSrc, `../i18n/locales/${langs[i]}.json`)
}

const resources = {
 [defaultLng]: {
  common: resourcesHash[defaultLng]
 }
}

exports.resources = resources;
exports.defaultLng = defaultLng;

逻辑也比较简单, 根据语言列表把对应的json 内容加进来。 作为示例,这里我设置的是 英文 和 印尼语。

下面看 i18n 文件里面的内容:

locales 里面放的是语言的json 文件, 内容大概是:

{
  "msg_Created": "Pesanan telah terbuat"
  // ...
}

localesHash.js:

module.exports = {
 SG: ['en'],
 ID: ['id']
}

resourcesHash.js:

module.exports = {
 'en': require('./locales/en.json'),
 'id': require('./locales/id.json')
}

index.js

const path = require('path')
const fs = require('fs')
const fetch = require('isomorphic-fetch')
const localesHash = require('./localesHash')

const argv = process.argv.slice(2)
const country = (argv[0] || '').toUpperCase()

const i18nServerURI = locale => {
 const keywords = {
  'en': 'en',
  'id': 'id'
 }
 const keyword = keywords[locale]
 return keyword === 'en'
  ? 'xxx/json/download'
  : `/${keyword}/json/download`
}

const fetchKeys = async (locale) => {
 const uri = i18nServerURI(locale)
 console.log(`Downloading ${locale} keys...\n${uri}`)
 const respones = await fetch(uri)
 const keys = await respones.json()
 return keys
}

const access = async (filepath) => {
 return new Promise((resolve, reject) => {
  fs.access(filepath, (err) => {
   if (err) {
    if (err.code === 'EXIST') {
     resolve(true)
    }
    resolve(false)
   }
   resolve(true)
  })
 })
}

const run = async () => {
 const locales = localesHash[country] || Object
  .values(localesHash)
  .reduce(
   (previous, current) =>
    previous.concat(current), []
  )
 if (locales === undefined) {
  console.error('This country is not in service.')
  return
 }
 for (const locale of locales) {
  const keys = await fetchKeys(locale)
  const data = JSON.stringify(keys, null, 2)
  const directoryPath = path.resolve(__dirname, 'locales')
  if (!fs.existsSync(directoryPath)) {
   fs.mkdirSync(directoryPath)
  }
  const filepath = path.resolve(__dirname, `locales/${locale}.json`)
  const isExist = await access(filepath)
  const operation = isExist ? 'update' : 'create'
  console.log(operation)
  fs.writeFileSync(filepath, `${data}\n`)
  console.log(`${operation}\t${filepath}`)
 }
}

run();

再看下src 中的配置:

i18nn.js

import i18next from 'i18next'
import { firstLetterUpper } from './common/helpers/util';
const env = process.env;
let LANGUAGE = process.env.LANGUAGE;
LANGUAGE = typeof LANGUAGE === 'string' ? JSON.parse(LANGUAGE) : LANGUAGE

const { defaultLng, resources } = LANGUAGE

i18next
 .init({
  lng: defaultLng,
  fallbackLng: defaultLng,
  defaultNS: 'common',
  keySeparator: false,
  debug: env.NODE_ENV === 'development',
  resources,
  interpolation: {
   escapeValue: false
  },
  react: {
   wait: false,
   bindI18n: 'languageChanged loaded',
   bindStore: 'added removed',
   nsMode: 'default'
  }
 })

function isMatch(str, substr) {
 return str.indexOf(substr) > -1 || str.toLowerCase().indexOf(substr) > -1
}

export const changeLanguage = (locale) => {
 i18next.changeLanguage(locale)
}

// Uppercase the first letter of every word. abcd => Abcd or abcd efg => Abcd Efg
export const tUpper = (str, allWords = true) => {
 return firstLetterUpper(i18next.t(str), allWords)
}

// Uppercase all letters. abcd => ABCD
export const tUpperCase = (str) => {
 return i18next.t(str).toUpperCase()
}

export const loadResource = lng => {
 let p;

 return new Promise((resolve, reject) => {
  if (isMatch(defaultLng, lng)) resolve()

  switch (lng) {
   case 'id':
    p = import('../i18n/locales/id.json')
    break
   default:
    p = import('../i18n/locales/en.json')
  }

  p.then(data => {
   i18next.addResourceBundle(lng, 'common', data)
   changeLanguage(lng)
  })
   .then(resolve)
   .catch(reject)
 })
}

export default i18next
// firstLetterUpper

export const firstLetterUpper = (str, allWords = true) => {
 let tmp = str.replace(/^(.)/g, $1 => $1.toUpperCase())
 if (allWords) {
  tmp = tmp.replace(/\s(.)/g, $1 => $1.toUpperCase())
 }
 return tmp;
}

这些准备工作做好后, 还需要把i18n 注入到app中:

index.js:

import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import rootReducer from './common/redux/reducers';
import { configureStore } from './common/redux/store';
import { Router } from 'react-router-dom';
import createBrowserHistory from 'history/createBrowserHistory';
import { I18nextProvider } from 'react-i18next';
import i18n from './i18n';
import './common/styles/index.less';
import App from './App';
export const history = createBrowserHistory();

const ROOT = document.getElementById('root');

render(
 <I18nextProvider i18n={i18n}>
  <Provider store={configureStore(rootReducer)} >
   <Router history={history}>
    <App />
   </Router>
  </Provider>
 </I18nextProvider>,
 ROOT
);

如何使用

加入上面的代码后, 控制台会有一些log 信息, 表示语言已经加载好了。

react 国际化的实现代码示例

在具体的业务组件中,使用方法是:

// ...
import i18n from '@src/i18n';

console.log('哈哈哈哈哈i18n来一发:', i18n.t('INVALID_ORDER'));

控制台中:

react 国际化的实现代码示例

对应json 中的信息:

react 国际化的实现代码示例

后面你就可以愉快的加各种词条了。

Tips

我们在src 中的文件中引入了src 目录外的文件, 这是create-react-app 做的限制, 编译会报错, 把它去掉就好了:

react 国际化的实现代码示例

react 国际化的实现代码示例

结语

这里作为例, 就是把语言的json 文件下载下来放到locales 目录里, 如果想实时拉取,要保证文件下载完之后再render app.

类似:

loadResource(getLocale())
 .then(() => {
  import('./app.js')
 })

当然你也可以免了这一步,直接下载好放到工程里来。

大概就是这样,以上就是实现国际化的全部代码,希望对大家有所帮助。也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
使用PHP+JQuery+Ajax分页的实现
Apr 23 Javascript
jquery实现一个简单好用的弹出框
Sep 26 Javascript
javascript中使用new与不使用实例化对象的区别
Jun 22 Javascript
Jquery插件easyUi实现表单验证示例
Dec 15 Javascript
深入浅析JavaScript中的scrollTop
Jul 11 Javascript
js处理层级数据结构的方法小结
Jan 17 Javascript
JavaScript 程序错误Cannot use 'in' operator to search的解决方法
Jul 10 Javascript
浅谈React高阶组件
Mar 28 Javascript
详解js根据百度地图提供经纬度计算两点距离
May 13 Javascript
JS动态显示倒计时效果
Dec 12 Javascript
前端开发之便利店收银系统代码
Dec 27 Javascript
ant-design-vue 时间选择器赋值默认时间的操作
Oct 27 Javascript
详解Vue项目中出现Loading chunk {n} failed问题的解决方法
Sep 14 #Javascript
vue3.0 CLI - 2.5 - 了解组件的三维
Sep 14 #Javascript
vue3.0 CLI - 2.4 - 新组件 Forms.vue 中学习表单
Sep 14 #Javascript
vue3.0 CLI - 2.3 - 组件 home.vue 中学习指令和绑定
Sep 14 #Javascript
vue3.0 CLI - 2.2 - 组件 home.vue 的初步改造
Sep 14 #Javascript
Element-ui之ElScrollBar组件滚动条的使用方法
Sep 14 #Javascript
node中的session的具体使用
Sep 14 #Javascript
You might like
PHP 安全检测代码片段(分享)
2013/07/05 PHP
php对csv文件的读取,写入,输出下载操作详解
2013/08/10 PHP
PHP实现将视频转成MP4并获取视频预览图的方法
2015/03/12 PHP
PHP易混淆知识整理笔记
2015/09/24 PHP
Yii框架中jquery表单验证插件用法示例
2016/10/18 PHP
详解ThinkPHP3.2.3验证码显示、刷新、校验
2016/12/29 PHP
在textarea中屏蔽js的某个function的javascript代码
2007/04/20 Javascript
jQuery Ajax文件上传(php)
2009/06/16 Javascript
js控制淡入淡出示例代码
2013/11/12 Javascript
JavaScript四种调用模式和this示例介绍
2014/01/02 Javascript
js获取光标位置和设置文本框光标位置示例代码
2014/01/09 Javascript
jQuery+ajax实现鼠标单击修改内容的思路
2014/06/29 Javascript
JS获取iframe中marginHeight和marginWidth属性的方法
2015/04/01 Javascript
Bootstrap每天必学之表格
2015/11/23 Javascript
微信小程序-横向滑动scroll-view隐藏滚动条
2017/04/20 Javascript
详解微信小程序 template添加绑定事件
2017/06/23 Javascript
基于canvas粒子系统的构建详解
2017/08/31 Javascript
[03:18]【TI9纪实】社区大触GL与木木
2019/08/25 DOTA
Python中列表、字典、元组、集合数据结构整理
2014/11/20 Python
Python实现的简单文件传输服务器和客户端
2015/04/08 Python
python使用wmi模块获取windows下硬盘信息的方法
2015/05/15 Python
详解tensorflow训练自己的数据集实现CNN图像分类
2018/02/07 Python
python opencv之SIFT算法示例
2018/02/24 Python
使用python将多个excel文件合并到同一个文件的方法
2019/07/09 Python
python实现宿舍管理系统
2019/11/22 Python
PyCharm汉化安装及永久激活详细教程(靠谱)
2020/01/16 Python
世界上最大的家庭自动化公司:Smarthome
2017/12/20 全球购物
企业管理培训感言
2014/01/27 职场文书
养生餐厅创业计划书范文
2014/03/26 职场文书
南京青奥会口号
2014/06/12 职场文书
社区端午节活动总结
2015/02/11 职场文书
2015年商场工作总结
2015/04/27 职场文书
六一晚会主持词开场白
2015/05/28 职场文书
关于运动会的广播稿
2015/08/19 职场文书
中秋节作文(五年级)之关于月亮
2019/09/11 职场文书
Golang实现可重入锁的示例代码
2022/05/25 Golang