为 json 项实施 knockout.js 本地化
Implement knockout.js localization for json items
我正在构建一个单页应用程序来管理通过 json 数据检索的数千件商品的订单输入。
订单条目的结构是深度嵌套的:items、packaging、customer、vendor、city、country、week、month等等(大约十层)。
我决定对这个 Web 应用程序使用 knockout.js,下面的项目例如在最低级别上是用可观察数组实现的:
{"Code": "BA", "Description": "Bananas", "UnitPrice": 0.35};
{"Code": "AP", "Description": "Apples", "UnitPrice": 0.25};
{"Code": "OR", "Description": "Oranges", "UnitPrice": 0.45};
...
我现在正在寻找将此贸易项目的描述本地化的正确方法,因为要求之一是始终显示订单摘要并为所有订单提供某种人类可读的统计数据。
我知道已经有一些经过良好测试的 knockout 插件和模块可以处理本地化,例如 i18n,但我还需要本地化用户界面和代码中项目的翻译。所以,我需要的是所有描述始终以当地语言提供。
我决定使用项目代码来匹配本地化的项目描述,因此翻译文件的结构如下:
{BA: 'Bananas', AP: 'Apples', OR: 'Oranges'}
{BA: 'Bananen', AP: 'Äpfel', OR: 'Orangen'}
{BA: 'Banány', AP: 'Jablka', OR: 'Pomeranče'}
此后以非常简单的方式检索本地化描述:
function translateByCode(code) {
var t = vm.language.translation();
if (t.hasOwnProperty(code))
return t[code]
else
return '';
}
现在,因为我是 knockout.js 的新手,我测试了三种不同的方法来实现解决方案:
sub-observables, computed observables and extenders
1) Sub-observable:我可以翻译 observable-underlying-array,然后通过调用 valueHasMutated() 一步将更改应用到所有项目,但用户界面必须手动更新。
2) Computed observable:易于实现并自动工作,但我是否需要数千个额外的 observable 来进行本地化?一旦应用,翻译在订单输入期间永远不会改变。
3) Extender:也许是正确且最优雅的解决方案,但我不知道这是否与计算的可观察对象具有相同的开销,或者有什么缺点。
这是我的意思的示例:https://jsfiddle.net/ch9ubdu1/3/
在我看来,所有这些树解决方案都有利有弊,我想问:是否已经有一个清晰、完善的模式?
或者有人可以解释为什么我绝对应该使用例如计算的可观察量而不是其他的?
编辑:
这里是一个更加结构化的示例,其中描述 lines 作为附加到项目的数组,并绑定到 knockout.js foreach 循环。
https://jsfiddle.net/ch9ubdu1/7/
描述是快速而肮脏的构建方式,但此示例展示了如何将描述集成到模型中以动态构建标记。
我不会选择你建议的前两个选项,原因如下:
- 必须定期致电
valueHasMutated()
可能不是一个好习惯。淘汰赛的全部意义在于,它会为您努力跟踪依赖关系/更新 UI。
- 我同意您对这种方法的评论。即使您的翻译需要是动态的,为每个翻译创建一个计算的可观测值也会很快使您的视图模型变得混乱。
扩展器是一种解决方案。自定义绑定也可以完成这项工作。在这些方法之间进行选择可能更多地取决于个人喜好,但自定义绑定确实有一个我能想到的优势。
我注意到您的示例使用语言视图模型来访问标签文本的翻译,例如"order" 和 "total"。使用自定义出价翻译可以在不需要底层视图模型的情况下实现,您只需将正确的代码放入具有正确绑定的视图中。
所以而不是
<span data-bind="text: language.translation().TOT">
你可以
<span data-bind="translationFor: 'TOT', language: selectedLanguage"></span>
如果您想了解更多信息,请参阅 this JSFiddle for an example that uses custom bindings. The knockout documentation 对自定义绑定的详细描述。
没有最好的独特的全局方法,所以我认为答案是:"use the best of all possibilities that knockout has to offer for this job"。
对于我使用的本地化,我使用了一个单独的视图模型,因为这是一个已经过测试的剪切和粘贴解决方案,我可以在任何地方重复使用这个视图模型,里面没有特殊的功能或引用。
var LanguageViewModel = function () {
var self = this;
self.languages = ko.observableArray(languages);
self.selectedLanguage = ko.observable(languages[2]);
self.translation = ko.computed(function () {
return resources[self.selectedLanguage().id];
});
self.descriptionByCode = function (code) {
var translation = self.translation();
if (translation.hasOwnProperty(code)) {
var resource = translation[code];
if (resource.hasOwnProperty('name')) return resource.name
else return resource;
} else return '';
};
self.descriptionLinesByCode = function (code) {
var translation = self.translation();
if (translation.hasOwnProperty(code)) {
var resource = translation[code];
if (resource.hasOwnProperty('text')) return resource.text
else return [resource];
} else return [];
};
};
与许多其他常见的本地化模式一样,所有要翻译的内容都必须有一个 代码,所以两个“核心功能” descriptionByCode() 和 descriptionLinesByCode() 是一个常见的实现。
所以,我通过以下实现结束了:
1) 使用扩展器和可选的通用翻译自定义绑定:
ko.extenders.translation = function (target, option) {
target.description = function () {
var code = target.peek();
return vm.language.descriptionByCode(code);
};
target.descriptionLines = function () {
var code = target.peek();
return vm.language.descriptionLinesByCode(code);
};
return target;
};
现在,我可以使用这样的数据绑定:
data-bind="text: observable.description()"
...但是通过使用像这样的简单自定义绑定:
ko.bindingHandlers.description = {
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var valueUnwrapped = ko.unwrap(valueAccessor());
ko.bindingHandlers.text.update(element, valueAccessor().description, allBindings, viewModel, bindingContext);
}
};
...我可以使用更简洁的语法:
data-bind="description: observable".
...也适用于可观察对象内的复杂对象(感谢 neilculver):
data-bind="description: 'subproperty'".
Finally, i understood that i cannot skip using computed observables at
all, but
- i can limit the number of observable properties, because i can put inside an observable an
object or an array of objects as well
- i can
keep my view-model clean and small by nesting sub-observable properties and computed observables which are not belonging to the first level
of their view-model
作为概念证明,这里还有两个例子:
2) 使用 knockout 自定义函数,例如 selected 项目的本地化摘要:
ko.observableArray.fn.summary = function () {
return ko.computed(function () {
var items = this(),
total = 0,
descriptions = [];
for (var i = 0, l = items.length; i < l; i++) {
var item = items[i];
var quantity = ko.unwrap(item.quantity);
if (quantity > 0) {
descriptions.push(quantity + ' x ' + item.code.description());
total += quantity;
}
}
return descriptions;
}, this);
}
我被允许这样做,因为我总是有一个数量和一个代码,而且我的视图模型保持非常简单和干净:
var OrdersViewModel = function (data) {
var self = this;
self.fruits = ko.observableArray(ko.utils.arrayMap(data, function (item) {
return new Fruit(item);
}));
self.fruits.orderSummary = self.fruits.summary();
};
3) 对其他人使用子可观察对象,例如用于淘汰选项绑定
当项目的选项也与我的数据一起提供时,我的项目视图模型将如下所示:
var items =
[{Code: "BA", Quantity: 1, UnitPrice: 0.35},
{Code: "AP", Quantity: 1, Types: ["RED", "GREEN", "YELLOW"], UnitPrice: 0.25},
{Code: "OR", Quantity: 0, UnitPrice: 0.45}];
function Fruit(data) {
var self = this;
self.code = ko.observable(data.Code).extend({
translation:null
});
self.code.unitPrice = data.UnitPrice;
self.code.typeOptions = data.Types;
self.type = ko.observable('');
self.type.options = ko.computed(function () {
var l = vm.language.selectedLanguage();
var typeOptions = self.code.typeOptions;
var items = ko.utils.arrayMap(typeOptions, function (item) {
var description = vm.language.descriptionByCode(item);
return {code: item, description: description};
});
return items;
});
self.quantity = ko.observable(data.Quantity);
self.totalPrice = ko.computed(function () {
return self.code.unitPrice * self.quantity();
});
}
...现在我可以为本地化的 select 元素创建如下所示的标记,该元素会自动响应语言切换:
<select data-bind="options:item.type.options,optionsText:'description',optionsValue:'code',value:item.type"></select>
本地化选项数组的创建也可以从视图模型中提取并放入一个单独的函数中 returns ko.computed.
这是一个适用于上述所有内容的 fiddle:https://jsfiddle.net/ch9ubdu1/10/
我正在构建一个单页应用程序来管理通过 json 数据检索的数千件商品的订单输入。 订单条目的结构是深度嵌套的:items、packaging、customer、vendor、city、country、week、month等等(大约十层)。 我决定对这个 Web 应用程序使用 knockout.js,下面的项目例如在最低级别上是用可观察数组实现的:
{"Code": "BA", "Description": "Bananas", "UnitPrice": 0.35};
{"Code": "AP", "Description": "Apples", "UnitPrice": 0.25};
{"Code": "OR", "Description": "Oranges", "UnitPrice": 0.45};
...
我现在正在寻找将此贸易项目的描述本地化的正确方法,因为要求之一是始终显示订单摘要并为所有订单提供某种人类可读的统计数据。
我知道已经有一些经过良好测试的 knockout 插件和模块可以处理本地化,例如 i18n,但我还需要本地化用户界面和代码中项目的翻译。所以,我需要的是所有描述始终以当地语言提供。
我决定使用项目代码来匹配本地化的项目描述,因此翻译文件的结构如下:
{BA: 'Bananas', AP: 'Apples', OR: 'Oranges'}
{BA: 'Bananen', AP: 'Äpfel', OR: 'Orangen'}
{BA: 'Banány', AP: 'Jablka', OR: 'Pomeranče'}
此后以非常简单的方式检索本地化描述:
function translateByCode(code) {
var t = vm.language.translation();
if (t.hasOwnProperty(code))
return t[code]
else
return '';
}
现在,因为我是 knockout.js 的新手,我测试了三种不同的方法来实现解决方案:
sub-observables, computed observables and extenders
1) Sub-observable:我可以翻译 observable-underlying-array,然后通过调用 valueHasMutated() 一步将更改应用到所有项目,但用户界面必须手动更新。
2) Computed observable:易于实现并自动工作,但我是否需要数千个额外的 observable 来进行本地化?一旦应用,翻译在订单输入期间永远不会改变。
3) Extender:也许是正确且最优雅的解决方案,但我不知道这是否与计算的可观察对象具有相同的开销,或者有什么缺点。
这是我的意思的示例:https://jsfiddle.net/ch9ubdu1/3/
在我看来,所有这些树解决方案都有利有弊,我想问:是否已经有一个清晰、完善的模式? 或者有人可以解释为什么我绝对应该使用例如计算的可观察量而不是其他的?
编辑:
这里是一个更加结构化的示例,其中描述 lines 作为附加到项目的数组,并绑定到 knockout.js foreach 循环。
https://jsfiddle.net/ch9ubdu1/7/
描述是快速而肮脏的构建方式,但此示例展示了如何将描述集成到模型中以动态构建标记。
我不会选择你建议的前两个选项,原因如下:
- 必须定期致电
valueHasMutated()
可能不是一个好习惯。淘汰赛的全部意义在于,它会为您努力跟踪依赖关系/更新 UI。 - 我同意您对这种方法的评论。即使您的翻译需要是动态的,为每个翻译创建一个计算的可观测值也会很快使您的视图模型变得混乱。
扩展器是一种解决方案。自定义绑定也可以完成这项工作。在这些方法之间进行选择可能更多地取决于个人喜好,但自定义绑定确实有一个我能想到的优势。
我注意到您的示例使用语言视图模型来访问标签文本的翻译,例如"order" 和 "total"。使用自定义出价翻译可以在不需要底层视图模型的情况下实现,您只需将正确的代码放入具有正确绑定的视图中。
所以而不是
<span data-bind="text: language.translation().TOT">
你可以
<span data-bind="translationFor: 'TOT', language: selectedLanguage"></span>
如果您想了解更多信息,请参阅 this JSFiddle for an example that uses custom bindings. The knockout documentation 对自定义绑定的详细描述。
没有最好的独特的全局方法,所以我认为答案是:"use the best of all possibilities that knockout has to offer for this job"。
对于我使用的本地化,我使用了一个单独的视图模型,因为这是一个已经过测试的剪切和粘贴解决方案,我可以在任何地方重复使用这个视图模型,里面没有特殊的功能或引用。
var LanguageViewModel = function () {
var self = this;
self.languages = ko.observableArray(languages);
self.selectedLanguage = ko.observable(languages[2]);
self.translation = ko.computed(function () {
return resources[self.selectedLanguage().id];
});
self.descriptionByCode = function (code) {
var translation = self.translation();
if (translation.hasOwnProperty(code)) {
var resource = translation[code];
if (resource.hasOwnProperty('name')) return resource.name
else return resource;
} else return '';
};
self.descriptionLinesByCode = function (code) {
var translation = self.translation();
if (translation.hasOwnProperty(code)) {
var resource = translation[code];
if (resource.hasOwnProperty('text')) return resource.text
else return [resource];
} else return [];
};
};
与许多其他常见的本地化模式一样,所有要翻译的内容都必须有一个 代码,所以两个“核心功能” descriptionByCode() 和 descriptionLinesByCode() 是一个常见的实现。
所以,我通过以下实现结束了:
1) 使用扩展器和可选的通用翻译自定义绑定:
ko.extenders.translation = function (target, option) {
target.description = function () {
var code = target.peek();
return vm.language.descriptionByCode(code);
};
target.descriptionLines = function () {
var code = target.peek();
return vm.language.descriptionLinesByCode(code);
};
return target;
};
现在,我可以使用这样的数据绑定:
data-bind="text: observable.description()"
...但是通过使用像这样的简单自定义绑定:
ko.bindingHandlers.description = {
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var valueUnwrapped = ko.unwrap(valueAccessor());
ko.bindingHandlers.text.update(element, valueAccessor().description, allBindings, viewModel, bindingContext);
}
};
...我可以使用更简洁的语法:
data-bind="description: observable".
...也适用于可观察对象内的复杂对象(感谢 neilculver):
data-bind="description: 'subproperty'".
Finally, i understood that i cannot skip using computed observables at all, but
- i can limit the number of observable properties, because i can put inside an observable an object or an array of objects as well
- i can keep my view-model clean and small by nesting sub-observable properties and computed observables which are not belonging to the first level of their view-model
作为概念证明,这里还有两个例子:
2) 使用 knockout 自定义函数,例如 selected 项目的本地化摘要:
ko.observableArray.fn.summary = function () {
return ko.computed(function () {
var items = this(),
total = 0,
descriptions = [];
for (var i = 0, l = items.length; i < l; i++) {
var item = items[i];
var quantity = ko.unwrap(item.quantity);
if (quantity > 0) {
descriptions.push(quantity + ' x ' + item.code.description());
total += quantity;
}
}
return descriptions;
}, this);
}
我被允许这样做,因为我总是有一个数量和一个代码,而且我的视图模型保持非常简单和干净:
var OrdersViewModel = function (data) {
var self = this;
self.fruits = ko.observableArray(ko.utils.arrayMap(data, function (item) {
return new Fruit(item);
}));
self.fruits.orderSummary = self.fruits.summary();
};
3) 对其他人使用子可观察对象,例如用于淘汰选项绑定
当项目的选项也与我的数据一起提供时,我的项目视图模型将如下所示:
var items =
[{Code: "BA", Quantity: 1, UnitPrice: 0.35},
{Code: "AP", Quantity: 1, Types: ["RED", "GREEN", "YELLOW"], UnitPrice: 0.25},
{Code: "OR", Quantity: 0, UnitPrice: 0.45}];
function Fruit(data) {
var self = this;
self.code = ko.observable(data.Code).extend({
translation:null
});
self.code.unitPrice = data.UnitPrice;
self.code.typeOptions = data.Types;
self.type = ko.observable('');
self.type.options = ko.computed(function () {
var l = vm.language.selectedLanguage();
var typeOptions = self.code.typeOptions;
var items = ko.utils.arrayMap(typeOptions, function (item) {
var description = vm.language.descriptionByCode(item);
return {code: item, description: description};
});
return items;
});
self.quantity = ko.observable(data.Quantity);
self.totalPrice = ko.computed(function () {
return self.code.unitPrice * self.quantity();
});
}
...现在我可以为本地化的 select 元素创建如下所示的标记,该元素会自动响应语言切换:
<select data-bind="options:item.type.options,optionsText:'description',optionsValue:'code',value:item.type"></select>
本地化选项数组的创建也可以从视图模型中提取并放入一个单独的函数中 returns ko.computed.
这是一个适用于上述所有内容的 fiddle:https://jsfiddle.net/ch9ubdu1/10/