写点什么

构建国际化框架,Web 开发让语言无阻

作者 | Hemanth Murali

  • 2024-01-02
    北京
  • 本文字数:6660 字

    阅读完需:约 22 分钟

构建国际化框架,Web开发让语言无阻

快速阅读


  • 国际化(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<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 方法使用标识符获取所需的翻译字符串。


优点:


  • 保留了上述方式的所有优点:覆盖面广,加载后无需使用网络就能进行翻译,代码易于搜索和阅读。

  • 动态加载:可根据用户的本地语言动态加载翻译。只需加载必要的翻译,从而带来潜在的性能优势。

  • 可扩展性:添加新语言更容易。只需为该语言添加一个新的配置文件,应用程序就能处理它,无需任何代码修改。

  • 更整洁的代码:逻辑与翻译分离,代码更简洁、更易维护。

  • 中心化的管理:所有的翻译都集中在一个文件中,因此更易于管理、审查和更新。这种方法提供了一种更可扩展、更简洁的国际化处理方式,尤其适用于大型应用程序。

缺点:


  • 可能会导致配置文件过大:随着应用程序的增长和对多种语言的支持,这些配置文件可能会变得相当大。这可能会导致应用程序的初始加载出现滞后,尤其是在配置文件需要前期加载的情况中。


从 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 的一个重要组成部分,但它只是软件全球化这一更广泛挑战的一个方面而已。


有效的本地化不仅仅是翻译,还要解决其他的关键问题,例如书写方向,阿拉伯语等语言的书写方向(从右到左)和文本长度或大小各不相同,泰米尔语等语言的文字可能比英语更长。通过精心定制这些策略来满足特定的本地化需求,你就可以为软件提供真正全球化的、适用于不同文化的用户体验。


原文链接:


https://www.infoq.com/articles/internationalization-framework/

相关阅读:


你可以错过 Web3,但不要错过 Web5

别人不会告诉你的 Web3 未来

Web3当下,最佳投资就是投资自己

Web3的反思,不要抱怨

2024-01-02 10:108295

评论 1 条评论

发布
用户头像
这个图片来自青岛好像
2024-01-05 14:24 · 陕西
回复
没有更多了

读《Software Engineering at Google》(22)

术子米德

架构师成长笔记

读《Software Engineering at Google》(19)

术子米德

架构师成长笔记

[Day34]-[二叉树]有序链表转换二叉搜索树

方勇(gopher)

LeetCode 二叉树 数据结构和算法

Go链表与切片比较

jinjin

List 链表 slice Go 语言 切片

网站开发进阶(九)应用内存检测

No Silver Bullet

内存泄露 变量声明 5月月更

如何登录到你的 WordPress 管理仪表板

海拥(haiyong.site)

5月月更

《数据中心白皮书 2022》揭秘“东数西算”下数据中心高性能计算的六大趋势八大技术

蓝海大脑GPU

用户体验思维 7 大陷阱

龙国富

产品设计 UI UX 用户体验 产品设计与思考

读《Software Engineering at Google》(23)

术子米德

架构师成长笔记

【愚公系列】2022 年 05 月 二十三种设计模式(三)-建造者模式(Builder Pattern)

愚公搬代码

5月月更

超级原始人系列盲盒即将上线,PlatoFarm赋能超多权益

西柚子

SAP 电商云的 Spartacus Storefront 部署到 CCV2 的前提条件

汪子熙

typescript 电商 SAP commerce 5月月更

C语言总结_字符串全方位练习

DS小龙哥

5月月更

maven构建docker镜像三部曲之三:推送到远程仓库(内网和阿里云)

程序员欣宸

Java Docker 5月月更

SecurityContextHolder之策略模式源码分析

急需上岸的小谢

5月月更

电阻电路的等效变换 (Ⅱ)

謓泽

5月月更

DevOps系列之 —— DevOps概览(二)新型软件技术及交付模式

若尘

DevOps 5月月更

读《Software Engineering at Google》(21)

术子米德

架构师成长笔记

网站开发进阶(五)网站发布之道

No Silver Bullet

Apache tomcat IIS 5月月更 网站发布

nginx配置系列(五)限制连接数

乌龟哥哥

5月月更

MyBatis 的动态 SQL超详细讲解

顾言

mybatis 动态SQL

读《Software Engineering at Google》(24)

术子米德

架构师成长笔记

Git进阶系列 | 3. 基于Pull Request实现更好的协作

俞凡

git 最佳实践

读《Software Engineering at Google》(20)

术子米德

架构师成长笔记

读《Software Engineering at Google》(18)

术子米德

架构师成长笔记

不造芯,不配做互联网巨头

IC男奋斗史

芯片行业思考

开源字节系统白皮书

源字节1号

软件开发

Git进阶系列 | 4. 合并冲突

俞凡

git 最佳实践

互联网跨界造芯的“菊与刀”

IC男奋斗史

芯片 芯片行业思考

Git进阶系列 | 5. Rebase vs Merge

俞凡

git 最佳实践

【愚公系列】2022年05月 二十三种设计模式(二)-抽象工厂模式(Abstract Factory Pattern)

愚公搬代码

5月月更

构建国际化框架,Web开发让语言无阻_框架_InfoQ精选文章