【型安全な実装】型安全にvue-i18nの辞書から配列を取得して、変数に設定する【の掘り下げ方】
結論
i18nの辞書ファイル
{
"path": {
"to": {
"wordList": ["a", "b", "c"]
}
}
}
ユーティリティ関数
(今回の主な結論。getI18nArray
を使えば達成できます。)
/**
* 引数未指定にすると、普通に`const i18n = useI18n()`とすると入ってくる型になる。
* 型引数の使い方については、そのままuseI18nの型引数の指定方法を参照のこと。
*/
export type UseI18nReturnType<Options extends UseI18nOptions = UseI18nOptions> =
Composer<
NonNullable<Options['messages']>,
NonNullable<Options['datetimeFormats']>,
NonNullable<Options['numberFormats']>,
Options['locale'] extends unknown ? string : Options['locale']
>
/**
* @example
* ```ts
* import { useI18n } from 'vue-i18n'
* const i18n = useI18n() // messageは { path: { to: { wordList: ['a', 'b', 'c'] } } } とする
* const wordList = getI18nArray(i18n, 'path.to.wordList') // ['a', 'b', 'c']
* ```
*/
export const getI18nArray = (i18n: UseI18nReturnType, key: string): string[] =>
Object.entries<VueMessageType>(i18n.tm(key)).map(([_, term]) => i18n.rt(term))
配列を要求する側のコンポーネント
<template>
<ul>
<li v-for="word in wordList" :key="word">{{ word }}</li>
</ul>
</template>
<script setup lang="ts">
defineProps<{
wordList: string[] // なんらかの事情でArrayを要求している
}>()
</script>
配列を取得して、propsとして渡す側のコンポーネント
(※:wordList
は:wordList="wordList"
の略記です。)
<template>
<div>
<!-- ... -->
<ShowWordList :wordList />
<!-- ... -->
</div>
</template>
<script setup lang="ts">
const i18n = useI18n()
const wordList = getI18nArray(i18n, 'path.to.wordList')
</script>
Draft PRですが以下に、弊社のNuxt QuickStarterリポジトリに、実際のPRを作成しています。
問題
サイトを作っていると、多言語対応をしたいという要望があります。
それを実現するVue向けの方法の一つが、vue-i18nです。
そして今回、多言語化をしつつ、そのi18n辞書から直接配列を取得したいというニーズが出てきました。
つまり以下のような方法では不十分ということです。
i18nの辞書ファイル
{
"path": {
"to": {
"item1": "a",
"item2": "b",
"item3": "c"
}
}
}
配列を取得して、propsとして渡す側のコンポーネント
<template>
<div>
<!-- ... -->
<ShowWordList :wordList />
<!-- ... -->
</div>
</template>
<script setup lang="ts">
const i18n = useI18n()
// !! ここでは個別にアイテムを取得したくない
const wordList = [
`${i18n.t('path.to.item1')}`,
`${i18n.t('path.to.item2')}`,
`${i18n.t('path.to.item3')}`,
]
</script>
i18nの辞書ファイル
{
"path": {
"to": {
"wordList": ["a", "b", "c"]
}
}
}
配列を取得して、propsとして渡す側のコンポーネント
<template>
<div>
<!-- ... -->
<ShowWordList :wordList />
<!-- ... -->
</div>
</template>
<script setup lang="ts">
const i18n = useI18n()
// !! 一度に配列を取得している
const wordList = getI18nArray(i18n, 'path.to.wordList')
</script>
解決案
調べると、次のような案が見つかりました。
<template>
<p v-for="name in tm('heroPage.names')" :key="name">
{{ rt(name) }}
</p>
</template>
<script>
import { useI18n } from 'vue-i18n'
export default {
setup() {
const { tm, rt } = useI18n();
return { rt, tm }
}
}
</script>
しかし我々が求めているのは、v-for="name in tm('heroPage.names')"
することではなく、直接配列を取得することです。
ただしi18n.tm
, i18n.rt
は覚えておいた方がよさそうです。
次に'vue-i18nでまるっとオブジェクトを取得する - Blogメモφ(..)'を発見しました。
ここでは次のようなコードが例示されています。
for (const [, item] of Object.entries($tm('items.space') as any) as any) {
console.log(item.text)
}
or
for (const item of Object.values($tm('items.space') as any) as any) {
console.log(item.text)
}
('vue-i18nでまるっとオブジェクトを取得する - Blogメモφ(..)'より引用)
これは完成に近そうです!
次のようにできれば、解決できそうです。
ついでに、私達の環境向けに書き替えておきます。
const wordList =
Object.entries(i18n.tm('path.to.wordList') as any)
.map(([_, item]) => i18n.rt(item))
しかしas any
を使うのは、あまりよくないです。
any
を使用すると、そのコードが正しいのか静的に検証できなくなるからです。
いわゆる「型安全でない」というものです。
(プロジェクト規模が小さい場合は、それでよいかもしれませんが、プロジェクト単位が中規模以上であればany
は使わない方がよいでしょう。)
解決策
i18n.rt
とi18n.tm
の存在、およびany
を使った解決策は発見できました。
あとはこれらを使って、any
の代わりに適切な型を指定するだけです。
では、適切な型とは何でしょうか。
ここまできたら、lspをガンガン使って、実地調査をしていきましょう。
Object.entries
まずは「any
を使った解決策」から、Object.entries
が使えることがわかるので、型を見てみます。
entries<T>(o: { [s: string]: T } | ArrayLike<T>): [string, T][];
これをもっと簡単に考えると、次のような感じです。
entries<T>(o: Record<string, T>): [string, T][];
ここのT
にany
の変わりを指定すれば、よさそうですね。
i18n.tm
明らかにT
になるのは、i18n.tm
の要素の型、言い換えればi18n.rt
の引数の型です。
ではi18n.tm
の型を見てみましょう。
tm<Key extends string, ResourceKeys extends PickupKeys<Messages> = PickupKeys<Messages>, Locale extends PickupLocales<NonNullable<Messages>> = PickupLocales<NonNullable<Messages>>, Target = IsEmptyObject<Messages> extends false ? NonNullable<Messages>[Locale] : RemoveIndexSignature<{
[K in keyof DefineLocaleMessage]: DefineLocaleMessage[K];
}>, Return = ResourceKeys extends ResourcePath<Target> ? ResourceValue<Target, ResourceKeys> : Record<string, any>>(key: Key | ResourceKeys): Return;
なんのこっちゃ?
手動簡約するのはめっちゃがんばればいけそうですが、今回はi18n.rt
の型定義がもっと簡単であることに望みをかけて、そちらを見てみましょう。
rt(message: MessageFunction<VueMessageType> | VueMessageType): string;
ビンゴです!
Object.entries
の型指定は、Object.entries<MessageFunction<VueMessageType>>
もしくはObject.entries<VueMessageType>
になりますね。
ではMessageFunction<VueMessageType>
とVueMessageType
のどちらになるのでしょうか?
これはi18n.tm
の型定義を調べるのが、前述の通り静的には難しかったので、簡単にChrome DevToolsで調べました。
どうやら関数ではなさそうだったので、VueMessageType
のようです。
解
材料は全てあつまりました。
答えは次になります。
const wordList =
Object.entries<VueMessageType>(i18n.tm('path.to.wordList'))
.map(([_, item]) => i18n.rt(item))
やった!
応用: 関数化する
本稿の答えは出ましたが、関数化しておくと、より使いやすくなります。
単純に考えて、次のようになりそうです。
export const getI18nArray = (i18n: ???, key: string): string[] =>
Object.entries<VueMessageType>(i18n.tm(key)).map(([_, term]) => i18n.rt(term))
???
の部分は、useI18n
の返り値の型を指定すればよさそうです。
useI18n
の型定義を見てみましょう。
export declare function useI18n<
Options extends UseI18nOptions = UseI18nOptions
>(options?: Options):
Composer<
NonNullable<Options['messages']>,
NonNullable<Options['datetimeFormats']>,
NonNullable<Options['numberFormats']>,
Options['locale'] extends unknown ? string : Options['locale']
>;
実のところ簡単に考えれば、???
にはReturnType<typeof useI18n<UseI18nOptions>>
を指定すればよさそうですが、実はuseI18n
は次のように型引数を取ることがあります。
import ja from '@/locales/ja.json'
const i18n = useI18n<{ message: typeof ja }>()
この場合、???
にReturnType<typeof useI18n<UseI18nOptions>>
を指定していると型がミスマッチするので、一応柔軟に定義しておきましょう。
次のようになります。
export type UseI18nReturnType<Options extends UseI18nOptions = UseI18nOptions> =
Composer<
NonNullable<Options['messages']>,
NonNullable<Options['datetimeFormats']>,
NonNullable<Options['numberFormats']>,
Options['locale'] extends unknown ? string : Options['locale']
>
export const getI18nArray = (i18n: UseI18nReturnType, key: string): string[] =>
Object.entries<VueMessageType>(i18n.tm(key)).map(([_, term]) => i18n.rt(term))
ついにこれで完成です!
const i18n = useI18n()
const wordList = getI18nArray(i18n, 'path.to.wordList')
まとめ
今回はvue-i18nの辞書から配列を取得して、変数に設定する方法を解説しました。
型安全も仮定しています。
もう一度見てみます。
i18nの辞書ファイルは次のようになっています。
{
"path": {
"to": {
"wordList": ["a", "b", "c"]
}
}
}
今回の解決策は、結局のところ次のようになります。
/**
* 引数未指定にすると、普通に`const i18n = useI18n()`とすると入ってくる型になる。
* 型引数の使い方については、そのままuseI18nの型引数の指定方法を参照のこと。
*/
export type UseI18nReturnType<Options extends UseI18nOptions = UseI18nOptions> =
Composer<
NonNullable<Options['messages']>,
NonNullable<Options['datetimeFormats']>,
NonNullable<Options['numberFormats']>,
Options['locale'] extends unknown ? string : Options['locale']
>
/**
* @example
* ```ts
* import { useI18n } from 'vue-i18n'
* const i18n = useI18n() // messageは { path: { to: { wordList: ['a', 'b', 'c'] } } } とする
* const wordList = getI18nArray(i18n, 'path.to.wordList') // ['a', 'b', 'c']
* ```
*/
export const getI18nArray = (i18n: UseI18nReturnType, key: string): string[] =>
Object.entries<VueMessageType>(i18n.tm(key)).map(([_, term]) => i18n.rt(term))
上述のgetI18nArray
を使った例を見てみましょう。
これは配列を要求する側のコンポーネントです。
<template>
<ul>
<li v-for="word in wordList" :key="word">{{ word }}</li>
</ul>
</template>
<script setup lang="ts">
defineProps<{
wordList: string[] // なんらかの事情でArrayを要求している
}>()
</script>
次がgetI18nArray
を使い、配列を取得して、propsとして渡す側のコンポーネントです。
<template>
<div>
<!-- ... -->
<ShowWordList :wordList />
<!-- ... -->
</div>
</template>
<script setup lang="ts">
const i18n = useI18n()
const wordList = getI18nArray(i18n, 'path.to.wordList')
</script>
このようにして、vue-i18nの辞書から配列を取得して、変数に設定することができます。
おつかれさまでした!
Draft PRですが以下で、弊社のNuxt QuickStarterリポジトリの、実際のPRを見ることができます。
以下、引用したコードのライセンス条項です。
vue-i18n(MIT)
The MIT License (MIT)
Copyright (c) 2016 kazuya kawaguchi
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Discussion
TODO:
と前置きしたにも関わらず、柔軟に定義できていない(型引数を受け取っていない)ので、修正する。