2017-2-7
背景
項目原先並沒有考慮到后續國際化的需求,所以凡是用到字符串時,大都是直接寫在了代碼或布局里,比如// 更新秀幣tv_coins.setText("余額coins為:" + getuCoin() + "元" );
或者<TextViewandroid:text="包青天" />現已經寫了十幾萬行代碼,老板突然說要搞國際化(其實是搞一個繁體版,所以下面的策略也僅僅是搞一個繁體版),該咋整呢?
開工之前一定要先想好大致的過程,不然做的時候很可能做很多無用功
大致步驟為:
- 遍歷所有【指定類型】的文件,逐個的【讀取】並獲取文件中的全部內容
- 然后使用【正則】表達式檢索內容,只要檢索到符合條件的字符,則提取出來
- 然后按照不同的條件對檢索出的內容【使用指定的格式】寫入到一個文件中
- 其中,為了易讀及防止命名沖突,【命名】時也要根據不同的來源采用不同的規則
- 最后,還要針對不同情況分別用不同格式后的字符串對搜到的內容進行【替換】
其中,還要考慮一系列復雜的細節問題
處理結果:
大約搞出了3500個
遺留問題:
- 1、某些不需要替換的可能也替換了,極極極少數需要替換的因為某些原因可能並沒有替換……這些可通過調整【正則】表達式規則來解決
- 2、命名時是采用類似【包名(文件夾名)_類名(文件名)_編號(1…2…3)】前面沒問題,但是后面那些數字等編號可能某些挑剔的領導會有意見
適用范圍:
- 1、如果只是為了"應付"工作,或者老板"希望一天搞定而不在意實現方式是否優雅",或者"希望先發一個包出去,后續再優化",那么這個工具類完全能夠達到目的。
- 2、即使此工具類完成的結果可能不夠優雅,但部分功能模塊還是可以使用的,比如"提取出所有中文"。
- 3、對於簡繁體轉換,由於使用一些小工具很容易實現,故基本不耗費人工時間,但是如果想搞一個英文版本,那么人工翻譯是少不的了。
代碼
/*** 作用:提取出Android項目中java文件和xml文件中的中文字符串,並放到strings.xml中<p>* */public class I18NTool {/**要處理的文件的根目錄*/public static final String SEARCH_ROOT_PATH = "e:/test";// public static final String SEARCH_ROOT_PATH = "D:/96/640/國際版/95xiu6.4.0/src/com/lokinfo/m95xiu";// public static final String SEARCH_ROOT_PATH = "D:/96/640/國際版/95xiu6.4.0/res/layout";/**寫入到文件(strings.xml文件)的路徑*/public static final String WRITE_FILE_PATH = "D:/96/640/國際版/95xiu6.4.0/res/values/strings.xml";/**正則表達式:以【"】開頭以【"】結尾,中間包含至少一個中文,且中文【前】可以有任意個任意字符但不能有【"】,且中文【后】還不能有換行符*/public static final String REGEX = "\"[^\"]*[\\u4e00-\\u9fa5]+[^\"\n]*\"";//即【"[^"]*[\u4e00-\u9fa5]+[^"\n]*"】這里是一切操作的基石!// public static final String REGEX = "\".*[\\u4e00-\\u9fa5]+.*\"";///**只遍歷指定格式的文件*/public static final String FILEEXTENSIONS[] = { ".java", ".JAVA", ".xml", ".XML" };/**文件的編碼*/public static final String ENCODING = "UTF8";/**要導的包*/public static final String[] PACKAGE_NAMES = { "import com.lokinfo.m95xiu.util.LanguageUtils;", "import com.dongbai.mm.xiu.R;","import com.lokinfo.m95xiu.application.LokApp;" };private static FilenameFilter FILTER = new FilenameFilter() {public boolean accept(File dir, String name) {if (new File(dir, name).isDirectory()) return true;//如果是目錄直接通過檢索else {for (int i = 0; i < FILEEXTENSIONS.length; i++) {if (name.endsWith(FILEEXTENSIONS[i])) return true;}return false;}}};
public static void main(String[] args) throws IOException {List<File> fileList = new ArrayList<File>();I18NUtils.getDirFiles(SEARCH_ROOT_PATH, FILTER, false, fileList);for (File file : fileList) {I18NUtils.matcherAndReplaceAndWriteToRes(file, ENCODING, REGEX, WRITE_FILE_PATH, PACKAGE_NAMES, true);}System.out.println("已完成");}}
代碼-工具類
/**國際化工具類*/public class I18NUtils {public static final String LINE_SEPARATOR = System.getProperty("line.separator");//行分隔符,linux中為\n,Windows中為\r\n/*** 匹配originalFile中符合regex的字符串,找到后為其命名為name,然后按指定格式以encoding編碼逐個寫入到file中。同時根據不同的類型進行替換* @param originalFile 要處理的文件* @param encoding 文件編碼格式* @param regex 要匹配的正則表達式* @param writeToFilePath 把搜索到的字符串寫到指定文件中,若果文件不存在會自動創建* @param packages 要導入的包* @param isTestMode 是否是測試模式,為true時只打印檢索出的結果,不進行文件的修改。建議先設為true,在確認無誤時再進行改寫*/public static void matcherAndReplaceAndWriteToRes(File originalFile, String encoding, String regex, String writeToFilePath, String[] packages,boolean isTestMode) {// 1、讀取原始文件中的內容String contentString = readFileToString(originalFile, encoding);//2、獲取文件相關信息,包括:formatName 命名;isJava 是否是Java中的字符串;className 類名Map<String, Object> map = getInfosFromFile(originalFile);//3、通過正則匹配Pattern pattern = Pattern.compile(regex);Matcher matcher = pattern.matcher(contentString);int index = 0;File writeToFile = new File(writeToFilePath);String matcheString, matcheStringName, formatResString, formatJavaOrLayoutString;while (matcher.find()) {//逐個遍歷index++;//匹配的子串matcheString = matcher.group();//如果此字符串不適合處理,就不要處理了,不然可能要改半天bugif (isSpecialCase(matcheString)) return;//為此字串命名matcheStringName = (String) map.get("formatName") + "_0" + index;//格式化此匹配的子序列,最終格式為:<string name="【包名_類名_編號】">【字符串】</string>formatResString = " <string name=\"" + matcheStringName + "\">" + matcheString + "</string>" + LINE_SEPARATOR;//把指定字符串寫到指定文件中if (!isTestMode) writeStringToFile(writeToFile, formatResString, encoding, true);if ((boolean) map.get("isJava")) {formatJavaOrLayoutString = "LanguageUtils.getString(" + "LokApp.app().getApplicationContext()" + ", R.string." + matcheStringName + ")";//將當前匹配子串替換為指定字符串contentString = matcher.replaceFirst(formatJavaOrLayoutString);//不能用replaceAll,因為我要對匹配到的字符串逐個單獨命名//導包contentString = importPackage(contentString, packages);} else {formatJavaOrLayoutString = "\"@string/" + matcheStringName + "\"";contentString = matcher.replaceFirst(formatJavaOrLayoutString);}//替換原先的內容if (!isTestMode) writeStringToFile(originalFile, contentString, encoding, false);//重新指定要匹配的內容,否則會陷入死循環matcher = pattern.matcher(contentString);}}//****************************************************************************************************************************//// 匹配到的特殊情況////****************************************************************************************************************************/*** 檢查此字符串是否適合處理。注意:控制台最多能打印1500行* @param matcheString* @return*/public static boolean isSpecialCase(String matcheString) {if (matcheString.contains("%") || matcheString.contains("//")) {//strings.xml中不能有%System.out.println("********************************************************************" + matcheString);return true;}if (matcheString.contains("Log.") || matcheString.contains("%")) {//可能是日志System.out.println("********************************************************************" + matcheString);return true;}if (getKeyStringCount(matcheString, "\"") > 2) {//類似這樣的東西【"包青天", "白乾濤"】System.out.println("********************************************************************" + matcheString);return true;}if (matcheString.length() > 50) {//很可能是大段注釋System.out.println("********************************************************************" + matcheString);return true;}System.out.println(matcheString);return false;}/*** 統計一個子串在整串中出現的次數。注意:("baaab","aa")的結果為1,若需要此匹配結果為2,請按知識更改*/public static int getKeyStringCount(String str, String key) {int index = 0, coun = 0;while (str.indexOf(key, index) != -1) {index = str.indexOf(key, index) + key.length();//("aaa","aa")匹配結果為1;若改為index = str.indexOf(key, index) + 1; 則結果為2coun++;}return coun;}//****************************************************************************************************************************//// 獲取文件信息////****************************************************************************************************************************/*** 從指定文件中提取文件的一些信息,以集合形式返回。當是java文件時【后兩位的包名+類名】,xml時【layout+文件名】* @param file 字符串所在的文件* @return 返回集合中formatName的格式為【m95xiu_login_loginactivity】或【layout_activity_badge】*/public static Map<String, Object> getInfosFromFile(File file) {StringBuilder formatString = new StringBuilder(file.getAbsolutePath());//用一個集合保存解析到的信息Map<String, Object> map = new HashMap<String, Object>();//獲取最后一個分隔符的位置,此分隔符后面即為文件名int lastIndex = formatString.lastIndexOf("\\");//提取文件后綴名。這里沒有判斷是否有后綴名,請使用者自行保證!int dotIndex = formatString.lastIndexOf(".");String fileExtension = formatString.substring(dotIndex);//判斷是java文件還是xml文件if (".java".equalsIgnoreCase(fileExtension)) {map.put("isJava", true);//獲取java文件的類名String className = formatString.substring(lastIndex + 1, dotIndex);map.put("className", className);//為防止命名沖突,替換最后兩個分隔符為下划線for (int i = 0; i < 2; i++) {if (lastIndex > 0) {formatString.replace(lastIndex, lastIndex + 1, "_");lastIndex = formatString.lastIndexOf("\\");}}} else {map.put("isJava", false);map.put("className", "XML文件沒有類名哦");//替換最后一個分隔符為下划線if (lastIndex > 0) {formatString.replace(lastIndex, lastIndex + 1, "_");lastIndex = formatString.lastIndexOf("\\");}}//刪除最后一個分隔符前面的所有字符formatString.delete(0, lastIndex + 1);//刪除后綴名formatString.delete(formatString.lastIndexOf("."), formatString.length());//需要重新獲取一下后綴符號的位置map.put("formatName", formatString.toString().toLowerCase());return map;}//****************************************************************************************************************************//// 導包////****************************************************************************************************************************/*** 給指定的字符串導入指定的包* @param contentString 原始內容* @param packages 要導入的包* @return 導入指定包后的內容*/public static String importPackage(String contentString, String[] packages) {int index = contentString.indexOf("package");//查找第一個package的位置,package必須放在最上面(但是前面可以有空行),import要放在他下面if (index < 0) index = 0;//如果沒有包名//查找package后第一個換行符的位置,在其后面導包index = 1 + contentString.indexOf("\n", index);//注意這里不能用LINE_SEPARATOR,因為字符串是存在於內存中的,其存在形式是【\n】if (index < 0) index = 0;//其實不用判斷,沒找到時index=1+(-1)=0,為了更好的擴展性,還是判斷一下的好StringBuffer buffer = new StringBuffer(contentString);//StringBuffer才有insert方法,所以用StringBuffer封裝一下for (int i = 0; i < packages.length; i++) {if (!contentString.contains(packages[i])) {//沒有時才導包,避免重復導包buffer.insert(index, packages[i] + LINE_SEPARATOR);//注意這里一定要用LINE_SEPARATOR,因為字符串寫在windows文件中時是【\r\n】}}return buffer.toString();}//****************************************************************************************************************************//// 文件讀寫////****************************************************************************************************************************/*** 一次性讀取文本文件中的所有內容,以指定編碼格式的字符串返回* @param file 要讀取的文件,最大支持單個4G的文件* @param encoding 返回字符串的編碼格式,也即要讀取的文件的編碼格式*/public static String readFileToString(File file, String encoding) {byte[] filecontent = new byte[(int) file.length()];//因為int類型為32位,所以最大支持單個4G的文件try {FileInputStream in = new FileInputStream(file);//以字節流形式讀取,所以可以是二進制文件,但是因為最后返回的是字符串,所以肯定亂碼in.read(filecontent);in.close();return new String(filecontent, encoding);//裝換為字符串時需指定編碼} catch (FileNotFoundException e) {e.printStackTrace();return null;} catch (IOException e) {e.printStackTrace();return null;}}/*** 把指定字符串寫到指定文件中* @param file 要寫入的文件,若果文件不存在會自動創建* @param content 要寫入的字符串* @param encoding 要寫入的文件的編碼格式,也即content的編碼格式* @param append 是否使用append模式* @return 成功放回true,異常則返回false*/public static boolean writeStringToFile(File file, String content, String encoding, boolean append) {try {FileOutputStream fos = new FileOutputStream(file, append);fos.write(content.getBytes(encoding));fos.close();return true;} catch (IOException e) {e.printStackTrace();return false;}}//****************************************************************************************************************************//// 獲取指定目錄下的全部文件////****************************************************************************************************************************/*** 對指定目錄中的文件進行深度遍歷,並按照指定過濾器進行過濾,將過濾后的內容存儲到一個指定的集合中* @param dirPath 要遍歷的目錄,必須是一個目錄* @param filter 只遍歷目錄中的指定類型的文件,如果要遍歷所有文件請設為null* @param isContainDir 是否包含目錄文件* @param fileList 將結果保存到指定的集合中。由於要遞歸遍歷(不能定義為局部變量,否則每次遞歸時都是把結果放到了一個新的集合中) ;* 並且是靜態方法(定義為靜態成員時,下次調用此方法時此集合還包含之前調用后保存的值),所以最后保存到指定的集合中* @return 遍歷到的文件數量,也即集合的大小*/public static int getDirFiles(String dirPath, FilenameFilter filter, boolean isContainDir, List<File> fileList) {File dir = new File(dirPath);if (!dir.exists() || !dir.isDirectory()) throw new RuntimeException("目錄不存在或不是一個目錄");if (fileList == null) throw new RuntimeException("指定的集合不存在");File[] files = dir.listFiles();//也可以使用dir.listFiles(filter)在獲取列表時直接過濾,注意這種方式檢索時不要遺漏了目錄文件for (File file : files) {//遍歷if (file.isDirectory()) {//目錄if (isContainDir) {//返回集合中是否要包含目錄fileList.add(file);}getDirFiles(file.getAbsolutePath(), filter, isContainDir, fileList);//遞歸} else {//文件if (filter == null || filter.accept(dir, file.getName())) {//是否滿足過濾規則fileList.add(file);}}}return fileList.size();}}
附件列表