ЗАКРУТКА УГЛОВАЯ ЛОКАЛИЗАЦИЯ
Текущие пакеты i18n, предоставляемые Angular для этой цели, хорошо подходят для этой цели. В этой серии статей я хочу создать альтернативное решение для небольших приложений.
- Альтернативный способ локализации в Angular
- Обслуживание многоязычного приложения Angular с помощью ExpressJS
- Обслуживание одной и той же сборки Angular с разными URL
Ранее мы заканчивали тем, что считали, что перезагрузка сайта для смены языка лучше, чем изменение на стороне клиента. Есть несколько проблем с отсутствием обновления:
- Проблема взаимодействия с пользователем: если кнопка смены языка изменяет только текст, отображаемый на экране, изменение может быть слишком тонким, чтобы его можно было заметить. Это можно исправить, создав иллюзию перезагрузки.
- Другие ресурсы: иногда изменение языка влечет за собой изменение направления, стилей и шрифтов. Например, с языка LTR на язык RTL. Или с латинского базового алфавита на русский алфавит. Изменение шрифтов означает загрузку нового шрифта поверх существующего. Смена стилей настолько непредсказуема, что может мигать экран без стилей в течение нескольких миллисекунд.
Этого может быть недостаточно, чтобы убедить вас во всех случаях, что языковая перезагрузка приложения стоит дешево, учитывая, что пользователи не часто будут это делать. Сегодня мы собираемся адаптировать наш сервер для обслуживания различных таблиц стилей в зависимости от языка.
Исходный код находится на StackBlitz
RTL против LTR
При создании локализованных версий для языков с письмом справа налево необходимо внести еще пару изменений в index.html
, и, поскольку теперь у нас есть HTML-движки, мы можем заменить не только base href
.
Замена стилей
Таблица стилей по умолчанию внедряется в Angular. Чтобы сервер выбрал другую таблицу стилей, мы должны сначала отключить автоматическое внедрение таблицы стилей.
// angular.json remove injection
{
"projects": {
"cr": {
"architect": {
"build": {
"options": {
// ...
// bundle up styles and styles.rtl
"styles": [
{
"input": "src/assets/css/styles.css",
"bundleName": "styles.ltr",
"inject": false
},
{
"input": "src/assets/css/styles.rtl.css",
"bundleName": "styles.rtl",
"inject": false
}
],
// ...
},
"configurations": {
"production": {
"optimization": {
"scripts": true,
// lets also remove auto inlining of fonts and critical css
"fonts": false,
"styles": {
"inlineCritical": false,
"minify": true
}
},
// ...
}
Конфигурация Оптимизация для стилей позволяет inlineCritical
. Который будет встраивать два включенных css
, это не то, что нам сейчас нужно, поэтому мы отключаем его.
В приложении, не основанном на URL:
На нашем экспресс-сервере следующая строка в экспрессе должна быть выше статического модуля экспресс:
app.get('/styles.css', (req, res) => {
// reroute to either styles.ltr or styles.rtl
if (res.locals.lang === 'ar') {
res.sendFile(config.rootPath + 'client/styles.rtl.css');
} else {
res.sendFile(config.rootPath + 'client/styles.ltr.css');
}
});
В приложении на основе URL:
// route styles under URL specific language
app.get('/:lang/styles.css', (req, res) => {
if (res.locals.lang === 'ar') {
res.sendFile(config.rootPath + 'client/styles.rtl.css');
} else {
res.sendFile(config.rootPath + 'client/styles.ltr.css');
}
});
index.html
будет иметь прочную ссылку:
<link rel="stylesheet" href="styles.css">
Что касается шрифтов, у нас есть два варианта, горько-сладкий. Если мы используем шрифты Google, рекомендуемые фрагменты кода:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Signika:wght@300..700&display=swap" rel="stylesheet">
1. Импортируйте URL-адрес шрифта непосредственно в основные стили.
Если мы импортируем шрифты непосредственно в стили, внешняя таблица стилей будет ждать, пока таблица стилей не будет готова, что сделает сам шрифт менее доступным по времени. Так что давайте сделаем альтернативу и импортируем стили в свою собственную таблицу стилей.
2. Импортировать в отдельную таблицу стилей (fonts.css)
Этот вариант немного лучше, так как файл шрифтов будет иметь только один импорт, и загрузка шрифта начнется, как только загрузится таблица стилей и будет запрошен шрифт.
Давайте создадим новый файл fonts.[dir].css
, добавим в angular.json
и также напишем экспресс-путь для fonts.css
.
// angular.json
"options": {
// ...
// bundle up fonts and fonts.rtl
"styles": [
// ...
{
"input": "src/assets/css/fonts.css",
"bundleName": "fonts.ltr",
"inject": false
},
{
"input": "src/assets/css/fonts.rtl.css",
"bundleName": "fonts.rtl",
"inject": false
}
],
Добавьте его как есть в index.html
:
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<!-- Add the fonts as well well --> <link rel="stylesheet" href="fonts.css"> <link rel="stylesheet" href="styles.css">
fonts.ltr
и fonts.rtl
, которые вы создадите в процессе разработки и включите в сборку, имеют только один оператор импорта:
/* import the right google font for each language */
@import url('https://fonts.googleapis.com/css2?family=Signika:wght@300..700&display=swap');
Наконец, экспресс-пути аналогичны путям стилей:
// in non-url based (routes.js) app.get('/fonts.css', (req, res) => { if (res.locals.lang === 'ar') { res.sendFile(config.rootPath + 'client/fonts.rtl.css'); } else { res.sendFile(config.rootPath + 'client/fonts.ltr.css'); } });
// in url-based (routes-url.js) app.get('/:lang/fonts.css', (req, res) => { if (res.locals.lang === 'ar') { res.sendFile(config.rootPath + 'client/fonts.rtl.css'); } else { res.sendFile(config.rootPath + 'client/fonts.ltr.css'); } });
Что касается SSR, все то же самое, но есть небольшая проблема, которую мы должны исправить в первую очередь.
Любопытный случай использования inlineCriticalCSS в Angular SSR:
При реализации вышеуказанного и запуске для ssr, который отображается через ngExpressEngine
, процесс выглядит следующим образом:
- двигатель считывает код
index.html
- находит ссылку
styles.css
- открывает его
- и встраивает критический CSS (используя плагин Critters).
Это отличный вариант на стороне сервера, но styles.css
не существует (и не должен). Так что приходится обходиться без него. Единственная проблема — это неприятное сообщение 404, которое появляется в журналах сервера.
Чтобы исправить это, немного углубившись в скомпилированные файлы, встроенный критический css на сервере является недокументированной опцией для ngExpressEngine
!
res.render('', {
req,
res,
document: rendered,
// add this line to skip css file processing
inlineCriticalCss: false
});
Прежде чем мы примем эту недокументированную (поэтому ненадежную) функцию и откажемся от приятной функции встраивания критического css на сервер, давайте перейдем к другому варианту.
Регенерируйте index.html
с помощью нашего собственного механизма шаблонов.
Производительность привязки таблицы стилей шрифтов из HTML выше, чем у двух вышеперечисленных вариантов. Но для этого нам нужно регенерировать сам index.html
.
В моем небольшом приложении, тестируемом с медленной сетью, я могу только согласиться с этим утверждением, потому что я не увидел реальной разницы в производительности.
Есть несколько хороших движков HTML, таких как PUG, handlebars и mustache, но они слишком сильны для наших нужд. Давайте создадим URL двух шрифтов. и так как мы на этом, давайте сделаем то же самое для стилей.
Мы начнем с этого в index.html
<!-- in production index.html, add all different styles and fonts, and language specific --> <html lang="$lang">
<!-- #LTR --> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Signika:wght@300..700&display=swap"> <link rel="stylesheet" href="styles.ltr.css"> <!-- #ENDLTR --> <!-- #RTL --> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tajawal:wght@300;500&display=swap"> <link rel="stylesheet" href="styles.rtl.css"> <!-- #ENDRTL -->
And in the body we can also specify two loading messages, hey, why not? <app-root> <!-- #LTR --> loading <!-- #ENDLTR --> <!-- #RTL --> انتظر <!-- #ENDRTL --> </app-root>
Помните: вся наша обработка находится в производственном индексе.
Вернемся к нашим экспресс-движкам HTML. Давайте создадим функцию для обработки index.html
.
Для простоты, поскольку экспресс-сервер выходит из-под контроля, нам потребуется файл config
вместо его передачи.
// process the html, remove base href, and styles and fonts // create the regular expressions to look for in index // we can have as many languages as we need
const fs = require('fs'); const config = require('./config');
const reLTR = /<!-- #LTR -->([\s\S]*?)<!-- #ENDLTR -->/gim; const reRTL = /<!-- #RTL -->([\s\S]*?)<!-- #ENDRTL -->/gim;
const process = function (html, lang) { let contents = html.toString(); // if config is with URL, change the base href if (config.urlBased) { contents = contents.replace('<base href="/">', `<base href="/${lang}/">`); } // if lang is ar, remove ltr and keep rtl if (lang === 'ar') { contents = contents.replace(reLTR, ''); } else { contents = contents.replace(reRTL, ''); } return contents; };
В наших маршрутах мы можем использовать эту функцию всякий раз, когда нам это нужно. Найдите функции рендерера, сгруппированные в разделе host/server/renderer.js. Чтобы использовать экспортированные функции, мы используем такие строки:
renderer.htmlEngine(app);
renderer.htmlRender(res);
renderer.ngEngine(req, res);
Назад к оптимизации стилей и шрифтов
Помните, в начале этой статьи нам нужно было отключить оптимизацию стилей и шрифтов Angular, чтобы управлять ими через экспресс-маршруты. Теперь, когда мы регенерируем индекс, давайте добавим их обратно. Вывод после сборки по-прежнему содержит наши теги, и регенерация с помощью нашего средства визуализации по-прежнему работает, как и ожидалось.
The fonts and the styles are optimized, our comment tags are preserved
<!-- #LTR -->
<style type="text/css">@font-face{font-family:'Signika';font-style:normal...}</style>
<style>:root{...}</style><link rel="stylesheet" href="styles.ltr.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.ltr.css"></noscript>
<!-- #ENDLTR -->
<!-- #RTL -->
<style type="text/css">@font-face{font-family:'Tajawal';font-style:normal;...}</style>
<style>:root{...}</style><link rel="stylesheet" href="styles.rtl.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.rtl.css"></noscript>
<!-- #ENDRTL -->
Атрибут языка
Одним из хороших преимуществ регенерацииindex.html
в целом является то, что теперь мы можем производить еще больше замен. Давайте также изменим атрибут языка HTML. В функции процесса:
const reLang = /\$lang/gim;
const process = function(html, lang) { // ... replace $lang when found contents = contents.replace(reLang, lang);
// ... }
А в нашем index.html
:
<html lang="$lang">
Строить, обслуживать… работать. Идем дальше.
Улучшения и корректировки
Как мы выяснили ранее, символы валюты локали являются частью встроенной библиотеки, и если эта конкретная валюта не указана, происходит откат к ее коду. Есть ли способ настроить это поведение? Это, в дополнение к созданию пользовательского интерфейса для переключения языка, будет в следующем эпизоде. 😴
Спасибо, что дочитали до этого места, я знаю, что это сбивает с толку, дайте мне знать, если у вас есть вопросы.
- Альтернативный способ локализации в Angular
- Обслуживание многоязычного приложения Angular с помощью ExpressJS
- Обслуживание одной и той же сборки Angular с разными URL
- Обслуживание разных index.html в сборке Angular для разных языков