ЗАКРУТКА УГЛОВАЯ ЛОКАЛИЗАЦИЯ
В последних 8 эпизодах этой длинной статьи, чтобы изменить и заменить пакет i18n Angular, мы сделали следующее:
- Мы воссоздали функцию перевода с помощью
pipe
и добавили языковые скрипты, загружаемые извне, чтобы иметь возможность использовать единую сборку для всех языков. Мы нашли лучший способ сделать множественную функциональность и настроить локали, предоставленные Angular, до определенного безопасного предела (точнее, мы добавили валютуWoolong
) - Мы создали сервер ExpressJS для обслуживания одной сборки на разных языках, управляемой URL:
/en/route
или управляемой файлом cookie, сохраненным в браузере/route
. - Мы добавили несколько гаек и болтов, чтобы он работал в SSR.
- Мы погрузились в создание различных
index.html
во время выполнения с помощью Express Template Engines и во время сборки с помощью компоновщиков Angular или задач Gulp. - Мы добавили элементы пользовательского интерфейса для переключения языка по URL-адресу или сохранению файла cookie, мы извлекли подвижные настраиваемые части в свой собственный файл
config
. - Мы тестировали облачные хосты с немного более ограниченной средой, чем Express, в основном с Netlify, Firebase и Surge.
Что хорошо в этом решении:
- Одна сборка, обслуживайте все, будь то URL-адрес или файл cookie, SSR или клиент, экспресс-хостинг или облачный хостинг.
- Языковые скрипты являются внешними файлами, ими можно управлять отдельно
- Мы по-прежнему использовали готовые библиотеки для локалей.
- Я могу быть предвзятым здесь, но я не думаю, что это так сложно, как классическое решение, как вы думаете?
Извлечь задачу
Последним этапом этой миссии является извлечение ключей перевода в cr-lang
скрипты. Это делается с помощью Angular Builder или Gulp. Поскольку мы запускаем задачу локально перед сборкой, пакеты Gulp не нужно коммитить в git нашего проекта. Это лучше при работе с удаленными конвейерами, где хост устанавливает пакеты npm
; потому что пакеты Gulp не поддерживаются должным образом.
Задача должна сделать следующее:
- Сканировать файлы
.html
и.ts
в исходной папке (где находятся компоненты) - Найдите закономерности
"something" | translate:"code"
- Создайте ключ:
"code": "something"
готовый для размещения в скриптах языка - Игнорировать уже существующие ключи: это на шаг впереди задачи
i18n extract
Angular, которая регенерирует весь файлxlf
, оставляя нам возможность объединить его с уже переведенным текстом. - Будьте проще, не создавайте клавиши Count и Select, чаще всего мы уже создали их во время разработки
- Если языковой файл не существует, сначала скопируйте из языка по умолчанию, сценарий языка по умолчанию выбирается таким, чтобы он отличался от стандартного
en
, который имеет встроенный код сценария.
Во-первых, давайте создадим правильные заменяющие теги комментариев в нужном месте в наших скриптах, начнем с нашего языка по умолчанию: /locale/en.js
, также давайте переместим любые ссылки на локали и языки на свои собственные const
// ... // locales/en.js or ar.js // let's move language references to a key at the top const _LocaleId = 'ar-JO'; const _Language = 'ar';
// ... const keys { NorRes: '', SomethingDone: '', // always have a comma at the end // place those two lines for Gulp and Angular Builder, at the end of the keys // inject:translations // endinject }
В Angular Builder мы создадим новую задачу: /extract/index.ts
и установим glob
, чтобы помочь нам собрать целевые файлы:
Это выглядит ужасно на Medium, проверьте исходный пост или StackBlitz для лучшей читабельности.
// we will use glob from npmjs/glob to find our files easier import glob from 'glob';
// languages, have name "ar" and localeId: "ar-JO", and isDefault to use script for new languages interface ILanguage { name: string, localeId: string, isDefault?: boolean; }
interface IOptions { // the source location to look for components scan: string; // the locales folder for scripts destination: string; // supported languages languages: ILanguage[]; // optional, if not provided, taken from other targets, for prefix-language file name prefix: string; }
// very generic regex: "words" | translate:"code" const _translateReg = /\s*["']([\w\d?.,!\s\(\)]+)["']\s*\|\s*translate:['"]([\w]+)['"]\s*/gim;
// I could have more distinctive patterns for select and plural, but I don't wish to
export default createBuilder(ExtractKeys);
// read script content, if not existent, copy isDefault language file const getScriptContent = (options: IOptions, prefix: string, lang: ILanguage): string => {
// read file /destination/prefix-lang.js const fileName = `${options.destination}/${prefix}-${lang.name}.js`;
let content = '';
// if does not exist, create it, copy the default language content if (!existsSync(fileName)) { const defaultLanguage = options.languages.find(x => x.isDefault); const defaultFileName = `${options.destination}/${prefix}-${defaultLanguage.name}.js`; const defaultContent = readFileSync(defaultFileName, 'utf8');
// replace language keys // example replace 'ar-JO' with 'fr-CA; This is why it is important to separate those // keys in the language script content = defaultContent .replace(`'${defaultLanguage.localeId}'`, `'${lang.localeId}'`) .replace(`'${defaultLanguage.name}'`, `'${lang.name}'`);
writeFileSync(fileName, content); } else { content = readFileSync(fileName, 'utf8'); }
return content;
}; // extract translation terms from all ts and html files under certain folder const extractFunction = (options: IOptions, prefix: string, lang: ILanguage) => { // per language const fileName = `${options.destination}/${prefix}-${lang.name}.js`;
// read content const script = getScriptContent(options, prefix, lang);
// get all ts and html files const files = glob.sync(options.scan + '/**/*.@(ts|html)'); // read files, for each, extract translation regex, add key if it does not exist let _keys: string = ''; files.forEach(file => { const content = readFileSync(file, 'utf8'); let _match; while ((_match = _translateReg.exec(content))) { // extract first and second match const key = _match[2]; // if already found skip, also check destination script if it has the key if (_keys.indexOf(key + ':') < 0 && script.indexOf(key + ':') < 0) { _keys += `${key}: '${_match[1]}',\n`; } } }); // write and save, keep the comment for future extraction _keys += '// inject:translations'; writeFileSync(fileName, script.replace('// inject:translations', _keys)); }; async function ExtractKeys( options: IOptions, context: BuilderContext, ): Promise<BuilderOutput> {
// read prefix from angular.json metadata const { prefix } = await context.getProjectMetadata(context.target.project);
try { options.languages.forEach(lang => { extractFunction(options, options.prefix || prefix.toString(), lang); }); } catch (err) { context.logger.error('Failed to extract.'); return { success: false, error: err.message, }; } context.reportStatus('Done.');
return { success: true }; }
Добавьте новую задачу и схемы в файл builders.json
{
"builders": {
// ... add new extract builder
"extract": {
"implementation": "./dist/extract/index.js",
"schema": "./extract/schema.json",
"description": "Extract translation terms"
}
}
}
В angular.json
создайте новую цель для задачи извлечения.
// in angular.json add the prefix in project metadata, or pass prefix to extract options
"prefix": "cr",
"architect": {
// new task for extractions, see builder/extract
"extract": {
"builder": "./builder:extract",
"options": {
"destination": "./src/locale",
"scan": "./src/app/components",
// if different that meta data, you can pass prefix override here
"prefix": "cr",
"languages": [
{
"name": "en",
"localeId": "en"
},
{
"name": "ar",
"localeId": "ar-JO",
// copy from default file that has the injected script
"isDefault": true
},
{
"name": "fr",
"localeId": "fr-CA"
}
]
}
},
// ...
Соберите, затем запустите ng run cr:extract
. Это генерирует правильные оставшиеся ключи и создает отсутствующие файлы, если это необходимо. Найдите код компоновщика в папке StackBlitz /builder/extract
.
В Gulp мы создаем недостающие файлы в простой последовательности:
gulp.src
gulp.transform
gulp.rename
gulp.dest
Затем я использовал библиотеку gulp-inject
для ввода ключей, которая довольно устарела, но в остальном она великолепна. Затем просто gulp.series
собрать их вместе. Найдите окончательный код в StackBlitz gulp/extract
папка.
Улучшения задач
Я могу делать это бесконечно, возвращаясь, чтобы исправить или улучшить несколько строк. Но я не буду утолять этот зуд.
Тем не менее, найдите под StackBlitz builder/locales/index.enhanced.ts
и под gulp/gulpfile.js
пару улучшений:
- Комбинированные генераторы индексных файлов в одной конфигурации, которые создают только один сценарий вместо обоих (на основе URL или файлов cookie,
index.[lang].html
или[lang]/index.html
) - использовал
getProjectMetadata
в Angular Builder, чтобы получить префикс проекта, чтобы не повторяться - Я также разделил параметры в Gulp на
gulpfile.js
для лучшего контроля.
Деталь
Одной из деталей, которых я избегал, было использование полных имен fr-CA
вместо двух ключей: fr
для языка и fr-CA
для локали. Я намеренно разделил их, потому что, на мой взгляд, французский — это французский для всех, кто на нем говорит, и выбор правильного языка — это деловое решение, которым мы не должны беспокоить наших пользователей. Приложение должно знать, является ли пользователь из Нигерии или из Канады. Однако разница в результатах не так уж велика. Индексные файлы будут называться index.fr-CA.html
, все наши перенаправления будут иметь fr-CA
вместо простого fr
, и наши схемы будут отражать это. Однако язык отображения должен быть конкретным, в файле конфигурации это будет примерно так:
languages: [
{name: 'en', display: 'English'},
{name: 'fr-CA', display: 'Canadian French'},
{name: 'fr-NG', display: 'Nigerian French'},
]
Но я пропускаю, так как для арабского языка довольно раздражает просить пользователя выбрать версию арабского языка для отображения.
Я уверен, что вы найдете другие улучшения на протяжении всего проекта, я сам не мог не вернуться к старым эпизодам для большего количества улучшений. Вы можете думать о любом? Поделитесь ими со мной, пожалуйста.
Спасибо, что дочитали до этого места, не нашли ли вы какую-либо часть слишком сложной? Стоило ли это усилий? Вы узнали, что такое вулонги? 🙂
Замена i18n
- Альтернативный способ локализации в Angular
- Обслуживание многоязычного приложения Angular с помощью ExpressJS
- Обслуживание одной и той же сборки Angular с разными URL
- Обслуживание разных index.html в сборке Angular для разных языков
- Валюта Angular pipe, переключение языка пользовательского интерфейса и вердикт
- Предварительное создание нескольких индексных файлов с использованием Angular Builders и задач Gulp для обслуживания многоязычного приложения Angular
- Использование токена Angular APP_BASE_HREF для обслуживания многоязычных приложений и размещение на Netlify
- Многоязычное приложение Angular, размещенное на Firebase и Surge с одинаковой сборкой