对于那些仍然对最佳实践感到困惑的人,那些觉得缺少某些东西的人,或者只是对如何以更好的方式实现有关 Durandal、Knockout、RequireJS 和一般客户端 Web 应用程序的东西感到好奇的人,这是对可能的更有用的概述的尝试。
这当然不完整,但希望这可以扩展一些思想。
一、2014年11月更新
我看到这个答案即使在一年后也经常被投票。当我进一步开发我们的特定解决方案(将 i18next 集成到 Durandal/AMD/Knockout)时,我犹豫了多次更新它。然而,由于内部困难和对 Durandal和我们堆栈其他部分的未来的“担忧”,我们最终放弃了依赖项目。因此,这个小小的整合工作也被取消了。
话虽如此,我希望将普遍适用的评论与下面的具体评论区分开来,所以我认为他们继续就这些问题提供有用的(甚至可能是非常需要的)观点。
如果您仍然想玩 Durandal、Knockout、AMD 和任意本地化库(顺便说一下,有一些新玩家需要评估),我在最后添加了一些我后来经历的笔记。
关于单例模式
这里单例模式的一个问题是很难配置每个视图。事实上,除了语言环境(复数形式、上下文、变量、性别)之外,翻译还有其他参数,这些参数本身可能特定于某些上下文(例如视图/视图模型)。
顺便说一句,重要的是您不要自己这样做,而是依赖本地化库/框架(它可能会变得非常复杂)。关于这些项目有很多关于 SO 的问题。
您仍然可以使用单例,但无论哪种方式,您都只完成了一半。
zewa666在另一个答案中探索的一种解决方案是创建一个 KO 绑定处理程序。可以想象这个处理程序从视图中获取这些参数,然后使用任何本地化库作为后端。通常,您需要在视图模型(或其他地方)中以编程方式更改这些参数,这意味着您仍然需要公开 JS API。
如果您仍然要公开这样的 API,那么您可以使用它来填充您的视图模型并完全跳过绑定处理程序。但是,对于可以直接从视图配置的字符串,它们仍然是一个不错的快捷方式。提供这两种方法是一件好事,但你可能离不开 JS API。
当前的 Javascript API,文档重新加载
大多数本地化库和框架都非常老派,其中许多都希望您在用户更改语言环境时重新加载整个页面,有时甚至在翻译参数更改时,出于各种原因。不要这样做,它违背了客户端 Web 应用程序所代表的一切。(如今,SPA 似乎是一个很酷的术语。)
主要原因是,否则您需要跟踪每次语言环境更改时需要重新翻译的每个 DOM 元素,以及每次参数更改时需要重新翻译的元素。手动执行此操作非常繁琐。
幸运的是,这正是像敲除这样的数据绑定器非常容易做到的。确实,我刚才所说的问题应该提醒您 KO 计算的 observables 和 KOdata-bind
属性试图解决的问题。
该插件都使用单例模式并希望您重新加载文档。禁止与 Durandal 一起使用。
您可以,但效率不高,而且您可能会或可能不会遇到无用的问题,具体取决于您的应用程序状态的复杂程度。
在本地化库中集成敲除
理想情况下,本地化库将支持淘汰 observables,这样每当您将可观察字符串传递给它们以使用可观察参数进行翻译时,该库都会为您提供可观察翻译。直观地说,每次语言环境、字符串或参数更改时,库都会修改可观察的翻译,如果它们绑定到视图(或其他任何东西),则视图(或其他任何东西)会动态更新,而无需您明确地做任何事情。
如果您的本地化库具有足够的可扩展性,您可以为其编写插件,或者要求开发人员实现此功能,或者等待更现代的库出现。
我现在什么都不知道,但是我对 JS 生态系统的了解非常有限。如果可以的话,请为这个答案做出贡献。
当今软件的真实世界解决方案
大多数当前的 API 都非常简单。以i18next为例。它的t
(translate) 方法采用字符串的键和包含参数的对象。只需一点点聪明,您就可以在不扩展它的情况下摆脱它,只使用胶水代码。
translate
模块
define(function (require) {
var ko = require('knockout');
var i18next = require('i18next');
var locale = require('locale');
return function (key, opts) {
return ko.computed(function () {
locale();
var unwrapped = {};
if (opts) {
for (var optName in opts) {
if (opts.hasOwnProperty(optName)) {
var opt = opts[optName];
unwrapped[optName] = ko.isObservable(opt) ? opt() : opt;
}
}
}
return i18next.t(key, unwrapped);
});
}
});
locale
模块
define(function (require) { return require('knockout').observable('en'); });
该translate
模块是一个翻译函数,它支持可观察的参数并返回一个可观察的(根据我们的要求),并且基本上包装了i18next.t
调用。
该locale
模块是一个可观察对象,包含在整个应用程序中全局使用的当前语言环境。我们在这里定义默认值(英文),您当然可以从浏览器 API、本地存储、cookie、URI 或任何其他机制中检索它。
i18next-specific 注意: AFAIK,i18next.t
API 无法为每次翻译采用特定的语言环境:它始终使用全局配置的语言环境。因此,我们必须通过其他方式(见下文)更改此全局设置,并对 locale observable 进行虚拟读取,以强制敲除将其作为依赖项添加到计算的 observable 中。没有它,如果我们更改可观察的语言环境,字符串将不会被重新翻译。
最好能够通过其他方式显式定义knockout计算的observables的依赖关系,但我不知道knockout目前也提供这样的API;请参阅相关文档。我也尝试使用显式订阅机制,但这并不令人满意,因为我认为目前不可能在不更改其依赖项之一的情况下触发计算显式重新运行。如果你放弃计算并只使用手动订阅,你最终会重写淘汰赛本身(试试看!),所以我更喜欢用计算的 observable 和虚拟读取来妥协。不管看起来多么奇怪,它可能只是这里最优雅的解决方案。不要忘记警告龙在评论中。
该函数有点基本,因为它只扫描选项对象的第一级属性以确定它们是否是可观察的,如果是,则解包它们(不支持嵌套对象或数组)。根据您使用的本地化库,打开某些选项而不是其他选项是有意义的。因此,正确地执行此操作需要您在包装器中模仿底层 API。
我把它作为一个附注只是因为我没有测试它,但你可能想使用敲除映射插件及其toJS
方法来解开你的对象,它看起来可能是一个单线。
以下是初始化 i18next 的方法(大多数其他库都有类似的设置过程),例如从您的 RequireJSdata-main
脚本(通常是 main.js)或您的 shell 视图模型(如果有的话):
var ko = require('knockout');
var i18next = require('i18next');
var locale = require('locale');
i18next.init({
lng: locale(),
getAsync: false,
resGetPath: 'app/locale/__ns__-__lng__.json',
});
locale.subscribe(function (value) {
i18next.setLng(value, function () {});
});
当我们的 locale observable 发生变化时,我们会在此处更改库的全局区域设置。通常,您会将 observable 绑定到语言选择器;请参阅相关文档。
i18next 的具体说明:如果要异步加载资源,由于 Durandal 应用程序的异步特性,运行起来会有点麻烦;事实上,我没有看到将其余视图模型设置代码包装在回调中的明显方法init
,因为它超出了我们的控制范围。因此,翻译将在初始化完成之前被调用。您可以通过手动跟踪库是否已初始化来解决此问题,例如通过在init
回调中设置变量(此处省略参数)。我对此进行了测试,效果很好。不过,为了简单起见,资源是同步加载的。
i18next 特定说明:空回调setLng
是其老派性质的产物;该库希望您始终在更改语言后开始重新翻译字符串(很可能通过使用 jQuery 扫描 DOM),因此需要该参数。在我们的例子中,一切都是自动更新的,我们不需要做任何事情。
最后,这是一个如何使用 translate 函数的示例:
var _ = require('translate');
var n_foo = ko.observable(42);
var greeting = _('greeting');
var foo = _('foo', { count: n_foo });
你可以在你的视图模型中公开这些变量,它们是简单的剔除计算的 observables。现在,每次更改语言环境或翻译参数时,都会重新翻译字符串。由于它是可观察的,所有的观察者(例如你的观点)都会被通知和更新。
var locale = require('locale');
locale('en_US');
n_foo(1);
...
无需重新加载文档。无需在任何地方显式调用翻译函数。它只是工作。
在淘汰赛中集成本地化库
您可能会尝试制作淘汰插件和扩展程序来添加对本地化库的支持(除了自定义绑定处理程序),但是我还没有探索过这个想法,所以我不知道这种设计的价值。再次,随时为这个答案做出贡献。
在 Ecmascript 5 访问器上
由于这些访问器随处都带有对象属性,我怀疑可能会使用诸如knockout-es5插件或Durandal observable 插件之类的东西将可观察对象透明地传递给不支持剔除的 API。但是,您仍然需要将调用包装在计算出的 observable 中,所以我不确定这能让我们走多远。
再说一次,这不是我经常看的东西,欢迎投稿。
On Knockout 扩展器
您可以潜在地利用KO 扩展器来增强正常的可观察量以即时翻译它们。虽然这在理论上听起来不错,但我认为它实际上不会起到任何作用。您仍然需要跟踪传递给扩展程序的每个选项,很可能是通过手动订阅每个选项并通过调用包装的翻译函数来更新目标。
如果有的话,那只是一种替代语法,而不是替代方法。
结论
感觉仍然缺少很多东西,但是通过一个 21 行的模块,我能够为标准的 Durandal 应用程序添加对任意本地化库的支持。对于最初的时间投资,我想可能会更糟。最困难的部分是弄清楚,我希望我在为你加速这个过程方面做得不错。
事实上,虽然做对了可能听起来有点复杂(好吧,我相信无论如何都是正确的方法),我非常有信心,像这样的技术可以让事情在全球范围内变得更简单,至少与你遇到的所有麻烦相比在文档重新加载后尝试一致地重建状态,或者在没有 Knockout 的情况下手动跟踪所有翻译的字符串。此外,它肯定更高效(UX 不能更流畅):只有需要重新翻译的字符串才会重新翻译,并且仅在必要时进行。
2014 年 11 月笔记
写完这篇文章后,我们将 i18next 初始化代码和translate
模块中的代码合并到一个 AMD 模块中。该模块有一个接口,旨在模仿库存 i18next AMD 模块的其余接口(尽管我们从未超越该translate
功能),因此库的“KO-ification”对应用程序是透明的(除了因为它现在识别 KO observables 并locale
在其配置中采用 observable 单例,当然)。我们甚至设法通过一些 require.js 路径技巧重用相同的“i18next”AMD 模块名称。
所以,如果你仍然想做这个集成工作,你可以放心,这是可能的,最终这似乎是我们最明智的解决方案。将locale
observable 保存在单例模块中也被证明是一个不错的决定。
至于翻译函数本身,使用 stockko.toJS
函数打开 observable 确实要容易得多。
i18next.js(淘汰赛集成包装器)
define(function (require) {
'use strict';
var ko = require('knockout');
var i18next = require('i18next-actual');
var locale = require('locale');
var namespaces = require('tran-namespaces');
var Mutex = require('komutex');
var mutex = new Mutex();
mutex.lock(function (unlock) {
i18next.init({
lng: locale(),
getAsync: true,
fallbackLng: 'en',
resGetPath: 'app/locale/__lng__/__ns__.json',
ns: {
namespaces: namespaces,
defaultNs: namespaces && namespaces[0],
},
}, unlock);
});
locale.subscribe(function (value) {
mutex.lock(function (unlock) {
i18next.setLng(value, unlock);
});
});
var origFn = i18next.t;
i18next.t = i18next.translate = function (key, opts) {
return ko.computed(function () {
return mutex.tryLockAuto(function () {
locale();
return origFn(key, opts && ko.toJS(opts));
});
});
};
return i18next;
});
require.js 路径诡计(好吧,没那么棘手)
requirejs.config({
paths: {
'i18next-actual': 'path/to/real/i18next.amd-x.y.z',
'i18next': 'path/to/wrapper/above',
}
});
该locale
模块与上面介绍的相同单例,该tran-namespaces
模块是另一个包含 i18next 命名空间列表的单例。这些单例非常方便,不仅因为它们提供了一种非常声明式的方式来配置这些东西,还因为它允许 i18next 包装器(这个模块)完全自初始化。换句话说,require
它永远不需要调用的用户模块init
。
现在,初始化需要时间(可能需要获取一些翻译文件),正如我在一年前已经提到的,我们实际上使用了异步接口(getAsync: true
)。这意味着调用的用户模块translate
实际上可能不会直接获得翻译(如果它在初始化完成之前或在切换语言环境时要求翻译)。请记住,在我们的实现中,用户模块可以立即开始调用i18next.t
,而无需显式等待来自init
回调的信号;他们不必调用它,因此我们甚至没有在我们的模块中为这个函数提供一个包装器。
这怎么可能?好吧,为了跟踪这一切,我们使用了一个“互斥锁”对象,它只包含一个布尔可观察对象。每当该互斥体“锁定”时,就意味着我们正在初始化或更改语言环境,并且不应进行翻译。该互斥锁的状态translate
由代表(未来)翻译的 KO 计算的可观察函数在函数中自动跟踪,因此当它变为“解锁”时将自动重新执行(感谢 KO 的魔力),因此真正的translate
功能可以重试并完成它的工作。
它可能比实际理解更难解释(如您所见,上面的代码并不太长),请随时要求澄清。
使用非常简单;只需var i18next = require('i18next')
在应用程序的任何模块中,然后i18next.t
随时调用。就像初始translate
函数一样,您可以将 observable 作为参数传递(其效果是每次更改此类参数时都会自动重新翻译该特定字符串),它将返回一个 observable 字符串。事实上,该函数不使用this
,因此您可以放心地将其分配给一个方便的变量:var _ = i18next.t
。
到目前为止,您可能正在查找komutex
您最喜欢的搜索引擎。好吧,除非有人有相同的想法,否则您将找不到任何东西,而且我不打算按原样发布该代码(我不能这样做而不失去我所有的信誉;))。上面的解释应该包含在没有这个模块的情况下实现相同类型的东西所需要知道的所有内容,尽管它使代码混乱,我个人倾向于像我在这里所做的那样在专用组件中提取问题。到最后,我们甚至不能 100% 确定互斥体抽象是正确的,所以即使它看起来简洁明了,我建议您考虑一下如何提取该代码(或者只是考虑是否提取提取与否)。
更一般地说,我还建议您寻求其他有关此类集成工作的说明,因为尚不清楚这些想法是否会过时(一年后,我仍然相信这种本地化/翻译的“反应式”方法绝对是正确的,但这只是我)。也许您甚至会发现更多现代库可以满足您开箱即用的需求。
无论如何,我极不可能再次重温这篇文章。再次,我希望这个小(!)更新和最初的帖子一样有用。
玩得开心!