平方X 发表于 2017-12-19 18:24:16

[2454]汉化过程中的问题总结



# 0x01 为什么放一个 jar 包就可以汉化了
Java 中使用 ResourceBundle 处理国际化,添加一个 jar 包后程序会从 jar 中读取相应的翻译。
(https://docs.oracle.com/javase/9/docs/api/java/util/ResourceBundle.html)
(https://docs.oracle.com/javase/tutorial/i18n/index.html)

# 0x02 为什么不同名字的 jar 包都可以读取
程序里初始化时会初始化的 ClassLoader ,ClassLoader 按顺序加载了 lib 中的所有 jar 包。
(加载过程的分析见后文)
最后又从 jar 包中查找 ResourceBundle

# 0x03 为什么标准命名为 _zh_CN,而该项目中命名为 _cn
程序会从 .properties 文件读取 Bundle,在查找 Bundle 时,会按一定的优先级加载使用。
比如,在中文环境下,会优先使用 _zh_CN.properties,_zh.properties,.properties等。
因为之前有用户反馈他的环境是英文环境,也想使用汉化包,于是该项目的汉化包不使用 _zh_CN.properties,而是直接使用 .properties

在使用 _zh_CN.properties 时,其优先级高于 .properties,因此和 jar 包的名字是没有任何关系的。
但改为 .properties 之后,必须保证汉化包顺序位于 resources_en.jar 之前。
因此不能命名为 resources_zh_CN,只能命名为 resources_cn 或其他。

## 3.1 为什么最后换回了 _zh_CN 的形式
之前有网友反馈,希望在英文操作系统上使用汉化包,于是将资源文件修改为不带 _zh_CN 的形式,然后利用 jar 包加载顺序优先加载。
但是却发现,在 Mac 上无法加载,见 #7 ,据网友反馈,首次安装,不打开放进去才生效。
我使用虚拟机测试是可以正常汉化的,但用朋友的 Mac 测试,确实无法汉化,不知道是否是有加载 jar 包的缓存,这个只有等我以后有苹果电脑再调试了。
如果有朋友知道原因,也请告之我,谢谢。
为解决此 bug ,有以下选择
①将资源文件改为 _zh_CN 的形式,但是英文系统将无法使用;
②依然保留原有的命名形式,但将 resources_en.jar 的所有内容都打包
这样如果有网友用不了,要以让其删除 resources_en.jar ,直接使用 resources_cn.jar
但是该方法 2 个 jar 包好像会导致 fileTemplates/Singleton.java.ft 及类似文件重复而出错。



# 0x04 为什么 __zh_CN 排在 _en 前面,但加载 jar 包时却排在后面
一开始我仍想使用标准的 _zh_CN,为了排在前面,添加一个下划线。
我认为改为 __zh_CN 后,因为下划线排在 _en 前面,那么应该可以优先加载的。
实际情况却是下划线排在字母后面。
根据 ASCII 表,大小字母<下划线<小写字母,但实际情况是
```
win7 explorer 中
__cn
_cn
_en
_EN2

cmd 中
_cn
_en
_EN2
__cn

手机 adb shell
test_EN2
text__cn
test_cn
test_en

只有在 adb shell 中才符合 ASCII 表,在程序中加载时
java.io.File#listFiles()
java.io.File#list()
java.io.FileSystem#list
java.io.WinNTFileSystem#list
其结果与 cmd 中的结果相同。
为什么会这样排,可能是因为 windows 是这样排的,相关文档没有查到,如果有人知道可以告诉我,谢谢。
```

# 0x05 为什么 tips 中的图片需要包含在汉化包中
这也是我不能理解的,分析源码过程中,我们知道资源是通过 ResourceUtil.getResource 获取的。
实际的源码调试过程中也是如此,当 _cn.jar 包中没有图片时,仍可以从 _en.jar 中找到图片并加载显示。
但是实际的情况却是无法正常显示。
IDEA 附加本地进程,找不到。
OD ,不能调试64 位程序,32 位的打开需要 32 位的 jdk ,32 位的 jdk 安装失败。
x64_dbg,没找到字符也不太会用,OD 和 x64_dbg 都不太会用,只是试着用一下看看。
也没有输出错误日志,但可以看到图片的大小已经设置了。

# 0x06 资源中的可选格式化是如何实现的
Fomart 的 3 个子类,DateFormat, MessageFormat, NumberFormat,还是自己不熟悉啊。
(https://docs.oracle.com/javase/9/docs/api/java/text/MessageFormat.html)
(https://docs.oracle.com/javase/9/docs/api/java/text/ChoiceFormat.html)
```
0.has.1.usages.that.are.not.safe.to.delete={0} has {1,choice,1#1 usage that is|2#{1,number} usages that are} not safe to delete.
搜索定位到
com.intellij.refactoring.RefactoringBundle#message(java.lang.String, java.lang.Object...)
com.intellij.CommonBundle#message(java.util.ResourceBundle, java.lang.String, java.lang.Object...)
com.intellij.BundleBase#message
com.intellij.BundleBase#messageOrDefault
    //替换 & 符号
    value = replaceMnemonicAmpersand(value);

    return format(value, params);
com.intellij.BundleBase#format
java.text.MessageFormat#format(java.lang.String, java.lang.Object...)
java.text.Format#format(java.lang.Object)
java.text.MessageFormat#format(java.lang.Object, java.lang.StringBuffer, java.text.FieldPosition)
java.text.MessageFormat#subformat
```


# 加载过程分析

首先删除语言包,诱发错误。
```
Internal Error. Please report to https://code.google.com/p/android/issues

java.lang.RuntimeException: java.util.MissingResourceException: Can't find bundle for base name messages.VfsBundle, locale zh_CN
    at com.intellij.idea.IdeaApplication.run(IdeaApplication.java:213)
    at com.intellij.idea.MainImpl$1.lambda$null$0(MainImpl.java:49)
    at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:311)
    at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:762)
    at java.awt.EventQueue.access$500(EventQueue.java:98)
    at java.awt.EventQueue$3.run(EventQueue.java:715)
    at java.awt.EventQueue$3.run(EventQueue.java:709)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:80)
    at java.awt.EventQueue.dispatchEvent(EventQueue.java:732)
    at com.intellij.ide.IdeEventQueue.dispatchEvent(IdeEventQueue.java:343)
    at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:201)
    at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
    at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
    at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)
Caused by: java.util.MissingResourceException: Can't find bundle for base name messages.VfsBundle, locale zh_CN
    at java.util.ResourceBundle.throwMissingResourceException(ResourceBundle.java:1564)
    at java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1387)
    at java.util.ResourceBundle.getBundle(ResourceBundle.java:1082)
    at com.intellij.AbstractBundle.getResourceBundle(AbstractBundle.java:91)
    at com.intellij.AbstractBundle.getBundle(AbstractBundle.java:65)
    at com.intellij.AbstractBundle.getMessage(AbstractBundle.java:59)
    at com.intellij.openapi.vfs.VfsBundle.message(VfsBundle.java:30)
    at com.intellij.openapi.vfs.newvfs.RefreshQueueImpl.<init>(RefreshQueueImpl.java:43)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at org.picocontainer.defaults.InstantiatingComponentAdapter.newInstance(InstantiatingComponentAdapter.java:193)
    at com.intellij.util.pico.CachingConstructorInjectionComponentAdapter.doGetComponentInstance(CachingConstructorInjectionComponentAdapter.java:103)
    at com.intellij.util.pico.CachingConstructorInjectionComponentAdapter.instantiateGuarded(CachingConstructorInjectionComponentAdapter.java:80)
    at com.intellij.util.pico.CachingConstructorInjectionComponentAdapter.getComponentInstance(CachingConstructorInjectionComponentAdapter.java:63)
    at com.intellij.openapi.components.impl.ServiceManagerImpl$MyComponentAdapter.getComponentInstance(ServiceManagerImpl.java:228)
    at com.intellij.util.pico.DefaultPicoContainer.getLocalInstance(DefaultPicoContainer.java:239)
    at com.intellij.util.pico.DefaultPicoContainer.getComponentInstance(DefaultPicoContainer.java:206)
    at com.intellij.openapi.components.ServiceManager.doGetService(ServiceManager.java:48)
    at com.intellij.openapi.components.ServiceManager.getService(ServiceManager.java:38)
    at com.intellij.openapi.vfs.newvfs.RefreshQueue.getInstance(RefreshQueue.java:32)
    at com.intellij.openapi.vfs.impl.local.LocalFileSystemBase.refreshFiles(LocalFileSystemBase.java:269)
    at com.intellij.openapi.vfs.VfsUtil.markDirtyAndRefresh(VfsUtil.java:567)
    at com.intellij.configurationStore.ApplicationStoreImpl$setPath$1.invoke(ApplicationStoreImpl.kt:56)
    at com.intellij.configurationStore.ApplicationStoreImpl$setPath$1.invoke(ApplicationStoreImpl.kt:39)
    at com.intellij.openapi.application.ActionsKt$invokeAndWaitIfNeed$2.run(actions.kt:54)
    at com.intellij.openapi.application.impl.ApplicationImpl.invokeAndWait(ApplicationImpl.java:672)
    at com.intellij.openapi.application.ActionsKt.invokeAndWaitIfNeed(actions.kt:54)
    at com.intellij.openapi.application.ActionsKt.invokeAndWaitIfNeed$default(actions.kt:40)
    at com.intellij.configurationStore.ApplicationStoreImpl.setPath(ApplicationStoreImpl.kt:53)
    at com.intellij.openapi.application.impl.ApplicationImpl.lambda$load$7(ApplicationImpl.java:441)
    at com.intellij.openapi.components.impl.ComponentManagerImpl.init(ComponentManagerImpl.java:102)
    at com.intellij.openapi.application.impl.ApplicationImpl.load(ApplicationImpl.java:425)
    at com.intellij.openapi.application.impl.ApplicationImpl.load(ApplicationImpl.java:411)
    at com.intellij.idea.IdeaApplication.run(IdeaApplication.java:206)
    ... 16 more


没有有用的信息直接定位,还是去找 main
com.intellij.idea.Main#main
    ...
    Bootstrap.main(args, Main.class.getName() + "Impl", "start");
    ...
com.intellij.ide.Bootstrap#main
    ...
    初始化 ClassLoader ,很重要的 ClassLoader
    ClassLoader newClassLoader = BootstrapClassLoaderUtil.initClassLoader(updatePlugins);
    ...
com.intellij.ide.BootstrapClassLoaderUtil#initClassLoader
    ...
    Collection<URL> classpath = new LinkedHashSet<>();
    addParentClasspath(classpath, false);
    在该方法中添加了 lib 中的 jar 包
    addIDEALibraries(classpath);
    addAdditionalClassPath(classpath);
    addParentClasspath(classpath, true);
    ...
com.intellij.ide.BootstrapClassLoaderUtil#addIDEALibraries
    ...
    这里就是添加 lib 目录了
    File libFolder = new File(PathManager.getLibPath());
    addLibraries(classpath, libFolder, selfRootUrl);
    ...
下面几个方法是找出 lib 目录
com.intellij.openapi.application.PathManager#getLibPath
    HomePath 加上 lib
    //private static final String LIB_FOLDER = "lib";
    return getHomePath() + File.separator + LIB_FOLDER;
com.intellij.openapi.application.PathManager#getHomePath
    String fromProperty = System.getProperty(PROPERTY_HOME_PATH, System.getProperty(PROPERTY_HOME));
    if (fromProperty != null) {
      ...
    }
    else {
      通过类查找 HomePath
      ourHomePath = getHomePathFor(PathManager.class);
      ...
    }
com.intellij.openapi.application.PathManager#getHomePathFor
    String rootPath = getResourceRoot(aClass, "/" + aClass.getName().replace('.', '/') + ".class");
    if (rootPath == null) return null;

    File root = new File(rootPath).getAbsoluteFile();
    向上查找,直到是 Idea 的 home
    do { root = root.getParentFile(); } while (root != null && !isIdeaHome(root));
    return root != null ? root.getPath() : null;
com.intellij.openapi.application.PathManager#isIdeaHome
    for (String binDir : getBinDirectories(root)) {
      如果 bin 目录下有属性文件,则是 home
      //public static final String PROPERTIES_FILE_NAME = "idea.properties";
      if (new File(binDir, PROPERTIES_FILE_NAME).isFile()) {
      return true;
      }
    }
    return false;
com.intellij.openapi.application.PathManager#getBinDirectories
    ...
    //private static final String BIN_FOLDER = "bin";
    String[] subDirs = {BIN_FOLDER, "community/bin", "ultimate/community/bin"};
    ...

于是我们看到程序查找了 lib 目录,把 jar 添加进 ClassLoader 的 urls


com.intellij.openapi.actionSystem.impl.ActionManagerImpl#getActionsResourceBundle
com.intellij.AbstractBundle#getResourceBundle
java.util.ResourceBundle#getBundle(java.lang.String, java.util.Locale, java.lang.ClassLoader, java.util.ResourceBundle.Control)
java.util.ResourceBundle#getBundleImpl
java.util.ResourceBundle#findBundle
注意这里传的 boolean reload,一开始 messages/VfsBundle.properties 为 false
java.util.ResourceBundle#loadBundle
java.util.ResourceBundle.Control#newBundle


在用源码调试的时候,一开始我删除了 F:\intellij-community\android\android\resources\messages\AndroidBundle.properties
然后我添加了 汉化的 jar 包,结果总是报找不到资源,调试半天发现报错的是 AndroidBundle ,而我仅添加了 ActionsBundle
后来删除 F:\intellij-community\platform\platform-resources-en\src\messages\ActionsBundle.properties
然后汉化包就生效了。
java.util.ResourceBundle.Control#newBundle
            String bundleName = toBundleName(baseName, locale);
            ResourceBundle bundle = null;
            if (format.equals("java.class")) {
                ...
            } else if (format.equals("java.properties")) {
                final String resourceName = toResourceName0(bundleName, "properties");
                if (resourceName == null) {
                  return bundle;
                }
                final ClassLoader classLoader = loader;
                final boolean reloadFlag = reload;
                InputStream stream = null;
                try {
                  stream = AccessController.doPrivileged(
                        new PrivilegedExceptionAction<InputStream>() {
                            public InputStream run() throws IOException {
                              InputStream is = null;
                              if (reloadFlag) {
                                    URL url = classLoader.getResource(resourceName);
                                    if (url != null) {
                                        URLConnection connection = url.openConnection();
                                        if (connection != null) {
                                          // Disable caches to get fresh data for
                                          // reloading.
                                          connection.setUseCaches(false);
                                          is = connection.getInputStream();
                                        }
                                    }
                              } else {
                                    is = classLoader.getResourceAsStream(resourceName);
                              }
                              return is;
                            }
                        });
                } catch (PrivilegedActionException e) {
                  throw (IOException) e.getException();
                }
                if (stream != null) {
                  try {
                        这里读取 stream 可以通过 stream 查看实际加载的文件
                        bundle = new PropertyResourceBundle(stream);
                  } finally {
                        stream.close();
                  }
                }
            } else {
                throw new IllegalArgumentException("unknown format: " + format);
            }
            return bundle;
为 false 时
java.lang.ClassLoader#getResourceAsStream
为 true 直接调的这里
java.lang.ClassLoader#getResource
java.lang.ClassLoader#findResource
com.intellij.util.lang.UrlClassLoader#findResource
com.intellij.util.lang.UrlClassLoader#findResourceImpl
com.intellij.util.lang.ClassPath#getResource
com.intellij.util.lang.ClasspathCache#iterateLoaders
com.intellij.util.lang.ClassPath.ResourceStringLoaderIterator#process
com.intellij.util.lang.Loader#getResource
com.intellij.util.lang.JarLoader#getResource
```



```
删除 tips 诱发错误
Unable to read Tip Of The Day
error.unable.to.read.tip.of.the.day
com.intellij.ide.util.TipUIUtil#getCantReadText

com.intellij.ide.TipOfTheDayManager#runActivity
com.intellij.ide.util.TipDialog#createForProject
com.intellij.ide.util.TipDialog#TipDialog(java.awt.Window)
com.intellij.ide.util.TipDialog#initialize
com.intellij.ide.util.TipPanel#nextTip
com.intellij.ide.util.TipPanel#setTip
com.intellij.ide.util.TipUIUtil#openTipInBrowser(com.intellij.ide.util.TipAndTrickBean, com.intellij.ide.util.TipUIUtil.Browser)
com.intellij.ui.TextAccessor#setText
com.intellij.ide.util.TipUIUtil#getTipText
    ...
    依然是前面分析的 ClassLoader
      PluginDescriptor pluginDescriptor = tip.getPluginDescriptor();
      ClassLoader tipLoader = pluginDescriptor == null ? TipUIUtil.class.getClassLoader() :
                              ObjectUtils.notNull(pluginDescriptor.getPluginClassLoader(), TipUIUtil.class.getClassLoader());

      URL url = ResourceUtil.getResource(tipLoader, "/tips/", tip.fileName);
com.intellij.ide.util.TipUIUtil#updateImages
    ...
    还是获取资源,最后跟前面分析的一样,最后还是在 JarLoader#getResource 中找到,格式为
    jar:file:/F:/intellij-community/lib/resources_en.jar!/tips/images/variable_name_completion.png
      URL url = ResourceUtil.getResource(tipLoader, "/tips/", path);
    ...

```
页: [1]
查看完整版本: [2454]汉化过程中的问题总结