Русский
Русский
English
Статистика
Реклама

Ispsystem

Увидеть истинное лицо продукта и выжить. Данные о пользовательских переходах как повод написать пару новых сервисов

31.07.2020 10:05:30 | Автор: admin


В интернете сотни статей о том, какую пользу приносит анализ поведения клиентов. Чаще всего это касается сферы ритейла. От анализа продуктовых корзин, ABC и XYZ анализа до retention-маркетинга и персональных предложений. Различные методики используются уже десятилетиями, алгоритмы продуманы, код написан и отлажен бери и используй. В нашем случае возникла одна фундаментальная проблема мы в ISPsystem занимаемся разработкой ПО, а не ритейлом.
Меня зовут Денис и на данный момент я отвечаю за бэкенд аналитических систем в ISPsystem. И это история о том, как мы с моим коллегой Данилом ответственным за визуализацию данных попытались посмотреть на наши программные продукты сквозь призму этих знаний. Начнем, как обычно, с истории.


В начале было слово, и слово было Попробуем?


В тот момент я работал разработчиком в R&D отделе. Все началось с того, что здесь, на Хабре, Данил прочитал про Retentioneering инструмент для анализа переходов пользователей в приложениях. Идею его применения у нас я воспринял несколько скептически. В качестве примеров разработчики библиотеки приводили анализ приложений, где целевое действие было четко определено оформление заказа или иная вариация того, как заплатить компании-владельцу. У нас же продукты поставляются on-premise. То есть пользователь сначала покупает лицензию, и только после начинает свой путь в приложении. Да, у нас есть демо-версии. В них можно опробовать продукт, чтобы не брать кота в мешке.


Но большая часть наших продуктов ориентирована на рынок хостинга. Это крупные клиенты, и о возможностях продукта их консультирует отдел бизнес-девелоперов. Из этого же следует, что на момент покупки наши клиенты уже знают в решении каких задач им поможет наше ПО. Их маршруты в приложении должны совпадать с заложенным в продукт CJM, а UX-решения помогут с них не сбиваться. Спойлер: так происходит не всегда. Знакомство с библиотекой было отложено но ненадолго.


Все изменилось с релизом нашего стартапа Cartbee платформы для создания интернет-магазина из аккаунта в Instagram. В этом приложении пользователю предоставлялся двухнедельный период на бесплатное использование всей функциональности. После нужно было принять решение, оформлять ли подписку. И это как нельзя хорошо ложилось в концепцию маршрут-целевое действие. Было решено: пробуем!


Первые результаты или откуда брать идеи


Мы с командой разработки буквально за день подключили продукт к системе сбора событий. Скажу сразу, что в ISPsystem используется своя система сбора событий о посещении страниц, однако ничто не мешает применять для тех же целей Яндекс.Метрику, которая позволяет бесплатно выгружать сырые данные. Были изучены примеры использования библиотеки, и спустя неделю сбора данных мы получили граф переходов.
Граф переходов. Основная функциональность, остальные переходы убраны для наглядности
Граф переходов. Основная функциональность, остальные переходы убраны для наглядности


Вышло прямо как в примере: планарно, понятно, красиво. Из этого графа мы смогли выявить самые частые маршруты и переходы, на которых люди задерживаются дольше всего. Это позволило нам понять следующее:


  • Вместо крупного CJM, который охватывает с десяток сущностей, активно используются всего две. Необходимо дополнительно направлять пользователей в нужные нам места при помощи UX-решений.
  • На некоторых страницах, задуманных UX-проектировщиками как сквозные, люди проводят неоправданно много времени. Нужно выяснять, что является стоп-элементами на конкретной странице и корректировать это.
  • После 10 переходов 20% людей начинали уставать и бросать сессию в приложении. И это с учетом того, что у нас в приложении было целых 5 страниц онбординга! Нужно выявлять страницы, на которых пользователи регулярно бросают сессии, и сокращать путь до них. Еще лучше: выявлять любые регулярные маршруты и позволять совершать быстрый переход из страницы-источника в страницу-назначение.
    Что-то общее с ABC-анализом и анализом брошенных корзин, не находите?

И здесь мы пересмотрели свое отношение к применимости этого инструмента для on-premise продуктов. Было решено проанализировать активно продающийся и использующийся продукт VMmanager 6. Он значительно сложнее, сущностей на порядок больше. Мы с волнением ждали, каким же окажется граф переходов.


О разочарованиях и воодушевлениях


Разочарование #1
Это был конец рабочего дня, конец месяца и конец года одновременно 27 декабря. Данные были накоплены, запросы написаны. Оставались секунды до того, как все обработается, и мы сможем взглянуть на результат своих трудов, чтобы узнать, с чего начнётся следующий рабочий год. R&D отдел, продакт-менеджер, UX-дизайнеры, тимлид, разработчики собрались перед монитором, чтобы увидеть как выглядят пути пользователей в их продукте, но мы увидели это:
Граф переходов, построенный библиотекой Retentioneering
Граф переходов, построенный библиотекой Retentioneering


Воодушевление #1
Сильно-связный, десятки сущностей, неочевидные сценарии. Понятно было лишь только то, что новый рабочий год начнется не с анализа, а с изобретения способа упростить работу с таким графом. Но меня не покидало чувство, что все намного проще, чем кажется. И после пятнадцати минут изучения исходников Retentioneering удалось экспортировать построенный граф в формат dot. Это позволило выгрузить граф в другой инструмент Gephi. А уже там раздолье для анализа графов: укладки, фильтры, статистики только и делай, что настраивай в интерфейсе нужные параметры. С этой мыслью мы ушли на новогодние выходные.


Разочарование #2
После выхода на работу оказалось, что пока все отдыхали, наши клиенты изучали продукт. Да так усердно, что в хранилище появились события, которых раньше не было. Это означало то, что нужно актуализировать запросы.


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


/host/item/24/ip(modal:modal/host/item/ip/create)


значит, что на странице IP-адреса пользователь добавлял IP-адрес. И здесь видны сразу две проблемы:


  • В URL есть какой-то path parameter ID виртуальной машины. Нужно его исключать.
  • В URL есть идентификатор модального окна. Нужно как-то распаковывать такие URL.
    Другая проблема заключалась в том, что в тех самых размеченных нами событиях были параметры. Например, попасть на страницу с информацией о виртуальной машине из списка можно было пятью различными способами. Соответственно, событие отправлялось одно, но с параметром, которые указывал, каким из способов пользователь осуществил переход. Таких событий было множество, и все параметры разные. А у нас вся логика извлечения данных на диалекте SQL для Clickhouse. Запросы на 150-200 строк начинали казаться чем-то привычным. Проблемы окружали нас.

Воодушевление #2
Одним ранним утром Данил, грустно скроля запрос вторую минуту, предложил мне: А давай пайплайны обработки данных напишем? Мы подумали и решили, что раз уж делать, то нечто вроде ETL. Чтобы и фильтровало сразу, и из других источников нужные данные подтягивало. Так родился наш первый аналитический сервис с полноценным бэкендом. Он реализует пять основных стадий обработки данных:


  1. Выгрузка событий из хранилища сырых данных и подготовка их к обработке.
  2. Уточнение распаковка тех самых идентификаторов модальных окон, параметров событий и прочих уточняющих событие деталей.
  3. Обогащение (от слова стать богатым) дополнение событий данными из сторонних источников. На тот момент сюда входила только наша биллинговая система BILLmanager.
  4. Фильтрация процесс отсеивания событий, которые искажают результаты анализа (события со внутренних стендов, выбросы и т.д.).
  5. Выгрузка полученных событий в хранилище, которое мы назвали чистыми данными.
    Теперь поддерживать актуальность можно было добавляя правила обработки события или даже группы похожих событий. Например, c того момента мы ни разу не актуализировали распаковку URL. Хотя, за это время добавилось несколько новых вариаций URL. Они соответствуют уже заложенным в сервис правилам и корректно обрабатываются.

Разочарование #3
Как только мы приступили к анализу, мы осознали, почему граф был настолько связным. Дело в том, что практически каждая N-грамма содержала в себе переходы, которые невозможно осуществить через интерфейс.


Началось небольшое расследование. Меня смущало, что не было неосуществимых переходов в рамках одной сущности. Значит, это не баг системы сбора событий или нашего ETL-сервиса. Складывалось ощущение, что пользователь одновременно работает в нескольких сущностях, не переходя из одной в другую. Как такого добиться? Используя разные вкладки в браузере.


При анализе Cartbee нас спасла его специфика. Приложение использовали с мобильных устройств, где работать из нескольких вкладок попросту неудобно. Здесь же десктоп и, пока выполняется задача в одной сущности, резонно хочется занять это время настройками или мониторингом состояния в другой. А чтобы не терять прогресс, просто открываешь еще одну вкладку.


Воодушевление #3
Коллеги из фронтед-разработки научили систему сбора событий различать вкладки. Можно было приступать к анализу. И мы приступили. Как и ожидалось, CJM не совпадали с реальными путями: пользователи проводили много времени на страницах-каталогах, бросали сессии и вкладки в самых неожиданных местах. При помощи анализа переходов мы смогли найти проблемы в некоторых билдах Mozilla. В них, из-за особенностей реализации, пропадали элементы навигации или отображались полупустые страницы, которые должны быть доступны только администратору. Страница открывалась, но контент с бэкенда не приходил. Подсчет переходов позволял оценить, какие фичи реально используются. Цепочки давали возможность понять, как пользователь получил ту или иную ошибку. Данные позволяли проводить тестирование на основе поведения пользователей. Это был успех, затея была не напрасной.


Автоматизация аналитики


На одной из демонстраций результатов мы показывали, как применяется Gephi для анализа графов. В этом инструменте данные о переходах можно вывести в таблицу. И руководитель отдела UX сказал одну очень важную мысль, которая повлияла на развитие всего направления аналитики поведения в компании: А давайте сделаем так же, но в Tableau и с фильтрами так будет удобнее.


Тогда я подумал: а почему бы и нет, Retentioneering хранит все данные в pandas.DataFrame структуре. А это уже, по большому счету, таблица. Так появился еще один сервис: Data Provider. Он не только делал из графа таблицу, но и рассчитывал, насколько страница и привязанная к ней функциональность пользуются популярностью, как влияет на удержание пользователей, насколько пользователи задерживаются на ней, с каких страниц чаще всего уходят пользователи. А использование визуализации в Tableau настолько сократило затраты на изучение графа, что время итерации анализа поведения в продукте сократилось практически вдвое.


О том, как данная визуализация применяется и какие выводы позволяет сделать расскажет Данил.


Больше таблиц богу таблиц!


В упрощенном виде задача была сформулирована так: отобразить граф переходов в Tableau, обеспечить возможность фильтрации, сделать максимально понятным и удобным.


Рисовать ориентированный граф в Tableau не очень-то хотелось. Да и в случае успеха выигрыш, по сравнению с Gephi, представлялся неочевидным. Нужно было что-то гораздо проще и доступнее. Таблица! Ведь граф легко представить в виде строк таблицы, где каждая строка ребро вида источник-назначение. Более того, такая таблица у нас уже была заботливо подготовлена средствами Retentioneering и Data Provider. Дело оставалось за малым: вывести таблицу в Tableau и пошарить отчет.
К слову о том, как все любят таблицы
К слову о том, как все любят таблицы


Однако здесь мы столкнулись еще с одной проблемой. Что делать с источником данных? Подключить pandas.DataFrame было нельзя, такого коннектора у Tableau нет. Поднимать отдельную базу для хранения графа казалось слишком радикальным решением с туманными перспективами. А варианты локальных выгрузок не подходили из-за необходимости постоянных ручных операций. Мы полистали список доступных коннекторов, и взгляд упал на пункт Web Data Connector, который сиротливо ютился в самом низу.


У Tableau богатый выбор коннекторов. Нашли и тот, который решил нашу задачу
У Tableau богатый выбор коннекторов. Нашли и тот, который решил нашу задачу


Что за зверь? Несколько новых открытых вкладок в браузере и стало понятно, что этот коннектор позволяет получать данные при обращении по URL. Backend для расчета самих данных уже был почти готов, оставалось подружить его с WDC. Несколько дней Денис изучал документацию и воевал с механизмами Tableau, а затем скинул мне ссылку, которую я вставил в окно подключения.


Форма подключения к нашему WDC
Форма подключения к нашему WDC. Денис сделал свой фронт и позаботился о безопасности


Через пару минут ожидания (данные рассчитываются динамически при запросе) появилась таблица:


Так выглядит сырой массив данных в интерфейсе Tableau
Так выглядит сырой массив данных в интерфейсе Tableau


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


Можно было бы вывести эту таблицу в отчет как есть, щедро посыпать фильтров и отправить инструмент в плавание. Звучит логично. Что можно сделать с таблицей? Но это не наш путь, ведь мы делаем не просто таблицу, а инструмент для анализа и принятия продуктовых решений.


Как правило, при анализе данных человек хочет получить ответы на вопросы. Отлично. С них и начнем.


  • Какие переходы самые частые?
  • Куда уходят с конкретных страниц?
  • Сколько в среднем проводят на этой странице до того, как ушли?
  • Как часто делают переход из A в B?
  • А на каких страницах заканчивается сессия?

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


Что же у нас получилось?


Куда чаще всего расходятся с дашборда?


image
Фрагмент нашего отчета. После дашборда все уходили либо на список ВМ либо на список нод


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


Откуда приходят в список кластеров?


Список страниц, с которых пользователи попадали в страницу с кластерами
Фильтры в отчетах работают в обе стороны: можно узнать откуда ушли, а можно куда шли


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


Спросим кое-что посложнее.


Откуда пользователи чаще всего бросают сессию?


Пользователи VMmanager часто работают в отдельных вкладках
Пользователи VMmanager часто работают в отдельных вкладках


Для этого нам нужен отчет данные которого агрегированы по источникам переходов. А в качестве назначений были взяты так называемые breakepoints события, которые послужили концом цепочки переходов.


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


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


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


Таблица с переходами пользователей
Таблица получилась довольно объемной, но как мы надеемся, не перестала быть читаемой


Отдельно стоит сказать про обучение наших внутренних клиентов: продуктологов и UX-проектировщиков. Для них специально были подготовлены мануалы с примерами анализа и подсказками при работе с фильтрами. Ссылки на мануалы мы вставили прямо в страницы отчетов.


Мануал по использованию отчета
Мануал мы сделали просто в виде презентации в Google Docs. Средства Tableau позволяют отображать веб-страницы прямо внутри книги с отчетами.


Вместо послесловия


Что в сухом остатке? Мы относительно быстро и дешево смогли получить инструмент на каждый день. Да, определённо это не замена самого графа, тепловой карты кликов или веб-визора. Но такие отчеты существенно дополняют перечисленные инструменты, дают пищу для размышлений и новых продуктовых и интерфейсных гипотез.


Эта история послужила лишь началом для развития аналитики в ISPsystem. За полгода появилось еще семь новых сервисов, включая цифровые портреты пользователя в продукте и сервис для формирования баз для Look-alike таргетинга, но о них в следующих эпизодах.

Подробнее..

ISPmanager 6. Что нового?

10.06.2021 20:10:36 | Автор: admin

Обзор версии ISPmanager 6


О панели ISPmanager, ее достоинствах и возможностях, кажется, знают все это одно из самых популярных решений для управления VPS и серверами на базе Linux.

Казалось бы, ну что еще добавить? Всё уже давно сказано. Но недавно разработчики из ISPsystem представили новую версию ISPmanager 6. Давайте разберемся, какие нововведения можно увидеть в решении уже сейчас и чего нам ждать в будущем.

Что такое ISPmanager?


Сначала поговорим немного о сути сервиса, как он работает.

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

ISPmanager как раз и является той самой графической панелью управления, упрощающей работу с установленным программным обеспечением.

Первая версия ISPmanager вышла в 2013 году, и с тех пор продукт активно развивается. За этот период разработчики внесли более 1500 изменений и более чем в пять раз расширили кодовую базу. Команда ISPsystem оперативно подстраивается под новые модули, операционные системы и дополнительные инструменты. ISPmanager 5 начал поддерживать:

  • Ubuntu 18.04 и 20.04,
  • CentOS 8,
  • Stream,
  • Debian 10.

Также подключились сервисы:

  • PHP 7.3 и 7.4,
  • MySQL 8.0,
  • Fail2ban.

ISPmanager улучшает и качество шифрования: теперь пользователи могут работать через Lets Encrypt ACME v2 и DNSSEC; появились новые протоколы: HTTP 2.0 и преобразователь NAT.

ISPsystem работает и над улучшением интерфейса. Сегодня графический интерфейс ISPmanager выглядит современно и удобно.

Возможности ISPmanager


На данный момент ISPmanager предлагает:

  • неограниченное количество пользователей в любом тарифном плане;
  • установка прав доступа;
  • полный контроль над доменами: интеграция CMS, работа с редиректами, SSL-сертификатами и прочими техническими моментами;
  • редактирование и управление DNS;
  • работа с почтовыми системами;
  • создание и администрирование баз данных;
  • подключение пользователей по FTP к определенным директориям;
  • файрвол;
  • резервное копирование;
  • полная статистика;
  • разделение пользователей по правам доступа.

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

Итак, пришло время рассказать о новой версии продукта ISPmanager 6.

Главные изменения в ISPmanager 6


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

Основные изменения коснулись тарифной системы. Розничные цены повысились, однако для партнеров будут специальные предложения. На сайте ISPsystem представлены следующие версии:

  • Lite. Минимальная версия ограничивает количество сайтов до десяти. Остальные функции остались такими же, как и в ISPmanager 5.
  • Pro. Единственное отличие от Lite количество сайтов равняется 50.
  • Host. Пользователь может создавать неограниченное количество сайтов с такой же функциональностью, как и в предыдущих версиях.
  • Business. Самый продвинутый тариф со множеством дополнительных инструментов, таких как Cloudlinux (планируется добавление в Host), управление реселлерами, IP-адресами и так далее.

Пользователи, которые привыкли к старой панели, используют собственные модули или управляют множеством сайтов одновременно, не будут насильно перебрасываться на новую версию. Переход будет постепенным и бесшовным, а предыдущие наработки продолжат функционировать. Служба поддержки продолжит работу с пользователями старых версий до конца 2021 года.

Ближайшие обновления


Хотя на данный момент кажется, что главные изменения только в тарифной системе, до конца 2021 года разработчики добавят множество новых инструментов, которых не было в ISPmanager 5.

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

До конца года появится мониторинг почты с новыми спам-фильтрами и некоторыми дополнительными функциями. Вместе с ним планируется запустить внешний и внутренний мониторинг: первый будет отображать доступность сайтов, а второй ресурсы и запущенные процессы. К аналитическим инструментам добавится проактивная система контроля ошибок, которая поможет выявлять проблемы в панели пользователя.

Многие сервисы работают с Telegram, и ISPmanager 6 не исключение. В будущем оповещения о состоянии системы будут приходить прямо в мессенджер.

Ко всему прочему постепенно интегрируются:

  • Python (Django);
  • node.js;
  • OpenVPN;
  • GIT;
  • Lightspeed.

Работаете ли вы с ISPmanager или с другой панелью? Как впечатления?

Подробнее..

Ленивая подгрузка переводов с Angular

22.07.2020 12:20:51 | Автор: admin

image


Если вы когда-нибудь участвовали в разработке крупного angular-проекта с поддержкой локализации, то эта статья для вас. Если же нет, то возможно, вам будет интересно, как мы решили проблему скачивания больших файлов с переводами при старте приложения: в нашем случае ~2300 строк и ~200 Кб для каждого языка.


Немного контекста


Всем привет! Я Frontend-разработчик компании ISPsystem в команде VMmanager.


Итак, мы имеем крупный frontend-проект. Под капотом angular 9-й версии на момент написания статьи. Поддержка локализации осуществляется библиотекой ngx-translate. Сами переводы в проекте лежат в json-файлах. Для взаимодействия с переводчиками используется сервис POEditor.


Что не так с большими переводами?


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


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


Что делать с этими проблемами?


Как можно догадаться из названия статьи: распилить переводы на кусочки, разложить по разным файлам и скачивать по мере их необходимости.
Эта концепция хорошо ложится на модульную архитектуру angular. Мы будем указывать в angular-модуле, какой кусок переводов нам для него нужен.


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


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


На основании перечисленных хотелок получается примерно такая структура файлов:


<projectRoot>/i18n/  ru.json  en.json  HOME/    ru.json    en.json  HOME.COMMON/    ru.json    en.json  ADMIN/    ru.json    en.json

Тут файлы json в корне это основные файлы, они будут скачиваться всегда (нужный, в зависимости от выбранного языка). Файлы в HOME переводы необходимые только обычному пользователю. ADMIN файлы необходимые только администратору. HOME.COMMON файлы необходимые и пользователю, и администратору.


Каждый json-файл внутри должен иметь структуру, соответствующую его namespace:


  • корневые файлы просто содержат {...};
  • файлы внутри ADMIN содержат { "ADMIN": {...} };
  • файлы внутри HOME.COMMON содержат { "HOME": { "COMMON": {...} } } ;
  • и т.д.

Пока что это можно воспринимать как мою причуду, далее это будет обоснованно.


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


ngx-translate из коробки этого всего не умеет, но предоставляет достаточный функционал, чтобы это можно было реализовать своими силами:


  • возможность обработать отсутствующий перевод используем это, чтобы докачать необходимые переводы налету;
  • возможность реализовать свой загрузчик файлов переводов а это чтобы скачивать сразу несколько файлов переводов, необходимых текущему модулю.

Реализация


Скачиватель переводов: TranslateLoader


Чтобы сделать свой скачиватель переводов, необходимо создать класс реализующий один метод abstract getTranslation(lang: string): Observable<any>. Для семантики можно унаследовать его от абстрактного класса TranslateLoader (импортируется из ngx-translate), который мы далее будем использовать для провайдинга.


Так как наш класс будет не просто скачивать переводы, но и как-то должен их объединять в один объект, кода будет чуть больше, чем один метод:


export class MyTranslationLoader extends TranslateLoader implements OnDestroy {  /** Глобальный кэш с флагами скачанных файлов переводов (чтобы не качать их повторно, для разных модулей) */  private static TRANSLATES_LOADED: { [lang: string]: { [scope: string]: boolean } } = {};  /** Сортируем ключи по возрастанию длины (маленькие куски будут вмердживаться в большие) */  private sortedScopes = typeof this.scopes === 'string' ? [this.scopes] : this.scopes.slice().sort((a, b) => a.length - b.length);  private getURL(lang: string scope: string): string {    // эта строка будет зависеть от того, куда и как вы кладете файлы переводов    // в нашем случае они лежат в корне проекта в директории i18n    return `i18n/${scope ? scope + '/' : ''}${lang}.json`;  }  /** Скачиваем переводы и запоминаем, что мы их скачали */  private loadScope(lang: string, scope: string): Observable<object> {    return this.httpClient.get(this.getURL(lang, scope)).pipe(      tap(() => {        if (!MyTranslationLoader.TRANSLATES_LOADED[lang]) {          MyTranslationLoader.TRANSLATES_LOADED[lang] = {};        }        MyTranslationLoader.TRANSLATES_LOADED[lang][scope] = true;      })    );  }  /**    * Все скачанные переводы необходимо объединить в один объект    * т.к. мы знаем, что файлы переводов не имеют пересечений по ключам,    * можно вместо сложной логики глубокого мерджа просто наложить объекты друг на друга,   * но надо делать это в правильном порядке, именно для этого мы выше отсортировали наши scope по длине,   * чтобы наложить HOME.COMMON на HOME, а не наоборот   */  private merge(scope: string, source: object, target: object): object {    // обрабатываем пустую строку для root модуля    if (!scope) {      return { ...target };    }    const parts = scope.split('.');    const scopeKey = parts.pop();    const result = { ...source };    // рекурсивно получаем ссылку на объект, в который необходимо добавить часть переводов    const sourceObj = parts.reduce(      (acc, key) => (acc[key] = typeof acc[key] === 'object' ? { ...acc[key] } : {}),      result    );        // также рекурсивно достаем нужную часть переводов и присваиваем    sourceObj[scopeKey] = parts.reduce((res, key) => res[key] || {}, target)?.[scopeKey] || {};    return result;  }  constructor(private httpClient: HttpClient, private scopes: string | string[]) {    super();  }  ngOnDestroy(): void {    // сбрасываем кэш, чтобы при hot reaload переводы перекачались    MyTranslationLoader.TRANSLATES_LOADED = {};  }  getTranslation(lang: string): Observable<object> {    // берем только еще не скачанные scope    const loadScopes = this.sortedScopes.filter(s => !MyTranslationLoader.TRANSLATES_LOADED?.[lang]?.[s]);    if (!loadScopes.length) {      return of({});    }    // скачиваем все и сливаем в один объект    return zip(...loadScopes.map(s => this.loadScope(lang, s))).pipe(      map(translates => translates.reduce((acc, t, i) => this.merge(loadScopes[i], acc, t), {}))    );  }}

Как можно заметить, scope здесь используется и как часть url для скачивания файла, и как ключ для доступа к необходимой части json, именно поэтому директория и структура в файле должны совпадать.


Как это использовать, описано чуть дальше.


Докачиватель переводов: MissingTranslationHandler


Чтобы реализовать эту логику, необходимо сделать класс, имеющий метод handle. Проще всего унаследовать класс от MissingTranslationHandler, который импортируется из ngx-translate.
Описание метода в репозитории ngx-translate выглядит так:


export declare abstract class MissingTranslationHandler {  /**   * A function that handles missing translations.   *   * @param params context for resolving a missing translation   * @returns a value or an observable   * If it returns a value, then this value is used.   * If it return an observable, the value returned by this observable will be used (except if the method was "instant").   * If it doesn't return then the key will be used as a value   */  abstract handle(params: MissingTranslationHandlerParams): any;}

Нас интересует как раз второй вариант развития событий: вернуть Observable на скачивание нужного куска переводов.


export class MyMissingTranslationHandler extends MissingTranslationHandler {  // кэшируем Observable с переводом, т.к. при входе на страницу, для которой еще нет переводов,  // каждая translate pipe вызовет метод handle  private translatesLoading: { [lang: string]: Observable<object> } = {};  handle(params: MissingTranslationHandlerParams) {    const service = params.translateService;    const lang = service.currentLang || service.defaultLang;    if (!this.translatesLoading[lang]) {      // вызываем загрузку переводов через loader (тот самый, который реализован выше)      this.translatesLoading[lang] = service.currentLoader.getTranslation(lang).pipe(        // добавляем переводы в общее хранилище ngx-translate        // флаг true говорит о том, что объекты необходимо смерджить        tap(t => service.setTranslation(lang, t, true)),        map(() => service.translations[lang]),        shareReplay(1),        take(1)      );    }    return this.translatesLoading[lang].pipe(      // вытаскиваем необходимый перевод по ключу и вставляем в него параметры      map(t => service.parser.interpolate(service.parser.getValue(t, params.key), params.interpolateParams)),      // при ошибке эмулируем стандартное поведение, когда нет перевода  возвращаем ключ      catchError(() => of(params.key))    );  }}

Мы в проекте всегда используем только строковые ключи (HOME.TITLE), но ngx-translate также поддерживает ключи в виде массива строк (['HOME', 'TITLE']). Если вы этим пользуетесь, то в обработке catchError необходимо добавить проверку вроде такой of(typeof params.key === 'string' ? params.key : params.key.join('.')).


Используем все вышеописанное


Чтобы использовать наши классы, необходимо указать их при импорте TranslateModule:


export function loaderFactory(scopes: string | string[]): (http: HttpClient) => TranslateLoader {  return (http: HttpClient) => new MyTranslationLoader(http, scopes);}// ...// app.module.tsTranslateModule.forRoot({  useDefaultLang: false,  loader: {    provide: TranslateLoader,    useFactory: loaderFactory(''),    deps: [HttpClient],  },})// home.module.tsTranslateModule.forChild({  useDefaultLang: false,  extend: true,  loader: {    provide: TranslateLoader,    useFactory: loaderFactory(['HOME', 'HOME.COMMON']),    deps: [HttpClient],  },  missingTranslationHandler: {    provide: MissingTranslationHandler,    useClass: MyMissingTranslationHandler,  },})// admin.module.tsTranslateModule.forChild({  useDefaultLang: false,  extend: true,  loader: {    provide: TranslateLoader,    useFactory: loaderFactory(['ADMIN', 'HOME.COMMON']),    deps: [HttpClient],  },  missingTranslationHandler: {/*...*/},})

Флаг useDefaultLang: false необходим для корректной работы missingTranslationHandler.
Флаг extend: true (добавлен в версии ngx-translate@12.0.0) необходим, чтобы дочерние модули работали с переводами главного модуля.


Также, чтобы избавиться от копирования подобных импортов для каждого модуля с отдельными переводами, можно сделать пару утилит:


export function translateConfig(scopes: string | string[]): TranslateModuleConfig {  return {    useDefaultLang: false,    loader: {      provide: TranslateLoader,      useFactory: httpLoaderFactory(scopes),      deps: [HttpClient],    },  };}@NgModule()export class MyTranslateModule {  static forRoot(scopes: string | string[] = [], config?: TranslateModuleConfig): ModuleWithProviders<TranslateModule> {    return TranslateModule.forRoot({      ...translateConfig([''].concat(scopes)),      ...config,    });  }  static forChild(scopes: string | string[], config?: TranslateModuleConfig): ModuleWithProviders<TranslateModule> {    return TranslateModule.forChild({      ...translateConfig(scopes),      extend: true,      missingTranslationHandler: {        provide: MissingTranslationHandler,        useClass: MyMissingTranslationHandler,      },      ...config,    });  }}

Такие импорты должны быть только в корневых модулях отдельных частей приложения, далее (чтобы использовать translate пайпу или директиву) надо просто импортировать TranslateModule.


В данный момент (на версии ngx-translate@12.1.2) можно заметить, что при переключении языка, пока происходит скачивание переводов, пайпа translate выводит [object Object]. Это ошибка внутри самой пайпы.


POEditor


Как я упоминал ранее, мы выгружаем наши переводы в сервис POEditor, чтобы там с ними мог работать переводчик. Для этого в сервисе есть соответствующее API:



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


Эту логику мы реализовали в python3 скрипте.
В общих чертах он использует тот же принцип объединения переводов, что и в MyTranslateLoader. Разделение происходит по той же схеме, только из большого файла, мы вычитаем куски.


В скрипте реализовано несколько команд:


  • split принимает на вход файл и директорию, в которой у вас подготовлена структура для переводов, и раскладывает переводы согласно этой структуре (в нашем примере это директория i18n);
  • join делает обратное действие: принимает на вход путь до директории с переводами и кладет склеенный json либо в stdout, либо в указанный файл;
  • download скачивает переводы из POEditor, затем либо раскладывает их по файлам в переданной директории, либо кладет в один файл, переданный в аргументы;
  • upload соответственно загружает в POEditor переводы либо из переданной директории, либо из переданного файла;
  • hash считает md5 сумму всех переводов из переданной директории. Пригодится в том случае, если вы подмешиваете хеш в параметры для скачивания переводов, чтобы они не кэшировались в браузере при изменении.

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


Исходник скрипта в рамках статьи рассматривать не будем, потому что он достаточно большой, но вы можете найти его в репозитории по ссылке ниже.
Также в репозитории представлен демо проект, в котором реализовано все, что описано в статье. А также этот проект залит на платформу stackblitz, где можно все потыкать.


GitHub Репозиторий
Демо на Stackblitz


К чему мы пришли


Сейчас такой подход уже используется в VMmanager 6. Конечно, все наши переводы за один раз мы не стали разделять, потому что их достаточно много. Постепенно отделяем их от основного файла, а новый функционал стараемся реализовывать уже с разделением переводов.


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


А как вы решаете проблему больших файлов локализации? Или почему не стали этого делать?

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru