NPM библиотека для интернационализации и локализации i18n очень популярна, однако за последние годы она сильно располнела. В ней много возможностей для локализации дат, чисел, установки нужных склонений, поддержки RTL языков, загрузки локалей с сервера и кучи еще чего. На сайте i18next она называется уже даже "интернационализационным фреймворком".

в тему

Новый авиалайнер. Входит стюардесса в пассажирский салон:

— Вы находитесь на нашем новом авиалайнере. В носовой части самолета у нас находится кинозал, в хвостовой — зал игровых автоматов, на нижней палубе — бассейн, на верхней — сауна. А теперь, уважаемые господа, пристегните ремни, и мы со всей этой хренотенью попробуем взлететь.

В то же время часто для локализации сайта в большинстве случаев требуются очень простые вещи, занимающие всего пару процентов от всего функционала тяжеловеса i18n.

В частности лично мне обычно нужны:

  1. Нахождение перевода по составному ключу -t("finance.transactions.deposit")

  2. Перевод с параметром - t("hello-message", "Вася")

  3. Массивы для списков или параграфов текста

На примере Vue 3 я покажу как можно избавиться от i18next без потери функционала в данном случае, не только облегчая js бандл, но и сокращая код, при этом с сохранением реактивности (смена языка сайта налету).

Простоту и элегантность нижеописанного рефакторинга обеспечит Vue 3 Composition API, но в целом данная методика должна подойти для любого реактивного фреймворка.

Свой i18n

Вот чистая реализация вышеуказанного функционала в 30 строчек супротив полутора мегабайт без каких-либо зависимостей - https://stackblitz.com/edit/i18n-detox?file=src%2FApp.vue

Проект Vue 3 с i18next

Стандартно в проекте с Composition API подключение и использование i18next происходят примерно следующим образом:

До
// main.js
import { i18n, useI18n } from  "@/app/composables/i18n";
const { initI18n } =  useI18n();

initI18n();
app.use(i18n);


// useI18n.js
import { ref } from  "vue";
import { api } from  "@/services";
import { createI18n } from  "vue-i18n";
const  locales  = [
  {
   code:  "en",
   name:  "English",
   flag:  "england",
  },
  {
   code:  "ru",
   name:  "Pусский",
   flag:  "russian",
  },
];
const locale  =  ref();
export const i18n  =  createI18n({
 I18nScope:  "global",
 globalInjection:  true,
 legacy:  false,
 allowComposition:  true,
 fallbackLocale:  import.meta.env.VITE_I18N_FALLBACK_LOCALE  ||  "en",
 formatFallbackMessages:  true,
});

export  function  useI18n() {
  
 function  initI18n() {
   const  lang  =
   localStorage.getItem(import.meta.env.VITE_APP_NAME  +  "_lang") ??
        (import.meta.env.VITE_DEFAULT_LOCALE  ||  "en");
   loadLanguage(lang);
  }
  
 async  function  loadLanguage(lang) {
   if (i18n.global.locale  !==  lang) {
     locale.value  =  locales.find((l) =>  l.code  ===  lang);
     const  data  =  await  api.utils.downloadLanguage(lang);
     i18n.global.setLocaleMessage(lang, data[lang]);
     i18n.global.locale.value  =  lang;
     localStorage.setItem(import.meta.env.VITE_APP_NAME  +  "_lang", lang);
    }
  }
 return {
   i18n,
   locale,
   locales,
   initI18n,
   loadLanguage,
  };
};


// Использование в компонентах Composition API
import { useI18n } from  "vue-i18n";
const { t } =  useI18n();
t("finance.transactions.deposit"),

// Использование в js файлах
import { i18n } from  "@/app/composables/i18n";
i18n.global.t("finance.transactions.deposit")

Всё, что нужно для избавления от i18next, это задать явно объект messages для хранения всех локалей, и добавить в useI18n() реактивную функцию t(), которая как раз и будет обрабатывать составной ключ, параметр и массив.

После этого можно закомментировать всё использование библиотеки vue-i18n

После
// main.js
import { useI18n } from  "@/app/composables/i18n";
const { initI18n } =  useI18n();

initI18n();
// app.use(i18n);


// useI18n.js
import { ref } from "vue";
import { api } from "@/services";

// import { createI18n } from "vue-i18n";

const locales = [
  {
    code: "en",
    name: "English",
    flag: "england",
  },
  {
    code: "ru",
    name: "Pусский",
    flag: "russian",
  },
];
// export const i18n = createI18n({
//   I18nScope: "global",
//   globalInjection: true,
//   legacy: false,
//   allowComposition: true,
//   fallbackLocale: import.meta.env.VITE_I18N_FALLBACK_LOCALE || "en",
//   formatFallbackMessages: true,
//   // messages: { en: messages }
// });

const locale = ref();
let messages;

// Делаем доступ для использования в js модулях
export const t = useI18n().t;

export function useI18n() {
  function initI18n() {
    messages = [];
    const lang =
      localStorage.getItem(import.meta.env.VITE_APP_NAME + "_lang") ??
      (import.meta.env.VITE_APP_DEFAULT_LOCALE || "en");
    loadLanguage(lang);
  }

  async function loadLanguage(lang) {
    if (locale.value !== lang) {
      const localeMessages = await api.utils.downloadLanguage(lang);
      messages[lang] = localeMessages[lang];
      locale.value = locales.find((l) => l.code === lang);
      // i18n.global.setLocaleMessage(lang, localeMessages[lang]);
      // i18n.global.locale.value = lang;
      localStorage.setItem(import.meta.env.VITE_APP_NAME + "_lang", lang);
    }
  }

  function t(msg, param = null) {
    let val = msg.split(".").reduce((val, part) => val[part], messages[locale.value.code]);
    if (param) {
      val = val.replace("{0}", param);
    }
    return val;
  }

  return {
    t,
    // i18n,
    locale,
    locales,
    initI18n,
    loadLanguage,
  };
}


// Использование в компонентах Composition API
import { useI18n } from  "@/app/composables/i18n";
const { t } =  useI18n();
t("finance.transactions.deposit"),

// ИЛИ

import { t } from  "@/app/composables/i18n";
t("finance.transactions.deposit")

// Использование в js файлах
import { t } from  "@/app/composables/i18n";
t("finance.transactions.deposit")

В данном примере перевод для конкретной локали грузится с сервера по запросу, но объект messages можно иметь на клиенте сразу.

Alias export const t = useI18n().t; позволяет использовать один синтаксис и в компонентах, и в js модулях.

I18next расширения

У I18next есть расширение для `Vue DevTools` (довольно бесполезное), и есть расширение I18next Ally для MS VS Code (весьма полезное). Так вот I18next Ally работает с новой реализацией если в package.json будет прописан пакет vue-i18n в dependencies (в коде подключать его не надо). Рекомендую. Оба расширения, впрочем, неплохо едят ресурсы, так что пользоваться ими лучше по надобности.

Итого

Мы закомментировали больше строк, чем добавили, и JavaScript бандл после билда уменьшился на 50 Кб. Функционал остался. Реактивный.

До (vue 3, vue-router, toaster, vue-i18n)

После (vue 3, vue-router, toaster)

Спасибо, I18next, и до свидания.

Другая моя статья по теме - "Работа с i18n — автоматизация Google Translate и другие полезные советы".

Комментарии (11)


  1. mayorovp
    22.05.2023 04:11
    +2

    Что-то вы вообще всё порезали. Вы же знаете русский язык, и знаете про склонение и про числительные...


    1. gmtd Автор
      22.05.2023 04:11
      -2

      Спасибо, подправил


      1. mayorovp
        22.05.2023 04:11
        +2

        Подправили что? Я говорил про функцию __n из пакета i18n, она у вас выкинута.


        1. gmtd Автор
          22.05.2023 04:11
          +1

          Я же в начале статьи написал, какой функционал будет дублироваться

          1. Нахождение перевода по составному ключу -t("finance.transactions.deposit")

          2. Перевод с параметром - t("hello-message", "Вася")

          3. Массивы для списков или параграфов текста

          Задачи отзеркалить i18n не стояло


          1. mayorovp
            22.05.2023 04:11
            +1

            Ну, собственно, вот именно к этим трём пунктам у меня и претензия.


            Их должно быть четыре, потому что __n нужна.


            1. gmtd Автор
              22.05.2023 04:11

              Если вам нужен функционал i18n, то вам нужна i18n, и решение данной статьи вам не подходит
              По-моему, это очевидно


    1. nin-jin
      22.05.2023 04:11
      +3

      Не заставляйте пользователя вчитываться в тексты. Такой текст:

      Заявка принята: 2023-05-20 23:51

      Менеджер: Алина Авдеева

      Позиция в очереди: 2

      Осталось ждать: 21:33

      К оплате: 5123р

      Услуги: Инвестиционная консультация, Семейная аналитика

      воспринимается быстрее, чем "очеловеченный":

      Ваша заявка поступила вчера. Она направлена менеджеру Алине Авдееве. Вы - вторая в очереди. Вас примут через 21 минуту и 33 секунды. Приготовьте 5123 рубля к оплате за инвестиционную консультацию и семейную аналитику.

      Курсивом отмечены вариативные тексты.


      1. SergeyMax
        22.05.2023 04:11
        +2

        Ваша заявка поступила вчера

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


  1. Cesavel
    22.05.2023 04:11
    +2

    у меня есть vscode расширение, который делает примерно то же самое что то расширение выше, только по hover, тык. не знаю, считается ли это рекламой ????


    1. LyuMih
      22.05.2023 04:11

      Не отработало на nx монорепозиторим (


      1. Cesavel
        22.05.2023 04:11

        Указали в настройках путь к папке с файлами локализации?