1.play framework中国际化信息大致分为两个步骤:一是从文件中读取国际化信息存放到类的static变量中。二是,根据request中的locale信息,从static变量中读取key所对应的value。涉及到的类基本如下
2.初始化过程
2.1play framemwork启动时(如果是dev模式,延迟到第一次访问时),读取play.plugins文件中的所有插件(如下图):
依次调用每个插件的onApplicationStart方法,MessagePlugin类的onApplicationStart方法中做了两件事:
一是从conf文件夹下的message文件(可以是多个:play根目录及各个module中)中读取默认信息放到Messages类的静态变量defaults属性中。
二是读取play支持的local(可以有多个:从Play.langs中读取),从conf文件夹下的message.local文件(local可以是zh、fr等等,文件也可以是多个:play根目录及各个module中)中读取local对应的国际化信息到Messages类的静态变量locals中。
@Override
public void onApplicationStart() {
//初始化默认国际化信息到defaults中
Messages.defaults = new Properties();
try {
FileInputStream is = new FileInputStream(new File(Play.frameworkPath, "resources/messages"));
Messages.defaults.putAll(IO.readUtf8Properties(is));
} catch(Exception e) {
Logger.warn("Defaults messsages file missing");
}
for(VirtualFile module : Play.modules.values()) {
VirtualFile messages = module.child("conf/messages");
if(messages != null && messages.exists()) {
Messages.defaults.putAll(read(messages));
}
}
VirtualFile appDM = Play.getVirtualFile("conf/messages");
if(appDM != null && appDM.exists()) {
Messages.defaults.putAll(read(appDM));
}
//初始化locale对应的国际化信息到locals中
//Play.langs在这之前已经初始化了,从配置项application.langs中获取,默认为空集合
//Play.modules在这之前已经初始化了,从配置文件play.plugins文件中读取
for (String locale : Play.langs) {
Properties properties = new Properties();
for(VirtualFile module : Play.modules.values()) {
VirtualFile messages = module.child("conf/messages." + locale);
if(messages != null && messages.exists()) {
properties.putAll(read(messages));
}
}
VirtualFile appM = Play.getVirtualFile("conf/messages." + locale);
if(appM != null && appM.exists()) {
properties.putAll(read(appM));
} else {
Logger.warn("Messages file missing for locale %s", locale);
}
Messages.locales.put(locale, properties);
}
lastLoading = System.currentTimeMillis();
}
2.2Messages类中还有一个方法detectChange,这个方法会在DEV模式下被调用。作用就是检查各个国际化文件(包括根目录及各个module下的conf文件夹下的message、message.zh文件)在onApplicationStart上次调用后有没有修改过。如果修改过,重新调用onApplicationStart方法。
@Override
public void detectChange() {
if (Play.getVirtualFile("conf/messages")!=null && Play.getVirtualFile("conf/messages").lastModified() > lastLoading) {
onApplicationStart();
return;
}
for(VirtualFile module : Play.modules.values()) {
if(module.child("conf/messages") != null && module.child("conf/messages").exists() && module.child("conf/messages").lastModified() > lastLoading) {
onApplicationStart();
return;
}
}
for (String locale : Play.langs) {
if (Play.getVirtualFile("conf/messages." + locale)!=null && Play.getVirtualFile("conf/messages." + locale).lastModified() > lastLoading) {
onApplicationStart();
return;
}
for(VirtualFile module : Play.modules.values()) {
if(module.child("conf/messages."+locale) != null && module.child("conf/messages."+locale).exists() && module.child("conf/messages."+locale).lastModified() > lastLoading) {
onApplicationStart();
return;
}
}
}
}
3.读取国际化信息。这个过程需要我们自己写代码实现,在控制层中调用Messages的get方法即可。
/**
* Given a message code, translate it using current locale.
* If there is no message in the current locale for the given key, the key
* is returned.
*
* @param key the message code
* @param args optional message format arguments
* @return translated message
*/
public static String get(Object key, Object... args) {
return getMessage(Lang.get(), key, args);
}
3.1get方法中调用了getMessage方法,这个方法的有三个参数:第一个参数是locale,第二个参数是国际化key,第三个参数是用于格式化的args。而locale参数通过Lang.get()方法获取。
Lang类使用了ThreadLocal模式。
如果当前线程有locale,直接返回。
如果没有,获取当前线程的request对象。
如果request不存在(表示没有请求进行),则调用setDefaultLocale方法,从Play.langs中取第一个值作为默认值并返回。
如果request存在,则调用resolvefrom方法设置locale。这个方法先从request的cookie中获取locale进行设值;如果cookei中没有locale信息,则从request的请求头的accept-language中获取信息进行设置。
Lang类的get方法如下:
public static ThreadLocal<String> current = new ThreadLocal<String>();
/**
* Retrieve the current language or null
* @return The current language (fr, ja, it ...) or null
*/
public static String get() {
//从当前线程中获取
String locale = current.get();
//如果不存在,则从request中解析
if (locale == null) {
// don't have current locale for this request - must try to resolve it
Http.Request currentRequest = Http.Request.current();
//如果request存在(表示有浏览器访问应用)
if (currentRequest!=null) {
// we have a current request - lets try to resolve language from it
resolvefrom( currentRequest );
} else {
//当没有request时设置默认值
// don't have current request - just use default
setDefaultLocale();
}
// get the picked locale
locale = current.get();
}
return locale;
}
Lang类的resolvefrom方法:
/**
* Guess the language for current request in the following order:
* <ol>
* <li>if a <b>PLAY_LANG</b> cookie is set, use this value</li>
* <li>if <b>Accept-Language</b> header is set, use it only if the Play! application allows it.<br/>supported language may be defined in application configuration, eg : <em>play.langs=fr,en,de)</em></li>
* <li>otherwise, server's locale language is assumed
* </ol>
* @param request
*/
private static void resolvefrom(Request request) {
// Check a cookie
String cn = Play.configuration.getProperty("application.lang.cookie", "PLAY_LANG");
if (request.cookies.containsKey(cn)) {
//从cookie中获取locale
String localeFromCookie = request.cookies.get(cn).value;
if (localeFromCookie != null && localeFromCookie.trim().length()>0) {
//如果cookie中的locale信息在Play.langs中存在,直接返回locale
if (set(localeFromCookie)) {
// we're using locale from cookie
return;
}
// could not use locale from cookie - clear the locale-cookie
Response.current().setCookie(cn, "");
}
}
//从request的headers中读取accept-language值,并且从Play.langs中找出匹配值。如果未找到匹配中,返回null
String closestLocaleMatch = findClosestMatch(request.acceptLanguage());
if ( closestLocaleMatch != null ) {
set(closestLocaleMatch);
} else {
// Did not find anything - use default
//最坏情况下,设置默认值
setDefaultLocale();
}
}
3.2Messages的getMessage方法:
首先遍历所有的插件并调用getMessage方法,得到message。
如果message存在直接返回。
如果message不存在,从静态变量locales中获取对应locale和对应key的值value。调用formatString(value,args)方法进行格式化并返回。
public static String getMessage(String locale, Object key, Object... args) {
// Check if there is a plugin that handles translation
//遍历所有插件,调用getMessage方法
String message = Play.pluginCollection.getMessage(locale, key, args);
if(message != null) {
//如果message存在,直接返回
return message;
}
String value = null;
if( key == null ) {
return "";
}
if (locales.containsKey(locale)) {
value = locales.get(locale).getProperty(key.toString());
}
if (value == null) {
value = defaults.getProperty(key.toString());
}
if (value == null) {
value = key.toString();
}
//格式化value
return formatString(value, args);
}
3.3Messages类的formatString方法
TODO:使用了两个正则表达式进行格式化,对正则表达式掌握不好,未看懂,求指导
static Pattern recursive = Pattern.compile("&\\{(.*?)\\}");
public static String formatString(String value, Object... args) {
String message = String.format(value, coolStuff(value, args));
Matcher matcher = recursive.matcher(message);
StringBuffer sb = new StringBuffer();
while(matcher.find()) {
matcher.appendReplacement(sb, get(matcher.group(1)));
}
matcher.appendTail(sb);
return sb.toString();
}
static Pattern formatterPattern = Pattern.compile("%((\\d+)\\$)?([-#+ 0,(]+)?(\\d+)?([.]\\d+)?([bBhHsScCdoxXeEfgGaAtT])");
@SuppressWarnings("unchecked")
static Object[] coolStuff(String pattern, Object[] args) {
// when invoked with a null argument we get a null args instead of an array with a null value.
if(args == null)
return NO_ARGS;
Class<? extends Number>[] conversions = new Class[args.length];
Matcher matcher = formatterPattern.matcher(pattern);
int incrementalPosition = 1;
while(matcher.find()) {
String conversion = matcher.group(6);
Integer position;
if(matcher.group(2) == null) {
position = incrementalPosition++;
} else {
position = Integer.parseInt(matcher.group(2));
}
if(conversion.equals("d") && position <= conversions.length) {
conversions[position-1] = Long.class;
}
if(conversion.equals("f") && position <= conversions.length) {
conversions[position-1] = Double.class;
}
}
Object[] result = new Object[args.length];
for(int i=0; i < args.length; i++) {
if(args[i] == null) {
continue;
}
if(conversions[i] == null) {
result[i] = args[i];
} else {
try {
// TODO: I think we need to type of direct bind -> primitive and object binder
result[i] = Binder.directBind(null, args[i] + "", conversions[i], null);
} catch(Exception e) {
// Ignore
result[i] = null;
}
}
}
return result;
}
4.小结
mvc框架的国际化信息过程基本一致。首先是加载国际化文件到静态变量中。然后就是根据locale和key从静态变量中取值的过程。play在实现的具体细节上还有一些没有弄明白,求指导。