diff --git a/client/package-lock.json b/client/package-lock.json index 638ed9da..22caee65 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -22,6 +22,7 @@ "d3": "^7.8.5", "dayjs": "^1.11.8", "graphql": "^16.6.0", + "i18next-vue": "^2.2.0", "lodash": "^4.17.21", "loglevel": "^1.8.0", "mitt": "^3.0.0", @@ -13209,6 +13210,15 @@ "node": ">=10.17.0" } }, + "node_modules/i18next-vue": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/i18next-vue/-/i18next-vue-2.2.0.tgz", + "integrity": "sha512-cU3ObbNf1veYWsaWsw9NS/cZGRaVN2GTn13Xmg+Dch+NYa9Wsmz0Ly81EImf2CNDVPIz9RlgizcsxqQ4wTmqaw==", + "peerDependencies": { + "i18next": ">=19", + "vue": "^3.2.43" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -30072,6 +30082,11 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "i18next-vue": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/i18next-vue/-/i18next-vue-2.2.0.tgz", + "integrity": "sha512-cU3ObbNf1veYWsaWsw9NS/cZGRaVN2GTn13Xmg+Dch+NYa9Wsmz0Ly81EImf2CNDVPIz9RlgizcsxqQ4wTmqaw==" + }, "iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", diff --git a/client/package.json b/client/package.json index 6865e3eb..0c743d65 100644 --- a/client/package.json +++ b/client/package.json @@ -34,6 +34,7 @@ "d3": "^7.8.5", "dayjs": "^1.11.8", "graphql": "^16.6.0", + "i18next-vue": "^2.2.0", "lodash": "^4.17.21", "loglevel": "^1.8.0", "mitt": "^3.0.0", diff --git a/client/src/components/FeedbackForm.vue b/client/src/components/FeedbackForm.vue index 8240271f..e9b7596e 100644 --- a/client/src/components/FeedbackForm.vue +++ b/client/src/components/FeedbackForm.vue @@ -17,12 +17,12 @@ import { useMutation } from "@urql/vue"; import { useRouteQuery } from "@vueuse/router"; import log from "loglevel"; import { computed, onMounted, reactive, ref } from "vue"; -import { useI18n } from "vue-i18n"; +import { useTranslation } from "i18next-vue"; const props = defineProps<{ page: LearningContentFeedback }>(); const courseSessionsStore = useCourseSessionsStore(); const circleStore = useCircleStore(); -const { t } = useI18n(); +const { t } = useTranslation(); onMounted(async () => { log.debug("Feedback mounted"); diff --git a/client/src/components/header/MainNavigationBar.vue b/client/src/components/header/MainNavigationBar.vue index 5a7f599b..e04414e5 100644 --- a/client/src/components/header/MainNavigationBar.vue +++ b/client/src/components/header/MainNavigationBar.vue @@ -13,7 +13,7 @@ import { useRouteLookups } from "@/utils/route"; import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue"; import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"; import { computed, onMounted, reactive } from "vue"; -import { useI18n } from "vue-i18n"; +import { useTranslation } from "i18next-vue"; log.debug("MainNavigationBar created"); @@ -24,7 +24,7 @@ const notificationsStore = useNotificationsStore(); const { inCockpit, inCompetenceProfile, inCourse, inLearningPath, inMediaLibrary } = useRouteLookups(); -const { t } = useI18n(); +const { t } = useTranslation(); const state = reactive({ showMobileNavigationMenu: false, showMobileProfileMenu: false, diff --git a/client/src/components/ui/RatingScale.vue b/client/src/components/ui/RatingScale.vue index 3b5c12f1..fa3805bc 100644 --- a/client/src/components/ui/RatingScale.vue +++ b/client/src/components/ui/RatingScale.vue @@ -74,9 +74,9 @@ import QuestionSummary from "@/components/ui/QuestionSummary.vue"; import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue"; import { computed } from "vue"; -import { useI18n } from "vue-i18n"; +import { useTranslation } from "i18next-vue"; -const { t } = useI18n(); +const { t } = useTranslation(); type RGB = [number, number, number]; const red: RGB = [221, 103, 81]; // red-600 diff --git a/client/src/i18n.ts b/client/src/i18n.ts index 58acc25c..9d92f3b3 100644 --- a/client/src/i18n.ts +++ b/client/src/i18n.ts @@ -1,5 +1,6 @@ import type { AvailableLanguages } from "@/stores/user"; import dayjs from "dayjs"; +import i18next from "i18next"; import { nextTick } from "vue"; import { createI18n } from "vue-i18n"; @@ -17,11 +18,11 @@ export function setupI18n( } export function setI18nLanguage(locale: string) { - if (i18n.mode === "legacy") { - i18n.global.locale = locale; - } else { - i18n.global.locale.value = locale; - } + // if (i18n.mode === "legacy") { + // i18n.global.locale = locale; + // } else { + // i18n.global.locale.value = locale; + // } /** * NOTE: * If you need to specify the language setting for headers, such as the `fetch` API, set it here. @@ -29,6 +30,7 @@ export function setI18nLanguage(locale: string) { * * axios.defaults.headers.common['Accept-Language'] = locale */ + i18next.changeLanguage(locale); document.querySelector("html")?.setAttribute("lang", locale); } diff --git a/client/src/i18nextWrapper.ts b/client/src/i18nextWrapper.ts new file mode 100644 index 00000000..678d7fba --- /dev/null +++ b/client/src/i18nextWrapper.ts @@ -0,0 +1,53 @@ +import i18next from "i18next"; +import I18NextVue from "i18next-vue"; +import { nextTick } from "vue"; + +i18next + // detect user language + // learn more: https://github.com/i18next/i18next-browser-languageDetector + // .use(LanguageDetector) + // init i18next + // for all options read: https://www.i18next.com/overview/configuration-options + .init({ + debug: true, + fallbackLng: "fr", + defaultNS: "translations", + resources: { + de: { + translation: {}, + }, + }, + }); + +export function i18nextWrapper(app) { + app.use(I18NextVue, { i18next }); + return app; +} + +export async function loadI18nextLocaleMessages(locale: any) { + // load locale messages with dynamic import + const messages = await import( + /* webpackChunkName: "locale-[request]" */ `./locales/${locale}.json` + ); + + // set locale and locale message + console.log(messages); + console.log(messages["welcome"]); + console.log( + "#######################################################################3" + ); + console.log(i18next); + window.i18next = i18next; + i18next.addResourceBundle(locale, "translations", messages, true, true); + // i18next.addResourceBundle( + // "de", + // "translations", + // { + // welcome: "Hallo, jetzt aber!", + // }, + // true, + // true + // ); + + return nextTick(); +} diff --git a/client/src/locales/de.json b/client/src/locales/de.json index 50ae1ca8..25fb3e55 100644 --- a/client/src/locales/de.json +++ b/client/src/locales/de.json @@ -8,6 +8,7 @@ "SSO Login/Registration": "SSO Login/Registration", "Trainerunterlagen": "Trainerunterlagen", "Zur Zeit sind keine Termine vorhanden": "Zur Zeit sind keine Termine vorhanden", + "welcome": "Hallo Welt", "assignment": { "acceptConditionsDisclaimer": "Bedingungen akzeptieren und Ergebnisse abgeben", "assessmentDocumentDisclaimer": "Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsinstrument bewertet:", @@ -89,7 +90,7 @@ "dashboard": { "courses": "Lehrgang", "nocourses": "Du wurdest noch keinem Lehrgang zugewiesen.", - "welcome": "Willkommen, {name}" + "welcome": "Willkommen, {{name}}" }, "feedback": { "answers": "Antworten", diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json index 6157b7f4..b9e11ffa 100644 --- a/client/src/locales/fr.json +++ b/client/src/locales/fr.json @@ -89,7 +89,7 @@ "dashboard": { "courses": "Formation", "nocourses": "Tu n’as été affecté(e) à aucune formation encore.", - "welcome": "Bienvenue, {name}" + "welcome": "Bienvenue, {{name}}" }, "feedback": { "answers": "Réponses", diff --git a/client/src/locales/it.json b/client/src/locales/it.json index 0ad47d5d..15b7c663 100644 --- a/client/src/locales/it.json +++ b/client/src/locales/it.json @@ -89,7 +89,7 @@ "dashboard": { "courses": "Corso", "nocourses": "Non sei ancora stato/a assegnato/a a nessun corso.", - "welcome": "Ti diamo il benvenuto, {name}" + "welcome": "Ti diamo il benvenuto, {{name}}" }, "feedback": { "answers": "Risposte", diff --git a/client/src/main.ts b/client/src/main.ts index d56d976f..d4dd8e1b 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -1,3 +1,4 @@ +import { i18nextWrapper, loadI18nextLocaleMessages } from "@/i18nextWrapper"; import * as Sentry from "@sentry/vue"; import * as log from "loglevel"; import { createPinia } from "pinia"; @@ -5,7 +6,6 @@ import { createApp, markRaw } from "vue"; import type { Router } from "vue-router"; import "../tailwind.css"; import App from "./App.vue"; -import { loadLocaleMessages, setupI18n } from "./i18n"; import router from "./router"; declare module "pinia" { @@ -25,8 +25,8 @@ if (appEnv.startsWith("prod")) { const commit = "VBV_VERSION_BUILD_NUMBER_VBV"; log.warn(`application started appEnv=${appEnv}, build=${commit}`); -const i18n = setupI18n(); -const app = createApp(App); +// const i18n = setupI18n(); +const app = i18nextWrapper(createApp(App)); Sentry.init({ app, @@ -37,7 +37,7 @@ Sentry.init({ }); // todo: define lang setup -loadLocaleMessages("de").then(() => { +loadI18nextLocaleMessages("de").then(() => { app.use(router); const pinia = createPinia(); @@ -45,7 +45,7 @@ loadLocaleMessages("de").then(() => { store.router = markRaw(router); }); app.use(pinia); - app.use(i18n); + // app.use(i18n); app.mount("#app"); }); diff --git a/client/src/pages/LoginPage.vue b/client/src/pages/LoginPage.vue index 49bd3961..a977c8f0 100644 --- a/client/src/pages/LoginPage.vue +++ b/client/src/pages/LoginPage.vue @@ -4,8 +4,10 @@ import type { LoginMethod } from "@/types"; import * as log from "loglevel"; import { reactive } from "vue"; import { useRoute } from "vue-router"; +import { useTranslation } from "i18next-vue"; const route = useRoute(); +const { t } = useTranslation(); defineProps<{ loginMethod: LoginMethod; @@ -26,6 +28,7 @@ const userStore = useUserStore();

Login

+

{{ t("welcome") }}

{ }); }); -const { t } = useI18n(); +const { t } = useTranslation(); const mobileMenuItems: MenuItem[] = [ { diff --git a/client/src/pages/learningPath/circlePage/DocumentSection.vue b/client/src/pages/learningPath/circlePage/DocumentSection.vue index a3378f32..edabf392 100644 --- a/client/src/pages/learningPath/circlePage/DocumentSection.vue +++ b/client/src/pages/learningPath/circlePage/DocumentSection.vue @@ -73,9 +73,9 @@ import type { CircleDocument, DocumentUploadData } from "@/types"; import dialog from "@/utils/confirm-dialog"; import log from "loglevel"; import { computed, ref, watch } from "vue"; -import { useI18n } from "vue-i18n"; import DocumentListItem from "./DocumentListItem.vue"; import DocumentUploadForm from "./DocumentUploadForm.vue"; +import { useTranslation } from "i18next-vue"; const courseSessionsStore = useCourseSessionsStore(); const circleStore = useCircleStore(); @@ -90,7 +90,7 @@ const dropdownLearningSequences = computed(() => ); // confirm dialog -const { t } = useI18n(); +const { t } = useTranslation(); const deleteDocument = async (doc: CircleDocument) => { const options = { title: t("circlePage.documents.deleteModalTitle"), diff --git a/client/src/pages/learningPath/circlePage/DocumentUploadForm.vue b/client/src/pages/learningPath/circlePage/DocumentUploadForm.vue index ac063a81..420eea55 100644 --- a/client/src/pages/learningPath/circlePage/DocumentUploadForm.vue +++ b/client/src/pages/learningPath/circlePage/DocumentUploadForm.vue @@ -2,7 +2,7 @@ import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import type { DocumentUploadData, DropdownSelectable } from "@/types"; import { reactive } from "vue"; -import { useI18n } from "vue-i18n"; +import { useTranslation } from "i18next-vue"; export interface Props { learningSequences?: DropdownSelectable[]; @@ -10,7 +10,7 @@ export interface Props { isUploading?: boolean; } -const { t } = useI18n(); +const { t } = useTranslation(); const props = withDefaults(defineProps(), { learningSequences: () => [], diff --git a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue index 76814f07..4479691b 100644 --- a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue +++ b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue @@ -11,7 +11,7 @@ import { useMutation } from "@urql/vue"; import type { Dayjs } from "dayjs"; import log from "loglevel"; import { computed, reactive } from "vue"; -import { useI18n } from "vue-i18n"; +import { useTranslation } from "i18next-vue"; const props = defineProps<{ assignment: Assignment; @@ -26,7 +26,7 @@ const emit = defineEmits<{ const courseSessionsStore = useCourseSessionsStore(); const courseSession = useCurrentCourseSession(); -const { t } = useI18n(); +const { t } = useTranslation(); const state = reactive({ confirmInput: false, diff --git a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue index f5771ce4..0fec60c3 100644 --- a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue +++ b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue @@ -22,9 +22,9 @@ import { useRouteQuery } from "@vueuse/router"; import dayjs from "dayjs"; import * as log from "loglevel"; import { computed, onMounted, reactive } from "vue"; -import { useI18n } from "vue-i18n"; +import { useTranslation } from "i18next-vue"; -const { t } = useI18n(); +const { t } = useTranslation(); const courseSession = useCurrentCourseSession(); const userStore = useUserStore(); diff --git a/client/src/pages/learningPath/learningContentPage/layouts/LearningContentFooter.vue b/client/src/pages/learningPath/learningContentPage/layouts/LearningContentFooter.vue index c77d0147..f4346886 100644 --- a/client/src/pages/learningPath/learningContentPage/layouts/LearningContentFooter.vue +++ b/client/src/pages/learningPath/learningContentPage/layouts/LearningContentFooter.vue @@ -1,7 +1,7 @@