🐈
TypeScript & GraphQL でToDoアプリを開発する #7
⬅️前回の記事はこちら
⭐️日本語/英語切り替え機能を実装
frontend/に国際化(i18n: internationalization)パッケージをインストール
% npm install @nuxtjs/i18n
frontend/にi18n/locales/en.jsonを作成
en.json
{
"title": "To-Do",
"new_task": "Enter a new task",
"add": "Add",
"edit": "Edit",
"delete": "Delete",
"save": "Save",
"cancel": "Cancel",
"loading": "Loading...",
"error": "Error"
}
frontend/i18n/locales/にja.jsonを作成
ja.json
{
"title": "やることリスト",
"new_task": "新しいタスクを入力",
"add": "追加",
"edit": "編集",
"delete": "削除",
"save": "保存",
"cancel": "キャンセル",
"loading": "読み込み中...",
"error": "エラー"
}
frontend/i18n/にi18n.config.tsを作成
i18n.config.ts
import { createI18n } from 'vue-i18n'
export default defineI18nConfig(() => ({
legacy: false,
globalInjection: true,
locale: 'ja',
fallbackLocale: 'ja'
}))
nuxt.config.tsに modules: i18n: を追記
nuxt.config.ts
modules: ['@nuxtjs/i18n'],
i18n: {
locales: [
{ code: 'ja', iso: 'ja-JP', file: 'ja.json', name: '日本語' },
{ code: 'en', iso: 'en-US', file: 'en.json', name: 'English' },
],
lazy: true,
langDir: 'locales/',
defaultLocale: 'ja',
vueI18n: './i18n.config.ts',
},
index.vueを修正
index.vue
<template>
<!-- メインの表示領域 -->
<v-container class="py-10 todo-container">
<!-- タスク追加フォーム -->
<v-card elevation="4" class="pa-4 mb-4">
<v-card-title class="d-flex justify-space-between align-center text-h6"> // 【修正】スタイルの適用
{{ t('title') }} // 【修正】多言語対応
<div class="d-flex align-center gap-2">
<v-btn icon @click="toggleLocale"> // 【修正】toggleLocaleの追記
<v-icon>{{ locale === 'ja' ? 'mdi-earth' : 'mdi-translate' }}</v-icon>
</v-btn>
<v-btn icon @click="toggleTheme">
<v-icon>{{ theme.global.name.value === 'dark' ? 'mdi-white-balance-sunny' : 'mdi-weather-night' }}</v-icon>
</v-btn>
</div>
</v-card-title>
<v-form @submit.prevent="createTodo" class="d-flex gap-2 mt-2">
<v-text-field
v-model="newTitle"
:label="t('new_task')"
dense
hide-details
variant="outlined"
/>
<v-btn color="primary" type="submit">{{ t('add') }}</v-btn> // 【修正】多言語対応
</v-form>
</v-card>
<!-- 読み込み中 / エラー表示 -->
<v-alert v-if="pending" type="info" variant="text" class="mb-4">{{ t('loading') }}</v-alert> // 【修正】多言語対応
<v-alert v-else-if="error" type="error" class="mb-4">{{ t('error') }}: {{ error.message }}</v-alert> // 【修正】多言語対応
<!-- タスクリストの表示 -->
<v-card
v-for="todo in todos"
:key="todo.id"
elevation="2"
class="pa-3 mb-3"
>
<div class="d-flex align-center justify-space-between w-100">
<!-- 編集モードではないときの表示 -->
<template v-if="editingId !== todo.id">
<div class="d-flex align-center gap-3">
<!-- 完了状態のチェックボックス -->
<v-checkbox
:model-value="todo.completed"
@update:model-value="() => toggleCompleted(todo)"
density="compact"
hide-details
/>
<!-- タイトル(完了時は取り消し線) -->
<span :class="{ 'text-decoration-line-through': todo.completed }">
{{ todo.title }}
</span>
</div>
<!-- 編集 / 削除 メニュー -->
<v-menu
:opened="menuOpenId === todo.id"
@update:opened="(val: boolean) => (menuOpenId = val ? todo.id : null)"
location="bottom"
>
<template #activator="{ props }">
<v-btn icon v-bind="props"><v-icon>mdi-dots-horizontal</v-icon></v-btn>
</template>
<v-list>
<v-list-item @click="startEdit(todo)">
<v-list-item-title>{{ t('edit') }}</v-list-item-title> // 【修正】多言語対応
</v-list-item>
<v-list-item @click="deleteTodo(todo.id)">
<v-list-item-title>{{ t('delete') }}</v-list-item-title> // 【修正】多言語対応
</v-list-item>
</v-list>
</v-menu>
</template>
<!-- 編集モードのときの表示 -->
<template v-else>
<v-text-field
v-model="editedTitle"
density="compact"
hide-details
class="flex-grow-1"
/>
<div class="d-flex gap-2">
<v-btn size="small" color="primary" @click="saveEdit(todo.id)">{{ t('save') }}</v-btn> // 【修正】多言語対応
<v-btn size="small" color="grey" @click="cancelEdit">{{ t('cancel') }}</v-btn> // 【修正】多言語対応
</div>
</template>
</div>
</v-card>
</v-container>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useTodos } from '@/composables/useTodos'
import { CREATE_TODO, UPDATE_TODO, DELETE_TODO } from '@/graphql/mutations'
import { useNuxtApp } from '#app'
import { useTheme } from 'vuetify'
import { useI18n } from 'vue-i18n' // 【修正】多言語対応
const theme = useTheme()
const { locale, t } = useI18n() // 【修正】多言語対応
const toggleTheme = () => {
theme.global.name.value = theme.global.name.value === 'dark' ? 'light' : 'dark'
}
const toggleLocale = () => { // 【修正】多言語対応
locale.value = locale.value === 'ja' ? 'en' : 'ja'
}
/**
* Todoの型定義
*/
interface Todo {
id: number
title: string
completed: boolean
}
/**
* Composableから状態取得
*/
const { todos, pending, error, refresh } = useTodos()
const { $apollo } = useNuxtApp()
/**
* 入力および編集中の状態管理
*/
const newTitle = ref('')
const editedTitle = ref('')
const editingId = ref<number | null>(null)
const menuOpenId = ref<number | null>(null)
/**
* タスクを新規作成する処理
*/
const createTodo = async () => {
if (!newTitle.value.trim()) return
await $apollo.mutate({
mutation: CREATE_TODO,
variables: { title: newTitle.value }
})
newTitle.value = ''
refresh()
}
/**
* 完了状態を切り替える処理
*/
const toggleCompleted = async (todo: Todo) => {
await $apollo.mutate({
mutation: UPDATE_TODO,
variables: { id: todo.id, completed: !todo.completed }
})
refresh()
}
/**
* タスクを削除する処理
*/
const deleteTodo = async (id: number) => {
await $apollo.mutate({
mutation: DELETE_TODO,
variables: { id }
})
refresh()
closeMenu()
}
/**
* 編集モードに切り替える処理
*/
const startEdit = (todo: Todo) => {
editingId.value = todo.id
editedTitle.value = todo.title
closeMenu()
}
/**
* 編集をキャンセルする処理
*/
const cancelEdit = () => {
editingId.value = null
editedTitle.value = ''
}
/**
* 編集内容を保存する処理
*/
const saveEdit = async (id: number) => {
if (!editedTitle.value.trim()) return
await $apollo.mutate({
mutation: UPDATE_TODO,
variables: { id, title: editedTitle.value }
})
editingId.value = null
editedTitle.value = ''
refresh()
}
/**
* メニューを閉じる処理
*/
const closeMenu = () => {
menuOpenId.value = null
}
</script>
<style scoped>
/* 完了タスクの打ち消し線 */
.text-decoration-line-through {
text-decoration: line-through;
}
/* コンポーネント間の余白調整 */
.gap-2 {
gap: 8px;
}
.gap-3 {
gap: 12px;
}
/* 中央寄せ・幅制限 */
.todo-container {
max-width: 600px;
margin: 0 auto;
}
</style>
動作確認
🚀日本語/英語 切り替え機能の実装が完了!!!
おわりに
日本語/英語 切り替え機能の実装 お疲れさまでした。
また、全7記事にわたる「TypeScript & GraphQL でToDoアプリを開発する」にお付き合いいただきありがとうございました。
ToDoアプリというシンプルな題材を通して、各技術の基本的な使い方を習得できたと思います。
Discussion