Motyw
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ł
corerozpakowywany jest do korzenia. Kluczelabels.*,buttons.*,validations.*,table.*,errors.*,dialogs.*,messages.*,placeholders.*,tooltips.*orazlocales.*mają więc zasięg globalny — odwołuje się do nich bez prefiksu modułu. - Nazwy modułów zapisywane są w kebab-case.
contentBuildermapowany jest na"content-builder", astaticTextna"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 przezt(..., 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żytkownicyPowó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ływania | Etykiety, przyciski, komunikaty panelu | Wersja językowa edytowanych rekordów |
| Parametr w API | lang | content_lang |
| Przełącznik | LocaleSwitcher | ContentLocaleSwitcher, umieszczony w nagłówku |
| Persystencja | ciasteczko locale | ciasteczko 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
- Store
core/localezapisuje ciasteczko oraz wywołujeapi.setLocale()isetQuasarLang(). - Interceptor axiosa dokłada nowe wartości
langicontent_langdo każdego żądania, co opisuje Warstwa API. - Plik
App.vueunieważnia cały cache Vue Query. MasterLayoutprzemontowujerouter-viewza 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żkiKorzysta z niego VStatusToggles, opisany w Formularzach.
Dodanie nowego języka
Store — dopisz wpis do tablic
localeslubcontentLocaleswsrc/modules/core/stores/core.js:js{ code: "en", name: "locales.english", short: "ENG", iso: "en-US" }Katalog tłumaczeń — utwórz
trans/<kod>/w każdym module oraz plikisrc/i18n/<kod>/index.jsisrc/i18n/<kod>/menu-items.js.Rejestracja — dopisz język w
src/i18n/index.js.Pakiet Quasara — obejmij kod ISO globem w pliku
instance.js:jsconst langList = import.meta.glob("../../node_modules/quasar/lang/(pl|en-US).js");Formaty dat — dodaj wpis do obiektu
datetimeFormatsw plikuinstance.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.