作者 | Hemanth Murali
译者 | 张卫滨
策划 | Tina
核心要点
i18n 这个流行词是什么意思?
想象一下,在这个世界上,无论每个人的母语是什么,你的软件都可以与他们流畅地交流。这就是国际化和本地化要实现的目标。虽然乍看上去没啥特别之处,但是请记住,本地化应用程序不仅仅是翻译文本。而是要根据用户的文化、地区和语言偏好提供量身定制的体验。
但是,这里有个障碍在等着你。深入了解 i18n 库的工具箱,你会发现以 JavaScript 为核心的解决方案占据了主导地位,尤其是那些围绕 React 的解决方案(如i18next、react-intl和react-i18next)。
如果跳出 JavaScript 的范畴,可选的方案就会越来越少。更糟糕的是,这些现成的工具通常都带有“一刀切”的特点,缺乏适配特定用例的能力。
不过,不必担心!如果鞋子不合适的话,为何不自己动手做呢?请继续往下阅读,我们将指导你从头开始构建一个国际化框架:一个为你的应用程序量身定制、跨语言和跨框架的解决方案。
准备好为你的应用程序签发全球通行证了吗?让我们开始这段旅程吧。
基础的方式
掌握国际化精髓的一个简单方法就是使用一个函数,该函数能够根据用户所在的地域获取信息。如下是一个使用 Java 编写的样例,它提供了一个基本但有效的方法:
public class InternationalizationExample {
public static void main(String[] args) {
System.out.println(getWelcomeMessage(getUserLocale()));
}
public static String getWelcomeMessage(String locale) {
switch (locale) {
case "en_US":
return "Hello, World!";
case "fr_FR":
return "Bonjour le Monde!";
case "es_ES":
return "Hola Mundo!";
default:
return "Hello, World!";
}
}
public static String getUserLocale() {
// This is a placeholder method. In a real-world scenario,
// you'd fetch the user's locale from their settings or system configuration.
return "en_US"; // This is just an example.
}
}在上面的样例中,getWelcomeMessage根据locale指定的语言返回欢迎信息。语言是由getUserLocale方法确定的。这种方法虽然非常基础,但是展示了根据用户特定的本地语言提供内容的原则。但是,随着内容的进展,我们将深入研究更先进的技术,并了解为何这种基础的方式对于大型应用程序可能无法具备可扩展性和高效率。
优点
在前一种方法的基础之上,我们努力保留其优点,同时解决其缺点。为了实现这一点,我们将代码库中的硬编码字符串值过渡到基于配置的设置。我们会为每种本地语言使用单独的配置文件,并以 JSON 格式进行编码。这种模块化方式简化了翻译的添加和修改,无需进行代码的变更。
如下是英语和西班牙语本地语言的配置文件:
文件名:en.json
{
"welcome_message": "Hello, World"
}文件名:es.json
{
"welcome_message": "Hola, Mundo"
}Java 中的实现:
首先,我们需要一种读取 JSON 文件的方式。这通常会使用像 Jackson 或 GSON 这样的库。在本例中,我们将使用 Jackson。
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.util.Map;
public class Internationalization {
private static final String CONFIG_PATH = "/path_to_configs/";
private Map<String, String> translations;
public Internationalization(String locale) throws IOException {
ObjectMapper mapper = new ObjectMapper();
translations = mapper.readValue(new File(CONFIG_PATH + locale + ".json"), Map.class);
}
public String getTranslation(String key) {
return translations.getOrDefault(key, "Key not found!");
}
}
public static class Program {
public static void main(String[] args) throws IOException {
Internationalization i18n = new Internationalization(getUserLocale());
System.out.println(i18n.getTranslation("welcome_message"));
}
private static String getUserLocale() {
// This method should be implemented to fetch the user's locale.
// For now, let's just return "en" for simplicity.
return "en";
}
}Internationalization 类在实例化的时候,会根据提供的本地语言读取上述代码中相关的 JSON 配置。getTranslation方法使用标识符获取所需的翻译字符串。
优点:
缓解可能出现大型配置文件的一种方法是将其托管到内容分发网络(Content Delivery Network,CDN)上。通过这种方式,应用程序可以根据用户的本地语言只加载必要的配置文件。这样既能保证应用程序的运行速度,又能减少用户不必要下载的数据量。当用户切换本地语言或探测到不同的本地语言时,可以根据需要从 CDN 获取配置。这为大规模应用程序提供了速度和灵活性之间的最佳平衡。为了简单起见,我们考虑使用基础的 HTTP 库来获取配置文件。在这个 Java 样例中,我们将使用虚构的 HttpUtil 库:
import java.util.Map;
import org.json.JSONObject;
public class InternationalizationService {
private static final String CDN_BASE_URL = "https://cdn.example.com/locales/";
public String getTranslatedString(String key) {
String locale = getUserLocale();
String configContent = fetchConfigFromCDN(locale);
JSONObject configJson = new JSONObject(configContent);
return configJson.optString(key, "Translation not found");
}
private String fetchConfigFromCDN(String locale) {
String url = CDN_BASE_URL + locale + ".json";
return HttpUtil.get(url); // Assuming this method fetches content from a given URL
}
private String getUserLocale() {
// Implement method to get the user's locale
// This can be fetched from user preferences, system settings, etc.
return "en"; // Defaulting to English for this example
}
}注意:上述代码只是一个简化的样例,在实际的场景中可能需要错误处理、缓存机制和其他优化。这里的想法是根据用户的本地语言直接从 CDN 获取必要的配置文件。用户的本地语言决定了配置文件的 URL,获取到之后,就会对配置文件进行解析,以获得所需的翻译。如果找不到相应地键,就会返回默认信息。这种方法的好处是,应用程序只需加载必要的翻译,从而确保了最佳性能。
优点
动态字符串处理
如果要翻译的字符串有一部分内容是动态的,那么就需要一种更灵活的解决方案。以 Facebook 为例,在 News Feed 中,我们会看到这里使用了自定义的字符串来表示每篇文章的“Likes”信息。比如,如果文章只有一个“Likes”信息,那么你可能会看到“John likes your post.”。如果有两个“Likes”信息,那么你可能会看到“John and David like your post.”。如果有两个以上的“Likes”信息,你可能会看到“John, David and 100 others like your post.”。在这种情况下,需要进行一些自定义。动词“like”和“likes”是根据喜欢文章的人数来确定的。如何做到这一点呢?
考虑如下的样例:“John, David and 100 other people recently reacted to your post.”,在这里“David”、“John”、“100”、“people”和“reacted”都是动态元素。
我们来分析一下:
如下是一个 Java 样例:
配置文件(适用于英语)
{
oneUserAction: {0} {1} your post,
twoUserAction: {0} and {1} {2} your post,
multiUserAction: {0}, {1} and {2} other {3} recently {4} to your post,
people: people,
likeSingular: likes,
likePlural: like,
}配置文件(适用于法语):
{
oneUserAction: {0} {1} votre publication,
twoUserAction: {0} et {1} {2} votre publication,
multiUserAction: {0}, {1} et {2} autres {3} ont récemment {4} à votre publication,
people: personnes,
likeSingular: aime,
likePlural: aiment,
}Java 实现:
import java.util.Locale;
import java.util.ResourceBundle;
public class InternationalizationExample {
public static void main(String[] args) {
// Examples
System.out.println(createMessage("David", null, 1, new Locale("en", "US"))); // One user
System.out.println(createMessage("David", "John", 2, new Locale("en", "US"))); // Two users
System.out.println(createMessage("David", "John", 100, new Locale("en", "US"))); // Multiple users
// French examples
System.out.println(createMessage("David", null, 1, new Locale("fr", "FR"))); // One user
System.out.println(createMessage("David", "John", 2, new Locale("fr", "FR"))); // Two users
System.out.println(createMessage("David", "John", 100, new Locale("fr", "FR"))); // Multiple users
}
private static String createMessage(String user1, String user2, int count, Locale locale) {
// Load the appropriate resource bundle
ResourceBundle messages = ResourceBundle.getBundle("MessagesBundle", locale);
if (count == 0) {
return ""; // No likes received
} else if (count == 1) {
return String.format(
messages.getString("oneUserAction"),
user1,
messages.getString("likeSingular")
); // For one like, returns "David likes your post"
} else if (count == 2) {
return String.format(
messages.getString("twoUserAction"),
user1,
user2,
messages.getString("likePlural")
); // For two likes, returns "David and John like your post"
} else {
return String.format(
messages.getString("multiUserAction"),
user1,
user2,
count,
messages.getString("people"),
messages.getString("likePlural")
); // For more than two likes, returns "David, John and 100 other people like your post"
}
}
}结 论
无论规模大小,开发有效的国际化(i18n)和本地化(l10n)框架对于软件应用都至关重要。这种方法可以确保你的应用能够与用户的母语和文化背景产生共鸣。虽然字符串翻译是 i18n 和 l10n 的一个重要组成部分,但它只是软件全球化这一更广泛挑战的一个方面而已。
有效的本地化不仅仅是翻译,还要解决其他的关键问题,例如书写方向,阿拉伯语等语言的书写方向(从右到左)和文本长度或大小各不相同,泰米尔语等语言的文字可能比英语更长。通过精心定制这些策略来满足特定的本地化需求,你就可以为软件提供真正全球化的、适用于不同文化的用户体验。
关于作者
Hemanth Murali 是加利福尼亚州门洛帕克 Facebook 的高级软件工程师和技术主管,他在广告组织中负责构建高度可扩展的广告体验和交付系统。
查看英文原文:
Going Global: A Deep Dive to Build an Internationalization Framework(https://www.infoq.com/articles/internationalization-framework/)
声明:本文由 InfoQ 翻译,未经许可禁止转载。