i18n для приложений Angular с рендерингом на стороне сервера

AngularInDepth уходит от Medium. Более свежие статьи размещаются на новой платформе inDepth.dev. Спасибо за то, что участвуете в глубоком движении!

🤔 Фон

Что означает i18n и почему в середине стоит цифра 18? Даже будучи инженером с более чем десятилетним опытом работы в этой области, я понятия не имел, пока не посмотрел. Это количество букв между i и n в слове интернационализация. Итак, i18n - это интернационализация. Довольно аккуратно. Одно из определений i18n:

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

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

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

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

В этой статье мы рассмотрим, как реализовать наши строки перевода в удобной для обслуживания манере, разрешить приложению загружать только необходимые ресурсы и разрешить браузеру запоминать выбранный язык. Затем мы включим рендеринг на стороне сервера (SSR) и решим проблемы, возникшие при включении SSR в приложении Angular.

Статья разбита на следующие части:

Часть 1. Установка сцены

Часть 2. Добавление SSR в приложение

Часть 3. Решение 1. Исправить путем предоставления отдельного модуля I18nModule для сервера.

Часть 4. Решение 2. Обеспечьте все в одном модуле

Часть 5. Повышение производительности с помощью TransferState

Часть 6. Мы уже там?

В первой части этой статьи мы будем следовать простым инструкциям по настройке приложения Angular и добавлению в него возможностей i18n. Разработчики начального уровня могут захотеть глубоко погрузиться в Часть 1. Более продвинутые разработчики могут взглянуть на код в следующих разделах и перейти к «Части 2. Добавление SSR в приложение», чтобы узнать, какие препятствия добавление SSR создаст и как их решить.

📝 Часть 1 из 6. Настройка сцены

В рамках этой статьи мы будем работать с простым приложением Angular, созданным с помощью AngularCLI. Чтобы следовать статье, мы сгенерируем приложение с помощью команды (при условии, что Angular CLI установлен глобально):

ng new ssr-with-i18n

Для примера добавим пару компонентов:

ng g c comp-a
ng g c comp-b

Теперь мы заменим содержимое app.component.html этими двумя компонентами:

*** Код до этого момента доступен здесь.

🗺️ Давайте добавим i18n

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

В этой статье мы воспользуемся популярной библиотекой: ngx-translate.

Примечание: концепции организации модулей и кода, описанные в этой статье, применимы не только к ngx-translate. Приложение может использовать новую блестящую библиотеку transloco, которая была выпущена в день написания этой статьи (15.08.2019) . Читатель может даже попытаться решить проблему, в которой ничего нет делать с переводами. Поэтому эта статья будет полезна всем, кто пытается решить проблему, связанную с SSR.

Использование ngx-translate позволит нам хранить наши строки в отдельных файлах JSON (файл для каждого языка), где каждая строка будет представлена ​​парой ключ-значение. Ключ - это строковый идентификатор, а значение - перевод строки.

  1. Установить зависимости

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

npm install @ngx-translate/core @ngx-translate/http-loader --save

2. Добавьте код

В инструкциях к пакету ngx-translate предлагается добавить соответствующий код непосредственно в AppModule. Однако я думаю, что мы можем добиться большего. Давайте создадим отдельный модуль, который будет инкапсулировать логику, связанную с i18n.

ng g m i18n --module app

Это добавит новый файл: /i18n/i18n.module.ts и укажет на него ссылку в app.module.ts.

Измените файл i18n.module.ts согласно документации. Полный код файла приведен ниже:

Ничего особенного не происходит. Мы просто добавили TranslateModule и настроили его для использования HttpClient для загрузки переводов. Мы также экспортировали TranslateModule, чтобы сделать канал transform доступным в AppModule и в шаблонах HTML. В конструкторе мы указали доступные языки и использовали функцию, предоставляемую ngx-translate, чтобы получить и использовать язык браузера по умолчанию.

По умолчанию TranslateHttpLoader загружает переводы из папки /assets/i18n/, поэтому давайте добавим туда пару файлов.

/assets/i18n/en.json

{
  "compA": "Component A works",
  "compB": "Component B works"
}

/assets/i18n/ru.json

{
  "compA": "Компонент А работает",
  "compB": "Компонент Б работает"
}

Примечание: мы используем один файл для каждого языка. В более сложных приложениях ничто не ограничивает нас в создании файлов на основе локали, например en-US.json, en-Gb.json. По сути, они будут рассматриваться как отдельные переводы.

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

// comp-a.component.html
<p>{{'compA' | translate}}</p>
// comp-b.component.html
<p>{{'compB' | translate}}</p>

Запустите приложение и обратите внимание, что оно использует переводы из файла en.json. Давайте добавим компонент, который позволит нам переключаться между двумя языками.

ng g c select-language --inlineStyle --inlineTemplate

Обновите содержимое файла select-language.component.ts.

Библиотека ngx-translate позволяет переключать языки с помощью простого translate.use() вызова API. Это также позволяет нам определить текущий выбранный язык, запросив свойство translate.currentLang.

Вставьте новый компонент в файл app.component.html после тега h1.

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

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

🙄 Запоминание выбранного языка

Сообщество Angular создало множество плагинов, улучшающих функциональность пакета ngx-translate. Один из них как раз то, что нам нужно - ngx-translate-cache. Следуя инструкциям, мы (1) установим пакет

npm install ngx-translate-cache --save

и (2) использовать его внутри I18nModule.

Теперь, если мы выберем язык ru и обновим страницу в браузере, мы увидим, что он запомнил наш выбор. Обратите внимание, что мы выбрали 'Cookie' как место для хранения выбранного языка. По умолчанию эта опция выбрана 'LocalStorage'. Однако LocalStorage недоступен на сервере. Большая часть этой статьи посвящена включению SSR, поэтому мы немного проактивны и сохраняем выбранный язык в том месте, где сервер может его прочитать.

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

*** Код до этого момента доступен здесь.

💪 Часть 2 из 6. Добавление SSR в приложение

Angular CLI потрясающий! В частности, функция схемы позволяет нам добавлять новые возможности в приложение с помощью простой команды. В этом случае мы запустим следующую команду, чтобы добавить возможности SSR.

ng add @nguniversal/express-engine --clientProject ssr-with-i18n

Запуск этой команды обновил и добавил несколько файлов.

Если мы посмотрим на файл package.json, то увидим, что теперь у нас есть несколько новых скриптов, которые мы можем выполнить. Двумя наиболее важными являются: (1) build:ssr и (2) serve:ssr. Давайте запустим эти команды и посмотрим, что произойдет.

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

TypeError: Cannot read property 'match' of undefined
    at new I18nModule (C:\Source\Random\ssr-with-i18n\dist\server\main.js:113153:35)

Небольшое расследование показывает, что код неисправности:

browserLang.match(/en|ru/)

Переменная browserLang - это undefined, что означает, что следующая строка кода не сработала:

const browserLang = translateCacheService.getCachedLanguage() || translate.getBrowserLang();

Это происходит потому, что мы пытаемся получить доступ к API-интерфейсам конкретного браузера во время рендеринга на стороне сервера. Даже название функции - getBrowserLang предполагает, что эта функция не будет работать на сервере. Мы еще вернемся к этой проблеме, но пока давайте исправим ее, жестко закодировав значение переменной browserLang:

const browserLang = 'en';

Снова соберите и обслужите приложение. На этот раз ошибки нет. Фактически, если мы посмотрим на вкладку сети в Инструментах разработчика, мы увидим, что SSR работает! Однако перевод не прошел.

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

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

ПРОБЛЕМА 1. Возможность определить правильный язык для загрузки как в клиентской, так и в серверной средах (вместо жесткого кодирования значения в en).

ПРОБЛЕМА 2. В зависимости от среды используйте соответствующий механизм для загрузки файла JSON, содержащего переводы.

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

🤔 Оценка существующих вариантов

Есть несколько способов заставить все работать. В репозитории ngx-translate есть закрытая проблема, связанная с включением SSR - issue # 754. Здесь можно найти несколько решений ПРОБЛЕМ 1 и 2.

Существующее решение 1. Исправить через HttpInterceptor

В одном из последних комментариев к проблеме №754 предлагается использовать решение из статьи Angular Universal: Как добавить многоязычную поддержку? Для решения ПРОБЛЕМЫ 2. К сожалению, ПРОБЛЕМА 1 в статье не рассматривается. Автор предлагает исправить это с помощью HttpInterceptor, который исправляет запросы на получение файлов JSON на сервере.

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

Существующее решение 2. Исправить путем прямого импорта файлов JSON

В нескольких недавних комментариях к той же проблеме № 754 предлагается импортировать содержимое файлов JSON прямо в файл, который определяет наш модуль. Затем мы можем проверить, в какой среде мы работаем, и использовать либо стандартную TranslateHttpLoader, либо пользовательскую среду, в которой используется импортированный JSON. Этот подход предлагает способ решения ПРОБЛЕМЫ 2 путем проверки среды, в которой выполняется код: if (isPlatformBrowser(platform)). Позже в этой статье мы воспользуемся аналогичной проверкой платформы.

Не делайте этого! Импортируя файлы JSON, как показано выше, они попадут в пакет браузера. Вся цель использования HttpLoader заключается в том, что он загружает требуемый языковой файл по запросу, уменьшая размер пакета браузера.

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

Хотя оба существующих решения обеспечивают решение ПРОБЛЕМЫ 2, у них есть свои недостатки. Один приводит к ненужным запросам, а другой снижает производительность. Ни один из них не предлагает решения ПРОБЛЕМЫ 1.

🔋 Лучший способ - предварительные условия

В следующих разделах я предложу два отдельных решения выявленных ПРОБЛЕМ. Оба решения потребуют следующих предварительных условий.

Предварительное условие 1. Нам необходимо установить и использовать зависимость под названием cookie-parser.

Предварительное условие 2. Знакомство с токеном инъекции Angular REQUEST

Предпосылка 1. Зачем нужен cookie-парсер?

Библиотека ngx-translate-cache отвечает за создание файла cookie в клиенте, когда пользователь выбирает язык. По умолчанию (хотя это можно настроить) файл cookie называется lang. В следующих решениях нам понадобится способ доступа к этому файлу cookie на сервере. По умолчанию мы можем получить доступ к необходимой нам информации из объекта req.headers.cookie в любом из обработчиков запросов Express. Значение будет выглядеть примерно так:

lang=en; other-cookie=other-value

Это свойство содержит всю необходимую информацию, но нам нужно проанализировать lang. Хотя это достаточно просто, нет необходимости изобретать велосипед, поскольку cookie-parser - это промежуточное ПО Express, которое делает именно то, что нам нужно.

Установите необходимые зависимости.

npm install cookie-parser
npm install @types/cookie-parser -D

Обновите файл server.ts, чтобы использовать установленный анализатор файлов cookie.

import * as cookieParser from 'cookie-parser';
app.use(cookieParser());

Под капотом cookie-parser проанализирует файлы cookie и сохранит их как объект словаря в req.cookies.

{
  "lang": "en",
  "other-cookie": "other-value"
}

Предварительное условие 2. Токен инъекции Angular REQUEST

Теперь, когда у нас есть удобный способ доступа к файлам cookie из объекта запроса, нам нужен доступ к объекту req в контексте приложения Angular. Это легко сделать с помощью жетона внедрения REQUEST.

Вот очевидный факт: токен внедрения REQUEST доступен под @nguniversal/express-engine/tokens. Вот не столь очевидный факт: тип объекта req - это Request, предоставленный определениями типов библиотеки express.

Это важно и может сбить нас с толку. Если об этом импорте забыть, машинописный текст примет другой Request тип из API Fetch, доступного в lib.dom.d.ts. В результате TypeScript не будет знать об объекте req.cookies и подчеркнет его красным цветом.

Теперь мы готовы к решениям

Сделайте мысленный снимок ЧАСТИ 2 ниже. Мы будем использовать этот код в качестве отправной точки для следующих двух частей этой серии, где мы исследуем, как исправить две ПРОБЛЕМЫ, описанные ранее.

ЧАСТЬ 2 Контрольно-пропускной пункт

*** Код до этого момента доступен здесь.

👌 Часть 3 из 6. Решение 1. Исправить, предоставив отдельный модуль I18nModule для сервера

В настоящее время наше приложение выглядит так:

На диаграмме выше показан путь выполнения кода, когда код выполняется в браузере (зеленый) и когда он выполняется на сервере (серый). Обратите внимание, что в пути на стороне клиента файл, который загружает все приложение (main.ts), напрямую импортирует AppModule. По пути на стороне сервера основной файл импортирует отдельный модуль AppServerModule, который, в свою очередь, импортирует AppModule. Также обратите внимание, что I18nModule является зависимостью от AppModule, что означает, что код I18nModule будет выполняться как на клиенте, так и на сервере.

В приведенном ниже решении мы сделаем часть браузера более похожей на серверную. Мы представим новый модуль - AppBrowserModule. Это будет модуль, который будет загружен. Мы также переименуем I18nModule в I18nBrowserModule и переместим его в список импорта AppBrowserModule. Наконец, мы представим новый модуль I18nServerModule, который будет использовать доступ к файловой системе для загрузки файлов JSON. Этот модуль будет импортирован в AppServerModule. См. Получившуюся структуру ниже:

Ниже приведен код нового I18nServerModule.

В приведенном выше коде происходят две основные вещи.

Во-первых, мы используем токен внедрения REQUEST, предоставленный Angular, чтобы получить полный объект запроса. Мы используем токен для доступа к объекту cookie, чтобы узнать, какой язык пользователь выбрал в браузере. Зная язык, мы вызываем метод use класса TranslateService, чтобы наш веб-сайт отображался на этом языке.

Во-вторых, действие, указанное выше, запустит наш настраиваемый механизм загрузки, определенный в классе TranslateFsLoader. В классе мы просто используем стандартный API узла для чтения файлов из файловой системы (fs).

Резюме решения 1

Это решение полностью отделяет путь компиляции для сервера от пути компиляции для браузера. ПРОБЛЕМА 1 решена из-за того, что translate.getBrowserLang() существует только в I18nBrowserModule, который никогда не будет работать в серверной среде.

ПРОБЛЕМА 2 аналогичным образом решается каждым модулем I18n - серверным и клиентским - с использованием собственного механизма загрузчика переводов - TranslateFsLoader и TranslateHttpLoader соответственно.

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

Однако есть еще один подход к решению этой задачи. Продолжай читать!

*** Код до этого момента доступен здесь.

👌 Часть 4 из 6. Решение 2. Обеспечьте все в одном модуле

Теперь, когда мы рассмотрели решение 1, давайте рассмотрим другой способ. В отличие от Решения 1, это решение не требует создания новых модулей. Вместо этого весь код будет помещен в I18nModule. Этого можно добиться с помощью функции isPlatformBrowser, предоставляемой фреймворком Angular.

Вернемся к ЧАСТИ 2 КПП.

git checkout step-2

Теперь мы сообщим I18nModule о платформе, на которой он работает, и используем соответствующий загрузчик в зависимости от среды - либо TranslateFsLoader, созданный в предыдущей части, либо TranslateHttpLoader, предоставленный библиотекой ngx-translate.

Добавьте PLATFORM_ID к характеристикам translateLoaderFactory. Это позволит нам выбрать загрузчик на заводе в зависимости от текущей платформы.

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

Если мы попытаемся собрать приложение сейчас, мы получим ошибку.

Module not found: Error: Can't resolve 'fs' in 'C:\ssr-with-i18n\src\app\i18n'
Module not found: Error: Can't resolve 'path' in 'C:\ssr-with-i18n\src\app\i18n'

Это потому, что зависимости fs и path, которые являются строго зависимостями узлов, теперь упоминаются в файле, скомпилированном для клиентской среды.

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

Эту проблему также можно легко исправить. Мы можем сообщить нашему компилятору, что не следует включать эти зависимости в пакет на стороне клиента, используя новое поле браузер в файле package.json.

Добавьте следующее в файл package.json.

"browser": {
  "fs": false,
  "path": false
}

Теперь все будет скомпилировано и запущено точно так же, как и в предыдущем решении.

Резюме решения 2

И ПРОБЛЕМА 1, и ПРОБЛЕМА 2 решаются путем отделения специфичного для браузера кода от специфичного для сервера кода с помощью оператора if, который оценивает текущую платформу:

isPlatformBrowser(this.platform)

Теперь, когда для обеих платформ существует только один путь компиляции, зависимости fs и path, которые являются строго зависимостями узлов, вызывают ошибку времени компиляции, когда процесс сборки компилирует пакет браузера. Это решается путем указания этих зависимостей в поле browser файла package.json и установки их значений на false.

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

Обновление: я только что узнал, что проверки платформы считаются антипаттерном, потому что «платформа» в целом - эфемерное понятие. Хотя это решение может показаться более простым и работает, решение 1 предпочтительнее.

*** Код до этого момента доступен здесь.

⚡ Часть 5 из 6. Повышение производительности с помощью TransferState

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

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

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

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

Обзор рабочего процесса

Чтобы использовать функцию TransferState, нам необходимо:

1. Добавьте модуль, предоставленный Angular для сервера и для клиента: ServerTransferStateModule и BrowserTransferStateModule

2. На сервере: установите данные, которые мы хотим передать под определенным ключом, используя API: transferState.set(key, value)

3. На клиенте: получить данные с помощью API: transferState.get(key, defaultValue)

Наша реализация

Во-первых, давайте добавим модули TransferState к импорту:

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

Во-вторых, translateLoaderFactory теперь будет выглядеть так:

TranslateFsLoader теперь будет использовать TransferState:

Как именно он передает состояние? Во время рендеринга на стороне сервера фреймворк будет включать данные в HTML-тег <script>. Смотрите это на изображении ниже.

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

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

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

Обновите translateLoaderFactory, чтобы использовать новый загрузчик:

Сводка TransferState

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

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

*** Код до этого момента доступен здесь.

🤷‍ Часть 6 из 6: Приехали ли мы?

Независимо от того, выбрали ли вы решение 1 или 2, похоже, что теперь все работает! Давайте закроем Инструменты разработчика браузера и насладимся чувством выполненного долга после долгой работы.

Чтобы убедиться, что все работает правильно, давайте обновим наши файлы JSON и добавим «!!!» в конце всех строк перевода, чтобы отпраздновать. Скомпилируйте и запустите приложение. Обновите страницу и… почешите затылок. Знак «!!!» там нет. Что случилось?

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

Но как браузер узнает, что нужно каждый раз загружать свежие файлы JavaScript и CSS? Это потому, что скрипты сборки Angular добавляют уникальный набор символов (хэш) к имени файла.

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

К счастью, реализация проста. Создадим папку /scripts и новый файл в ней: hash-translations.js.

Чтобы этот скрипт работал, нам нужно установить новую зависимость.

npm install md5 -D

Приведенный выше файл сценария определяет две важные переменные: исходный каталог и целевой каталог.

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

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

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

{
  "en": "[hash-for-file-1]",
  "ru": "[hash-for-file-2]"
}

Добавьте запись в package.json файл под полем сценариев, которая позволит нам выполнить созданный файл.

Также обновите сценарии start и build:ssr, чтобы запустить этот новый сценарий:

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

src/assets/i18n/autogen/*

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

Путь к файлу, который нам нужно загрузить, выглядит так: ./assets/i18n/autogen/${lang}.${hash}.json. Часть ./assets/i18n/autogen/ - это prefix. .${hash}.json - это suffix. Обе эти переменные необходимо настроить, чтобы можно было использовать автоматически сгенерированные файлы.

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

suffix должен обрабатываться в методе getTranslation каждого загрузчика, поскольку нам нужен доступ к переменной lang.

Во-первых, нам нужно получить автоматически сгенерированный файл map.json.

const i18nMap = require('../../assets/i18n/autogen/map.json');

Мы используем синтаксис require, потому что этот файл может быть доступен только во время компиляции.

В TranslateBrowserLoader будут внесены следующие изменения:

Для TranslateFsLoader это изменение кода в одну строку.

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

*** Окончательный код доступен здесь.

👏 Резюме статьи

В этой статье мы создали поддерживаемое решение для управления строками перевода приложений через отдельные файлы JSON. Мы использовали популярную библиотеку - ngx-translate. Мы также рассмотрели текущие решения для интеграции этой функции с приложениями с рендерингом на стороне сервера, предоставленными сообществом. Мы говорили о слабых сторонах этих решений и предлагали лучшие варианты. Наконец, мы реализовали несколько расширенных функций, таких как: (1) запоминание выбранного языка с помощью файлов cookie, (2) использование State Transfer, чтобы избежать ненужных HTTP-запросов к серверу, и (3) нарушение кеширования файлов перевода. .

Особая благодарность Ане Бока и Алексу Башмакову за рецензирование, тестирование и предоставление части кода для этой статьи.

Призыв к действию

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