Динамический перевод страницы на другой язык

Привет, Хабр.

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

Кто-то скажет, что осуществлять перевод на клиенте нецелесообразно, но у меня получилось как раз такая ситуация, что по-другому никак нельзя: для серверного перевода приходится принудительно закрывать веб-приложения на странице, чтобы не потерять данные; в случае же с динамическим изменением языка, просто заменятся тексты на элементах и работа продолжается. Думаю, не меня одного раздражало «Настройки будут применены при перезагрузке». Моя реализация хоть и сложновата, но решает эту проблему.

Для того чтобы не путаться, я определю для данной статьи следующий перечень терминов:
Словарь — хранилище ключей, по которым осуществляется доступ к локализации на данном языке. По сути дела представляет собой обычный JavaScript-объект, где свойства — ключи доступа, а их значения — переведенные строки.
Хэш — объект, который является результатом упорядоченного слияния словарей; общий словарь, из которого впоследствии ведётся выборка перевода.

Теперь более детально.


Для ускорения чтения я рекомендую пропустить главы Исходный код и Описание интерфейса, т.к. они дают описание объекта разговора с программной точки зрения. При желании с ними можно будет позже ознакомиться в случае, если вас заинтересовала статья.

Исходный код


Сразу же предлагаю исходный код. Пока не вдавайтесь в подробности, но к нему еще будем возвращаться.
lang = (function init_lang() {
    var BODY = document.body,
        //тело документа
        LANG_HASH = {},
        //собственно, результирующий хэш
        LANG_HASH_LIST = [],
        //список загружаемых словарей
        LANG_HASH_INDEX = {},
        //список имён словарей
        LANG_HASH_USER = {},
        //пользовательский словарь
        LANG_HASH_SYSTEM = {},
        //системный словарь
        LANG_QUEUE_TO_UPDATE = [],
        //очередь элементов для обновления
        LANG_PROPS_TO_UPDATE = {},
        //перечень имён для обновления
        LANG_UPDATE_LAST = -1,
        //индекс последнего обновлённого элемента
        LANG_UPDATE_INTERVAL = 0,
        //интервал обновления
        LANG_JUST_DELETE = false; //неперестраивание хэша при удалении словаря
    var hash_rebuild = function hash_rebuild() { //функция перестраивания хэша
            var obj = {};
            obj = lang_mixer(obj, LANG_HASH_USER);
            for (var i = 0, l = LANG_HASH_LIST.length; i < l; i++)
            obj = lang_mixer(obj, LANG_HASH_LIST[i]);
            LANG_HASH = lang_mixer(obj, LANG_HASH_SYSTEM);
        },
        lang_mixer = function lang_mixer(obj1, obj2) { //функция расширения свойствами
            for (var k in obj2)
            obj1[k] = obj2[k];
            return obj1;
        },
        lang_update = function lang_update(data) { //функция, инициирующая обновление
            switch (typeof data) {
            default:
                return;
            case "string":
                LANG_PROPS_TO_UPDATE[data] = 1;
                break;
            case "object":
                lang_mixer(LANG_PROPS_TO_UPDATE, data);
            }
            LANG_UPDATE_LAST = 0;
            if (!LANG_UPDATE_INTERVAL) LANG_UPDATE_INTERVAL = setInterval(lang_update_processor, 100);
        },
        lang_update_processor = function lang_update_processor() { //функция обновления
            var date = new Date;
            for (var l = LANG_QUEUE_TO_UPDATE.length, c, k; LANG_UPDATE_LAST < l; LANG_UPDATE_LAST++) {
                c = LANG_QUEUE_TO_UPDATE[LANG_UPDATE_LAST];
                if(!c)
                    continue;
                if (!c._lang || !(c.compareDocumentPosition(BODY) & 0x08)) {
                    LANG_QUEUE_TO_UPDATE.splice(LANG_UPDATE_LAST, 1);
                    LANG_UPDATE_LAST--;
                    if (!LANG_QUEUE_TO_UPDATE.length) break;
                    continue;
                }
                for (k in c._lang)
                if (k in LANG_PROPS_TO_UPDATE) lang_set(c, k, c._lang[k]);
                if (!(LANG_UPDATE_LAST % 10) && (new Date() - date > 50)) return;
            }
            LANG_PROPS_TO_UPDATE = {};
            clearInterval(LANG_UPDATE_INTERVAL);
            LANG_UPDATE_INTERVAL = 0;
        },
        lang_set = function lang_set(html, prop, params) { //установка атрибута элемента
            html[params[0]] = prop in LANG_HASH ? LANG_HASH[prop].replace(/%(\d+)/g, function rep(a, b) {
                return params[b] || "";
            }) : "#" + prop + (params.length > 1 ? "(" + params.slice(1).join(",") + ")" : "");
        };

    var LANG = function Language(htmlNode, varProps, arrParams) { //связывание элемента с ключами
            var k;
            if (typeof htmlNode != "object") return;
            if (typeof varProps != "object") {
                if (typeof varProps == "string") {
                    k = {};
                    k[varProps] = [htmlNode.nodeType == 1 ? "innerHTML" : "nodeValue"].
                    concat(Array.isArray(arrParams) ? arrParams : [])
                    varProps = k;
                } else return;
            }
            if (typeof htmlNode._lang != "object") htmlNode._lang = {};
            for (k in varProps) {
                if (!(Array.isArray(varProps[k]))) varProps[k] = [varProps[k]];
                htmlNode._lang[k] = varProps[k];
                lang_set(htmlNode, k, varProps[k]);
            }
            if (LANG_QUEUE_TO_UPDATE.indexOf(htmlNode) == -1) LANG_QUEUE_TO_UPDATE.push(htmlNode);
        };
    lang_mixer(LANG, {
        get: function get(strProp) { //получение перевода из хэша
            return LANG_HASH[strProp] || ("#" + strProp);
        },
        set: function set(strProp, strValue, boolSystem) { //установка ключа в пользовательском
            //или системном словаре
            var obj = !boolSystem ? LANG_HASH_USER : LANG_HASH_SYSTEM;
            if (typeof strValue != "string" || !strValue) delete obj[strProp];
            else obj[strProp] = strValue;
            hash_rebuild();
            lang_update(strProp + "");
            return obj[strProp] || null;
        },
        load: function load(strName, objData) { //загрузка словаря(ей)
            switch (typeof strName) {
            default:
                return null;
            case "string":
                if (LANG_HASH_INDEX[strName]) {
                    LANG_JUST_DELETE = true;
                    LANG.unload(strName);
                    LANG_JUST_DELETE = false;
                }
                LANG_HASH_LIST.push(objData);
                LANG_HASH_INDEX[strName] = objData;
                break;
            case "object":
                objData = {};
                for (var k in strName) {
                    if (LANG_HASH_INDEX[k]) {
                        LANG_JUST_DELETE = true;
                        LANG.unload(k);
                        LANG_JUST_DELETE = false;
                    }
                    LANG_HASH_LIST.push(strName[k]);
                    LANG_HASH_INDEX[k] = strName[k];
                    objData[k] = 1;
                }

            }
            hash_rebuild();
            lang_update(objData);
            return typeof strName == "string" ? objData : strName;
        },
        unload: function unload(strName) { //выгрузка словаря(ей)
            var obj, res = {}, i;
            if (!(Array.isArray(strName))) strName = [strName];
            if (!strName.length) return null;
            for (i = strName.length; i--;) {
                obj = LANG_HASH_INDEX[strName[i]];
                if (obj) {
                    LANG_HASH_LIST.splice(LANG_HASH_LIST.indexOf(obj), 1);
                    delete LANG_HASH_INDEX[strName[i]];
                    res[strName[i]] = obj;
                    if (LANG_JUST_DELETE) return;
                }
            }
            hash_rebuild();
            lang_update(obj);
            return strName.length == 1 ? res : obj;
        },
        params: function params(htmlElem, strKey, arrParams) {
            if (typeof htmlElem != "object" || !htmlElem._lang || !htmlElem._lang[strKey]) return false;
            htmlElem._lang[strKey] = htmlElem._lang[strKey].slice(0, 1).concat(Array.isArray(arrParams) ? arrParams : []);
            lang_set(htmlElem, strKey, htmlElem._lang[strKey]);
            return true;
        }
    });
    return LANG;
})();


Иерархия


Во-первых, хочу отметить, что в своей реализации я выделил несколько типов словарей:
1) загружаемый:
Ключи такого словаря не могут быть изменены пользователем по отдельности: их можно загрузить или выгрузить только целиком. Такой словарь всегда имеет какой-либо приоритет относительно других загружаемых словарей. Именован.
2) пользовательский:
Встроенный словарь, приоритет которого всегда ниже приоритета любого из загружаемых словарей. Не может быть загружен или выгружен целиком, изменяется только по отдельным ключам. Его смысл в том, чтобы хранить значения, которые были указаны пользователем, отдельно от остальных. Если выгрузить все загружаемые словари, то это не затронет пользовательский словарь и его значения будут по-прежнему доступны.
3) системный:
По смыслу он полностью повторяет пользовательский, но обладает наивысшим приоритетом.


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

Итак, при изменении пользовательского или системного словаря, а также при изменениях в списке загружаемых словарей происходит обновление хэша. Алгоритм следующий:
1) в объект хэша копируются ключи из объектов в последовательности, обратной указанной рисунке, т.е. в порядке понижения приоритета;
2) если такой ключ в хэше уже есть, то копирования не происходит.

Ключ\Словарь [User] Common System App1 App2 App3 [System] Хэш
OK OK OK OK
CANCEL Отмена Cancel Отмена Отмена
DONE Done Готово Готово
STRING String String

Таблица 1 — Переход ключей в хэш из более приоритетных словарей.

По умолчанию не загружено ни одного словаря; пользовательский и системный словари пустые. Кстати, обращение к несуществующему свойству словаря возвращает не пустую строку, а строку вида "#key", где за символом хэша следует имя ключа, к которому произведено обращение. Это сделано для того, чтобы на экране было сразу же видно, какие ключи не существуют.

Описание интерфейса


После выполнения исходного кода станет доступной глобальная переменная lang, значение которой — функция, связывающая атрибуты элемента с хэшем через ключ. В неё можно передать аргументы следующим образом:
lang(htmlTextNode,strKey);
lang(htmlTextNode,strKey,arrParams);
lang(htmlTextNode,objKeys);
lang(htmlElement,objKeys);
где:
htmlTextNode — текстовый узел, с которым происходит связывание;
strKey — ключ в словаре, по которому будет происходить обращение;
arrParams — параметры, подставляемые в перевод (об этом чуть позже);
objKeys — объект, содержащий в своих свойствах ключи, по которым должно происходить обращение, а в значениях — атрибут для связывания (в виде строки). В значении также можно указать параметры, подставляемые в перевод. Для этого значением должен быть массив, где первый элемент — атрибут для связывания, а остальные — значения параметров.
Переменная lang имеет собственные свойства-функции get, set, load, unload, params.

Использование get:
lang.get(strKey);
где:
strKey — ключ, значение которого нужно получить.
Возвращает строку перевода, связанную с переданным ключом.

Использование set:
lang.set(strKey, strValue,boolSystem);
где:
strKey — ключ, значение которого нужно установить;
strValue — значение для установки. Если оно пустое или не является строкой, то соответствующий ключ удаляется из словаря;
boolSystem — если этот параметр приводится к true, то запись происходит в системный словарь, иначе — в пользовательский.
Возвращает записанное значение или null, если ключ был удалён.

Использование load:
lang.load(strName,objData);
где:
strName — имя словаря (строка) или объект со множеством свойств, которые представляют собой имена словарей, а их значение — сами словари;
objData — если первый аргумент — строка, то этот аргумент является словарём.
Возвращает словарь(и), который(е) был(и) загружен(ы).

Использование unload:
lang.unload(strName);
где:
strName — имя (строка) или имена (массив) словарей, которые должны быть выгружены.
Вызращает выгруженный(ые) словарь(и).

Использование params:
lang. params(htmlElem,strKey,arrParams);
где:
htmlElem — элемент, который уже был связан со словарём;
strKey — ключ доступа к словарю;
arrParams — массив новых параметров.
Возвращает true, если новые параметры были установлены, или false в противном случае.
Пример в следующей главе должен привнести ясности в понимание интерфейса.

Взаимодействие с элементами


Для того чтобы можно было взаимодействовать со словарями, нужно привязать конкретный элемент или текстовый узел к хэшу через один из атрибутом этого элемента и ключ словаря. Этим атрибутом может быть, как innerHTML, так и title, alt и др. Если связывается текстовый узел, то связывание по умолчанию проходит через textContent. Ниже я опишу, почему.
Рассмотрим пример:
lang(document.body.appendChild(document.createElement("button")),{"just_a_text":"innerHTML" });
//в только что созданной кнопке привязываем innerHTML к ключу just_a_text
Кнопка связана через ключ, которого пока не существует (поэтому текст равен "#just_a_text"). Что касается технической реализации, то на элементе создаётся свойство _lang, свойствами которого являются ключи хэша, а значениями — массивы, где первыми элементами являются атрибуты этого элемента, в которые будут записаны значения хэша, а остальными — параметры, передаваемые в перевод.
Теперь есть 3 варианта записать ключ в словарь: пополнить список загружаемых словарей новым словарём либо присвоить значение в пользовательский или системный словарь. Рассмотрим все варианты в порядке возрастания приоритета (я бы посоветовал выполнять это построчно в консоли):
lang.set("just_a_text","Текст в кнопке"); //запись в пользовательский словарь
lang.load("def",{"just_a_text":"Текст из загружаемого словаря"});
lang.set("just_a_text"," Текст из системного словаря",1); //запись в системный словарь
Текст на кнопке будет с каждым шагом меняться на указанный в коде. Таким образом в данном случае используются все 3 вида словарей.
Теперь уберём ключи из словарей в обратной последовательности и убедимся, что заявленная иерархия действительно работает:
lang.set("just_a_text","",1); //удаление  из системного словаря
lang.unload("def");//выгрузка словаря с именем def
lang.set("just_a_text",""); // удаление  из пользовательского словаря
Теперь текст на кнопке снова станет «#just_a_text».

Параметры


Часто мне приходилось не просто подставлять текст из словаря, но и передавать в него некоторые параметры, поэтому возможность подстановки параметров также реализована. Их можно указать при связывании элемента с ключом, а можно и после связывания с помощью функции lang.params. Для указания позиции параметра внутри строки используется конструкция /%\d+/, где цифры обозначают номер передаваемого параметра. Если параметр не был передан, то подставляется пустая строка.
Особенно хорошо замена проявляет себя в связке с innerHTML:
lang(b=document.body.appendChild(document.createElement("button")),
	{"pr1":"innerHTML" });
lang.set("pr1","Значение 1: <b>%1</b>. Значение 2: <b>%2</b>. ");
lang.params(b,"pr1",[100,500]);
Теперь внутри кнопки переданные параметры будут выделены. Ссылка

Производительность


Для того чтобы распределить нагрузку (я уже писал об этом здесь), я использую интервальную, функцию, которая обрабатывает массив элементов, связанных со словарём.
Во-вторых, старайтесь как можно реже использовать innerHTML на элементах, вместо его используйте textContent на текстовых узлах этих элементов. innerHTML работает в 20+ раз медленнее, потому сеттер этого атрибута выполняет парсинг переданного значения как HTML кода. textContent не задумывается над парсингом HTML, а вставляет текст как есть (можно даже <,> на &lt;,&gt; не менять), но, к сожалению, это не всегда применимо, в частности, в прошлом примере.

Запуск под IE8


Немного поразмыслив, я понял, что эту штуку можно запустить и под восьмым IE. Для этого придётся прибегнуть к паре «грязных» хаков:
if (typeof Array.isArray != 'function') Array.isArray = function (value) {
    return Object.prototype.toString.call(value) === '[object Array]';
}
if (typeof Array.prototype.indexOf != 'function') Array.prototype.indexOf = function (value, offset) {
    offset = parseInt(offset);
    for (var i = offset > 0 ? offset : 0, l = this.length; i < l; i++)
    if (this[i] === value) return i;
    return -1;
};
//исправление невозможности присвоить новое свойство текстовому узлу для IE8
Text.prototype._lang = null;
//частично эмулируем функцию сравнения позиции, используя решение Джона Резига
if(typeof Element.prototype.compareDocumentPosition!="function")
	Text.prototype.compareDocumentPosition = Element.prototype.compareDocumentPosition = function compareDocumentPosition(node) {
		// Compare Position - MIT Licensed, John Resig
		function comparePosition(a, b) {
			return a.compareDocumentPosition ? a.compareDocumentPosition(b) : a.contains ? (a != b && a.contains(b) && 16) + (a != b && b.contains(a) && 8) + (a.sourceIndex >= 0 && b.sourceIndex >= 0 ? (a.sourceIndex < b.sourceIndex && 4) + (a.sourceIndex > b.sourceIndex && 2) : 1) + 0 : 0;
		}
		return comparePosition(this,node);
	};

Хотя на самом деле я считаю, что линейку IE до восьмой версии включительно нужно давно похоронить. Internets are belong to us.

Заключение


Проектируя эту штуку, я старался максимально её оптимизировать, например, свёл к минимуму изменения текста внутри элемента. Если значение по ключу не менялось, то оно не обновляется. В итоге получилось, что изменение текста внутри 10000 кнопок занимает (со включенным профилированием, через textContent) примерно секунду.

Есть небольшой минус: если «выдернуть» элемент из DOM, то данные на нём могут больше не обновляться. Для решения проблемы придётся повторно связать его с хэшем.

В принципе, данная реализация может быть использована не только для смены языка страницы. Основная её цель — изменение указанных атрибутов при изменении словаря. Т.е. область применения намного шире, указанной в заголовке.

Реализация, наверняка, не самая удачная. В комментариях я жду оценок и советов по улучшению. И, пожалуйста, аргументируйте поставленные минусы. Ах, да, лицензия MIT.

UPD. Пример можно глянуть тут. Просто откройте страницу при включенном интернете.

UPD2. Отдельная благодарность lahmatiy за наставления на путь истинный и за указание на недостатки.
+29
19 февраля 2012, 05:00
183
pashak 5,3

комментарии (27)

+2
ka8725 #
Было бы неплохо примерчик работающий увидеть. На стороне сервера нужно все равно подстраивать вмето текста название этих ключей, а потом в js вручную создавать хеш значений. Я правильно понимаю как использовать эту библиотеку? Если это так, то я, как разработчик RoR, никакого смысла в этом не вижу. Это просто самописная js библиотека наподобие гема i18n (только вот i18n работает и будет работать как надо без багов). Где нужно было применить вот эту библиотеку, на каком проекте? Очень хотелось бы знать
0
pashak #
> На стороне сервера нужно все равно подстраивать вмето текста название этих ключей, а потом в js вручную создавать хеш значений
Я не понимаю до конца, что вы здесь имеете в виду.
А применить можно в любом веб-приложении, где необходима локализация.
0
ka8725 #
Я имел ввиду, что генерить код на стороне сервера нужно будет наподобие этого:
  <a href='...'>this_is_cancel_link</a>
где this_is_cancel_link — это ключ, который потом можно будет перевести с помощью вашей библиотеки и готового словаря.
0
pashak #
Совершенно необязательно. Реализация связывания элементов с хэшем ложится на стороннего программиста, который использует эту библиотеку.
Да, можно использовать и приведённый вами пример; можно указывать ключ в атрибутах; можно вообще не указывать ключ, а совершать «накладывание» JavaScript-объекта с ключами на HTML-структуру.
Суть библиотеки не в замене ключей на их значения, а в изменении текущего значения элемента на новое при изменении словаря. Причём словарь может быть загружен или выгружен в произвольный момент времени.
0
pashak #
Пример уже есть в статье.
+2
Seldon #
Хочется пример большой страницы, где реально много всего, мне кажется что если у вас нагруженная структура то можно легкой сойти с ума делаю подобное, тем более ведь не каждый элемент вы создаете динамически.
+1
pashak #
Специально для вас:
clip2net.unet.by/page/m341/61867
Просто откройте при включенном интернете.
Сваял по-быстрому, поэтому возможны ошибки. Да и на английский не обижайтесь )
0
twenty #
Не пробовали выдернуть innerHTML из document.body, а потом вставить назад со всеми заменами?
0
pashak #
/*саркастично*/
Правильно — пересоздайте все элементы на странице и потеряйте обработчики событий на них.
0
twenty #
А почему бы этого не сделать до создания обработчиков?
0
twenty #
Извиняюсь, прочитал внимательнее: у вас другая цель и предложенный мною вариант — не вариант.
0
Armin #
Я делал динамическую смену языка следующим образом:

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

Понятно что все это делалось не ради динамической смены языка — она просто как бонус получилась сама собой.
0
pashak #
Еще раз для вас:
Суть библиотеки не в замене ключей на их значения, а в изменении текущего значения элемента на новое при изменении словаря. Причём словарь может быть загружен или выгружен в произвольный момент времени.
–1
Armin #
Я вас не критикую — просто написал к слову свой опыт работы с похожими вещами…
+4
Armin #
> Хотя на самом деле я считаю, что линейку IE до восьмой версии включительно нужно давно похоронить.

Я это уже пол года как практикую — и искренне сочувствую тем кто по каким либо причинам вынужден поддерживать IE версии ниже 9
0
dima_smol #
Любой проект, который делается для широких масс вынужден поддерживать IE7-8
0
Blangel #
Хочется Вам тоже посочувствовать… и вот почему:

Пожалуйста, попробуйте следующий сценарий:

IE — меню Сервис (нажать кнопку Alt, если не видно меню) — Свойства обозревателя — Дополнительно — поставьте галку «Не сохранять незашифрованные страницы на диск»
Перезапускаем браузер и пытаемся скачать любой файл с HTTPS-сайта

На IE9 и IE10 (Preview) у Вас это вряд ли выйдет.
На IE8 работает в большинстве сценариев (хотя если после перезапуска браузера сразу вбить прямую ссылку на файл на HTTPS-ресурсе — то тоже не скачает)

P.S. если сразу не сработает — то для большего эффекта попробуйте добавить сайт в надежные узлы.

Подобных багов несложно с десяток найти в разных версиях IE.

Если уж отказываться от IE — то от всех версий.

Если поддерживать IE — то хотя бы с 8 версии и выше (хотя бы потому, что 60-70% юзеров до сих пор сидят на Windows XP, для которого нет 9 версии IE — и только попробуйте им сказать «пользуйтесь другим браузером» — сразу в ответ получите «Я же Вас не прошу вместо Вашего (сисадминского) свитера носить нормальный костюм»).

Удачи!
0
pashak #
Как клиентский программист, я с вами согласен, что IE можно поддерживать с восьмой версии, но не ниже. Потому что в плане рендеринга страниц восьмая версия, конечно, отстаёт от девятой, но имеет результат гораздно более близкий к ожидаемому. Эта проблема относится к верстальщикам. Огорчает IE и программистов, потому что в своё время IE8 был очень большим продвижением, но всё равно его функционала не хватает для нормальной разработки: постоянно приходится что-то дописывать.

И мне всё равно, что этот комментарий заминусуют. Но поймите же вы, что, идя на уступки в виде поддержки старых браузеров, вы замедляете развитие интернетов!
+1
Blangel #
С одной стороны — да, замедляю.

С другой стороны:
— допустим, у пользователя куплена лицензионная Windows XP. Ради Internet Explorer 9.0 покупать Windows 7 — зачем? Абонента все устраивает… он привык работать на ней… мало того — КПД сотрудика может упасть, пока он будет обучаться Windows 7
— допустим, пользователю ставят Mozilla Firefox, Google Chrome или Opera вторым браузером (единственным браузером не всегда получится — есть куча бизнес-сайтов, использующих ActiveX). В конце концов это закончится тем, что пользователь перестанет пользоваться вторым браузером, потому что он либо запутается в закладках, либо его достанут постоянные обновления браузера, которые «всегда не вовремя», либо еще какая беда случится — я не раз наблюдал такое собственными глазами

К сожалению (по крайней мере, в компании, где я работаю) таких пользователей больше 50 000…

Поэтому я немного переформулирую исходную фразу:
«версии поддерживаемых сайт браузеров зависят не только от желания разработчиков сайта, от задания заказчика и от тенденций развития технологий, но и от результатов анализа, кто же будет посещать этот сайт»

Например, сайт Microsoft на многих страницах до сих пор хорошо рендерится в IE6 (хотя уже и плохо в IE 5.5 на Windows 2000). Причина такой совместимости — вынужденная необходимость из-за большой посещаемости сайта Microsoft с браузера IE6 на Windows XP.

И выражаю большую благодарность всем тем, кто дочитал мое мнение до конца ;-)
+1
Armin #
Я вас понимаю бизнес и все такое… Но я вольная птица и могу себе позволить использовать канвас без костылей. И по статистике моего проекта я теряю 2.5% пользователей. Но мог бы и 10% потерять — для меня это не так критично.
+2
egorinsk #
У вас ужасный стиль кода. Написанные в uppercase идентификаторы обычно считаются константами, а у вас — нет. Это запутывает. Также, у вас дурацкие имена методов. Например params() — это setParams() или что?

Также, меня бесят конструкции вида:

if(!(strName instanceof Array))
strName=[strName];

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

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

Подход, типа «ИЕ не нужен» на мой взгляд, характеризует разработчика с отрицатеьной стороны. В ИЕ (включая 7 и иногда 6) можно делать почти все, что нужно в современном веб-приложении (а что нельзя. делается через флеш) — было бы желание.
–2
pashak #
Толсто, друг мой.

Переменные верхнего регистра у меня обозначают замыкания, недоступные извне.

Если вас бесят конструкции приведенного вида, вы можете переписать их сами — было бы желание.

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

А еще у меня совсем не виджеты, а полноценные приложения. И да, архитектура намного сложнее, чем вы можете представить.

Пожалуйста, это ваше собственное желание — писать по IE. Кстати, да, вот вам баянчик.

И давайте не будем мыслить узко. Эта штука может не только языки менять, а текст как вам угодно в принципе. Например, кто-то независимо дописывает тексты в БД, и как только происходит сохранение, то изменения отражаются на клиенте почти моментально (например, с использованием лонг-поллинга или веб-сокетов).
+4
lahmatiy #
1. Соглашусь с предыдущим комментарием, код оформлен ужастно: проблемы с отступами, отсуствие пробелов, мешанина с именованием. Именуйте по общепринятыми правилам (КОНСТАНТЫ, Классы, остальныеНазвания) и прогоняйте ваш код через какой нибуть jsbeautifier.org перед публикацией.
2.
arrParams instanceof Array

Так массивы не проверяют. В ES5 есть функция Array.isArray, для старых браузеров делается простая замена:
if (typeof Array.isArray != 'function')
  Array.isArray = function(value){
      return Object.prototype.toString.call(value) === '[object Array]';
  }

3.
Объясните, для чего вы делаете так:
hash_rebuild=function hash_rebuild(){
...
LANG_HASH[prop].replace(/%(\d+)/g,function rep(a,b){return params[b]||"";})
...
get:function get(strProp){

Во всех этих случаях задавать имя функции не нужно. Хотя для первых функции не ясно зачем нужно делать из них переменные?

4. Вы первый на моей памяти человек который default запихнул в начало switch… Даже не знал что так будет работать :)
switch(typeof data){
    default:
        return;
    case "string":
        LANG_PROPS_TO_UPDATE[data]=1;
    break;
    case "object":
        lang_mixer(LANG_PROPS_TO_UPDATE,data);
}


5. Свойство textContent относительно новое свойство в DOM, и предназначено для других целей нежели используете вы. Самый близкий аналог это innerText, который поддерживают все кроме Firefox (то есть работает в IE/Opera/Chrome/Safari). Для текстовых узлов, атрибутов (AttributeNode) и комментариев поменять из значение можно через свойство nodeValue. Либо же свойство data, но оно работает не для всех типов.
Поэтому если использовать nodeValue — то минус один «хак», который решает вами же созданную проблему, и только для IE8 (defineProperty работает только там) и только для текстовых узлов.
То есть замените «textContent» на «nodeValue» и проблема будет решена.

6. Самый грязный хак из вашего примера это:
if("v"=="\v"){
  .. какие то хаки для каких то браузеров
}

Я намеренно не указал на IE, так как проверка не адресная: условие «v» == "\v" не определяет наличие или отсутствие фич у браузера. Здесь нужно проверять каждую фичу по отдельности.

7. indexOf входит в состав ES5 и относительно молодой метод, а это значит что реализация метода отсутсвует не только в IE8, но и в других браузерах (в версиях отличных от последних). Поэтому хорошо будет позаботиться о всех, и делается это так:
// для старых браузер не имеющих реализации indexOf для массивов
if (typeof Array.prototype.indexOf != 'function')
    Array.prototype.indexOf = function(value, offset){
        offset = parseInt(offset);
        for (var i = offset > 0 ? offset : 0, l = this.length; i < l; i++)
            if (this[i] === value)
                return i;
        return -1;
    };


8. Да, compareDocumentPosition не поддерживается в IE до 9 версии. Но там есть неплохая альтернатива с sourceIndex.
Вы использовали «магическую» константу, что затрудняет понимание кода, а это всего лишь DOCUMENT_POSITION_CONTAINS. Если используете compareDocumentPosition, то используйте и соответствующие константы (либо указывайте это в комментариях). Итого, вы проверяете находится ли узел внутри document.body или нет. Для этого есть более простое решение:
if (document.body.contains(node))
  alert('node находится внутри document.body');

Этот метод поддерживает даже IE6! А вот Firefox до недавнего времени его не имел, поэтому нужно сделать фикс для старых firefox
if (typeof Node.prototype.contains != 'function')
    Node.prototype.contains = function(node){
        return !!(this.compareDocumentPosition(node) & this.POSITION_CONTAINED_BY)
    }

Можете так же почитать статью John Resig по этой теме, там так же есть реализация кроссбраузерного comparePosition.

9. Что касается самого метода.
Во-первых, добавление у вас N^2/2. Когда узлов будет много вставка будет тормозить.
Во-вторых, при каждом обновлении словаря перетрясается DOM, дергается compareDocumentPosition и все обновляется. Причем не важно используется ли новое значение узлом или нет (не увидел проверок).
А потом вы проверяете сколько времени уже затратили:
  if((LANG_UPDATE_LAST%10)&&(new Date()-date>50))
     return;

Хотели проверять время каждый десятый раз? Так вот проверяется оно 9 раз из 10… а вызов new Date операция дорогая.
Если переписать lang_set можно значительно ускорить:
function lang_set(node, langKey, params){
  var propName = params[0];
  var newValue = langKey in LANG_HASH
    ? LANG_HASH[langKey].replace(/%(\d+)/g, function(a, b){return params[b]||""})
    : "#" + prop + (params.length > 1 ? "(" + params.slice(1) + ")" : "");
  if (node[propName] != newValue)
    node[propName] = newValue;
};

Самая медленная операция — это запись в DOM. Потом чтение из DOM. А вот javascript вычисления значительно дешевле. У вас тут в лучшем случае (коих предполагаю будет большинство) будет только чтение из DOM (node[propName]) и вычисление нового значения — без обязательной записи в DOM.

Вообще код еще пилить и пилить ;)
0
pashak #
1-2. За совет спасибо.

3. Почитайте, что такое strict mode. Если мне вдруг придётся сделать рекурсивную функцию, то с помощью arguments.callee я не смогу обратиться к текущей анонимной функции. Во-вторых, при отладке я сразу же буду видеть по именам в стеке вызовов, где произошла ошибка. Это, кстати, для меня и есть главная причина писать имена. В-третьих, гляньте пример:
(function init() {
    var f = function f() {
            alert(f);
        },
        z = f;
    f = 1;
    alert(f);
    z();
})();
В таком случае внутри функции f переменная f не будет замыканием.

6-7. В заголовке написано — именно для IE8.

9. Признаю фейл с оператором %. И, да, я потерял проверку, когда переписывал эту функцию.

За пункты 5, 8 и 9 ставлю плюс.

Отдельная благодарность будет в статье.
0
lahmatiy #
3. Почитайте, что такое strict mode. Если мне вдруг придётся сделать рекурсивную функцию, то с помощью arguments.callee я не смогу обратиться к текущей анонимной функции.

Что-то я не увидел у в коде strict режима. Как в прочем не увидел у вас рекурсивных функций. И что мешает объявить функции, как функции, а не как выражение?
(function(){
  function factorial(n){
    if (n <= 1) return 1;
    return n * factorial(n - 1);
  }

  function factorial5(){
    return factorial(5);
  }

  ...
})()

Во-вторых, при отладке я сразу же буду видеть по именам в стеке вызовов, где произошла ошибка. Это, кстати, для меня и есть главная причина писать имена.

В большинстве случаев, вы и так увидите имена функций, современные браузеры неплохо разрешают имена.

0
Oronro #
А что скажете про реализацию JSGettext?
0
pashak #
Я бы сказал, что слишком сложно. Может, быть, я не прав.
Я считаю, что чем проще структура, API, тем больше вероятность для фреймворка стать популярным.
В своей реализации я задействовал всего 6 методов для полноценной работы с переводами, в т.ч. с подставляемыми параметрами.
В JSGettext я даже рабочего примера не нашёл, да и как сваять его — тоже. Да и цели, по-моему, несколько различаются.
Вот это всё, что могу сказать.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.