feat: Add multi-language support (i18n)

Languages supported (8):
- English (en)
- 日本語 (ja)
- Deutsch (de)
- Français (fr)
- Español (es)
- 简体中文 (zh-CN)
- 繁體中文 (zh-TW)
- 한국어 (ko)

Changes:
- Add locale column to users table
- Add SetLocale middleware for automatic locale detection
- Add LocaleController for language switching
- Create language files with translations for all UI elements
- Add language selector to user profile page
- Update all Blade views to use translation strings
This commit is contained in:
2025-11-29 12:00:09 +09:00
parent ecfa21d56c
commit cdf0bf4bad
25 changed files with 1183 additions and 84 deletions

View File

@@ -15,7 +15,7 @@
<script>document.addEventListener('livewire:navigated', function() { hljs.highlightAll(); });</script>
<style>
pre code.hljs {
background: #1e1e1e !important; /* VSCode dark と同じ */
background: #1e1e1e !important;
color: #dcdcdc !important;
padding: 1rem;
border-radius: 8px;
@@ -50,7 +50,7 @@ class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
Quick Switch
{{ __('messages.quick_switcher.title') }}
<kbd class="ml-2 px-2 py-1 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded">
Ctrl+K
</kbd>
@@ -75,7 +75,7 @@ class="flex items-center text-sm font-medium text-gray-700 hover:text-gray-900 f
class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5"
>
<a href="{{ route('profile.edit') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Profile
{{ __('messages.nav.profile') }}
</a>
@if(Auth::user()->isAdmin())
<a href="{{ route('admin.users.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
@@ -83,21 +83,21 @@ class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
ユーザー管理
{{ __('messages.nav.user_management') }}
</span>
</a>
@endif
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="w-full text-left block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Logout
{{ __('messages.nav.logout') }}
</button>
</form>
</div>
</div>
@else
<a href="{{ route('login') }}" class="text-sm text-gray-700 hover:text-gray-900">
Login
{{ __('messages.nav.login') }}
</a>
@endauth
</div>
@@ -127,20 +127,17 @@ class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring
<!-- Global Keyboard Shortcuts -->
<script>
document.addEventListener('keydown', function(e) {
// Ctrl+K or Cmd+K for quick switcher
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
window.dispatchEvent(new CustomEvent('open-quick-switcher'));
}
});
// Sidebar folder state management
document.addEventListener('alpine:init', () => {
Alpine.data('sidebarState', () => ({
expandedFolders: [],
initExpandedFolders() {
// Load from localStorage
const stored = localStorage.getItem('kb_expanded_folders');
if (stored) {
try {
@@ -158,7 +155,6 @@ class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring
} else {
this.expandedFolders.push(path);
}
// Save to localStorage
localStorage.setItem('kb_expanded_folders', JSON.stringify(this.expandedFolders));
},