Заключение и последние улучшения

В последних 8 эпизодах этой длинной статьи по доработке и замене пакета i18n в Angular мы достигли следующего:

  • Мы воссоздали функциональность перевода через pipe, добавили внешние языковые скрипты, чтобы иметь возможность использовать единую сборку для всех языков, придумали лучший способ сделать функциональность множественного числа и настроили локали, предоставляемые Angular, до определенного безопасного предела (если быть точным, мы добавили валюту Woolong).
  • Мы создали сервер ExpressJS для обслуживания единой сборки с разными языками, управляемой URL: /en/route, или по cookie, сохраненному через браузер /route.
  • Мы добавили несколько гаек и болтов, чтобы заставить его работать в SSR.
  • Мы погрузились в создание различных index.html во время выполнения с помощью Express Template Engines, и во время сборки с помощью Angular builders, или Gulp задач.
  • Мы добавили элементы пользовательского интерфейса для переключения языка по URL или сохранения cookie, мы извлекли подвижные настраиваемые части в собственный файл config.
  • Мы тестировали на облачных хостингах с немного более ограниченным окружением, чем Express, в основном Netlify, Firebase и Surge.

Что хорошо в этом решении, так это:

  • Одна сборка, обслуживает всех, будь то URL или cookie, SSR или клиент, Express или облачный хостинг.
  • Языковые скрипты — это внешние файлы, ими можно управлять отдельно.
  • Мы все еще используем готовые библиотеки для локалей.
  • Возможно, я необъективен, но мне кажется, что это не так сложно, как классическое решение, а что думаете вы?

Задание на извлечение

Последний бит в этом задании — извлечение ключей перевода в скрипты cr-lang. Это делается с помощью Angular Builder или Gulp. Поскольку мы выполняем задание локально перед сборкой, пакеты Gulp не нужно фиксировать в git нашего проекта. Это лучше при работе с удаленными конвейерами, где хост устанавливает пакеты npm, поскольку пакеты Gulp не очень хорошо поддерживаются.

Задача должна делать следующее:

  • Сканировать файлы .html и .ts в папке с исходным кодом (где находятся компоненты).
  • Найти шаблоны "something" | translate: "code".
  • Создайте ключ: "code": "что-то", готовый для размещения в языковых скриптах.
  • Игнорируйте уже существующие ключи: это шаг вперед задачи Angular i18n extract, которая регенерирует весь файл 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, чтобы помочь нам собрать целевые файлы:

// 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*["']([wd?.,!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 driven, 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'},
]
Войти в полноэкранный режим Выйти из полноэкранного режима

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

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

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

РЕСУРСЫ

  • Gulp
  • gulp-inject
  • Проект StackBlitz
  • Regex для ключей перевода

Оцените статью
devanswers.ru
Добавить комментарий