Ранее мы вывели файл locales, содержащий все языковые ресурсы, чтобы подготовиться к их изоляции. Сегодня мы сосредоточимся на обслуживании через NodeJs и сервер ExpressJS. Мы будем обслуживать различные языки, используя куки, а позже полагаясь на URL. Но прежде чем мы углубимся в работу, еще одно преимущество нашего класса ресурсов.
- Доступ к ресурсам из любого места
- Файл JavaScript языка
- Пакет локали Angular
- Файлы для конкретного языка
- Приложение только для браузера
- Серверная платформа
- Применение на основе URL
- РЕСУРСЫ
- СООТВЕТСТВУЮЩИЕ ПОСТЫ
- Обслуживание многоязычного приложения Angular с помощью ExpressJS, Angular, Design — Sekrab Garage
Доступ к ресурсам из любого места
Из коробки Angular предоставляет адаптер $localize, но он ограничен использованием i18n. Наш класс res
можно использовать, даже если локализация не задана, а language.ts
используется напрямую. Мы уже использовали его для перехвата ошибок и сообщений тостов. Вот фрагмент того, как его можно свободно использовать:
// using the res class for any linguistic content
// extreme case of a warning when an upload file is too large
const size = Config.Upload.MaximumSize;
this.toast.ShowWarning(
// empty code to fallback
'',
// fallback to a dynamically created message
{ text: Res.Get('FILE_LARGE').replace('$0', size)}
);
// where FILE_LARGE in locale/language is:
// FILE_LARGE: 'The size of the file is larger than the specified limit ($0 KB)'
Примечание: Исходные файлы находятся в StackBlitz, но они не обязательно будут работать в StackBlitz, потому что среда слишком строгая.
Файл JavaScript языка
В предыдущей статье мы рассмотрели основы того, как внедрить внешний файл конфигурации в Angular, и пришли к выводу, что лучший способ — поместить файл javascript в индексный заголовок. На данном этапе у нас нет четкой модели, к которой нам нужно приводить, поэтому давайте начнем с простого тега script в index.html
:
<script src="locale/language.js" defer></script>
Чтобы это работало в разработке, мы добавим актив в angular.json
.
// angular.json options/assets
{
"glob": "*.js",
"input": "src/locale",
"output": "/locale"
}
Чтобы использовать коллекцию ключей JavaScript, мы объявляем в нашем typescript
. Класс res
— единственное место, использующее ключи, а app.module
— единственное место, использующее идентификатор локали. Поэтому давайте поместим все в res class
:
// in res class, we declare the keys and locale_id
declare const keys: any;
declare const EXTERNAL_LOCALE_ID: string;
export class Res {
// export the locale id somehow, a property shall do
public static LOCALE_ID = EXTERNAL_LOCALE_ID;
// using them directly: keys
public static Get(key: string, fallback?: string): string {
if (keys[key]) {
return keys[key];
}
return fallback || keys.NoRes;
}
// ...
}
// in app.module, we import the locale id
// ...
providers: [{provide: LOCALE_ID, useValue: Res.LOCALE_ID }]
Пакет локали Angular
Но как нам импортировать локаль из пакетов Angular? Самый простой и понятный способ — сделать точно так же, как описано выше. Добавьте скрипт и сделайте ссылку в angular.json
. Если мы хотим иметь несколько локалей, то включаем их все в активы:
{
// initially, add them all
"glob": "*.js",
"input": "node_modules/@angular/common/locales/global",
"output": "/locale"
}
Это означает, что файлы локалей копируются на хост при сборке, что идеально, потому что так мы знаем, что у нас всегда последняя версия локали. Один из способов заключается в следующем:
<script src="locale/ar-JO.js" defer></script>
Другой заключается в том, чтобы позволить языковому файлу создать тег. Помните, что этот файл в конечном итоге будет вызван на серверной платформе, поэтому мы хотим быть, по крайней мере, готовыми к этому.
// in browser platform
const script = document.createElement('script');
script.type = 'text/javascript';
script.defer = true;
script.src = 'locale/ar-JO.js';
document.head.appendChild(script);
// in server platform, we'll add this later
// require('./ar-JO.js');
Давайте сделаем один рефактор, прежде чем мы перейдем к обслуживанию файлов. Создадим один JavaScript-ключ и разделим его на пространства имен, чтобы 10x-щики не троллили нас, не то чтобы это имело значение.
// the locales/language.js file
const keys = {
NoRes: '',
// ...
};
// combine and namespace
// window will later be global
window.cr = window.cr || {};
window.cr.resources = {
language: 'en',
keys,
localeId: 'en-US'
};
cr — сокращение от cricket. Кодовое имя нашего проекта.
В нашем классе res
:
// in res class remove imported keys from /locales/language.ts
declare const cr: {
resources: {
keys: any;
language: string;
localeId: string;
};
};
export class Res {
// to use in app.module
public static get LocaleId(): string {
return cr?.resources.localeId;
}
// add a private getter for keys
private static get keys(): any {
return cr?.resources.keys;
}
// use it like this this
public static Get(key: string, fallback?: string): string {
const keys = Res.keys;
// ...
}
// ...
}
Файлы для конкретного языка
Теперь мы создадим два файла в папке locale, готовых к отправке: cr-en и cr-ar. cr-ar
содержит добавленный скрипт локали ar-JO, а cr-en
не содержит ничего особенного. Мы стараемся не конфликтовать с пакетами Angular, поскольку ar.js и en.js уже существуют.
(Упомянутый ниже en-AE приведен только для примера, мы не собираемся его использовать).
Сейчас мы собираем со следующими настройками angular.json
:
"projects": {
"cr": {
"architect": {
"build": {
"options": {
"resourcesOutputPath": "assets/",
"index": "src/index.html",
"assets": [
// ...
// add all locales in dev
{
"glob": "*.js",
"input": "src/locale",
"output": "/locale"
},
{
// add angular packages in dev, be selective
// en-AE is an example
"glob": "*(ar-JO|en-AE).js",
"input": "node_modules/@angular/common/locales/global",
"output": "/locale"
}
]
},
"configurations": {
"production": {
// place in client folder
"outputPath": "./host/client/",
// ...
// overwrite assets
"assets": [
// add only locales needed
// names clash with Angular packages, prefix them
{
"glob": "*(cr-en|cr-ar).js",
"input": "src/locale",
"output": "/locale"
},
{
// add angular packages needed
"glob": "*(ar-JO|en-AE).js",
"input": "node_modules/@angular/common/locales/global",
"output": "/locale"
}
]
}
}
},
// server build
"server": {
"options": {
// place in host server
"outputPath": "./host/server",
"main": "server.ts"
// ...
},
// ...
}
}
}
Давайте построим.
Приложение только для браузера
Начинаем с Angular builder:
ng build --configuration=production
Это генерирует выходной файл host/client. Внутри этой папки у нас есть папка locale, которая содержит все файлы javascript, которые мы включили в assets:
-
/host/client/locale/cr-en.js
-
/host/client/locale/cr-ar.js
-
/host/client/locale/ar-JO.js
.
Индексный файл содержит ссылку на locale/language.js, теперь наша задача — переписать этот URL в нужный языковой файл. Создание нескольких индексных файлов — это, безусловно, самый крайний вариант и лучшее решение. Но сегодня мы просто выполним перезапись с помощью маршрутизации ExpressJS.
В нашем основном файле server.js нам нужно создать промежуточное ПО для определения языка, пока что из cookie. Имя cookie можно легко потерять, поэтому сначала я хочу создать конфигурационный файл, в котором я размещу все мои подвижные части, это личное предпочтение, разработчики бэкенда, вероятно, имеют другое решение.
// server/config.js
const path = require('path');
const rootPath = path.normalize(__dirname + '/../');
module.exports = {
env: process.env.Node_ENV || 'local',
rootPath,
// we'll use this for cookie name
langCookieName: 'cr-lang',
// and this for prefix of the language file
projectPrefix: 'cr-'
};
Языковое промежуточное ПО:
// a middleware to detect language
module.exports = function (config) {
return function (req, res, next) {
// check cookies for language, for html request only
res.locals.lang = req.cookies[config.langCookieName] || 'en';
// exclude non html sources, for now exclude all resources with extension
if (req.path.indexOf('.') > 1) {
next();
return;
}
// set cookie for a year
res.cookie(config.langCookieName, res.locals.lang, {
expires: new Date(Date.now() + 31622444360),
});
next();
};
};
Это промежуточное ПО просто обнаруживает языковой файл cookie, устанавливает его в свойство response locals, а затем сохраняет язык в файлах cookie.
Основной сервер:
const express = require('express');
// get the config
const config = require('./server/config');
// express app
const app = express();
// setup express
require('./server/express')(app);
// language middleware
var language = require('./server/language');
app.use(language(config));
// routes
require('./server/routes')(app, config);
const port = process.env.PORT || 1212;
// listen
app.listen(port, function (err) {
if (err) {
return;
}
});
Маршруты для нашего приложения:
// build routes for browser only solution
const express = require('express');
// multilingual, non url driven, client side only
module.exports = function (app, config) {
// reroute according to lang, don't forget the prefix cr-
app.get('/locale/language.js', function (req, res) {
res.sendFile(config.rootPath +
`client/locale/${config.projectPrefix}${res.locals.lang}.js`
);
// let's move the path to config, this becomes
// res.sendFile(config.getLangPath(res.locals.lang));
});
// open up client folder, including index.html
app.use(express.static(config.rootPath + '/client'));
// serve index file for all other urls
app.get('/*', (req, res) => {
res.sendFile(config.rootPath + `client/index.html`);
});
};
Запустив сервер, я вижу куки, сохраненные в Chrome Dev tools, изменяю их, перезагружаю, все работает как ожидалось.
Давайте перенесем путь к языку в конфигурацию сервера, потому что я буду использовать его позже.
module.exports = {
// ...
getLangPath: function (lang) {
return `${rootPath}client/locale/${this.projectPrefix}${lang}.js`;
}
};
Серверная платформа
Возвращаясь к предыдущей статье: Загрузка внешних конфигураций в Angular Universal, мы изолировали сервер, и я специально упомянул одно из преимуществ — обслуживание многоязычного приложения с помощью одной сборки. Сегодня мы воспользуемся этим. При сборке для SSR, используя:
ng run cr:server:production
Файл, создаваемый в папке host/server, — main.js. Ниже приведены маршруты, сделанные с учетом SSR (в StackBlitz это host/server/routes-ssr.js).
const express = require('express');
// ngExpressEngine from compiled main.js
const ssr = require('./main');
// setup the routes
module.exports = function (app, config) {
// set engine, we called it AppEngine in server.ts
app.engine('html', ssr.AppEngine);
app.set('view engine', 'html');
app.set('views', config.rootPath + 'client');
app.get('/locale/language.js', function (req, res) {
// reroute according to lang
res.sendFile(config.getLangPath(res.locals.lang));
});
// open up client folder
app.use(express.static(config.rootPath + '/client', {index: false}));
app.get('/*', (req, res) => {
// render our index.html
res.render(config.rootPath + `client/index.html`, {
req,
res
});
});
};
Исключите файл index.html
в статическом промежуточном ПО, чтобы заставить корневой URL проходить через движок Angular.
Ранее мы использовали трюк для различения платформ сервера и браузера, чтобы включить один и тот же JavaScript на обеих платформах:
// in javascript, an old trick we used to make use of the same script on both platforms
if (window == null){
exports.cr = cr;
}
Если посмотреть на скрипты Angular Locale, то они обернуты следующим образом:
// a better trick
(function(global) {
global.something = 'something';
})(typeof globalThis !== 'undefined' && globalThis || typeof global !== 'undefined' && global ||
typeof window !== 'undefined' && window);
Это лучше. Почему я не подумал об этом раньше? Ну да ладно. Давайте перепишем наши языковые файлы, чтобы их можно было обернуть вызовом функции:
// locale/language.js (cr-en and cr-ar) make it run on both platforms
(function (global) {
// for other than en
if (window != null) {
// in browser platform
const script = document.createElement('script');
script.type = 'text/javascript';
script.defer = true;
script.src = 'locale/ar-JO.js';
document.head.appendChild(script);
} else {
// in server platform
require('./ar-JO.js');
}
const keys = {
NoRes: '',
// ...
};
global.cr = global.cr || {};
global.cr.resources = {
language: 'ar',
keys,
localeId: 'ar-JO',
};
})(
(typeof globalThis !== 'undefined' && globalThis) ||
(typeof global !== 'undefined' && global) ||
(typeof window !== 'undefined' && window)
);
В языковом промежуточном ПО требуйте файл.
module.exports = function (config) {
return function (req, res, next) {
// ... get cookie
// if ssr is used
require(config.getLangPath(res.locals.lang));
// ... save cookie
};
};
Запуск сервера. Мы сталкиваемся с двумя проблемами:
-
app.module
загружается немедленно, до того, как произойдет какая-либо маршрутизация. Он ищетLOCAL_ID
вglobal.cr.resources
, который еще нигде не был загружен. - Определив значение по умолчанию, локаль не меняется на сервере динамически, так как
app.module
уже запущен с первой локалью.
Чтобы динамически изменить LOCALE_ID на сервере — без перезапуска сервера, погуглил и нашел простой ответ. Реализация useClass
для провайдера в app.module
. Если посмотреть на код, сгенерированный через SSR, то это изменение устранило прямое обращение к LocalId
, и превратило его в оператор void 0.
exports.Res = exports.LocaleId = void 0;
Это повторяющаяся проблема в SSR, всякий раз, когда вы определяете статические элементы корневого уровня. Обратите внимание, что как только приложение гидратируется (превращается в браузерную платформу), это больше не имеет значения, браузерная платформа — это магия!
// in Res class, extend the String class and override its default toString
export class LocaleId extends String {
toString() {
return cr.resources.localeId || 'en-US';
}
}
// and in app.module, useClass instead of useValue
@NgModule({
// ...
providers: [{ provide: LOCALE_ID, useClass: LocaleId }]
})
export class AppModule {}
Это решает первую проблему. Это также частично решает вторую проблему. Новая проблема, с которой мы сейчас сталкиваемся, заключается в следующем:
- NodeJS требует файлы один раз. Если файл потребуется снова, он будет извлечен из кэша, и функция в нем не будет запущена. Таким образом, на серверной платформе переключение языка работает в первый раз, но переключение обратно на ранее загруженный язык не обновляет локаль.
Чтобы исправить это, нам нужно сохранить различные коллекции global.cr
в явных ключах, и в языковом промежуточном ПО назначить наш NodeJS global.cr.resources
нужной коллекции. В наших языковых JavaScript-файлах добавим явное назначение:
// in cr-en cr-ar, etc,
(function (global) {
// ...
// for nodejs, add explicit references
// global.cr[language] = global.cr.resources
global.cr.en = global.cr.resources;
})(typeof globalThis !== 'undefined' && globalThis || typeof global !== 'undefined' && global ||
typeof window !== 'undefined' && window);
В нашем языковом промежуточном ПО каждый раз, когда запрашивается новый язык, он добавляется в глобальную коллекцию. Затем мы извлекаем нужный нам язык:
// language middleware
module.exports = function (config) {
return function (req, res, next) {
// ...
require(config.getLangPath(res.locals.lang));
// reassign global.cr.resources
global.cr.resources = global.cr[res.locals.lang];
// ...
};
};
Запустив сервер, я не получаю никаких ошибок. При просмотре с отключенным JavaScript загружается язык по умолчанию. Изменяя куки в браузере несколько раз, он работает как ожидалось.
Это было не так сложно, не так ли? Давайте перейдем к языку на основе URL.
Применение на основе URL
Для сайтов, основанных на контенте, и публичных веб-сайтов выбор языка по URL имеет решающее значение. Чтобы настроить наш сервер на перехват выбранного языка по URL вместо куки, возвращайтесь на следующей неделе. 😴
Спасибо, что дочитали до конца очередного эпизода. Дайте мне знать, если я поднял бровь.
РЕСУРСЫ
- Динамическое изменение LocaleId в Angular
- Проект StackBlitz
- Angular $localize
- Локализация ответа ExpressJS
СООТВЕТСТВУЮЩИЕ ПОСТЫ
Загрузка внешних конфигураций в Angular Universal
Ловля и отображение ошибок пользовательского интерфейса с помощью тостовых сообщений в Angular

Обслуживание многоязычного приложения Angular с помощью ExpressJS, Angular, Design — Sekrab Garage
Твистинг локализации Angular
