Доброго времени суток, хабравчане!
В данной статье я расскажу о простом варианте решения задачи управления текстом и локализацией в веб-приложении, который вы сможете сами реализовать или же воспользоваться готовым.
Давно хотелось поделиться собственными мыслями и опытом… ну и, конечно же, поговорить за жизнь.
Очевидно, что решения по управлению текстами и локализацией уже существуют, но мне они не подошли по разными причинам: громоздко, неудобно в использовании, не подходит, не соответствует моему видению решения данной задачи, не хватает функционала.
К тому же, я не очень люблю сторонние библиотеки из-за их склонности к разрастанию (это когда из всего функционала нам требуется лишь малая его часть).
В компании, в которой я работаю, существует свое решение, но, на мой взгляд, оно тоже далеко от идеала. А необходимость обратной совместимости со старыми версиями делает его излишне сложным.
В какой-то момент захотелось чего-то простого, лёгкого, понятного и бесконечно расширяемого для разных задач.
Постановка задачи
Тут вроде бы всё понятно. Или нет? Давайте подумаем, чего бы нам хотелось.
Нам нужно каким-то образом получать локализованные тексты. Тексты могут содержать переменные. Переменные тоже могут локализоваться?! По идее да. А если переменная является датой или числом?! А ещё бы поддержку 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! :)
В заключение
Вот я и выполнил своё дааааавнее желание — написал статью на Хабр. Я очень надеюсь, что эта статья будет полезной и придётся по душе сообществу.
Спасибо за уделённое внимание.