作者 | Hemanth Murali 译者 | 张卫滨 策划 | Tina 核心要点 国际化(i18n)和本地化是 web 开发中的关键流程,能够确保软件适用于不同的语言和地区,并确保软件实际适配这些特定的需求。 尽管以 JavaScript 为核心的 i18n 库(如 i18next、react-intl 和 react-i18next)是该领域的主流工具,可帮助开发人员高效地处理翻译和本地化相关的配置,但它们仅适用于基于 Javascript 的 web 应用。我们需要一个与语言无关的国际化框架。 JSON 是一种广泛接受的格式,可用于存储翻译和本地化相关的配置,无论使用何种语言和框架,都能在各种应用程序中轻松集成和动态替换内容。 内容分发网络(Content Delivery Network,CDN)可被战略性地用于高效提供本地化相关的配置文件,从而减轻加载大型配置文件潜在的弊端。 构建自定义国际化框架,并将其与数据库或数据存储解决方案集成,可以实现动态和上下文感知的翻译,从而增强不同地区和语言的用户体验。你是否已经涉足 web 开发的汪洋大海?如果答案是肯定的,那你很快就会意识到,web 不仅仅是为英语使用者服务的,它是面向全球的。假设法国用户看到了一条令人困惑的纯英文错误信息,在你被类似的投诉淹没之前,我们先来讨论一下什么是国际化(internationalization,通常简写为 i18n)和本地化。 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方法确定的。这种方法虽然非常基础,但是展示了根据用户特定的本地语言提供内容的原则。但是,随着内容的进展,我们将深入研究更先进的技术,并了解为何这种基础的方式对于大型应用程序可能无法具备可扩展性和高效率。 优点 覆盖面广:由于所有的翻译都嵌入在代码中,因此我们可以使用多种语言,而不必担心外部依赖或缺失翻译。 无网络调用:翻译直接从代码中获取,无需任何网络开销或从外部源获取翻译相关的延迟。 便利的代码搜索:由于所有的翻译都是源码的一部分,因此搜索特定翻译或排查相关的问题变得很简单易行。 可读性:开发人员可以立即理解选择特定翻译背后的流程和逻辑,从而简化调试和维护。 减少外部依赖:无需依赖外部翻译服务或数据库,这意味着应用程序中少了一个故障点。 缺点: 更新操作需要发布新的版本:在移动应用或独立应用的场景中,添加新语言或调整现有的翻译需要用户下载并更新最新版本的应用。 冗余代码:随着要支持语言数量的增加,switch 和条件语句也会相应地增加,从而导致代码的重复和臃肿。 合并冲突:由于多个开发人员可能会对各种语言进行添加或修改,所以版本控制系统中出现合并冲突的风险会随之增加。 代码维护所面临的挑战:随着时间的推移,应用程序会进行扩展并支持更多的本地语言,直接在代码中管理和更新翻译会变得繁琐且容易出错。 灵活性有限:采用这种静态的方式很难添加像复数形式、特定上下文的翻译或动态获取翻译等特性。 性能开销:对于大规模应用而言,加载大块的翻译数据却仅使用其中很小的一部分会导致资源紧张,造成效率低下。 基于配置的国际化 在前一种方法的基础之上,我们努力保留其优点,同时解决其缺点。为了实现这一点,我们将代码库中的硬编码字符串值过渡到基于配置的设置。我们会为每种本地语言使用单独的配置文件,并以 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 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方法使用标识符获取所需的翻译字符串。 优点: 保留了上述方式的所有优点:覆盖面广,加载后无需使用网络就能进行翻译,代码易于搜索和阅读。 动态加载:可根据用户的本地语言动态加载翻译。只需加载必要的翻译,从而带来潜在的性能优势。 可扩展性:添加新语言更容易。只需为该语言添加一个新的配置文件,应用程序就能处理它,无需任何代码修改。 更整洁的代码:逻辑与翻译分离,代码更简洁、更易维护。 中心化的管理:所有的翻译都集中在一个文件中,因此更易于管理、审查和更新。这种方法提供了一种更可扩展、更简洁的国际化处理方式,尤其适用于大型应用程序。 缺点: 可能会导致配置文件过大:随着应用程序的增长和对多种语言的支持,这些配置文件可能会变得相当大。这可能会导致应用程序的初始加载出现滞后,尤其是在配置文件需要前期加载的情况中。 从 CDN 抓取配置 缓解可能出现大型配置文件的一种方法是将其托管到内容分发网络(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,获取到之后,就会对配置文件进行解析,以获得所需的翻译。如果找不到相应地键,就会返回默认信息。这种方法的好处是,应用程序只需加载必要的翻译,从而确保了最佳性能。 优点 继承了前一种方法的所有优势。 易于为新的本地语言组织和添加翻译。 只需获取必要的翻译,因此加载效率高。 缺点: 配置文件体积庞大,可能会降低应用程序的初始化速度。 字符串必须是静态的。无法直接支持动态字符串或需要运行时计算的字符串。如果需要在翻译中插入动态数据,这可能是一个限制。 依赖外部服务(CDN)。如果 CDN 遇到故障或出现问题,应用程序将无法获取翻译内容。但是,要解决这些缺点,我们可以采取如下措施:第一个缺点可以通过在 CDN 上存储配置文件并在需要时加载来缓解。第二个缺点可以通过在静态字符串中使用占位符并在运行时根据上下文替换来解决。第三个缺点则需要一个健壮的错误处理机制和一些潜在的后备策略。 动态字符串处理 如果要翻译的字符串有一部分内容是动态的,那么就需要一种更灵活的解决方案。以 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”都是动态元素。 我们来分析一下: “David”和“John”可以是从与用户相关的方法或数据库中获取的用户名。 “100”可以是从与文章相关的方法或数据库中获取的对文章做出反应的总人数,其中不包括 David 和 John。 当代指一个集体时,“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 翻译,未经许可禁止转载。