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 相关文章推荐
JavaScript 程序编码规范
Nov 23 Javascript
jquery操作select大全
Apr 25 Javascript
jQuery中extend函数详解
Feb 13 Javascript
JavaScript数据结构与算法之栈详解
Mar 12 Javascript
JS获取当前脚本文件的绝对路径
Mar 02 Javascript
vue源码学习之Object.defineProperty对象属性监听
May 30 Javascript
JavaScript事件委托原理与用法实例分析
Jun 07 Javascript
Vue 框架之动态绑定 css 样式实例分析
Nov 14 Javascript
Vue 实现分页与输入框关键字筛选功能
Jan 02 Javascript
在Vuex中Mutations修改状态操作
Jul 24 Javascript
解决Vue keep-alive 调用 $destory() 页面不再被缓存的情况
Oct 30 Javascript
JavaScript实现两个数组的交集
Mar 25 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调用Oracle存储过程
2006/10/09 PHP
php利用iframe实现无刷新文件上传功能的代码
2011/09/29 PHP
php并发对MYSQL造成压力的解决方法
2013/02/21 PHP
Laravel 5框架学习之表单验证
2015/04/08 PHP
CI框架常用经典操作类总结(路由,伪静态,分页,session,验证码等)
2016/11/21 PHP
php源码的使用方法讲解
2019/09/26 PHP
PHP7 其他语言层面的修改
2021/03/09 PHP
纯CSS3实现质感细腻丝滑按钮
2021/03/09 HTML / CSS
Prototype Class对象学习
2009/07/19 Javascript
Array.prototype.slice.apply的使用方法
2010/03/17 Javascript
javascript天然的迭代器
2010/10/29 Javascript
深入理解JavaScript定时机制
2010/10/29 Javascript
关于hashchangebroker和statehashable的补充文档
2011/08/08 Javascript
jquery ajax例子返回值详解
2012/09/11 Javascript
用js获取电脑信息(是使用与IE浏览器)
2013/01/15 Javascript
js+css实现的圆角边框TAB选项卡滑动门代码分享(2款)
2015/08/26 Javascript
jquery 动态增加删除行的简单实例(推荐)
2016/10/12 Javascript
vue-resource 拦截器(interceptor)的使用详解
2017/07/04 Javascript
react中fetch之cors跨域请求的实现方法
2018/03/14 Javascript
Vue中Table组件Select的勾选和取消勾选事件详解
2019/03/19 Javascript
sqlalchemy对象转dict的示例
2014/04/22 Python
CentOS6.5设置Django开发环境
2016/10/13 Python
PyQt5实现简易计算器
2020/05/30 Python
python 实现list或string按指定分段
2019/12/25 Python
python 在右键菜单中加入复制目标文件的有效存放路径(单斜杠或者双反斜杠)
2020/04/08 Python
Python json格式化打印实现过程解析
2020/07/21 Python
5款实用的python 工具推荐
2020/10/13 Python
安装python依赖包psycopg2来调用postgresql的操作
2021/01/01 Python
如何用Python输出一个Fibonacci数列
2016/08/28 面试题
文员个人的求职信范文
2013/09/26 职场文书
经济贸易专业自荐信
2014/06/11 职场文书
创优争先心得体会
2014/09/11 职场文书
工作检讨书大全
2015/01/26 职场文书
西岭雪山导游词
2015/02/06 职场文书
小学六一主持词开场白
2015/05/28 职场文书
Java详细解析==和equals的区别
2022/04/07 Java/Android