zoukankan      html  css  js  c++  java
  • 国际化 简繁体切换 正则表达式

    2017-2-7

    背景

    项目原先并没有考虑到后续国际化的需求,所以凡是用到字符串时,大都是直接写在了代码或布局里,比如
    // 更新秀币
    tv_coins.setText("余额coins为:" + getuCoin() + "元" );  
    或者
    <TextView
       android: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]+[^" ]*"";//即【"[^"]*[u4e00-u9fa5]+[^" ]*"】这里是一切操作的基石!
        //    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_PATHFILTERfalse, fileList);
            for (File file : fileList) {
                I18NUtils.matcherAndReplaceAndWriteToRes(file, ENCODINGREGEXWRITE_FILE_PATHPACKAGE_NAMEStrue);
            }
            System.out.println("已完成");
        }
    }

    代码-工具类
    /**国际化工具类*/
    public class I18NUtils {
        public static final String LINE_SEPARATOR = System.getProperty("line.separator");//行分隔符,linux中为 ,Windows中为
        /**
         * 匹配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();
                //如果此字符串不适合处理,就不要处理了,不然可能要改半天bug
                if (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; 则结果为2
                coun++;
            }
            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(" ", index);//注意这里不能用LINE_SEPARATOR,因为字符串是存在于内存中的,其存在形式是【 】
            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文件中时是【 】
                }
            }
            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 == nullthrow 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();
        }
    }

    附件列表

    • 相关阅读:
      506Relative Ranks(LeetCode)
      计算二进制中1的个数
      vector<vector<int>> 简单知识介绍
      167. Two Sum II
      561. Array Partition I(LeetCode)
      sizeof 用法部分总结
      530. Minimum Absolute Difference in BST(LeetCode)
      JS计算两个日期之间的天数
      路演会上会登记结论的委员信息页面
      eclipse安装SVN插件
    • 原文地址:https://www.cnblogs.com/baiqiantao/p/5593573.html
    Copyright © 2011-2022 走看看