前言
這是我第一次發博客,2020年立個flag以后要經常發。
最近應公司上層要求,需要將現有項目盡快支持多語言,而中文內容可以找專業人員翻譯。那么咱們說干就干,首先我們項目的前端是用vue寫的spa程序且組件方面用的element ui,那么自然而然想到用vue官方推薦的vue i18n,我快速過了下i18n整個Guide官方文檔,看來使用很簡單,主要步驟就是:
- npm 安裝vue i18n插件
- 定義多語言資源字典對象
- 實例化vue i18n實例
- vue實例上掛載vue i18n 實例
這里有一個最簡單的實例
相信大家都看就懂,但大家有沒想過,我前面說了公司要求是把現有項目盡快支持多語言,那說明我們的項目已經存在大量的代碼。duang,尼瑪,那不是做“定義多語言資源字典對象”是一個體力活? 要知道,我們這個前端項目至少有成百上千個頁面,如果讓我一個一個的翻譯里面的中文跟換成vue i18n要求的$t('key'),那估計我跟團隊得累死呀!!!所以,為了不干體力活,就有了這篇文章。
先隨便拿一個前端文件看看最終的效果圖:
自動替換前:

自動替換后:

感覺如何?最終的代碼點這里
下面我們來詳細介紹下實現的各個步驟
要想替換前端代碼為vue i18n的語法,就涉及到自己編寫一套程序來實現准確替換,在替換之前,我們需要考慮:
- 可以將
<template>跟<script>里面的中文代碼准確找出來(需要排除注釋等特殊情況,至少能替換98%以上的代碼,少量代碼可以自己手動替換) - 可以將中文導出excel文件,好讓專業的人可以進行翻譯
- 可以將翻譯后的excel文件生成我們代碼要求的“定義多語言資源字典對象”
一,將所有中文Key找出來
由於vue i18n的資源key是可以包含中文英文跟特殊字符的,所以我們可以直接將文字直接當成key,這樣代碼中的中文信息就算換成多語言函數后也照樣能很容易讀懂,那么這里直接上這塊核心的.net core代碼將所有key找出來(自然想到正則表達式去匹配),並保持到一個.txt文件
/// <summary>
/// 抽離代碼文件中的中文到文件
/// </summary>
/// <param name="filePath">代碼文件路徑</param>
/// <param name="keyFilePath">需要保持的包含所有中文字典的文本文件路徑</param>
static void CreateKeyTxt(string filePath, string keyFilePath)
{
var regex = new Regex(@"((?<key>\w+) *= *['""](?<str>([^'""]*[\u4e00-\u9fa5]+[^'""]*))['""])|((?<start>[`""'>}]*)(?<str>[^\s`""'>}]*[\u4e00-\u9fa5]+[^\s`""'<{]*)(?<end>[`""'<{]*))");
string fileContent = File.ReadAllText(filePath);
var chineseMatchs = regex.Matches(fileContent);
// 有中文需要處理
if (chineseMatchs.Count > 0)
{
Dictionary<string, string> lines = new Dictionary<string, string>();
foreach (Match htmlMatch in chineseMatchs)
{
var str = htmlMatch.Groups["str"].Value.TrimEnd(':');
if (str.Contains("//") || str.Contains("/*") || str.Contains("*/") || str.Contains("<!--") || str.Contains("微軟雅黑"))
{
continue;
}
lines[str] = "";
}
using (StreamWriter fs = new StreamWriter(keyFilePath, true, Encoding.UTF8))
{
foreach (var line in lines)
{
fs.WriteLine(line.Key);
}
}
}
}
然后,你就可以拿這這個包含所有需要翻譯的內容,高高興興拿給翻譯人員讓他們辛苦勞作了!
二,根據包含所有中文Key跟翻譯內容的excel生成vue i18n要求的“定義多語言資源字典對象”文件
這個步驟其實就是生成兩個js文件,一個是zh-cn.js中文資源文件,一個是比如en.js的英文資源文件,而文件的內容就是簡單的K-V文件,比如:
export default {
"取 消": "CANCEL",
"確 定": "OK",
"取消": "CANCEL",
"確定": "OK",
"確認": "OK",
"@表示RR,- 表示AA": "@ is RR, - is AA",
}
主要代碼是:
static void SaveI18N()
{
// 需要生成或者更新的i18n js資源文件夾地址
var i18nResourceOutputFolderPath = Configuration["i18nResourceOutputFolderPath"];
// 需要生成或者更新的i18n js資源文件名
var i18nResourceFileName = Configuration["i18nResourceFileName"];
if (string.IsNullOrEmpty(i18nResourceOutputFolderPath))
{
throw new ApplicationException("失敗:請先配置需要生成或者更新的i18n js資源文件夾地址");
}
if (string.IsNullOrEmpty(i18nResourceFileName))
{
throw new ApplicationException("失敗:請先配置需要生成或者更新的i18n js資源文件名");
}
// 獲取前端資源字典文件數據
Dictionary<string, string> chineseDic = new Dictionary<string, string>();
Dictionary<string, string> tDic = new Dictionary<string, string>();
for (int i = 1; i < ExcelResourceFileData.Rows.Count; i++)
{
var shortName = (ExcelResourceFileData.Rows[i][0].ToString()).Trim();
var chineseContent = (ExcelResourceFileData.Rows[i][1].ToString()).Trim();
var tContent = (ExcelResourceFileData.Rows[i][2].ToString()).Trim();
if (string.IsNullOrEmpty(shortName))
{
throw new ApplicationException($"失敗:在第{i + 1}行存在空白的簡稱");
}
if (string.IsNullOrEmpty(chineseContent))
{
throw new ApplicationException($"失敗:在第{i + 1}行存在空白的中文");
}
var key = $"\"{shortName}\"";
chineseDic[key] = $"\"{chineseContent}\"";
tDic[key] = $"\"{tContent}\"";
}
SaveI18NFile(i18nResourceOutputFolderPath, "zh-cn.js", chineseDic);
SaveI18NFile(i18nResourceOutputFolderPath, i18nResourceFileName, tDic);
}
private static void SaveI18NFile(string i18nResourceOutputFolderPath, string fileName, Dictionary<string, string> resourceDic)
{
resourceDic = GetNewestResourceDic(i18nResourceOutputFolderPath, fileName, resourceDic);
// 構建資源文件內容
StringBuilder newFileContent = new StringBuilder();
newFileContent.AppendLine("export default {");
foreach (var chineseKeyValue in resourceDic)
{
newFileContent.AppendLine($" {chineseKeyValue.Key}: {chineseKeyValue.Value},");
}
newFileContent.AppendLine("}");
File.WriteAllText(Path.Combine(i18nResourceOutputFolderPath, fileName), newFileContent.ToString(), Encoding.UTF8);
}
三,最后當然就是重頭戲,替換中文前端代碼
對於 <template> 里面的代碼,我們需要給property還有innerText分別單獨處理,比如
<el-button @click="onCancel" title="取消此上傳功能">取 消</el-button>
其中的title="取消此上傳功能"這個property是需要替換成:title="$t('取消此上傳功能')" 而innerText 取 消是需要替換成{{$t(取 消)}}的,最終替換為
<el-button @click="onCancel" :title="$t('取消此上傳功能')">{{$t(`取 消`)}}</el-button>
其中對 <template> 的替換核心代碼為:
/// <summary>
/// 替換代碼文件template中的中文為資源key
/// </summary>
/// <param name="input">代碼內容</param>
/// <param name="resourceTypeStr">資源類型</param>
/// <param name="pattern">正則表達式</param>
/// <param name="isProperty">是否是屬性</param>
/// <returns></returns>
static string ReplaceChineseToI18NKeyForTemplate(string input, string resourceTypeStr, string pattern, bool isProperty = false)
{
var htmlMatchs = Regex.Matches(input, pattern, RegexOptions.IgnoreCase);
int changedLength = 0;
foreach (Match htmlMatch in htmlMatchs)
{
var newHtmlMatchIndex = htmlMatch.Index + changedLength;
var chineseWordsMatch = Regex.Match(htmlMatch.Value, wordPattern, RegexOptions.IgnoreCase);
var key = GetResourceKey(chineseWordsMatch.Value);
// key不會空才需要替換
if (!string.IsNullOrEmpty(key))
{
string newHtml;
if (isProperty)
{
newHtml = ":" + Regex.Replace(htmlMatch.Value, wordPattern, "$t('" + key + "')");
}
else
{
newHtml = Regex.Replace(htmlMatch.Value, wordPattern, "{{$t('" + key + "')}}");
}
input = input.Substring(0, newHtmlMatchIndex) + newHtml + input.Substring(newHtmlMatchIndex + htmlMatch.Length);
changedLength += newHtml.Length - htmlMatch.Length;
}
}
return input;
}
而<script> 的替換核心代碼跟<template> 類似,最主要的區別是js代碼里面使用的是this.$t('key')。
到這里我們就將整個前端系統的中文文本代碼全部修改成通過資源key動態顯示對應的語言文本了。
本代碼的一些不足
- 上面的正則只能提取跟替換大部分中文情況,少數情況還需要手動修改;但即使這樣,也比一個一個替換節省了大量的人力物力。
- 對
<script>替換為this.$t('key')中的this有不對的情況,改進方案是在代碼前面import i18n對象,然后將this換成i18n對象,由於時間有限,本文的代碼並沒涉及這塊,但實現起來比較容易。
一些思考
各位,相信你們跟我一樣,認為本文並沒有多少技術亮點,說白了,無非就是運用正則表達式替換一些代碼而已。但我想說的,如果我們一開始就一個頁面一個頁面弄,那得弄多久才能完成老板給我們的任務,所以往往解決問題,需要我們多靜下心來多思考一下,然后運用一些簡單的技術,即可快速實現我們想要的東西。特別是現在是一個快速發展的時代,更需要我們高效的解決問題,這樣才能體現我們的價值。
最后希望大家多多評論,2020年身體健康,過得順心!!!
