[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]