Aug 21, 2020

「Nuxtでプログを構築しよう!!」の最終回。スクラッチで言語切替に挑戦してみた。

「Nuxt でプログを構築しよう!!」の最終回です。言語切替には vue-i18n を利用すると思いますが、今回は自前で言語切替を実装してみたいと思います。言語情報を各コンポーネントから共有するために composables コンポーネントにもチャレンジしたいと思います。

記事の準備

英語の記事は、content/articles/en のフォルダに保存します。日本語の記事は、英語の記事と同じファイル名にして content/articles/ja のフォルダに保存します。

メニュー、メッセージの準備

英語と日本語のメニュー、メッセージを assets フォルダに json 形式で保存します。

/assets/menu.json
{
  "en": {
    "/": "new",
    "/search/server": "server",
    "/search/network": "network",
    "/search/nuxt": "nuxt",
    "/search/appliances": "appliances",
    "/search/etc": "etc."
  },
  "ja": {
    "/": "新着",
    "/search/server": "サーバ",
    "/search/network": "ネットワーク",
    "/search/nuxt": "NUXT",
    "/search/appliances": "情報家電",
    "/search/etc": "その他"
  }
}
/assets/msg.json
{
  "en": {
    "000": "",
    "001": "'There are no articles in this category.'"
  },
  "ja": {
    "000": "",
    "001": "記事はありません。"
  }
}

composables コンポーネントの作成

言語情報を各コンポーネントから共有するための composables コンポーネントを作成していきます。構造はシンプルで getter,setter で ref で各変数を参照できるようにします。

/composables/i18n-like.ts
import { ref, computed } from '@nuxtjs/composition-api';
import msgfile from '@/assets/msg.json';
import menufile from '@/assets/menu.json';
export const i18nLikeKey = Symbol('i18nLike');
export function i18nLike() {
  const langRef = ref('');
  const msgRef = ref('');
  const menuRef = ref('');
  const msgs: { [key: string]: { [key: string]: string } } = msgfile;
  const menus: { [key: string]: { [key: string]: string } } = menufile;
  const lang = computed({
    get: () => langRef.value,
    set: (lang: string) => (langRef.value = lang),
  });
  const msg = computed({
    get: () => msgRef.value,
    set: (key: string) => (msgRef.value = msgs[langRef.value][key]),
  });
  const menu = computed({
    get: () => menuRef.value,
    set: (key: string) => (menuRef.value = menus[langRef.value][key]),
  });
  function isEmpty(str: string) {
    return !str;
  }
  return {
    lang,
    msg,
    menu,
    isEmpty,
  };
}
export type Multilingual = ReturnType<typeof i18nLike>;

各コンポーネントから参照できるように plugin に登録します。

/plugins/i18n-like.ts
import { onGlobalSetup, provide } from '@nuxtjs/composition-api';
import { i18nLikeKey, i18nLike } from '~/composables/i18n-like';
export default () => {
  onGlobalSetup(() => {
    provide(i18nLikeKey, i18nLike());
  });
};

合わせて nuxt.config.js に plugin を設定しておきます。

nuxt.config.js]
      plugins: ["@/plugins/i18nLikePlugin.ts"],

コンポーネントの修正

まずは Menu コンポーネントを修正していきます。i18n-like コンポーネントを import します。

/compoments/Menu.vue
    <script lang="ts">
    import {
      defineComponent,
      reactive,
      inject,
      computed,
      watch,
    } from "@nuxtjs/composition-api";
    import { i18nLikeKey, Multilingual } from "@/composables/i18n-like.ts";
 
    type Menu = {
      link: string;
      icon: string;
      text: string;
    };

言語切替に合わせてメニュー情報を読み替えるようにします。

/compoments/Menu.vue
    export default defineComponent({
      setup() {
        const state = reactive<{
          menus: Menu[];
        }>({
          menus: [
            {
              link: "/",
              icon: "fas fa-home",
              text: "",
            },
            {
              link: "/search/server",
              icon: "fas fa-server",
              text: "",
            },
            {
              link: "/search/network",
              icon: "fas fa-network-wired",
              text: "",
            },
            {
              link: "/search/nuxt",
              icon: "fas fa-globe",
              text: "",
            },
            {
              link: "/search/appliances",
              icon: "fas fa-icons",
              text: "",
            },
           {
              link: "/search/etc",
              icon: "fas fa-mortar-pestle",
              text: "",
            },
          ],
        });
        const { lang, menu } = inject(i18nLikeKey) as Multilingual;
        const menus = computed(() => state.menus);
        watch(lang, (_) => {
          menu.value = lang.value;
          state.menus.forEach((elem, index) => {
            menu.value = elem.link;
            state.menus[index].text = menu.value;
          });
        });
        return { menus };
      },
    });
    </script>

同様に Article コンポーネントも修正していきます。

/compoments/Article.vue
    <script lang="ts">
    import {
      defineComponent,
      reactive,
      inject,
      computed,
      useContext,
      watch,
    } from "@nuxtjs/composition-api";
    import { i18nLikeKey, Multilingual } from "@/composables/i18n-like.ts";
    type Props = {
      target: string;
      tag: string;
      parent: string;
      img: string;
    };
    export default defineComponent({
      props: {
        target: String,
        tag: String,
        parent: String,
        img: String,
      },
      setup(props: Props) {
        const state = reactive<{
          articles: [Object];
        }>({
          articles: [{}],
        });
        const { lang, msg } = inject(i18nLikeKey) as Multilingual;
        const articles = computed(() => state.articles);
        // @ts-ignore
        const { $content } = useContext();
        updateLang(
          props.target,
          props.tag
        );
        watch(lang, (_) => {
          updateLang(
            props.target,
            props.tag,
          );
        });
        function updateLang(
          target: string,
          tag: string,
        ) {
          getArticles(target, tag);
          async function getArticles(target: string, tag: string) {
            let whereObj = {};
            if (tag !== "all") {
              whereObj = { tags: [software,Nuxt]: [software,: [network,: { $contains: tag } };
            }
            try {
              state.articles = await $content(target, lang.value)
                .only(["title", "description", "img", "slug", "updatedAt"])
                .where(whereObj)
                .fetch();
              if (state.articles.length > 0) {
                msg.value = "000";
              } else {
                msg.value = "001";
              }
            } catch (error) {
              state.articles = [{}];
              msg.value = "001";
            }
          }
       }
        return { articles };
      },
    });
    </script>

Articles コンポーネントも修正していきます。

/compoments/Articles.vue
    <script lang="ts">
    import {
      defineComponent,
      reactive,
      inject,
      computed,
      useContext,
      watch,
    } from "@nuxtjs/composition-api";
    import { i18nLikeKey, Multilingual } from "@/composables/i18n-like.ts";
    type Props = {
      target: string;
      tag: string;
      img: string;
    };
    export default defineComponent({
      props: {
        target: String,
        tag: String,
        img: String,
      },
      setup(props: Props) {
       const state = reactive<{
          articles: [Object];
        }>({
          articles: [{}],
        });
        const { lang, msg } = inject(i18nLikeKey) as Multilingual;
        const articles = computed(() => state.articles);
        // @ts-ignore
        const { $content } = useContext();
        updateLang(
          props.target,
          props.tag,
        );
        watch(lang, (_) => {
          updateLang(
            props.target,
            props.tag,
          );
        });
        function updateLang(
          target: string,
          tag: string,
        ) {
          getArticles(target, tag, parent);
          async function getArticles(target: string, tag: string) {
            let whereObj = {};
            if (tag !== "all") {
              whereObj = { tags: [software,Nuxt]: [software,: [network,: { $contains: tag } };
            }
            try {
              state.articles = await $content(target, lang.value)
                .only(["title", "description", "img", "slug", "updatedAt"])
                .where(whereObj)
                .fetch();
              if (state.articles.length > 0) {
                msg.value = "000";
              } else {
                msg.value = "001";
              }
            } catch (error) {
              state.articles = [{}];
              msg.value = "001";
            }
          }
        }
        return { articles };
      },
    });
    </script>
 

これでヘッダーの言語を切り替えるたびに英語記事と日本語記事の切替ができるようになりました。

追記:記事を日本語と英語両方書くのが苦痛になってきたので、言語切替はこのホームページでは今は廃止しました。