ЗАКРУТКА УГЛОВАЯ ЛОКАЛИЗАЦИЯ

В последних 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

  1. Альтернативный способ локализации в Angular
  2. Обслуживание многоязычного приложения Angular с помощью ExpressJS
  3. Обслуживание одной и той же сборки Angular с разными URL
  4. Обслуживание разных index.html в сборке Angular для разных языков
  5. Валюта Angular pipe, переключение языка пользовательского интерфейса и вердикт
  6. Предварительное создание нескольких индексных файлов с использованием Angular Builders и задач Gulp для обслуживания многоязычного приложения Angular
  7. Использование токена Angular APP_BASE_HREF для обслуживания многоязычных приложений и размещение на Netlify
  8. Многоязычное приложение Angular, размещенное на Firebase и Surge с одинаковой сборкой

РЕСУРСЫ