Search code examples
vue.jsinternationalizationvue-i18n

How can I change the locale of a VueI18n instance in a Vue file, and have it apply to all other Vue files in the application?


I am currently trying to implement a feature where a user can select a language from a dropdown menu in a Settings page (SettingsDialog.vue), updating all of the text to match the new language. This application has multiple Vue files like a MenuBar.vue, HelpDialog.vue, each pulling from translation.ts for their English translations. However, I noticed that selecting a language from the dropdown menu only changed the elements inside my SettingsDialog.vue file, not all of the other Vue files I have.

I tried using the Vue-I18n documentation implementation of changing locale globally in the file. I was expecting for the locale of the entire application to change after selecting a language in SettingsDialog.vue, applying my English translations in translation.ts to the Menu Bar, Help Page, etc. What happened is that the translations from translation.ts only applied to the SettingsDialog.vue page, no where else.

I guess it would be helpful to add that this is an Electron application, and the Vue files in the project use Quasar. Each file does have the correct import statements.

main.ts:

// ...
window.datalayer = [];

const i18n = createI18n({
  legacy: false,
  locale: "",
  messages,
});

createApp(App)
  .use(store, storeKey)
  .use(router)
  .use(
    createGtm({
      id: process.env.VUE_APP_GTM_CONTAINER_ID ?? "GTM-DUMMY",
      vueRouter: router,
      enabled: false,
    })
  )
  .use(Quasar, {
    config: {
      brand: {
        primary: "#a5d4ad",
        secondary: "#212121",
      },
    },
    iconSet,
    plugins: {
      Dialog,
      Loading,
    },
  })
  .use(ipcMessageReceiver, { store })
  .use(markdownItPlugin)
  .use(i18n)
  .mount("#app");

SettingsDialog.vue

// ...
            <!-- Language Setting Card -->
            <q-card flat class="setting-card">
              <q-card-actions>
                <div id="app" class="text-h5">{{ $t("言語") }}</div>
              </q-card-actions>
              <q-card-actions class="q-px-md q-py-sm bg-setting-item">
                <div id="app">{{ $t("言語を選択する") }}</div>
                <q-space />
                <q-select
                  filled
                  v-model="locale"
                  dense
                  emit-value
                  map-options
                  options-dense
                  :options="[
                    { value: 'ja', label: '日本語 (Japanese)' },
                    { value: 'en', label: '英語 (English)' },
                  ]"
                  label="Language"
                >
                  <q-tooltip
                    :delay="500"
                    anchor="center left"
                    self="center right"
                    transition-show="jump-left"
                    transition-hide="jump-right"
                  >
                    Test
                  </q-tooltip>
                </q-select>
              </q-card-actions>
            </q-card>
// ...
<script lang="ts">
import { useI18n } from "vue-i18n";
// ...
  setup(props, { emit }) {
    const { t, locale } = useI18n({ useScope: "global" });
// ...
    return {
      t,
      locale,
      // ...
    };

MenuBar.vue

<template>
  <q-bar class="bg-background q-pa-none relative-position">
    <div
      v-if="$q.platform.is.mac && !isFullscreen"
      class="mac-traffic-light-space"
    ></div>
    <img v-else src="icon.png" class="window-logo" alt="application logo" />
    <menu-button
      v-for="(root, index) of menudata"
      :key="index"
      :menudata="root"
      v-model:selected="subMenuOpenFlags[index]"
      :disable="menubarLocked"
      @mouseover="reassignSubMenuOpen(index)"
      @mouseleave="
        root.type === 'button' ? (subMenuOpenFlags[index] = false) : 
        undefined
      "
    />
// ...
<script lang="ts">
import { defineComponent, ref, computed, ComputedRef, watch } from "vue";
import { useStore } from "@/store";
import MenuButton from "@/components/MenuButton.vue";
import TitleBarButtons from "@/components/TitleBarButtons.vue";
import { useQuasar } from "quasar";
import { HotkeyAction, HotkeyReturnType } from "@/type/preload";
import { setHotkeyFunctions } from "@/store/setting";
import {
  generateAndConnectAndSaveAudioWithDialog,
  generateAndSaveAllAudioWithDialog,
  generateAndSaveOneAudioWithDialog,
} from "@/components/Dialog";
import { useI18n } from "vue-i18n";
import messages from "../translation";

type MenuItemBase<T extends string> = {
  type: T;
  label?: string;
};

export type MenuItemSeparator = MenuItemBase<"separator">;

export type MenuItemRoot = MenuItemBase<"root"> & {
  onClick: () => void;
  subMenu: MenuItemData[];
};

export type MenuItemButton = MenuItemBase<"button"> & {
  onClick: () => void;
};

export type MenuItemCheckbox = MenuItemBase<"checkbox"> & {
  checked: ComputedRef<boolean>;
  onClick: () => void;
};

export type MenuItemData =
  | MenuItemSeparator
  | MenuItemRoot
  | MenuItemButton
  | MenuItemCheckbox;

export type MenuItemType = MenuItemData["type"];

export default defineComponent({
  name: "MenuBar",

  components: {
    MenuButton,
    TitleBarButtons,
  },

  setup() {
    const { t } = useI18n({
      messages,
    });
  // ...
  };
    const menudata = ref<MenuItemData[]>([
      {
        type: "root",
        label: t("ファイル"),
        onClick: () => {
          closeAllDialog();
        },
    // ...
    ]);

translation.ts

const messages = {
    en: {
        // MenuBar.vue
        ファイル: "File",
        エンジン: "Engine",
        ヘルプ: "Help",
        // SettingDialog.vue
        言語: 'Language',
        言語を選択する: 'Select Language',
        オフ: 'OFF',
        エンジンモード: 'Engine Mode',
        // HelpDialog.vue
        ソフトウェアの利用規約: 'test',
    }
};


export default messages;

Solution

  • Maybe there are more problems but now I see two:

    1. Your menudata should be computed instead of just ref. Right now you are creating a JS object and setting it label property to result of t() call. When global locale changes this object is NOT created again. It still holds same value the t() function returned the only time it was executed - when setup() was running

    // correct

    const menudata = computed<MenuItemData[]>(() => [
          {
            type: "root",
            label: t("ファイル"),
            onClick: () => {
              closeAllDialog();
            },
        // ...
        ]);
    
    

    This way whenever i18n.global.locale changes, your menudata is created again with new translation

    As an alternative, set label to key and use t(label) inside the template. However computed is much more effective solution...

    1. You don't need to pass messages to useI18n() in every component. Only to the global instance. By passing config object into a useI18n() in a component you are creating Local scope which makes no sense if you are storing all translations in a single global place anyway