Ранее мы закончили на том, что перезагрузка сайта для изменения языка лучше, чем изменение на стороне клиента. Есть несколько проблем, связанных с отказом от перезагрузки:
- Проблема пользовательского опыта: Если кнопка смены языка изменяет только текст, отображаемый на экране, изменение может быть слишком тонким, чтобы его заметить. Это можно исправить, создав иллюзию перезагрузки.
- Другие ресурсы: иногда изменение языка влечет за собой изменение направления, стилей и шрифтов. Например, с языка LTR на язык RTL. Или с латинского базового алфавита на русский. Изменение шрифтов означает загрузку нового шрифта поверх существующего. Изменение стилей настолько непредсказуемо, что на несколько миллисекунд может появиться экран без стилей.
Этого может быть недостаточно, чтобы убедить вас в том, что во всех случаях перезагрузка приложения с помощью языка обходится дешево, учитывая, что пользователи не будут часто это делать. Сегодня мы собираемся адаптировать наш сервер для обслуживания различных таблиц стилей в зависимости от языка.
Код находится на StackBlitz
- RTL против LTR
- Замена стилей
- 1. Импортировать URL-адрес шрифта непосредственно в основные стили.
- 2. Импорт в отдельную таблицу стилей (fonts.css)
- Любопытный случай с inlineCriticalCSS в Angular SSR:
- Регенерация index.html собственным шаблонизатором
- Вернемся к оптимизации стилей и шрифтов
- Атрибут Lang
- Усовершенствования и корректировки
- РЕСУРСЫ
- Обслуживание разного index.html в сборке Angular для разных языков, Angular — Sekrab Garage
RTL против LTR
При создании локализованных версий для RTL языков, есть еще пара изменений, которые нужно сделать в 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, которое появляется в логах сервера.
Чтобы исправить это, после погружения немного вглубь скомпилированных файлов, inline critical 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 -->([sS]*?)<!-- #ENDLTR -->/gim;
const reRTL = /<!-- #RTL -->([sS]*?)<!-- #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 -->
Атрибут Lang
Одним из преимуществ регенерации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">
Строим, обслуживаем… работаем. Двигаемся дальше.
Усовершенствования и корректировки
Как мы выяснили ранее, символы валюты локали являются частью встроенной библиотеки, и если эта конкретная валюта не предоставлена, происходит откат к ее коду. Есть ли способ настроить это поведение? Это, в дополнение к созданию пользовательского интерфейса для переключения языков, будет в следующем эпизоде. 😴
Спасибо, что дочитали до конца, я знаю, что это было запутанно, дайте мне знать, если у вас есть вопросы.
РЕСУРСЫ
- Плагин Critters
- Проект StackBlitz

Обслуживание разного index.html в сборке Angular для разных языков, Angular — Sekrab Garage
Локализация с помощью Angular
