Vue.js で作る多言語サイト(i18n)の最適解

グローバル化が進む現代において、多言語対応は Web サイトの必須要件となっています。Vue.js で多言語サイトを構築する際、多くの開発者が「どのように実装すれば効率的か」「パフォーマンスはどうなるか」「保守性は保てるか」といった課題に直面します。
この記事では、Vue.js の公式 i18n ライブラリを使用した実践的な多言語サイト構築方法をご紹介します。実際のプロジェクトで発生するエラーとその解決策、パフォーマンス最適化のテクニックまで、現場で即座に活用できる知識をお届けします。
Vue.js i18n の基本概念
Vue.js の国際化(i18n)は、アプリケーションを複数の言語に対応させるための仕組みです。単純なテキストの翻訳だけでなく、日付、数値、通貨、複数形など、文化的な違いも考慮した包括的なソリューションを提供します。
i18n の 3 つの基本要素
- ロケール(Locale): 言語と地域の組み合わせ(例:ja-JP、en-US)
- メッセージ(Messages): 翻訳テキストの集合
- 数値・日付フォーマット: 地域固有の表示形式
Vue.js i18n の最大の魅力は、コンポーネントベースのアーキテクチャと自然に統合できる点です。テンプレート内で翻訳キーを指定するだけで、動的に言語を切り替えることができます。
必要なライブラリとセットアップ
Vue.js で多言語対応を実現するには、公式のvue-i18n
ライブラリを使用します。まずは必要なパッケージをインストールしましょう。
bashyarn add vue-i18n@next
Vue 3 を使用している場合は、@next
タグを付けることで最新版をインストールできます。
基本的なセットアップ
まず、i18n の設定ファイルを作成します。このファイルで翻訳メッセージとロケール設定を管理します。
javascript// src/i18n/index.js
import { createI18n } from 'vue-i18n';
// 日本語の翻訳メッセージ
const ja = {
message: {
hello: 'こんにちは',
welcome: 'ようこそ',
goodbye: 'さようなら',
},
};
// 英語の翻訳メッセージ
const en = {
message: {
hello: 'Hello',
welcome: 'Welcome',
goodbye: 'Goodbye',
},
};
// i18nインスタンスの作成
const i18n = createI18n({
legacy: false, // Vue 3のComposition APIに対応
locale: 'ja', // デフォルト言語
fallbackLocale: 'en', // フォールバック言語
messages: {
ja,
en,
},
});
export default i18n;
メインアプリケーションでの設定
作成した i18n 設定をメインアプリケーションに統合します。
javascript// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import i18n from './i18n';
const app = createApp(App);
app.use(i18n);
app.mount('#app');
基本的な実装手順
i18n の基本的な使用方法を学びましょう。Vue.js のテンプレート内で翻訳キーを使用する方法をご紹介します。
テンプレートでの翻訳使用
Vue.js のテンプレート内では、$t
関数を使用して翻訳メッセージを表示できます。
vue<!-- src/components/HelloWorld.vue -->
<template>
<div class="hello-world">
<h1>{{ $t('message.hello') }}</h1>
<p>{{ $t('message.welcome') }}</p>
<button @click="sayGoodbye">
{{ $t('message.goodbye') }}
</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
methods: {
sayGoodbye() {
alert(this.$t('message.goodbye'));
},
},
};
</script>
Composition API での使用
Vue 3 の Composition API を使用する場合は、useI18n
関数を使用します。
vue<!-- src/components/ModernHello.vue -->
<template>
<div class="modern-hello">
<h1>{{ t('message.hello') }}</h1>
<p>{{ t('message.welcome') }}</p>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>
動的パラメータの使用
翻訳メッセージに動的な値を含めることも可能です。
javascript// 翻訳メッセージの定義
const ja = {
message: {
hello: 'こんにちは、{name}さん',
items: '{count}個のアイテムがあります',
},
};
const en = {
message: {
hello: 'Hello, {name}',
items: 'There are {count} items',
},
};
vue<!-- テンプレートでの使用 -->
<template>
<div>
<p>{{ $t('message.hello', { name: userName }) }}</p>
<p>{{ $t('message.items', { count: itemCount }) }}</p>
</div>
</template>
<script>
export default {
data() {
return {
userName: '田中',
itemCount: 5,
};
},
};
</script>
動的言語切り替えの実装
ユーザーが言語を動的に切り替えられる機能を実装しましょう。これにより、ユーザーエクスペリエンスが大幅に向上します。
言語切り替えコンポーネント
言語切り替え用のコンポーネントを作成します。
vue<!-- src/components/LanguageSwitcher.vue -->
<template>
<div class="language-switcher">
<select
v-model="currentLocale"
@change="changeLanguage"
>
<option value="ja">日本語</option>
<option value="en">English</option>
<option value="zh">中文</option>
</select>
</div>
</template>
<script>
export default {
name: 'LanguageSwitcher',
data() {
return {
currentLocale: this.$i18n.locale,
};
},
methods: {
changeLanguage() {
this.$i18n.locale = this.currentLocale;
// ローカルストレージに保存
localStorage.setItem('locale', this.currentLocale);
},
},
mounted() {
// ページ読み込み時に保存された言語を復元
const savedLocale = localStorage.getItem('locale');
if (savedLocale) {
this.currentLocale = savedLocale;
this.$i18n.locale = savedLocale;
}
},
};
</script>
<style scoped>
.language-switcher {
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
select {
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
</style>
より高度な言語切り替え
フラグアイコンとドロップダウンメニューを使用した、より洗練された言語切り替えコンポーネントも作成できます。
vue<!-- src/components/AdvancedLanguageSwitcher.vue -->
<template>
<div class="advanced-language-switcher">
<div class="current-language" @click="toggleDropdown">
<span class="flag">🇯🇵</span>
<span class="language-name">{{
getCurrentLanguageName()
}}</span>
<span class="arrow">▼</span>
</div>
<div v-if="isDropdownOpen" class="dropdown">
<div
v-for="lang in availableLanguages"
:key="lang.code"
class="language-option"
@click="selectLanguage(lang.code)"
>
<span class="flag">{{ lang.flag }}</span>
<span class="language-name">{{ lang.name }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'AdvancedLanguageSwitcher',
data() {
return {
isDropdownOpen: false,
availableLanguages: [
{ code: 'ja', name: '日本語', flag: '🇯🇵' },
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'zh', name: '中文', flag: '🇨🇳' },
],
};
},
computed: {
currentLocale() {
return this.$i18n.locale;
},
},
methods: {
toggleDropdown() {
this.isDropdownOpen = !this.isDropdownOpen;
},
selectLanguage(locale) {
this.$i18n.locale = locale;
localStorage.setItem('locale', locale);
this.isDropdownOpen = false;
},
getCurrentLanguageName() {
const lang = this.availableLanguages.find(
(l) => l.code === this.currentLocale
);
return lang ? lang.name : '日本語';
},
},
mounted() {
const savedLocale = localStorage.getItem('locale');
if (savedLocale) {
this.$i18n.locale = savedLocale;
}
},
};
</script>
<style scoped>
.advanced-language-switcher {
position: relative;
display: inline-block;
}
.current-language {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
user-select: none;
}
.dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.language-option {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
}
.language-option:hover {
background: #f5f5f5;
}
.flag {
font-size: 16px;
}
.language-name {
font-size: 14px;
}
.arrow {
font-size: 12px;
color: #666;
}
</style>
ルーティングと多言語対応
Vue Router と組み合わせて、URL に言語情報を含める多言語ルーティングを実装しましょう。これにより、SEO 対策とユーザビリティの両方を向上させることができます。
多言語ルーティングの設定
Vue Router で言語プレフィックスを含むルーティングを設定します。
javascript// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';
// サポートする言語
const supportedLocales = ['ja', 'en', 'zh'];
// ルートの定義
const routes = [
{
path: '/',
redirect: '/ja',
},
{
path: '/:locale',
component: {
template: '<router-view />',
},
beforeEnter: (to, from, next) => {
const locale = to.params.locale;
if (supportedLocales.includes(locale)) {
next();
} else {
next('/ja');
}
},
children: [
{
path: '',
name: 'home',
component: Home,
},
{
path: 'about',
name: 'about',
component: About,
},
],
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
ナビゲーションコンポーネント
多言語対応のナビゲーションメニューを作成します。
vue<!-- src/components/MultilingualNavigation.vue -->
<template>
<nav class="multilingual-nav">
<div class="nav-links">
<router-link
:to="{
name: 'home',
params: { locale: currentLocale },
}"
class="nav-link"
>
{{ $t('navigation.home') }}
</router-link>
<router-link
:to="{
name: 'about',
params: { locale: currentLocale },
}"
class="nav-link"
>
{{ $t('navigation.about') }}
</router-link>
</div>
<LanguageSwitcher />
</nav>
</template>
<script>
import LanguageSwitcher from './LanguageSwitcher.vue';
export default {
name: 'MultilingualNavigation',
components: {
LanguageSwitcher,
},
computed: {
currentLocale() {
return this.$route.params.locale || 'ja';
},
},
};
</script>
<style scoped>
.multilingual-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.nav-links {
display: flex;
gap: 2rem;
}
.nav-link {
text-decoration: none;
color: #333;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.2s;
}
.nav-link:hover {
background: #e9ecef;
}
.nav-link.router-link-active {
background: #007bff;
color: white;
}
</style>
ルートガードでの言語設定
ルート変更時に自動的に言語を設定するガードを実装します。
javascript// src/router/index.js に追加
import i18n from '../i18n';
router.beforeEach((to, from, next) => {
const locale = to.params.locale;
if (locale && supportedLocales.includes(locale)) {
i18n.global.locale = locale;
localStorage.setItem('locale', locale);
}
next();
});
パフォーマンス最適化
多言語サイトでは、翻訳ファイルのサイズと読み込み速度が重要な課題となります。効率的な実装方法をご紹介します。
遅延読み込み(Lazy Loading)
必要な言語の翻訳ファイルのみを動的に読み込むことで、初期読み込み時間を短縮できます。
javascript// src/i18n/index.js
import { createI18n } from 'vue-i18n';
// 基本言語(日本語)は即座に読み込み
const ja = {
message: {
hello: 'こんにちは',
welcome: 'ようこそ',
},
};
const i18n = createI18n({
legacy: false,
locale: 'ja',
fallbackLocale: 'ja',
messages: {
ja,
},
});
// 動的言語読み込み関数
export async function loadLanguageAsync(locale) {
if (locale === 'ja') {
return; // 日本語は既に読み込み済み
}
try {
const messages = await import(`./locales/${locale}.js`);
i18n.global.setLocaleMessage(locale, messages.default);
i18n.global.locale = locale;
} catch (error) {
console.error(
`Failed to load language: ${locale}`,
error
);
// フォールバック言語に切り替え
i18n.global.locale = 'ja';
}
}
export default i18n;
翻訳ファイルの分割
大きな翻訳ファイルを機能別に分割することで、必要な部分のみを読み込むことができます。
javascript// src/i18n/locales/ja/common.js
export default {
navigation: {
home: 'ホーム',
about: '会社概要',
contact: 'お問い合わせ'
},
buttons: {
submit: '送信',
cancel: 'キャンセル',
save: '保存'
}
}
// src/i18n/locales/ja/auth.js
export default {
login: {
title: 'ログイン',
email: 'メールアドレス',
password: 'パスワード',
submit: 'ログインする'
},
register: {
title: '新規登録',
name: 'お名前',
email: 'メールアドレス',
password: 'パスワード',
confirmPassword: 'パスワード(確認)'
}
}
翻訳のキャッシュ機能
一度読み込んだ翻訳をキャッシュして、再読み込みを防ぎます。
javascript// src/i18n/cache.js
class TranslationCache {
constructor() {
this.cache = new Map();
}
async get(locale) {
if (this.cache.has(locale)) {
return this.cache.get(locale);
}
const messages = await this.loadMessages(locale);
this.cache.set(locale, messages);
return messages;
}
async loadMessages(locale) {
const modules = await Promise.all([
import(`./locales/${locale}/common.js`),
import(`./locales/${locale}/auth.js`),
]);
return modules.reduce((acc, module) => {
return { ...acc, ...module.default };
}, {});
}
clear() {
this.cache.clear();
}
}
export default new TranslationCache();
実装例とサンプルコード
実際のプロジェクトで使用できる、包括的な多言語サイトの実装例をご紹介します。
完全な多言語サイトの構造
vue<!-- src/App.vue -->
<template>
<div id="app">
<MultilingualNavigation />
<main class="main-content">
<router-view />
</main>
<footer class="footer">
<p>{{ $t('footer.copyright') }}</p>
</footer>
</div>
</template>
<script>
import MultilingualNavigation from './components/MultilingualNavigation.vue';
export default {
name: 'App',
components: {
MultilingualNavigation,
},
};
</script>
<style>
#app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
padding: 2rem;
}
.footer {
background: #f8f9fa;
padding: 1rem;
text-align: center;
border-top: 1px solid #e9ecef;
}
</style>
多言語対応のフォームコンポーネント
vue<!-- src/components/MultilingualForm.vue -->
<template>
<form
@submit.prevent="handleSubmit"
class="multilingual-form"
>
<div class="form-group">
<label for="name">{{ $t('form.name') }}</label>
<input
id="name"
v-model="formData.name"
type="text"
:placeholder="$t('form.namePlaceholder')"
required
/>
<span v-if="errors.name" class="error">{{
errors.name
}}</span>
</div>
<div class="form-group">
<label for="email">{{ $t('form.email') }}</label>
<input
id="email"
v-model="formData.email"
type="email"
:placeholder="$t('form.emailPlaceholder')"
required
/>
<span v-if="errors.email" class="error">{{
errors.email
}}</span>
</div>
<div class="form-group">
<label for="message">{{ $t('form.message') }}</label>
<textarea
id="message"
v-model="formData.message"
:placeholder="$t('form.messagePlaceholder')"
rows="4"
required
></textarea>
<span v-if="errors.message" class="error">{{
errors.message
}}</span>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">
{{ $t('form.submit') }}
</button>
<button
type="button"
@click="resetForm"
class="btn-secondary"
>
{{ $t('form.reset') }}
</button>
</div>
</form>
</template>
<script>
export default {
name: 'MultilingualForm',
data() {
return {
formData: {
name: '',
email: '',
message: '',
},
errors: {},
};
},
methods: {
validateForm() {
this.errors = {};
if (!this.formData.name.trim()) {
this.errors.name = this.$t(
'validation.nameRequired'
);
}
if (!this.formData.email.trim()) {
this.errors.email = this.$t(
'validation.emailRequired'
);
} else if (!this.isValidEmail(this.formData.email)) {
this.errors.email = this.$t(
'validation.emailInvalid'
);
}
if (!this.formData.message.trim()) {
this.errors.message = this.$t(
'validation.messageRequired'
);
}
return Object.keys(this.errors).length === 0;
},
isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
},
async handleSubmit() {
if (!this.validateForm()) {
return;
}
try {
// フォーム送信処理
await this.submitForm(this.formData);
this.$emit(
'success',
this.$t('form.submitSuccess')
);
this.resetForm();
} catch (error) {
this.$emit('error', this.$t('form.submitError'));
}
},
resetForm() {
this.formData = {
name: '',
email: '',
message: '',
};
this.errors = {};
},
async submitForm(data) {
// 実際のAPI呼び出しをここに実装
console.log('Submitting form:', data);
},
},
};
</script>
<style scoped>
.multilingual-form {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
input,
textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
input:focus,
textarea:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.error {
color: #dc3545;
font-size: 14px;
margin-top: 0.25rem;
display: block;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.btn-primary,
.btn-secondary {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover {
background: #0056b3;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #545b62;
}
</style>
よくある問題と解決策
多言語サイト開発で頻繁に発生する問題とその解決策をご紹介します。
問題 1: 翻訳キーが見つからないエラー
csharp[Vue warn]: Cannot translate the value of keypath 'message.hello'. Use the value of keypath as default.
このエラーは、指定した翻訳キーが存在しない場合に発生します。
解決策: フォールバック機能を実装します。
javascript// src/i18n/index.js
const i18n = createI18n({
legacy: false,
locale: 'ja',
fallbackLocale: 'en',
missing: (locale, key) => {
console.warn(
`Missing translation: ${key} for locale: ${locale}`
);
return key; // キーをそのまま返す
},
messages: {
ja,
en,
},
});
問題 2: 動的コンテンツの翻訳
ユーザーが入力したコンテンツを翻訳する必要がある場合の対処法です。
vue<!-- src/components/DynamicContent.vue -->
<template>
<div class="dynamic-content">
<div v-if="isTranslated">
<h3>{{ $t('content.translated') }}</h3>
<p>{{ translatedContent }}</p>
</div>
<div v-else>
<h3>{{ $t('content.original') }}</h3>
<p>{{ originalContent }}</p>
<button
@click="translateContent"
class="btn-translate"
>
{{ $t('content.translateButton') }}
</button>
</div>
</div>
</template>
<script>
export default {
name: 'DynamicContent',
data() {
return {
originalContent: 'This is original content',
translatedContent: '',
isTranslated: false,
};
},
methods: {
async translateContent() {
try {
// 翻訳APIを呼び出し
const response = await this.callTranslationAPI(
this.originalContent,
this.$i18n.locale
);
this.translatedContent = response.translatedText;
this.isTranslated = true;
} catch (error) {
console.error('Translation failed:', error);
this.$emit(
'translation-error',
this.$t('errors.translationFailed')
);
}
},
async callTranslationAPI(text, targetLocale) {
// 実際の翻訳API呼び出しをここに実装
// 例: Google Translate API, DeepL API など
return new Promise((resolve) => {
setTimeout(() => {
resolve({
translatedText: `翻訳されたテキスト: ${text}`,
});
}, 1000);
});
},
},
};
</script>
問題 3: SEO 対策とメタタグ
多言語サイトでの SEO 対策は重要です。各言語版に適切なメタタグを設定します。
vue<!-- src/components/MetaTags.vue -->
<template>
<div>
<!-- 動的にメタタグを更新 -->
</div>
</template>
<script>
export default {
name: 'MetaTags',
watch: {
'$i18n.locale': {
handler(newLocale) {
this.updateMetaTags(newLocale);
},
immediate: true,
},
},
methods: {
updateMetaTags(locale) {
const title = this.$t('meta.title');
const description = this.$t('meta.description');
const keywords = this.$t('meta.keywords');
// タイトルの更新
document.title = title;
// メタタグの更新
this.updateMetaTag('description', description);
this.updateMetaTag('keywords', keywords);
// hreflangタグの更新
this.updateHreflangTags(locale);
},
updateMetaTag(name, content) {
let meta = document.querySelector(
`meta[name="${name}"]`
);
if (!meta) {
meta = document.createElement('meta');
meta.name = name;
document.head.appendChild(meta);
}
meta.content = content;
},
updateHreflangTags(currentLocale) {
const supportedLocales = ['ja', 'en', 'zh'];
const currentUrl = window.location.pathname;
// 既存のhreflangタグを削除
const existingHreflangs = document.querySelectorAll(
'link[rel="alternate"][hreflang]'
);
existingHreflangs.forEach((tag) => tag.remove());
// 新しいhreflangタグを追加
supportedLocales.forEach((locale) => {
const link = document.createElement('link');
link.rel = 'alternate';
link.hreflang = locale;
link.href = `${
window.location.origin
}/${locale}${currentUrl.replace(
/^\/[a-z]{2}/,
''
)}`;
document.head.appendChild(link);
});
},
},
};
</script>
問題 4: パフォーマンス最適化
大量の翻訳ファイルがある場合の最適化手法です。
javascript// src/i18n/optimizer.js
class TranslationOptimizer {
constructor() {
this.loadedModules = new Set();
this.preloadQueue = [];
}
// 必要な翻訳のみをプリロード
preloadCriticalTranslations(locale) {
const criticalKeys = ['navigation', 'common', 'errors'];
criticalKeys.forEach((key) => {
this.preloadModule(locale, key);
});
}
async preloadModule(locale, moduleName) {
const moduleKey = `${locale}-${moduleName}`;
if (this.loadedModules.has(moduleKey)) {
return;
}
try {
await import(`./locales/${locale}/${moduleName}.js`);
this.loadedModules.add(moduleKey);
} catch (error) {
console.warn(
`Failed to preload module: ${moduleKey}`
);
}
}
// 使用されていない翻訳をクリーンアップ
cleanupUnusedTranslations() {
// 実装例: 使用されていない翻訳キーを特定して削除
console.log('Cleaning up unused translations...');
}
}
export default new TranslationOptimizer();
まとめ
Vue.js で多言語サイトを構築する際の最適解について、実践的なアプローチでご紹介しました。
重要なポイントをまとめると:
- 適切なライブラリ選択:
vue-i18n
の最新版を使用し、Vue 3 の Composition API に対応 - 効率的なファイル管理: 翻訳ファイルを機能別に分割し、遅延読み込みを活用
- ユーザー体験の向上: 動的言語切り替えとローカルストレージでの設定保存
- SEO 対策: 多言語ルーティングとメタタグの適切な設定
- パフォーマンス最適化: 必要な翻訳のみを読み込み、キャッシュ機能を活用
- エラーハンドリング: 翻訳キーが見つからない場合の適切なフォールバック
多言語サイトの構築は複雑に感じられるかもしれませんが、適切な設計と実装により、保守性が高く、ユーザーフレンドリーなサイトを作成できます。
実際のプロジェクトでは、最初から完璧を目指すのではなく、段階的に機能を追加していくことをお勧めします。まずは基本的な翻訳機能から始めて、徐々に高度な機能を実装していくことで、安定した多言語サイトを構築できるでしょう。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来