前端开发过程当中偶有须要动态配置的国际化,好比:您购物车中的布加迪威龙、LV和《点赞JS强迫症患者》已下架,请从新加入购物车。更有:"我的信息"栏目中的AAA,BBB,CCC不能为空。javascript
笔者在实际开发过程当中找到了三个解决方案,最终获得我的认为较优的实现。前端
最简单也是最容易想到的方法是根据不一样的语言来切换不一样的字符串,弊端显而易见:国际化充斥代码,支持多语言难度大。具体以下:java
// 模拟国际化
const lang = {
data: {
zh: {},
en: {},
['客家话']: {}
},
current: 'zh',
getLang(key) {
const {
data,
current
} = this
return data[current][key] || key
},
setLang(language = 'zh') {
this.current = language
},
getCurrent() {
return this.current
}
}
function badSolution(lan) {
lan && lang.setLang(lan)
const language = lang.getCurrent()
let showMessage = ''
const notFound=['布加迪威龙','LV','《点赞JS强迫症患者》']
switch (language) {
case 'zh':
showMessage = `您购物车中的${notFound.join('、')}已下架,请从新加入购物车`;
break
case 'en':
showMessage = `${notFound.join(',')} in your shopping cart have been removed, please re-add to the shopping cart`;
break
case '客家话':
showMessage = `渔买给${notFound.join('、')}某A,嗯该重新买`;
break
}
return showMessage
}
console.log(badSolution())
// badSolution-zh : 您购物车中的布加迪威龙、LV、《点赞JS强迫症患者》已下架,请从新加入购物车
console.log(badSolution('en'))
// badSolution-en : 布加迪威龙,LV,《点赞JS强迫症患者》 in your shopping cart have been removed, please re-add to the shopping cart
console.log(badSolution('客家话'))
// badSolution-客家话 : 渔买给布加迪威龙、LV、《点赞JS强迫症患者》某A,嗯该重新买
复制代码
很差意思,上面的方案是本人初涉社会社会写的代码,回过头来看,想打屎本身。做为一个强迫症患者,确定要重构一下,这时候就想起了String的replace API,便产生了下面代码:安全
function solution(lan) {
lan && lang.setLang(lan)
// 增长模拟数据,实际开发过程当中在国际化文件中配置
lang.data = {
zh: {
notFound: `您购物车中的notFound已下架,请从新加入购物车`,
join: '、'
},
en: {
notFound: `notFound in your shopping cart have been removed, please re-add to the shopping cart`,
join: ','
},
['客家话']: {
notFound: `渔买给notFound某A,嗯该重新买`,
join: '、'
}
}
const notFound = ['布加迪威龙', 'LV', '《点赞JS强迫症患者》']
return lang.getLang('notFound').replace('notFound', notFound.join(lang.getLang('join')))
}
console.log('solution-zh : ', solution('zh'))
// solution-zh : 您购物车中的布加迪威龙、LV、《点赞JS强迫症患者》已下架,请从新加入购物车
console.log('solution-en : ', solution('en'))
// solution-en : 布加迪威龙,LV,《点赞JS强迫症患者》 in your shopping cart have been removed, please re-add to the shopping cart
console.log('solution-客家话 : ', solution('客家话'))
// solution-客家话 : 渔买给布加迪威龙、LV、《点赞JS强迫症患者》某A,嗯该重新买
复制代码
上面代码看起来顺眼多了。可是重构代码里国际化的时候发现有些国际化要动态配置两个甚至更多值,这时候弊端体现出来了:多个变量的替换繁琐,占位符命名难(要避免和国际化冲突)服务器
function solutions(lan) {
lan && lang.setLang(lan)
// 增长模拟数据,实际开发过程当中在国际化文件中配置
lang.data = {
zh: {
SectionEmpty: `Section栏目中的cantEmpty不能为空`,
join: '、'
},
en: {
SectionEmpty: `cantEmpty in the Section column cannot be empty`,
join: ','
},
['客家话']: {
SectionEmpty: `Section栏目中给cantEmpty嗯扣以空`,
join: '、'
}
}
const notFound = ['AAA', 'BBB', 'CCC']
const section = '我的信息'
return lang.getLang('SectionEmpty')
.replace('cantEmpty', notFound.join(lang.getLang('join')))
.replace('Section', section)
}
console.log('solutions-zh : ', solutions('zh'))
// solutions-zh : 我的信息栏目中的AAA、BBB、CCC不能为空
console.log('solutions-en : ', solutions('en'))
// solutions-en : AAA,BBB,CCC in the 我的信息 column cannot be empty
console.log('solutions-客家话 : ', solutions('客家话'))
// solutions-客家话 : 我的信息栏目中给AAA、BBB、CCC嗯扣以空
复制代码
上面方案差强人意,动态国际化少且变量少的时候能够采用,做为一个强迫症患者,孰能忍。工具
这时候Node服务器的template技术给了我灵感:能够创建string template,经过传入上下文,调用方法获得对应国际化。因为引入三方插件成本高,公司安全策略要求高,不能随便引入插件,因此就没有搜索相关的插件,想一想功能也不复杂,就动手实践了一个。我采用的是工具类的形式,而不是侵入式的污染String的prototype,如嫌弃调用复杂且项目组容许往prototype上添加方法能够适当改造(改造部分类比能够实现),工具类的实现以下:ui
function isUndefined(val) {
return Object.prototype.toString.call(val).includes('Undefined')
}
/** * 传入'a.b'获得context.a.b * @param {string} link * @param {object} context */
function getValueByKeyLink(link = '', context = {}) {
const keys = link.split('.')
let nextContext = JSON.parse(JSON.stringify(context)) // 为了避免影响外部参数,简单深拷贝
let isFound = true
keys.forEach(key => {
if (!isUndefined(nextContext[key])) {
nextContext = nextContext[key] // 此处有bug
} else {
isFound = false
}
})
return isFound ? nextContext : undefined
}
/** * 字符串模板根据上下文替换 * demo: ; (() => { const str = ` { util } is helpful, { name} can try it. {util} is wanderful, {name} must try it! It also can replace c.cc to { c.cc }. If no match,It would't replace {notFound} or { c. cc} ` const context = { util: 'replaceByContext', name: 'you', c: { cc: 'CCC' } } console.log(replaceByContext(str, context)) // replaceByContext is helpful, you can try it. // replaceByContext is wanderful, you must try it! // It also can replace c.cc to CCC. // If no match,It would't replace {notFound} or { c. cc} })(); * @param {string} str 字符串模板 * @param {object} context 上下文 */
function replaceByContext(str = '', context = {}) {
const reg = /{\s*([A-Za-z0-9\\.\\_]+)\s*}/g
// 去重 匹配 去空格
const matchs = [...new Set(str.match(reg).map(item => item.replace(/\ /g, '')))]
// [ '{util}', '{name}', '{c.cc}', '{notFound}' ]
console.log(matchs)
let replaceTime = matchs.length // 去重后找到4个合法的上下文,要替换4次
while (replaceTime > 0) {
replaceTime--
reg.test(str)
const keyStr = RegExp.$1
const contextValue = getValueByKeyLink(keyStr, context)
if (!isUndefined(contextValue)) { // 有值的时候才替换
// /{name}/g 'you'
str = str.replace(new RegExp(`{\\s*${keyStr}\\s*}`, 'g'), contextValue)
}
}
return str
}
复制代码
终于写完,激动时刻:this
function goodSolution(lan) {
lan && lang.setLang(lan)
// 增长模拟数据,实际开发过程当中在国际化文件中配置
lang.data = {
zh: {
SectionEmpty: `{ Section}栏目中的{notFounds}不能为空`,
join: '、'
},
en: {
SectionEmpty: `{notFounds} in the {Section} column cannot be empty`,
join: ','
},
['客家话']: {
SectionEmpty: `{Section}栏目中给{notFounds}嗯扣以空`,
join: '、'
}
}
const notFounds = ['AAA', 'BBB', 'CCC']
const context = {
Section: '我的信息',
notFounds: notFounds.join(lang.getLang('join')),
}
return replaceByContext(lang.getLang('SectionEmpty'), context)
}
console.log('goodSolution-zh : ', goodSolution('zh'))
// goodSolution-zh : 我的信息栏目中的AAA、BBB、CCC不能为空
console.log('goodSolution-en : ', goodSolution('en'))
// goodSolution-en : AAA,BBB,CCC in the 我的信息 column cannot be empty
console.log('goodSolution-客家话 : ', goodSolution('客家话'))
// goodSolution-客家话 : 我的信息栏目中给AAA、BBB、CCC嗯扣以空
复制代码