All files / src/stores i18n.ts

75.94% Statements 60/79
53.84% Branches 14/26
66.66% Functions 4/6
75.94% Lines 60/79

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131  2x 2x 2x 2x                                                               2x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x   4x                         4x 1x 1x 1x     1x   4x 1x 1x   1x 1x     4x 3x 3x 3x 3x     3x 3x 3x 3x 3x   3x   1x 1x 1x 1x 1x   3x           3x 3x 3x 2x     3x 3x   2x     2x 2x   2x 3x   4x 1x 1x 1x   4x 4x    
// region Dependency Injection
import {defineStore} from 'pinia'
import {ref, shallowRef} from 'vue'
import api from '@/libs/axios'
import { i18n } from '@/libs/i18n'
// endregion Dependency Injection
 
// region Component Injection
// endregion Component Injection
 
// region interface
type LocaleItem = { code: string; label: string }
// endregion interface
 
// region constants
// endregion constants
 
// region props
// endregion props
 
// region variable
// endregion variable
 
// region properties
// endregion properties
 
// region emits
// endregion emits
 
// region validator
// endregion validator
 
// region methods
// endregion methods
 
// region export
export const useI18nStore = defineStore("i18n", () => {
  const current = ref(
    sessionStorage.getItem("lang") ||
    localStorage.getItem("lang") ||
    (navigator.language?.split("-")[0] ?? "ja")
  );
  const locales = ref<LocaleItem[]>([]);
  const bundles = shallowRef(new Map<string, any>());        // locale -> messages(flat)
  const etags   = shallowRef(new Map<string, string>());     // locale -> ETag
  const loading = ref(false);
  const error   = ref<unknown | null>(null);
 
  async function init() {
    loading.value = true;
    try {
      await fetchLocales();
      await loadLocale(current.value);
      document.documentElement.lang = current.value;
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  }
 
  async function fetchLocales() {
    const res = await api.get<{ locales: LocaleItem[] }>("/i18n/list");
    locales.value = res.data.locales ?? [];
    if (!current.value && locales.value[0]) {
      setCurrent(locales.value[0].code);
    }
  }
 
  function setCurrent(locale: string, persist: "session" | "local" = "session") {
    current.value = locale;
    if (persist === "session") sessionStorage.setItem("lang", locale);
    else localStorage.setItem("lang", locale);
    document.documentElement.lang = locale;
  }
 
  // バックエンドの /i18n/bundle から辞書取得 → vue-i18n に注入
  async function loadLocale(locale = current.value) {
    if (!locale) return {};
    const etag = etags.value.get(locale);
    let parameters: Record<string, any> = { lang: locale }
    if (import.meta.env.VITE_APP_ENV === 'local') {
      parameters.bust = Date.now()
    }
    const res = await api.get("/i18n/bundle", {
      params: parameters, // axios が自動でエンコード
      headers: etag ? { "If-None-Match": etag } : undefined,
      validateStatus: () => true,               // 304 を例外にしない
    });
 
    if (res.status === 304) {
      // 既存キャッシュを vue-i18n に反映して locale 切替だけ
      const cached = bundles.value.get(locale) || {};
      i18n.global.setLocaleMessage(locale, cached);
      i18n.global.locale.value = locale;
      return cached;
    }
 
    if (res.status >= 400) {
      throw new Error(`${res.status} ${res.statusText}`);
    }
 
    // バンドル形式に合わせて展開
    // 例: { etag: "...", locale: "ja", namespaces: { "ui.layout..": "..." } }
    const payload = res.data || {};
    const messages =
      payload.namespaces && typeof payload.namespaces === "object"
        ? payload.namespaces
        : payload;
 
    const nextEtag = (res.headers?.etag ?? res.headers?.ETag) as string | undefined;
    if (nextEtag) etags.value.set(locale, nextEtag);
 
    bundles.value.set(locale, messages);
 
    // vue-i18n へセット & 切替
    i18n.global.setLocaleMessage(locale, messages);
    i18n.global.locale.value = locale;
 
    return messages;
  }
 
  async function changeLocale(next: string, persist: "session" | "local" = "session") {
    setCurrent(next, persist);
    await loadLocale(next);
  }
 
  return { current, locales, bundles, etags, loading, error, init, fetchLocales, loadLocale, changeLocale };
})
// endregion export