File Comparison Tool
前言
一款需要多國語言的應用(真正受歡迎的至少需要中文和英文,不管是PC端還是移動端),那么應用程序開發過程中定義的字串文件就需要進行翻譯,而這項工作在大公司往往是多人(甚至是多部門)協同完成。比如移動端開發常見的為strings.xml,默認是在res/values目錄下,嚴格來說該目錄對應的是英文環境,而中文簡體環境規范的目錄為res/values-zh-rCH。
需求
那么問題來了,開發人員定義好應用中需要用到的字串(最初可能是英文,也可能是中文)並且送翻(提交給專門的翻譯人員進行字串翻譯)后,怎么判斷返回的翻譯結果是完整的呢?或者說,中途接手一個項目,怎么快速掌握原來的字串翻譯情況呢?
之前有篇文章講過Lint工具可以分析出項目中資源冗余及翻譯不全等問題,但給出的信息不夠簡潔,用其來完成專門的字串翻譯文件對比並不是很合適。通過下圖可以看出,對於某一條字串的翻譯缺失,Lint給出的信息是什么樣的。
協同工作的好處是,項目文件的commit、pull、push及review均可以在項目管理工具或者工具對應的命令行環境中進行。而review還可以在項目對應的遠程網頁中進行,便於管理者查看新提交的文件到底進行了哪些改動。但是其和文件比較工具(如beyond compare)類似,只能一個一個文件地查看,若想提取出所有的修改結果(或者不同點),只能認為地復制與黏貼到自己建立的文件中。
所以,若想以最初的strings.xml為基礎,查看其它語言環境的翻譯情況,並將未翻譯的字串統一寫入到一個特定的文件中,就需要自己進行實現了。
實現
用來實現該功能的語言平台為Eclipse中的Java,過程大致可以分為四步:
1、 以項目res目錄為起始路徑,獲取其中包含的所有子文件夾;
2、 在1的基礎上,獲取名稱中包含values的文件夾,因為其他文件夾和strings.xml無關(當然,這是一般而言的);
3、 在2的基礎上,獲取存在的strings.xml的完整路徑;
4、 文件內容對比,針對最初的strings.xml文件中的某一條字串(需要翻譯的,即不包括文件定義頭<?xml ... ?>、<resources>…</resources>及顯示說明不需翻譯的字串等),若其他語言環境對應的strings.xml文件中不存在該字串的翻譯,那么就將該字串的行號和內容寫入結果文件。為了方便閱讀和修改,結果是以文件為單位,而不是像Lint工具那樣以字串為單位,即原來是某條字串未進行翻譯的國家有A、B、C、D、……,而現在是某個語言環境未翻譯的字串有A、B、C、D、……;
完整的Java實現代碼如下:
1 package com.filediff; 2 3 import java.awt.image.WritableRaster; 4 import java.io.BufferedReader; 5 import java.io.File; 6 import java.io.FileNotFoundException; 7 import java.io.FileReader; 8 import java.io.FileWriter; 9 import java.io.IOException; 10 import java.io.Writer; 11 import java.time.chrono.JapaneseChronology; 12 import java.util.ArrayList; 13 import java.util.Date; 14 15 import javax.swing.JFrame; 16 17 @SuppressWarnings("serial") 18 public class FileDiff extends JFrame { 19 20 //resources root path 21 private static String rootDir = "res"; 22 //directory name values 23 private static String dirFilter = "values"; 24 //file name 25 private static String fileName = "strings.xml"; 26 //all directory in rootDir 27 private static String[] subDirs = null; 28 //all directory(name contain values) in rootDir 29 private static ArrayList<String> valuesDirs = new ArrayList<String>(); 30 //all file full names as strings.xml 31 private static ArrayList<String> fileFullNames = new ArrayList<String>(); 32 //result file name 33 private static String result = "Result.xml"; 34 private static int fileCounts = 0; 35 private static int fileIndex = 0; 36 private static String indicate = "name="; 37 38 private static String preDate = new Date().toString()+"\n\n"; 39 40 public static void main(String[] args) throws FileNotFoundException { 41 getSubDirs(); 42 getValuesDirs(); 43 getfileFullNames(); 44 45 fileCounts = fileFullNames.size(); 46 47 //打開一個寫文件器,構造函數中的第二個參數true表示以追加形式寫文件 48 FileWriter writer; 49 try { 50 writer = new FileWriter(result); 51 writer.write(preDate); 52 writer.write("與"+fileFullNames.get(0)+"相比, "); 53 writer.close(); 54 } catch (IOException e) { 55 // TODO Auto-generated catch block 56 e.printStackTrace(); 57 } 58 //fileCounts 59 for(fileIndex=1;fileIndex<fileCounts;++fileIndex){ 60 compareFiles(); 61 } 62 } 63 64 private static void compareFiles(){ 65 FileWriter writer; 66 try { 67 writer = new FileWriter(result, true); 68 String preContent2 = "\n\n"+fileFullNames.get(fileIndex)+"未翻譯的字串為: \n"; 69 writer.write(preContent2); 70 71 File fileSrc = new File(fileFullNames.get(0)); 72 File fileDes = new File(fileFullNames.get(fileIndex)); 73 BufferedReader readerSrc = null; 74 BufferedReader readerDes = null; 75 try { 76 readerSrc = new BufferedReader(new FileReader(fileSrc)); 77 String stringSrc = null; 78 String stringDes = null; 79 boolean contantFlag = false; 80 int line = 1; 81 // 一次讀入一行,直到讀入null為文件結束 82 while ((stringSrc = readerSrc.readLine()) != null) { 83 if(stringSrc.contains(indicate) && !stringSrc.contains("translatable=\"false\"")){ 84 String str1 = stringSrc.substring(stringSrc.indexOf("=")+2); 85 String str2 = str1.substring(0,str1.indexOf("\"")); 86 readerDes = new BufferedReader(new FileReader(fileDes)); 87 contantFlag = false; 88 while ((stringDes = readerDes.readLine()) != null) { 89 if(stringDes.contains(str2)){ 90 contantFlag = true; 91 break; 92 } 93 } 94 if (!contantFlag) { 95 writer.write("Line number and content is: "+line+", "+stringSrc+"\n"); 96 } 97 } 98 line++; 99 } 100 readerSrc.close(); 101 } catch (IOException e) { 102 e.printStackTrace(); 103 } finally { 104 if (readerSrc != null) { 105 try { 106 readerSrc.close(); 107 } catch (IOException e1) { 108 } 109 } 110 } 111 112 writer.close(); 113 } catch (IOException e) { 114 // TODO Auto-generated catch block 115 e.printStackTrace(); 116 } 117 } 118 119 //get all directory in rootDir 120 private static void getSubDirs(){ 121 File fileDirRoot = new File(rootDir); 122 subDirs = fileDirRoot.list(); 123 } 124 125 //get all directory(name contain values) in rootDir 126 private static void getValuesDirs(){ 127 for(int i=0;i<subDirs.length;++i){ 128 if(subDirs[i].contains(dirFilter)){ 129 valuesDirs.add(subDirs[i]); 130 } 131 } 132 } 133 134 //get all fileFullNames(contain strings.xml) in rootDir 135 private static void getfileFullNames(){ 136 for(int i=0;i<valuesDirs.size();++i){ 137 File file = new File(rootDir+"\\"+valuesDirs.get(i)+"\\"+fileName); 138 if(file.exists()){ 139 fileFullNames.add(rootDir+"\\"+valuesDirs.get(i)+"\\"+fileName); 140 } 141 } 142 } 143 }
從代碼可以看出,以上描述的四個步驟對應的方法分別為getSubDirs()、getValuesDirs()、getfileFullNames()、compareFiles()。
在結果文件Result.xml的開頭,寫入了當前時間,其對應字串的獲取方式為:new Date().toString()。
注意,如代碼138-140行所示,在將某個語言環境對應的strings.xml文件完整名稱添加入ArrayList對象fileFullNames中之前,對其存在性進行了判斷,也可以在后面的文件比較環節進行個文件的存在性判斷,但不能不判斷就進行文件的讀取(多余,也容易引起異常)。
res目錄下的文件夾獲取情況:
res目錄下的名稱中包含values的文件夾獲取情況:
res目錄下的名稱中包含values的文件夾中strings.xml的完整名稱獲取情況:
所有翻譯字串最終的對比結果文件Result.xml的部分內容為:
有了這樣的描述,就很容易查看當前多國語言的翻譯情況了。這里的字串是隨意敲的,不然命名的level也太低了。
總結
雖然實現的功能具有局限性,但已經可以滿足上面的需求。正是因為一般通用的工具不能完成項目開發過程中遇到的所有情況,所以才要自己動手針對特定的需求進行實現。這篇文章只是起到拋磚引玉的作用,對於其他各種各樣的場景,實現的思路還是相同的。
歡迎感興趣的朋友一起討論、學習與進步!