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

I18n

I18n в Angular

27.08.2020 14:07:16 | Автор: admin

Angular i18n


Цель статьи это описать детальные шаги интернационализации вашего приложения на Angular с помощью родного функционала.


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


Angular i18n
Illustration by Thomas Renon


Есть два подхода использовать встроенный в angular i18n: генерация под каждую локаль своего бандла приложения, либо использовать библиотеки вроде transloco, которые предлагают хранить переводы во внешних json файлах или разных других форматах и динамически подставлять/менять локаль по запросу пользователя. Не мало холиваров было о вопросе как удобнее, но однозначно ясно одно если у нас уже написаное приложение расставлять в нем токены дело не сильно приятное. В то время как родные средства Angular более подходят, для того чтобы взять и готовое приложение сделать многоязычным.


Тут вы найдете ответы на вопросы:


  • Как вынести текущий язык в токены
  • Как добавить новый язык переводов
  • Как модифицировать языки
  • Как деплоить и собирать приложение
  • Как быть если есть токены в ts файле или они приходят по API

Вступление


Как это работает?


Помимо черной магии в angular есть специальный атрибут i18n для поддержи интернационализации. Работает он совсем не так как обычные атрибутивные компоненты в angular (как ngClass к примеру). Потому что на самом деле это не компонента, Это фактически директива препроцессора. Да, для интернационализации Angular предлагает не использовать Angular, а использовать хитрый препроцессор во время сборки проекта. Именно такой подход отчасти и позволяет нам локализовать приложение которое уже написанно с минимальными вложениями в этот процесс (оставим за кадром RTL языки, поддержка которых хромает на обе ноги везде). Соответственно разметив все строки в шаблонах мы говорим angular-cli извлеки все строки в проекте и сделай мне файл для переводов.


Итого Для создания мультиязычных интерфейсов Angular предлагает использовать механизм разметки HTML шаблонов при помощи специального атрибута i18n который после компиляции удаляется из финального кода.


1. Вопрос: "Как токенизировать текущий язык и не создать путаницу токенов"


  • Советую всегда привязываться к id
  • Для себя выберите правила токенизации приложения которым должны следовать все на проекте

Теперь вопрос: "как создать сам id"?
Какие правила придумать?


В проекте следует по возможности для маркера указывать дополнительные параметры которые отображаются в специализированных редакторах использующихся для перевода и дополняют переводимый текст служебной информацией призванной помочь переводчику. Это параметры передаются в формате Значение|Описание или только Описание. Обязательно следует указывать @@id это будет токен для перевода. Идентификатор пишется своеобразным синтаксисом используя префикс @@.


<div i18n="форма логина | поле @@login.email">Email</div><button i18n="форма логина | кнопка @@login.post">save</button>

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


Пример соглашения по наименованию токенов


Находясь в компоненте <info-statuses> токен следует называть таким образом:


<th i18n="Статусы покупателя | колонка @@(селектор компонента и поле)info-statuses.date">Дата добавления</th>

Различные варианты использования токенов


Иногда у нас есть необходимость задать перевод для текста который является фрагментом внутри большего блока или текст вне тегов. Используем ng-container, который не рендерится в финальный код.


<ng-container     i18n="Генерация архива | поле @@generate-archive.title">I don't output any element</ng-container>

Возможно так же делать перевод для атрибутов тегов. Указывается i18n-attrName


<img     [src]="logo"     i18n-title="картинка @@company.logo"    title="Angular logo" />

Вот мы заполнили все шаблоны тегами i18n и что теперь? Теперь нужно создать файл переводов, Angular приходит на помощь и говорит, просто вызываем команду i18n-extract и генерируем файл с переводами. Глянуть описание аргументов можно тут https://angular.io/cli/xi18n


В моём случае команда выглядит таким образом (я указываю исходную локаль файлов перевода. "uk")


"extract-i18n": "ng xi18n projectName --i18n-format xlf --output-path i18n --i18n-locale uk

Теперь вы знаете ответ как локализовать приложение под один язык.


2. Как добавить новый язык для переводов


Сейчас мы поговорим о инструментах, что помогают работать с дефолтным форматом выгрузки ключей в angular i18n это xliffmerge


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


Вывод для настройки и удобной генерации новых языков xliffmerge наше спасение.


https://www.npmjs.com/package/@ngx-i18nsupport/ngx-i18nsupport
https://github.com/martinroob/ngx-i18nsupport/wiki/Tutorial-for-using-xliffmerge-with-angular-cli


"xliffmerge": {  "builder": "@ngx-i18nsupport/tooling:xliffmerge",  "options": {    "xliffmergeOptions": {      "i18nFormat": "xlf",      "srcDir": "projects/my-test/i18n",      "genDir": "projects/my-test/i18n",      "verbose": true,      "defaultLanguage": "uk",      "languages": [        "uk",        "en"      ]    }  }}

В настройках angular.json мы добавляем новую конфигурацию.
Эта конфигурация при запуске, принимает дефолтный исходный файл. B зависимости от настроек генерирует производные или дополняет уже существующие файлы с переводом новыми ключами. Важно! При добавлении ключа в базовую локаль он будет добавлен всем производным локалям. Это все делается автоматически, не нужно править XML руками.


Таким образом. Когда мне нужно добавить новую локаль. Я добавляю поле в блок languages с именем языка, к примеру en и запускаю ng run my-test:xliffmerge чтобы на выходе получить новый файл xlf с локалью en.
Теперь команда генерации файлов переводов выглядит таким образом


"extract-i18n": "ng xi18n crm --i18n-format xlf --output-path i18n --i18n-locale ru && ng run my-test:xliffmerge",

Было бы классно ещё пропускать переводы через google translate, чтобы сэкономить на переводах и иметь какой-то черновой вариант подумал я. Как выяснилось xliffmerge имеет и такую опцию.


Дополняем конфиг xliffmerge а angular.json:


"autotranslate": ["en"],"apikey": "yourAPIkey",

Хорошо, теперь при изменениях в нашем html запуск команды extract-i18n будет обновлять все локали.


Осталось последнее, как собирать бандл для деплоя.


"build-prod:my-test:en": "ng build my-test --configuration=productionEN --base-href /en/ --resources-output-path ../""build-prod:my-test:uk": "ng build my-test --configuration=productionUK --base-href /uk/ --resources-output-path ../","build-prod:locales": "npm run build-prod:my-test:en && npm run build-prod:my-test:uk",

Под каждую локаль своя команда, к сожалению, в А8 на то время нельзя было ставить аргументы через запятую --configuration=production,en, поэтому пришлось дублировать конфиги в angular.json


"productionEN": {  "outputPath": "dist/my-test/en",  "fileReplacements": [    {      "replace": "projects/my-test/src/environments/environment.ts",      "with": "projects/my-test/src/environments/environment.en.prod.ts"    }  ],  ... like in production},

Мы настроили билд так, чтобы assets были общими (resources-output-path ../), вы можете убрать resources path и максимально отделить разные версии между собой. Для большинства приложений ресурсы в разных языковых версиях не будут отличаться, поэтому такой ход оправдан. В случае общих ресурсов перезагрузка бандла при смене языка будет происходить существенно быстрее, потому что часть ресурсов уже будет в кеше браузера.


Теперь запоминаем самое главное: файлы .xlf никогда руками не правим, через специальные инструменты (weblate к примеру OpenSource инструмент) можно записывать верные переводы и пушить в ветку, а там ваш билд это всё подхватит и всё супер.


На выходе в нашем проекте мы разметили все шаблоны, дополнили список команд для работы с переводами, сняли с себя задачу актуализации json между несколькими языками, наняли несколько переводчиков в штат, чтобы они изучили CMS Weblate и поседели раньше чем разработчики. На самом деле для небольших проектов экономия на снятии сопровождения переводов с разработчиков дает весьма значимый эффект.


Таким образом мы теперь должны доработать свой CI/CD папйплайн чтобы генерировать несколько языковых версий вместо одной.


Всё работает, но оказывается у в проекте есть текст который зашит в ts файлах и как его переводить, если подстановка i18n атрибута работает только в шаблонах.


Переводы в коде


Есть список с текстом, который никак не завязан на бэкенд, поэтому и переводов у нас с бэканда этой сущности нет. Что делать? Ответ один локализируй через шаблон.


Вот пример как это будет


  list = [    {      token: 'login-info-1',      value: 1,    },    {      token: 'login-info-2',      value: 2,    },  ];

 <div style="display: none"       #el       i18n-login-info-1="поле @@login-form.first"       login-info-1="первое условие это..."       i18n-login-info-2="поле @@login-form.second"       i18n-login-info-2="второе условие это..."  >  </div>   <div *ngFor="let item of list">      <label>        {{ item.token | customPipeI18n: el }}      </label>  </div>

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


Пример пайпа


@Pipe({ name: 'customPipeI18n', pure: true })export class TranslatePipe {  transform(key: string, value: HTMLElement): any {    const lowerKey = key.toLowerCase();    if (value && value.hasAttribute(lowerKey)) {      return value.getAttribute(lowerKey);    }    console.log('key: ', lowerKey);    return '*not found key*';  }}

Хорошо это работает. А что делать если у меня схожие тексты повторяются на многих страницах? Добавлять на каждую по скрытому элементу с идентичной логикой?


Согласен, что вариант с таким пайпом не сильно удобен и было бы круто сделать так, чтобы все тексты, что в есть в ts файлах определенного модуля были вынесены куда-то в одном место где их легко смотреть редактировать и менять. Было принято решение написать реестр переводов.


Для этого создана комбинация:
сервис(ElementRegistry) для хранения элемента;
директива(ElementDirective) для регистрации шаблона с атрибутами и сохранения его в сервис;
пайп(ElementPipe) для получения перевода из сервиса;


Пример использования:


Имеем модуль auth
в корневом компоненте создаём элемент с атрибутами объявляем директиву и регистрируем имя шаблона auth


<div  i18nElement="auth"  le="display: none"  i18n-login-info-1="поле @@login-form.first"  login-info-1="первое условие это..."  i18n-login-info-2="поле @@login-form.second"  i18n-login-info-2="второе условие это..."></div>

Для перевода вызывается pipe i18nElement туда передаётся название шаблона в котором объявлены атрибуты с токенами.


   <div *ngFor="let item of list">      <label>        {{ item.token | i18nElement: 'auth' }}      </label>  </div>

Это решает следующие проблемы:


  • Eсли мы используем текст который приходит из сторонего API, а локализовать необходимо ответ
  • У вас в ts файле просто почему-то оказался текст который нужно локализовать

Итог


Simple-Made-Easy нативные средства i18n в Angular не смотря на меньшую популярность чем классический подход с кучей json файлов тоже работают и весьма удобны\продуманны в практическом применнии.
xliff как формат хранения переводов помимо непригодности для редактирования руками имеет много удобных интсрументов для переводчиков, позволяющих анотировать и групировать переводы. Отказ от использования json и переход на xliff позволяет упростить работу с переводами для команды локализации, особенно вместе с инструментами вроде weblate или аналогами.
Некоторые сложности вызывает использование переводов вне шаблонов, но все они в целом решаемые при помощи подходв описанных в статье.


P.S.


в 9-10м Ангуляре есть изменения в работе с локализацией. Ставьте палец вверх и будет ещё одна статья про облегчение с 9м


Пригласить автора на хабр: skochkobo
Метода опробована на проектах: nodeart.io

Подробнее..

Советы по эффективной локализации продукта

16.07.2020 12:18:27 | Автор: admin


Локализация это воплощение принципа удобства для пользователя: примерно 75% людей в мире чувствуют себя более свободно и уверенно при использовании продуктов на родном языке, а не на английском.


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


Сложности локализации на стыке с разработкой


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


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


Правильный подход с самого начала


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


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


Этапы локализации продукта и рекомендации


В стратегии по локализации важны налаженные процессы и конкретные инструкции: они обеспечивают эффективную локализацию в рамках разработки цифровых продуктов.


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


Этап 1. Проверка исходных текстов


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


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


Этап 2. Тестирование локализации (псевдо-локализация)


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


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


Этап 3. Работа со сторонней компанией по локализации


На этом этапе рекомендуется остановить изменения в пользовательском интерфейсе, заблокировать контент и перенести обновления на будущий спринт. Переводческой компании нужно передать все необходимые материалы и сведения, в том числе об используемых для перевода инструментах и программном обеспечении (облачных платформах, CRM, CMS и других инструментах для перевода). Для автоматизации и оптимизации качества переводов часто используются платформы управления переводами (например, Crowdin), в которых работают все стороны, в том числе агентства по локализации.



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


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


Этап 4. Оценка качества перевода


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


Этап 5. Исправление интерфейса и перевод остальных текстов


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


Проблемы и решения в непрерывной локализации


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


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


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



Интеграция программного обеспечения для управления переводами


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


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


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


Оценка качества


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


Лингвистическое тестирование это когда специалист по локализации тестирует сборку на соответствующей локальной версии ОС. При этом делаются снимки экрана с проблемами (непереведенный текст, не влезающие в экран строки, неправильные кодировка, направление текста или контекст) и с помощью разработчиков вносятся изменения в ресурсные файлы.


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


Нужна помощь с локализацией / переводом? Мы в Alconost всегда рады помочь!


О нас


Alconost профессионально занимается локализацией игр, приложений и сайтов на более 70 языков. Лингвистическое тестирование, облачная платформа с API, непрерывная локализация, менеджмент проектов 24/7, любые форматы строковых ресурсов.
Мы также делаем видеоролики.
Подробнее

Подробнее..

Продолжаем интернационализацию поиска по адресам с помощью Sphinx или Manticore. Теперь Metaphone

05.04.2021 08:09:25 | Автор: admin

Это продолжение публикации Интернационализация поиска по городским адресам. Реализуем русскоязычный Soundex на Sphinx Search, в которой я разбирал, как реализовать поддержку фонетических алгоритмов Soundex в Sphinx Search, для текста написанного кириллицей. Для текста на латинице поддержка Soundex уже есть. С Metphone аналогично, для латиницы есть, для кириллицы не очень, но попытаемся исправить этот досадный факт с помощью транслитерации, регулярных выражений и напильника.

Это прямое продолжение, в котором разберём как реализовать оригинальный Metaphone, русскийMetaphone (в том смысле что транслитерация не понадобится), Caverphone, и не сможем сделать DoubleMetaphone.

Реализация подойдёт как для использования на платформе SphinxSearch, так и ManticoreSearch.

В конце, в качестве бонуса, посмотрим как Metaphone воспримет "ракомакофон".

Докер образ

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

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

Оригинальный Metaphone

Реализуется элементарно, создаются регулярные выражения для транслитерации:

regexp_filter = (А|а) => aregexp_filter = (Б|б) => bregexp_filter = (В|в) => v

И включаем metaphone:

morphology = metaphone

Всё, как и с оригинальным Soundex. В прошлый раз, мы пришли к выводу, что лучше всего, из всех Soundex алгоритмов, использовать именно оригинальный Soundex, единственный недостаток которого коллизии, разрешается вычислением расстояния Левенштейна.

В этот раз, забегая вперёд, скажу, что снова сделал бы свой выбор в пользу оригинального Metaphone + транслит. А вот причина небанальна.

Дело в том что у Sphinx есть в такой параметр blend_chars. Смысл следующий, Sphinx индексирует по словам, а слова он находит по разделителям, например, если между буквами есть пробел, значит буквы два разных слова, перенос строки, табуляция, знаки препинания и т.д., и т.п. Но могут быть символы, которые одновременно разделяют слова, а могут быть и частью одного слова, например, &. M&Ms это сколько слов? А Рога&Копыта? Для таких случаев и существует blend_chars.

И тут можно пойти на хитрость, и добавить в blend_chars пробел:

blend_chars = U+0020

Теперь, когда кто-то будет искать улицу Мориса Тореза, то найдёт её, даже если подумает, что это одно слово. Да что там слитное написание, он её найдёт даже если решит, что Мать Тереза и Морис Торез родственники.

mysql> select * from metaphone where match('Морисатереза');+------+--------------------------------------+-----------+---------------------------+| id   | aoguid                               | shortname | offname                   |+------+--------------------------------------+-----------+---------------------------+| 1130 | e21aec85-0f63-4367-b9bb-1943b2b5a8fb | ул        | Мориса Тореза             |+------+--------------------------------------+-----------+---------------------------+

Можем увидеть, как работает индекс для Мориса Тореза, вызвав call keywords:

mysql> call keywords ('Мориса Тореза', 'metaphone');+------+---------------+------------+| qpos | tokenized     | normalized |+------+---------------+------------+| 1    | morisa toreza | MRSTRS     || 1    | morisa        | MRS        || 2    | toreza        | TRS        |+------+---------------+------------+

Обратите внимание, что два слова было воспринято как три: morisa, toreza и morisa toreza, притом при создании кода Metaphone, пробел был съеден.

Это особенность реализации Metaphone в Sphinx Search. Самостоятельно, с помощью регулярных выражений её не реализовать. Нет, конечно мы можем добавить регулярное выражение, удаляющее пробелы:

regexp_filter = [ ] => 

но тогда Мориса Тореза, и другие, будут всегда восприниматься как одно слово, а нам этого не надо.

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

Caverphone пробел сохраняет, поэтому при слитном написании просто не находит.

mysql> call keywords ('Мориса Тореза', 'caverphone');+------+-----------+------------+| qpos | tokenized | normalized |+------+-----------+------------+| 1    | mrsa trza | mrsa trza  || 1    | mrsa      | mrsa       || 2    | trza      | trza       |+------+-----------+------------+mysql> select * from caverphone where match('Морисатереза');Empty set (0.00 sec)

Оригинальный Soundex (из предыдущей публикации), в котором используется базовая реализация Sphinx, просто сходит с ума, и не понимает, как кодировать слово, в котором встретился пробел, morisa и toreza закодирован, а morisa toreza нет:

mysql> call keywords ('Мориса Тореза', 'simple_soundex');+------+---------------+---------------+| qpos | tokenized     | normalized    |+------+---------------+---------------+| 1    | morisa toreza | morisa toreza || 1    | morisa        | m620          || 2    | toreza        | t620          |+------+---------------+---------------+

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

А это дорогого стоит.

Double Metaphone

Для двойного Metaphone планировал использовать два индекса, вместо одного, как обычно, а затем искать поочерёдно в обоих.

К сожалению, я не понял, как реализовать двойной Metaphone с помощью регулярных выражений. Когда я начал искать по нему описание, меня как будто в гугле забанили, зато нашёл много реализаций на различных языках программирования, например, DoubleMetaphone.java. Я даже попытался понять этот алгоритм и реализовать его в виде регулярок, но дошёл до буквы C, и понял, что рассудок покидает меня.

Если Вы гуру гуглапоиска, или просто знаете способ, как сделать реализацию с помощью регулярных выражений пишите, пишите мне, пишите публикацию на хабр, придумаем как прикрутить его к Sphinx и Manticore.

Но будет нечестно, если про двойной Metaphone совсем ничего не написать. Опишу как бы я его сделал, если было бы ну очень нужно. Sphinx не понадобится. Но придётся программировать.

Для начала я бы подключил уже готовую библиотеку в которой он реализован, благо их нашлось множество для разных языков, я использую Java, поэтому подключаю Commons Codec. Библиотека не работает с кириллицей требуется транслитерация, и кодирует только первое слово. Поэтому если вам надо реализовать кодирование большого текста, разделённого пробелами то самостоятельно придётся разбивать строку на слова, и в цикле их перебирать.

В таблице БД, где хранятся данные, я бы добавил две новые колонки, в которых бы хранил закодированное значение. Это один из недостатков данной реализации требуется допиливать таблицы в БД.

Затем, перед вставкой новых записей в таблицу, я бы вызывал:

DoubleMetaphone dm = new DoubleMetaphone();String metaphone1 = dm.doubleMetaphone("Text", false);String metaphone2 = dm.doubleMetaphone("Text", true);

И сохранял metaphone1 и metaphone2 вместе с данными.

Это вторая большая проблема вся вставка теперь должна проходить через эту процедуру.

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

Без Sphinx всё стало очень неудобно.

Русский Metaphone

Не подойдёт для целей интернационализации.

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

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

Ну и ещё один момент, который нужно учесть при выборе, алгоритм не сжимает окончания. Значит, если улица Ленина, то надо так и писать Ленина, Ленин без окончания, не найдётся:

mysql> call keywords ('Ленина Ленин', 'rus_metaphone');+------+--------------+--------------+| qpos | tokenized    | normalized   |+------+--------------+--------------+| 1    | линина       | линина       || 2    | линин        | линин        |+------+--------------+--------------+

Реализуем регулярные выражения. Полный конфигурационный файл, как и ранее, лежит на GitHub Gist manticore.conf.

  • Переделываем гласные:

regexp_filter = (?i)(йо|ио|йе|ие) => иregexp_filter = (?i)(о|ы|я) => аregexp_filter = (?i)(е|ё|э) => иregexp_filter = (?i)(ю) => у
  • Для всех согласных букв, за которыми следует любая согласная, кроме Л, М, Н или Р, провести оглушение:

regexp_filter = (?i)(б)(б|в|г|д|ж|з|й|к|п|с|т|ф|х|ц|ч|ш|щ) => п\2regexp_filter = (?i)(г)(б|в|г|д|ж|з|й|к|п|с|т|ф|х|ц|ч|ш|щ) => к\2regexp_filter = (?i)(в)(б|в|г|д|ж|з|й|к|п|с|т|ф|х|ц|ч|ш|щ) => ф\2regexp_filter = (?i)(д)(б|в|г|д|ж|з|й|к|п|с|т|ф|х|ц|ч|ш|щ) => т\2regexp_filter = (?i)(ж)(б|в|г|д|ж|з|й|к|п|с|т|ф|х|ц|ч|ш|щ) => ш\2regexp_filter = (?i)(з)(б|в|г|д|ж|з|й|к|п|с|т|ф|х|ц|ч|ш|щ) => с\2
  • Для согласных на конце слова, провести оглушение

regexp_filter = (?i)б\b => пregexp_filter = (?i)г\b => кregexp_filter = (?i)в\b => фregexp_filter = (?i)д\b => тregexp_filter = (?i)ж\b => шregexp_filter = (?i)з\b => з
  • Склеиваем ТС и ДС в Ц

regexp_filter = (?i)(тс|дс|ц) => ц

Caverphone

Здесь сначала транслитерация.

  • Затем, нужно перевести транслитерированное в нижний регистр:

regexp_filter = (A|a) => aregexp_filter = (B|b) => b

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

  • Удалить e на конце

regexp_filter = e\b =>
  • Происходит преобразование начала слова, но это актуально для новозеландских фамилий, этот шаг можно и пропустить:

regexp_filter = \b(cough) => cou2fregexp_filter = \b(rough) => rou2f
  • Провести замены символов

regexp_filter = (cq) => 2qregexp_filter = (ci) => si
  • Заменить все гласные в начале слова на a, в остальных случаях на 3

regexp_filter = (?i)\b(a|e|i|o|u|y) => Aregexp_filter = (?i)(a|e|i|o|u|y) => 3
  • Провести очередные замены

regexp_filter = (j) => yregexp_filter = \b(y3) => Y3
  • Удалить все цифры 2

regexp_filter = 2 => 
  • Если на конце слова осталась цифра 3, то заменить её на A

regexp_filter = 3\b => A
  • Удалить все цифры 3

regexp_filter = 3 =>

До 10 символов не сокращаю и не дополняю.

Проверим:

mysql> select * from caverphone where match ('Ленина');+------+--------------------------------------+-----------+------------------+| id   | aoguid                               | shortname | offname          |+------+--------------------------------------+-----------+------------------+|    5 | 01339f2b-6907-4cb8-919b-b71dbed23f06 | ул        | Линейная         ||  387 | 4b919f60-7f5d-4b9e-99af-a7a02d344767 | ул        | Ленина           |+------+--------------------------------------+-----------+------------------+

Кроме Ленина находит и Линейная. Согласен, некоторое сходство есть, другие алгоритмы так не смогли, ну разве что Daitch Mokotoff Soundex из предыдущей публикации выкинул что-то подобное с Лунная:

mysql> select * from daitch_mokotoff_soundex where match ('Ленина');+------+--------------------------------------+-----------+--------------+| id   | aoguid                               | shortname | offname      |+------+--------------------------------------+-----------+--------------+|  387 | 4b919f60-7f5d-4b9e-99af-a7a02d344767 | ул        | Ленина       ||  541 | 69b8220e-a42d-4fec-a346-1df56370c363 | ул        | Лунная       |+------+--------------------------------------+-----------+--------------+

Можем посмотреть как это всё кодируется:

mysql> call keywords ('Ленина Линейная Лунная', 'caverphone');+------+-----------+------------+| qpos | tokenized | normalized |+------+-----------+------------+| 1    | lnna      | lnna       || 2    | lnna      | lnna       || 3    | lna       | lna        |+------+-----------+------------+mysql> call keywords ('Ленина Линейная Лунная', 'daitch_mokotoff_soundex');+------+-----------+------------+| qpos | tokenized | normalized |+------+-----------+------------+| 1    | 866       | 866        || 2    | 8616      | 8616       || 3    | 866       | 866        |+------+-----------+------------+

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

Бонус: ищем ракомакофон. Вместо заключения

Это лишено практического смысла, но наблюдение забавное, поэтому напишу. Just for fun.

Помните ракомакофон, который слышится вместо rock the microphone?! Было интересно, сможет ли Metaphone понять ракомакофон. И ведь почти!

Во-первых, добавляем пробел в blend_chars, ведь нам надо чтобы три слова rock the microphone, воспринимались как одно:

blend_chars = U+0020

Поскольку у нас только один алгоритм умеет адекватно работать в такой ситуации - оригинальный metaphone, то его и применим.

Проверим с помощью keywords как оно воспринимается Sphinx:

mysql> call keywords ('ракомакофон', 'metaphone');+------+-------------+------------+| qpos | tokenized   | normalized |+------+-------------+------------+| 1    | rakomakofon | RKMKFN     |+------+-------------+------------+

И rock the microphone:

mysql> call keywords ('rock the microphone', 'metaphone');+------+---------------------+------------+| qpos | tokenized           | normalized |+------+---------------------+------------+| 1    | rock the microphone | RK0MKRFN   || 1    | rock                | RK         || 2    | the                 | 0          || 3    | microphone          | MKRFN      |+------+---------------------+------------+

Получилось RK0MKRFN, и RKMKFN, расстояние Левенштейна между ними всего 2(!). А если найти способ исключить the из кодирования, то получится RKMKRFN:

mysql> call keywords ('rock microphone', 'metaphone');+------+-----------------+------------+| qpos | tokenized       | normalized |+------+-----------------+------------+| 1    | rock microphone | RKMKRFN    || 1    | rock            | RK         || 2    | microphone      | MKRFN      |+------+-----------------+------------+

Между RKMKRFN и RKMKFN, расстояние Левенштейна всего 1! Мы почти у цели.

Проблема убрать the, параметр stopwords здесь не поможет, ибо из-за blend_chars = U+0020 the не будет восприниматься самостоятельно. Но даже если удастся сделать предобработку, то всё равно расстояние в 1, не позволит обнаружить похожий.

Надежда на qsuggest не оправдалась, - не даст подсказок. Почему? Можно заметить, что при вызове keywords есть два столбца tokenized и normalized, qsuggest даёт подсказку по столбцу tokenized и измеряет расстояние Левенштейна относительно него, qsuggest всё равно, что там, в normalized, расстояние равно 1.

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

Подробнее..

Ленивая подгрузка переводов с 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