Skip to content

Tłumaczenia ​

Panel korzysta z vue-i18n w wersji 11, działającej w trybie Composition API (legacy: false). Kluczowa różnica wobec typowej konfiguracji: język występuje tu w dwóch niezależnych osiach — języka interfejsu i języka treści.

Instancja ​

js
// src/i18n/instance.js
export const i18nInstance = createI18n({
  locale: "pl",
  legacy: false,
  pluralRules: pluralizationRules,
  datetimeFormats,
  messages,
});

export const { global: i18n } = i18nInstance;

Eksportowany obiekt i18n, reprezentujący zakres globalny, służy do tłumaczeń poza komponentami — na przykład w guardach nawigacji i funkcjach pomocniczych:

js
// src/modules/core/helpers/navigation-guards.js
import { i18n } from "src/i18n/instance";

Notify.create({ message: i18n.t("errors.unauthorized", 2), type: "negative" });

Funkcja setQuasarLang(isoCode) doładowuje pakiet językowy Quasara, korzystając leniwie z import.meta.glob, dzięki czemu komunikaty jego komponentów również prezentowane są w odpowiednim języku.

Agregacja tłumaczeń ​

Pliki src/i18n/{en,pl}/index.js zbierają tłumaczenia wszystkich modułów:

js
// src/i18n/pl/index.js
export default {
  "menu-items": menuItems,
  ...core,                       // ← rozpakowany do korzenia
  user,
  auth,
  media,
  "content-builder": contentBuilder,
  "static-text": staticText,
  // ...

  // --place where generator will inject new translations
};

Struktura ta ma dwie właściwości, które łatwo przeoczyć:

  • Moduł core rozpakowywany jest do korzenia. Klucze labels.*, buttons.*, validations.*, table.*, errors.*, dialogs.*, messages.*, placeholders.*, tooltips.* oraz locales.* mają więc zasięg globalny — odwołuje się do nich bez prefiksu modułu.
  • Nazwy modułów zapisywane są w kebab-case. contentBuilder mapowany jest na "content-builder", a staticText na "static-text". Katalog zostaje przy camelCase, klucz i18n używa kebab-case.
js
$t("labels.title")                     // z core, klucz globalny
$t("blog.articles.title.index")        // z modułu blog
$t("content-builder.section-groups.labels.section", 2)

Struktura tłumaczeń modułu ​

js
// src/modules/blog/trans/pl/index.js
import messages from "modules/blog/trans/pl/messages";
import articles from "modules/blog/trans/pl/articles";
import categories from "modules/blog/trans/pl/categories";

export default {
  name: "Blog",
  articles,
  categories,
  messages,
};
js
// src/modules/blog/trans/pl/articles.js
export default {
  title: {
    index: "Artykuły",
    edit: "Edycja artykułu",
    create: "Tworzenie artykułu",
  },
  tooltips: {
    create_slug: "Utwórz slug na podstawie tytułu",
  },
};

Kształt tłumaczeń stanowi kontrakt

Composable useCoreBreadcrumbs odczytuje dokładnie następujące klucze:

  • <moduł>.name — nazwa modułu, pobierana przez t(..., 2), czyli w formie mnogiej,
  • <moduł>.<modele>.title.<akcja> — tytuł ekranu.

Brak klucza title.<akcja> skutkuje wyświetleniem surowego klucza w breadcrumbs i tytule strony. Po dodaniu niestandardowej trasy — na przykład reorder — uzupełnij klucz title.reorder.

Nazwa modułu w formie mnogiej

Generator tworzy wpis name: "Artykuł | Artykuły". Gdy nazwa modułu nie podlega odmianie, jak "Blog", wystarczy forma pojedyncza — t("blog.name", 2) zwróci wtedy ten sam tekst.

Liczba mnoga ​

Reguła polska zdefiniowana jest ręcznie:

js
// src/i18n/pluralization-rules.js
export function polish(choice, _choicesLength, _orgRule) {
  if (choice === 1) return 0;
  if (choice % 10 >= 2 && choice % 10 <= 4 && (choice % 100 < 10 || choice % 100 >= 20)) return 1;
  return 2;
}

Poszczególne formy oddzielane są pionową kreską:

js
user: "Użytkownik | Użytkownicy",
review: "Opinia | Opinie",

O wyborze formy decyduje drugi argument:

js
t("menu-items.user", 1)   // Użytkownik
t("menu-items.user", 2)   // Użytkownicy

Powód stosowania wartości 2 w pozycjach menu

Pozycje menu prowadzą do list rekordów, dlatego konsekwentnie używana jest forma mnoga.

Etykiety menu ​

js
// src/i18n/pl/menu-items.js
export default {
  user: "Użytkownik | Użytkownicy",
  dashboard: "Dashboard",
  slider: "Slider | Slidery",
  "shared-section": "Sekcja współdzielona | Sekcje współdzielone",
  // ...
};

Generator nie dodaje kluczy w tym pliku

Generator plop module wstawia pozycję menu odwołującą się do t("menu-items.<nazwa>", 2), jednak samego klucza nie tworzy. Trzeba dopisać go ręcznie w obu wersjach językowych, co opisują Moduły i generatory.

Język interfejsu a język treści ​

Obie osie są od siebie niezależne:

Język interfejsu (locale)Język treści (contentLocale)
Zakres oddziaływaniaEtykiety, przyciski, komunikaty paneluWersja językowa edytowanych rekordów
Parametr w APIlangcontent_lang
PrzełącznikLocaleSwitcherContentLocaleSwitcher, umieszczony w nagłówku
Persystencjaciasteczko localeciasteczko content_locale

Dzięki temu rozdzieleniu redaktor może pracować w panelu po polsku, edytując jednocześnie angielską wersję artykułu.

Composable useLocale() ​

js
const {
  locale,                     // język interfejsu (zapis i odczyt)
  contentLocale,              // język treści (zapis i odczyt)
  locales,                    // dostępne języki interfejsu
  contentLocales,             // dostępne języki treści
  hasMultipleContentLocales,  // informacja, czy istnieje więcej niż jeden język treści
  setLocale,                  // setLocale(code, reboot = false)
  t, tm, te,                  // funkcje vue-i18n z zakresu globalnego
} = useLocale();

Sprawdzenie istnienia klucza funkcją te()

Wywołanie te(klucz) weryfikuje, czy tłumaczenie istnieje. Korzysta z tego useErrorMessage, aby komunikat pozbawiony tłumaczenia wyświetlić dosłownie, zamiast prezentować surowy klucz.

Przebieg zmiany języka ​

  1. Store core/locale zapisuje ciasteczko oraz wywołuje api.setLocale() i setQuasarLang().
  2. Interceptor axiosa dokłada nowe wartości lang i content_lang do każdego żądania, co opisuje Warstwa API.
  3. Plik App.vue unieważnia cały cache Vue Query.
  4. MasterLayout przemontowuje router-view za pośrednictwem atrybutu :key.

Niezapisane zmiany w formularzu zostają utracone

Krok czwarty przemontowuje stronę — celowo, bo formularz musi wczytać wartości w nowym języku treści. Na ekranach z długimi formularzami uprzedź o tym użytkownika.

Pole translation_active ​

Rekordy podlegające tłumaczeniu mają dwa przełączniki: active dotyczy rekordu globalnie, a translation_active — konkretnej wersji językowej. Przy jednym skonfigurowanym języku treści drugi traci sens, więc synchronizuje go composable:

js
// src/modules/blog/components/BlogArticleForm.vue
useTranslationActiveSync();               // pola na najwyższym poziomie
useTranslationActiveSync("translations"); // z prefiksem ścieżki

Korzysta z niego VStatusToggles, opisany w Formularzach.

Dodanie nowego języka ​

  1. Store — dopisz wpis do tablic locales lub contentLocales w src/modules/core/stores/core.js:

    js
    { code: "en", name: "locales.english", short: "ENG", iso: "en-US" }
  2. Katalog tłumaczeń — utwórz trans/<kod>/ w każdym module oraz pliki src/i18n/<kod>/index.js i src/i18n/<kod>/menu-items.js.

  3. Rejestracja — dopisz język w src/i18n/index.js.

  4. Pakiet Quasara — obejmij kod ISO globem w pliku instance.js:

    js
    const langList = import.meta.glob("../../node_modules/quasar/lang/(pl|en-US).js");
  5. Formaty dat — dodaj wpis do obiektu datetimeFormats w pliku instance.js.

Reguła liczby mnogiej

Języki inne niż polski i angielski mogą wymagać własnej reguły zdefiniowanej w pliku src/i18n/pluralization-rules.js.

Prekompilacja ​

Wtyczka @intlify/unplugin-vue-i18n skonfigurowana jest w quasar.config.js na katalog src/i18n:

js
// quasar.config.js
include: [fileURLToPath(new URL("./src/i18n", import.meta.url))],

Komunikaty kompilowane są podczas budowania, co przyspiesza start aplikacji.

Made by Moonbite