Доброго времени суток, хабравчане!
В данной статье я расскажу о простом варианте решения задачи управления текстом и локализацией в веб-приложении, который вы сможете сами реализовать или же воспользоваться готовым.
Давно хотелось поделиться собственными мыслями и опытом… ну и, конечно же, поговорить за жизнь.
Очевидно, что решения по управлению текстами и локализацией уже существуют, но мне они не подошли по разными причинам: громоздко, неудобно в использовании, не подходит, не соответствует моему видению решения данной задачи, не хватает функционала.
К тому же, я не очень люблю сторонние библиотеки из-за их склонности к разрастанию (это когда из всего функционала нам требуется лишь малая его часть).
В компании, в которой я работаю, существует свое решение, но, на мой взгляд, оно тоже далеко от идеала. А необходимость обратной совместимости со старыми версиями делает его излишне сложным.
В какой-то момент захотелось чего-то простого, лёгкого, понятного и бесконечно расширяемого для разных задач.
Постановка задачи
Тут вроде бы всё понятно. Или нет? Давайте подумаем, чего бы нам хотелось.
Нам нужно каким-то образом получать локализованные тексты. Тексты могут содержать переменные. Переменные тоже могут локализоваться?! По идее да. А если переменная является датой или числом?! А ещё бы поддержку markdown. И в завершение, какое-нибудь решение на случай, если текст не найден.
Реализация
В основе будет простой объект, где ключ — это код текста, а значение — собственно нужный текст, ничего сложного:
const textsBundle = {
'button.open': 'Open',
'button.save': 'Save',
};
function TextManager(texts) {
this.getText = function(code) {
return texts[code];
};
}
const textManager = new TextManager(textsBundle);
textManager.getText('button.open');
Наименование ключей — это отдельная тема. Лучше сразу договориться о каком-то одном варианте, иначе разные ключи будут "подбешивать" :). Нет какого-то одного решения, выбирайте, как вам покажется удобнее и больше соответствует проекту. Лично мне нравится первый из предложенных:
'button.open.label'
'button.open.help_text'
или
'button.label.open'
'button.help_text.open'
или
'label.button.open'
'help_text.button.open'
Далее нам нужен механизм, который бы умел совершать какие-то манипуляции с текстом до того, как выдаст конечный результат, например, вставлять параметры. И тут мне пришла интересная идея — а что, если использовать middleware для манипуляций с текстом? Ведь таких решений я не встречал… ну или плохо искал :).
Определимся с требованиями к middleware: на входе middleware будет принимать текст и параметры, а выдавать — результирующий текст, после нужных манипуляций.
Первое middleware будет получать изначальный текст, а последующие — текст от предыдущего middleware. Допишем недостающий код:
function TextManager(texts, middleware) {
function applyMiddleware(text, parameters, code) {
if (!middleware) return text;
return middleware.reduce((prevText, middlewareItem) => middlewareItem(prevText, parameters, code), text);
}
this.getText = function(code, parameters) {
return applyMiddleware(texts[code], parameters, code);
};
}
TextManager умеет выдавать текст по его коду. Также он может быть расширен использованием middleware, которое открывает много возможностей, например:
- обработка случая, когда текст не найден
- использование параметров в тексте
- локализация параметров
- использование markdown
- экранирование текста и т.п.
Практика
Напишем пару необходимых middleware. Они вам 100% понадобятся.
InsertParams
Позволяет использовать параметры в текстах. Например нам нужно отобразить текст "Привет {{username}}". Следующее middleware это обеспечит:
function InsertParams(text, parameters) {
if (!text) return text;
if (!parameters) return text;
let nextText = text;
for (let key in parameters) {
if (parameters.hasOwnProperty(key)) {
nextText = text.replace('{{' + key + '}}', parameters[key]);
}
}
return nextText;
}
UseCodeIfNoText
Позволяет вернуть код текста, вместо undefined
, если текст не был найден:
function UseCodeIfNoText(text, parameters, code) {
return text ? text : code;
}
Итого получаем примерно следующее использование:
const textsBundle = {
'text.hello': 'Hello',
'text.hello_with_numeric_parameter': 'Hello {{0}}',
'text.hello_with_named_parameter': 'Hello {{username}}',
};
const textManager = new TextManager(textsBundle, [InsertParams, UseCodeIfNoText]);
textManager.getText('nonexistent.code') // 'nonexistent.code'
textManager.getText('text.hello') // 'Hello'
textManager.getText('text.hello_with_numeric_parameter', ['Vasya']) // 'Hello Vasya'
textManager.getText('text.hello_with_named_parameter', { username: 'Petya' }) // 'Hello Petya'
Пример использования в React приложении
Для начала инициализируем на топ-уровне TextManager
и добавляем тексты.
На мой взгляд, лучше всего тянуть тексты с сервера, но для простоты я этого делать не буду.
const textsBundle = {
'text.hello': 'Hello {{username}}'
}
function TextManagerProvider({ children }) {
const textManager = new TextManager(textsBundle, [InsertParams, UseCodeIfNoText]);
return (
<TextManagerContext.Provider value={textManager}>
{children}
</TextManagerContext.Provider>
)
}
Далее в компоненте используем textManager
, например с помощью хука, и получаем нужный текст по коду.
function SayHello({ username }) {
const textManager = useContext(TextManagerContext);
return (
<div>
{textManager.getText('text.hello', { username })}
</div>
)
}
Локализация
Вы спросите "При чем тут локализация?".
Всё очень просто — при смене языка создаёте новый экземпляр TextManager
, добавляете тексты и сразу получаете результат.
Глава предпоследняя :)
Как видно из примеров — использование предельно простое, а благодаря middleware расширять функционал можно до бесконечности.
Свою реализацию я выложил на github и планирую в дальнейшем развивать text-manager. Пользуйтесь, предлагайте улучшения и, как говорят у них там, You're welcome! :)
В заключение
Вот я и выполнил своё дааааавнее желание — написал статью на Хабр. Я очень надеюсь, что эта статья будет полезной и придётся по душе сообществу.
Спасибо за уделённое внимание.
Комментарии (6)
andreymal
02.07.2019 18:36+1локализация параметров
Не увидел этого в вашем решении. Как мне, меняя параметр-число, получать строки вида «1 комментарий», «4 комментария», «14 комментариев»?
7Sage Автор
02.07.2019 20:58Не сразу понял, что вы имеете ввиду. Я этого аспекта не касался, т.к. на мой взгляд это тема для отдельной статьи. А решить это можно отдельным middleware для локализации параметров с использованием, например Intl.PluralRules
upd. не хочу обещать, но в планах было заняться этим в ближайшем будущем
EgorVolokitin
02.07.2019 21:15Дели число на 10 и на 100. или следи за последними 2мя цифрами числа. если там 11-14 — окончание ев. в остальных случаях смотри последнюю цифру. там сам разберёшься. все повторяется
TheGodfather
Для тех кто в танке — а почему вообще JS? Ну т.е. вроде сгенерировать страничку с нужными текстами — это вполне себе бекенд, PHP тот же, разве нет?
andreymal
Вы отстали от моды лет на восемь, сейчас уже никто не генерирует странички на бекенде, даже мобильная версия Хабра рисуется целиком и полностью через JS
JimmDiGreez
Сгенерировать страницу с нужными текстами на бекенде — в общем случае вообще к беку не относится, разве что проект строго с такой спецификой. Так просто исторически сложилось с тех времен когда фронтенд был еще глупый.
Сейчас по разному может быть. Чаще всего бек не волнует вообще, какой там фронт и чем этот фронт занимается (сайт, нативка на мобильные платформы, десктоп, что угодно), а сам он занимается тем, чем беку положено — данными и бизнес-логикой. Потому вопросами красоты на клиентах (текстовой в том числе) он скорее всего не будет озабочен.
В ряде проектов еще бек немало в этом участвует, но все же меньше, чем раньше.
Ну а так, конечно, вопросы локализации далеко не только js на фронте касаются. Реализации везде есть, хотя проблемы примерно одинаковые и это можно рассматривать как единую проблему с разными способами решения.