Приложение Durandal (нокаут) с поддержкой нескольких языков

Я создаю многоязычную поддержку для приложения, над которым я работаю. После некоторых исследований и чтения SO (лучшей практики интернационализации) я пытаюсь интегрировать это в "дружественный к среде" способ. То, что я сделал в данный момент, следующее:

Созданы модули .resource, отформатированные так:

resources.en-US.js

define(function () {
   return {
       helloWorlLabelText: "Hello world!"
   }
});

На app.start я получаю модуль ресурсов с requirejs и присваиваю все данные app.resources. Внутри каждого модуля определенный ресурс присваивается наблюдаемым и связан с привязкой текста к меткам и другим связанным с текстам вещам. Например:

define(function (require) {
   var app = require('durandal/app'),
       router = require('durandal/plugins/router')
   };
   return{
       helloWorldLabelText: ko.observable(app.resources.helloWorldLabelText),

       canDeactivate: function () { 
      }
   }
});

On the view:

<label for="hello-world" data-bind="text: helloWorldLabelText"></label>

Ресурсы заменяются путем назначения нового модуля app.resources.

Теперь проблема в том, когда язык изменен, и некоторые из представлений уже отображены, значения предыдущего языка все еще существуют. Поэтому я закончил переназначение наблюдаемых внутри метода активации. Также попытался обернуть app.resources в наблюдаемый, но это тоже не сработало.

Я не думаю, что закончил с самым чистым способом, и, возможно, у кого-нибудь еще был другой способ, который мог бы поделиться. Спасибо.

Ответы

Ответ 1

Для тех, кто все еще путается с лучшими практиками, те, кто чувствует, что чего-то не хватает, или тех, кому просто интересно, как лучше реализовать вещи в отношении Durandal, Knockout, RequireJS и клиентской сети приложения в целом, вот попытка более полезного обзора того, что возможно.

Это, конечно, не полно, но, надеюсь, это немного расширит некоторые умы.

Во-первых, обновление в ноябре 2014 года

Я вижу, что этот ответ продолжает регулярно повышаться даже через год. Я не решался обновлять его несколько раз, так как я разработал наше конкретное решение (интегрировав i18next в Durandal/AMD/Knockout). Однако в конечном итоге мы отказались от зависимого проекта из-за внутренних трудностей и "проблем" в отношении будущего Durandal и других частей нашего стека. Следовательно, эта небольшая интеграционная работа также была отменена.

Считаю, что, я надеюсь, достаточно хорошо отнесены общеприменимые замечания от конкретных замечаний ниже, поэтому я думаю, что они продолжают предлагать полезные (возможно, даже необходимые) перспективы по этим вопросам.

Если вы все еще хотите играть с Durandal, Knockout, AMD и произвольной библиотекой локализации (кстати, есть некоторые новые игроки для оценки), я добавил пару заметок из моего более позднего опыта на конец.

На одноэлементном шаблоне

Одна проблема с шаблоном singleton здесь заключается в том, что трудно настроить per-view; действительно, существуют и другие параметры для переводов, чем их языковой стандарт (учитывает множественные формы, контекст, переменные, пол), и они сами могут быть конкретными для определенных контекстов (например, видов/моделей представлений).

Кстати, важно, чтобы вы не делали этого сами и вместо этого полагались на библиотеку/структуру локализации (она может стать очень сложной). Существует много вопросов относительно этих проектов.

Вы все еще можете использовать синглтон, но в любом случае вы находитесь на полпути.

В обработчики привязки нокаутов

Одним из решений, рассмотренных zewa666 в другом ответе, является создание обработчика привязки KO, Можно представить, что этот обработчик принимает эти параметры из представления, а затем использует любую библиотеку локализации в качестве бэкэнд. Чаще всего вам необходимо программно изменить эти параметры в модели viewmodel (или в другом месте), что означает, что вам все равно нужно выставить JS API.

Если вы все равно подвергаете такой API, вы можете использовать его для заполнения вашей модели представления и вообще пропускать обработчики привязки. Тем не менее, они по-прежнему являются хорошим ярлыком для тех строк, которые могут быть настроены непосредственно из представления. Предоставление обоих методов - это хорошо, но вы, вероятно, не можете обойтись без JS API.

Текущие API-интерфейсы Javascript, перезагрузка документа

Большинство библиотек и фреймворков локализации - довольно старая школа, и многие из них ожидают, что вы перезагрузите всю страницу всякий раз, когда пользователь изменяет локаль, иногда даже когда параметры перевода меняются по разным причинам. Не делайте этого, это противоречит всему, на что указывает веб-приложение на стороне клиента. (В настоящее время SPA - это крутой термин для этого.)

Основная причина заключается в том, что в противном случае вам нужно будет отслеживать каждый элемент DOM, который вам нужен для ретрансляции, каждый раз, когда изменяется локаль, и какие элементы ретранслируются каждый раз при изменении любого из их параметров. Это очень утомительно делать вручную.

К счастью, именно то, что связывают данные, такие как нокаут, очень легко сделать. В самом деле, проблема, о которой я только что сказал, должна напоминать вам о том, как KO вычисляет наблюдаемые и KO data-bind атрибуты пытаются решить.

В RequJS плагин i18n

Плагин использует шаблон singleton и ожидает перезагрузки документа. No-go для использования с Durandal.

Вы можете, но это не эффективно, и вы можете или не можете бесполезно сталкиваться с проблемами в зависимости от того, насколько сложным является ваше состояние приложения.

Интеграция нокаута в библиотеках локализации

В идеале библиотеки локализации будут поддерживать наблюдаемые нокауты, так что всякий раз, когда вы передаете им наблюдаемую строку для перевода с наблюдаемыми параметрами, библиотека дает вам наблюдаемый перевод назад. Интуитивно, каждый раз, когда язык, строка или параметры изменяются, библиотека изменяет наблюдаемый перевод, и если они связаны с представлением (или чем-либо еще), представление (или что-то еще) динамически обновляется, не требуя от вас делать что-либо явно.

Если ваша библиотека локализации достаточно расширяема, вы можете написать для нее плагин или попросить разработчиков реализовать эту функцию или дождаться появления более современных библиотек.

Я не знаю сейчас, но мои знания о экосистеме 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 браузера, локального хранилища, куки, URI или любого другого механизма.

примечание i18next: AFAIK, API i18next.t не имеет возможности использовать определенную локаль для перевода: он всегда использует глобально настроенную локаль. Из-за этого мы должны изменить эту глобальную настройку другими способами (см. Ниже) и поместить фиктивное чтение в наблюдаемую локаль, чтобы заставить нокаут добавить его как зависимость от вычисленного наблюдаемого. Без него строки не будут ретранслироваться, если мы изменим наблюдаемый локаль.

Было бы лучше иметь возможность явно определять зависимости для вычисляемых наблюдаемых нокаутов другими способами, но я не знаю, что нокаут в настоящее время предоставляет такой API; см. соответствующую документацию. Я также попытался использовать механизм явной подписки, но это было неудовлетворительно, так как я не думаю, что в настоящее время это возможно вызывать вычисление для повторного запуска без изменения одной из своих зависимостей. Если вы откажетесь от вычисленной и используете только ручную подписку, вы в конечном итоге переработаете нокаут (попробуйте!), Поэтому я предпочитаю идти на компромисс с вычисленным наблюдаемым и фиктивным чтением. Как бы ни странно это выглядело, это могло бы быть самым элегантным решением здесь. Не забудьте предупредить о драконах в комментарии.

Функция несколько базовая, поскольку она только сканирует свойства первого уровня объекта опций, чтобы определить, являются ли они наблюдаемыми, и, если это так их разворачивает (нет поддержки вложенных объектов или массивов). В зависимости от библиотеки локализации, которую вы используете, имеет смысл развернуть определенные параметры, а не другие. Следовательно, для правильной работы вам потребуется подражать базовому API в вашей обертке.

Я включаю это как побочное примечание только потому, что я его не тестировал, но вы можете использовать плагин отображения нокаута и его метод toJS для разворачивания вашего объекта, который выглядит как однострочный.

Вот как вы можете инициализировать i18next (большинство других библиотек имеют аналогичную процедуру настройки), например, из вашего RequireJS data-main script (обычно main.js) или вашей модели представления оболочки, если у вас ее есть:

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 () {});
});

Здесь мы меняем глобальную локальную настройку библиотеки, когда меняются наблюдаемые локали. Обычно вы привязываете наблюдаемый к языковому селектору; см. соответствующую документацию.

примечание i18next:Если вы хотите загрузить ресурсы асинхронно, вы столкнетесь с небольшими проблемами из-за асинхронного аспекта приложений Durandal; действительно, я не вижу очевидного способа обернуть оставшийся код установки моделей представлений в обратном вызове init, поскольку он находится вне нашего контроля. Следовательно, переводы будут вызываться до завершения инициализации. Вы можете исправить это, вручную отслеживая, инициализирована ли библиотека, например, установив переменную в обратном вызове init (аргумент опущен здесь). Я тестировал это, и он отлично работает. Однако для простоты ресурсы загружаются синхронно.

Заметка i18next: Пустой обратный вызов setLng является артефактом из его старой школы; библиотека ожидает, что вы всегда начнете ретранслировать строки после изменения языка (скорее всего, сканируя DOM с помощью jQuery), и, следовательно, требуется аргумент. В нашем случае все обновляется автоматически, нам не нужно ничего делать.

Наконец, вот пример использования функции перевода:

var _ = require('translate');

var n_foo = ko.observable(42);
var greeting = _('greeting');
var foo = _('foo', { count: n_foo });

Вы можете выставить эти переменные в своих моделях просмотра, это простые вычисленные вычисленные выходы. Теперь, каждый раз, когда вы меняете языковой стандарт или параметры перевода, строка будет ретранслироваться. Поскольку это наблюдение, все наблюдатели (например, ваши взгляды) будут уведомлены и обновлены.

var locale = require('locale');

locale('en_US');
n_foo(1);
...

Не требуется перезагрузка документа. Нет необходимости явно вызывать функцию перевода в любом месте. Он просто работает.

Интеграция библиотек локализации в нокаут

Вы можете попытаться сделать плагины для нокаута и расширители, чтобы добавить поддержку библиотек локализации (помимо настраиваемых обработчиков привязки), однако я не изучил эту идею, поэтому значение этого дизайна мне неизвестно. Опять же, не стесняйтесь вносить свой вклад в этот ответ.

В объективах с символикой 5

Поскольку эти аксессоры несут свойства объектов во всем мире, я подозреваю что-то вроде knockout-es5 или Durandal наблюдаемый плагин может использоваться для прозрачного прохождения наблюдаемых API-интерфейсов, которые не поддерживают нокаут. Тем не менее, вам все равно нужно обернуть вызов в вычисленном наблюдаемом, поэтому я не уверен, насколько дальше нас это добивается.

Опять же, это не то, что я много смотрел, приветствуются вклады.

На удлинителях нокаута

Вы можете потенциально использовать KO extener для увеличения обычных наблюдаемых, чтобы перевести их на лету. Хотя это звучит хорошо в теории, я не думаю, что это действительно послужило бы какой-либо цели; вам все равно нужно будет отслеживать все параметры, которые вы передаете расширителю, скорее всего, вручную подписавшись на каждую из них и обновив цель, вызвав завернутую функцию перевода.

Во всяком случае, это просто альтернативный синтаксис, а не альтернативный подход.

Заключение

Похоже, что все еще не хватает, но с 21-строчным модулем я смог добавить поддержку для любой библиотеки локализации в стандартное приложение Durandal. Для первоначального инвестирования времени, я думаю, это может быть хуже. Самая сложная часть - это выяснить, и я надеюсь, что я сделал достойную работу, чтобы ускорить этот процесс для вас.

На самом деле, если делать это правильно, может показаться немного сложным (ну, во всяком случае, я считаю, что это правильный путь), я довольно уверен, что подобные методы делают вещи глобально проще, по крайней мере, по сравнению со всеми неприятностями вы можете попытаться восстановить состояние последовательно после перезагрузки документа или вручную отслеживать все переведенные строки без нокаута. Кроме того, он эффективнее определенно (UX не может быть более гладким): ретранслируются только те строки, которые должны быть ретранслированы, и только при необходимости.

Примечания за ноябрь 2014 года

После написания этого сообщения мы объединили код инициализации i18next и код из модуля translate в одном модуле AMD. Этот модуль имел интерфейс, предназначенный для имитации остальной части интерфейса модуля i18next AMD (хотя мы никогда не проходили мимо функции translate), так что "KO-ification" библиотеки была бы прозрачной для приложений (за исключением того факта, что он теперь распознал наблюдаемые KO и, конечно, взял наблюдаемый синглтон locale в своей конфигурации). Нам даже удалось повторно использовать одно и то же имя "i18next" AMD с некоторыми трюками require.js.

Итак, если вы все еще хотите сделать эту интеграционную работу, вы можете быть уверены, что это возможно, и в конечном итоге это казалось самым разумным решением для нас. Хорошим решением было также наблюдение за наблюдаемым locale в одноэлементном модуле.

Что касается самой функции перевода, то разворачивание наблюдаемых с использованием функции запаса ko.toJS было действительно намного проще.

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 trickery (ОК, не так сложно)

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 явно; им не нужно вызывать его, и поэтому мы даже не предоставляем оболочку для этой функции в нашем модуле.

Как это возможно? Ну, чтобы отслеживать все это, мы используем объект "Mutex", который просто содержит логическое наблюдаемое. Всякий раз, когда этот мьютекс "заблокирован", это означает, что мы инициализируем или меняем локали, и переводы не должны проходить. Состояние этого мьютекса автоматически отслеживается функцией translate функцией KO, вычисленной наблюдаемым, которая представляет (будущий) перевод и, таким образом, будет автоматически перезаписана (благодаря магии KO), когда она изменится на "разблокированные", после чего реальная функция translate может повторять и выполнять свою работу.

Вероятно, труднее объяснить, чем понимать на самом деле (как вы можете видеть, код выше не слишком длинный), не стесняйтесь просить разъяснения.

Использование очень просто; просто var i18next = require('i18next') в любом модуле вашего приложения, а затем вызовите i18next.t в любое время. Так же, как и исходная функция translate, вы можете передавать наблюдаемые в качестве аргументов (что автоматически ретранслирует эту конкретную строку при каждом изменении такого аргумента), и она вернет наблюдаемую строку. Фактически, функция не использует this, поэтому вы можете смело назначить ее удобной переменной: var _ = i18next.t.

Теперь вы можете искать komutex в своей любимой поисковой системе. Ну, если у кого-то не было такой же идеи, вы ничего не найдете, и я не собираюсь публиковать этот код как есть (я не мог этого сделать, не потеряв всякого доверия;)). В приведенном выше объяснении должно быть все, что вам нужно знать, чтобы реализовать те же самые вещи без этого модуля, хотя он загромождает код с проблемами, которые я лично склонен извлекать в выделенных компонентах, как я здесь. К концу мы не были даже на 100% уверены, что абстракция мьютекса была правильной, поэтому, хотя она может выглядеть аккуратно и просто, я советую вам подумать о том, как извлечь этот код (или просто извлеките его или нет).

В более общем плане, я бы также посоветовал вам искать другие учетные записи такой интеграционной работы, так как ее неясно, будут ли эти идеи стареть хорошо (через год я по-прежнему считаю, что этот "реактивный" подход к локализации/переводу является абсолютно правый, но это только я). Возможно, вы даже найдете более современные библиотеки, которые будут делать то, что вам нужно, чтобы сделать это из коробки.

В любом случае, очень маловероятно, что я снова просмотрю этот пост. Опять же, я надеюсь, что это небольшое (!) Обновление так же полезно, как и исходное сообщение.

Удачи!

Ответ 2

Я был вдохновлен ответами в SO по этой теме, поэтому я придумал собственную реализацию модуля i18n + привязка для Knockout/Durandal.

Взгляните на мой репортаж github

Выбор еще одного модуля i18n состоял в том, что я предпочитаю хранить переводы в базах данных (которые когда-либо набираются для каждого проекта) вместо файлов. С этой реализацией вы просто имеете бэкэнд, который должен отвечать объектом JSON, содержащим все ваши переводы, с ключом.

@RainerAtSpirit Хороший совет с классом singleton был очень полезен для модуля

Ответ 3

Возможно, у вас есть один модуль i18n, который возвращает одноэлемент со всеми необходимыми наблюдаемыми. Кроме того, функция init, которая принимает объект i18n для инициализации/обновления.

define(function (require) {
   var app = require('durandal/app'),
       i18n = require('i18n'),
       router = require('durandal/plugins/router')
   };
   return{

       canDeactivate: function () { 
      }
   }
});

On the view:

<label for="hello-world" data-bind="text: i18n.helloWorldLabelText"></label>

Ответ 4

Вот пример repo, сделанный с помощью i18next, Knockout.Punches и нокаут 3 с Дюрандалом:

https://github.com/bestguy/knockout3-durandal-i18n

Это позволяет использовать Handlebars/ Angular -строчные вложения локализованного текста через текстовый фильтр i18n, поддерживаемый i18next:

<p>
  {{ 'home.label' | i18n }}
</p>

также поддерживает атрибуты:

<h2 title="{{ 'home.title' | i18n }}">
  {{ 'home.label' | i18n }}
</h2>

А также позволяет передавать параметры:

<h2>
  {{ 'home.welcome' | i18n:name }}
  <!-- Passing the 'name' observable, will be embedded in text string -->
</h2>

Пример JSON:

Русский (ru):

{
  "home": {
    "label": "Home Page",
    "title": "Type your name…"
    "welcome": "Hello {{0}}!",
  }
}

Китайский (zh):

{
  "home": {
    "label": "家",
    "title": "输入你的名字……",
    "welcome": "{{0}}您好!",
  }
}