Internationalization (i18n)
DeployStack supports multiple languages through Vue I18n with a modular file structure that organizes translations by feature. This approach makes it easy to maintain translations and add new languages.
Architecture Overview
The i18n system is designed with modularity and maintainability in mind:
- Feature-based organization: Translations are grouped by application features
- Modular structure: Each feature has its own translation file
- Scalable approach: Easy to add new languages and features
- Type safety: TypeScript support for translation keys
Directory Structure
src/i18n/
├── index.ts # Main i18n initialization
└── locales/
├── en/ # English translations
│ ├── index.ts # Exports all English translations
│ ├── common.ts # Common translations (buttons, labels, etc.)
│ ├── login.ts # Login page specific translations
│ ├── register.ts # Register page specific translations
│ ├── dashboard.ts # Dashboard specific translations
│ ├── mcp-servers.ts # MCP server management translations
│ └── deployment.ts # Deployment related translations
└── de/ # German translations (example)
├── index.ts
├── common.ts
└── ...
Setting Up i18n
Main Configuration
The main i18n configuration in src/i18n/index.ts
:
import { createI18n } from 'vue-i18n'
import en from './locales/en'
import de from './locales/de' // Example additional language
const i18n = createI18n({
legacy: false, // Use Composition API
locale: 'en', // Default language
fallbackLocale: 'en', // Fallback language
messages: {
en,
de
}
})
export default i18n
Locale Index File
Each locale has an index file that exports all translations:
// src/i18n/locales/en/index.ts
import common from './common'
import login from './login'
import register from './register'
import dashboard from './dashboard'
import mcpServers from './mcp-servers'
import deployment from './deployment'
export default {
common,
login,
register,
dashboard,
mcpServers,
deployment
}
Creating Translation Files
Common Translations
File: src/i18n/locales/en/common.ts
export default {
// Navigation
navigation: {
dashboard: 'Dashboard',
mcpServers: 'MCP Servers',
deployments: 'Deployments',
settings: 'Settings',
logout: 'Logout'
},
// Common buttons and actions
buttons: {
save: 'Save',
cancel: 'Cancel',
delete: 'Delete',
edit: 'Edit',
create: 'Create',
deploy: 'Deploy',
stop: 'Stop',
restart: 'Restart',
view: 'View',
download: 'Download'
},
// Status indicators
status: {
running: 'Running',
stopped: 'Stopped',
error: 'Error',
pending: 'Pending',
deploying: 'Deploying',
healthy: 'Healthy',
unhealthy: 'Unhealthy'
},
// Common labels
labels: {
name: 'Name',
description: 'Description',
status: 'Status',
created: 'Created',
updated: 'Updated',
version: 'Version',
author: 'Author',
email: 'Email',
password: 'Password'
},
// Validation messages
validation: {
required: 'This field is required',
email: 'Please enter a valid email address',
minLength: 'Must be at least {min} characters',
maxLength: 'Must be no more than {max} characters',
passwordMismatch: 'Passwords do not match'
},
// Common messages
messages: {
loading: 'Loading...',
saving: 'Saving...',
saved: 'Saved successfully',
error: 'An error occurred',
noData: 'No data available',
confirmDelete: 'Are you sure you want to delete this item?'
}
}
Feature-Specific Translations
File: src/i18n/locales/en/mcp-servers.ts
export default {
title: 'MCP Servers',
subtitle: 'Manage your Model Context Protocol servers',
catalog: {
title: 'MCP Server Catalog',
description: 'Browse and deploy MCP servers from our community catalog',
search: 'Search servers...',
categories: {
all: 'All Categories',
databases: 'Databases',
apis: 'APIs',
tools: 'Development Tools',
productivity: 'Productivity',
integrations: 'Integrations'
}
},
deployment: {
title: 'Deploy MCP Server',
selectProvider: 'Select Cloud Provider',
configure: 'Configure Deployment',
credentials: {
title: 'Credentials',
description: 'Configure API keys and authentication',
addCredential: 'Add Credential',
apiKey: 'API Key',
secretKey: 'Secret Key',
token: 'Access Token'
},
environment: {
title: 'Environment Variables',
description: 'Configure environment variables for your MCP server',
addVariable: 'Add Variable',
key: 'Variable Name',
value: 'Variable Value'
}
},
management: {
myServers: 'My Deployed Servers',
serverDetails: 'Server Details',
logs: 'View Logs',
metrics: 'Performance Metrics',
scale: 'Scale Server',
instances: 'Instances',
uptime: 'Uptime',
lastDeployed: 'Last Deployed'
},
forms: {
serverName: {
label: 'Server Name',
placeholder: 'Enter a name for your server deployment',
description: 'This will be used to identify your deployment'
},
region: {
label: 'Deployment Region',
placeholder: 'Select region',
description: 'Choose the region closest to your users'
}
},
actions: {
deployNow: 'Deploy Now',
viewInCatalog: 'View in Catalog',
manageServer: 'Manage Server',
viewLogs: 'View Logs',
stopServer: 'Stop Server',
restartServer: 'Restart Server',
deleteDeployment: 'Delete Deployment'
},
notifications: {
deploymentStarted: 'Deployment started successfully',
deploymentCompleted: 'Server deployed successfully',
deploymentFailed: 'Deployment failed: {error}',
serverStopped: 'Server stopped successfully',
serverRestarted: 'Server restarted successfully',
serverDeleted: 'Server deployment deleted'
}
}
Using Translations in Components
Basic Usage
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<div class="mcp-server-card">
<!-- Simple translation -->
<h2>{{ $t('mcpServers.title') }}</h2>
<!-- Translation in script -->
<p>{{ t('mcpServers.subtitle') }}</p>
<!-- Translation with parameters -->
<p>{{ $t('common.validation.minLength', { min: 8 }) }}</p>
<!-- Translation in attributes -->
<input :placeholder="$t('mcpServers.catalog.search')" />
<!-- Translation in v-if -->
<div v-if="isLoading">{{ $t('common.messages.loading') }}</div>
</div>
</template>
Advanced Usage with Computed Properties
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
interface Props {
serverStatus: 'running' | 'stopped' | 'error'
errorMessage?: string
}
const props = defineProps<Props>()
const statusText = computed(() => {
return t(`common.status.${props.serverStatus}`)
})
const errorMessage = computed(() => {
if (props.serverStatus === 'error' && props.errorMessage) {
return t('mcpServers.notifications.deploymentFailed', {
error: props.errorMessage
})
}
return null
})
</script>
<template>
<div class="server-status">
<span class="status-badge" :class="serverStatus">
{{ statusText }}
</span>
<p v-if="errorMessage" class="error-message">
{{ errorMessage }}
</p>
</div>
</template>
Form Validation with i18n
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const email = ref('')
const password = ref('')
const emailError = computed(() => {
if (!email.value) {
return t('common.validation.required')
}
if (!isValidEmail(email.value)) {
return t('common.validation.email')
}
return null
})
const passwordError = computed(() => {
if (!password.value) {
return t('common.validation.required')
}
if (password.value.length < 8) {
return t('common.validation.minLength', { min: 8 })
}
return null
})
function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
</script>
<template>
<form class="login-form">
<div class="form-field">
<label>{{ $t('common.labels.email') }}</label>
<input
v-model="email"
type="email"
:placeholder="$t('login.form.email.placeholder')"
:class="{ error: emailError }"
/>
<span v-if="emailError" class="error-text">{{ emailError }}</span>
</div>
<div class="form-field">
<label>{{ $t('common.labels.password') }}</label>
<input
v-model="password"
type="password"
:placeholder="$t('login.form.password.placeholder')"
:class="{ error: passwordError }"
/>
<span v-if="passwordError" class="error-text">{{ passwordError }}</span>
</div>
<button type="submit" :disabled="emailError || passwordError">
{{ $t('login.form.submit') }}
</button>
</form>
</template>
Adding New Languages
1. Create Language Directory
mkdir -p src/i18n/locales/de
2. Create Translation Files
Start with the common translations:
// src/i18n/locales/de/common.ts
export default {
navigation: {
dashboard: 'Dashboard',
mcpServers: 'MCP Server',
deployments: 'Bereitstellungen',
settings: 'Einstellungen',
logout: 'Abmelden'
},
buttons: {
save: 'Speichern',
cancel: 'Abbrechen',
delete: 'Löschen',
edit: 'Bearbeiten',
create: 'Erstellen',
deploy: 'Bereitstellen',
stop: 'Stoppen',
restart: 'Neu starten',
view: 'Anzeigen',
download: 'Herunterladen'
},
// ... continue with other translations
}
3. Create Locale Index
// src/i18n/locales/de/index.ts
import common from './common'
import login from './login'
import register from './register'
// ... import other feature translations
export default {
common,
login,
register,
// ... export other translations
}
4. Update Main i18n Configuration
// src/i18n/index.ts
import en from './locales/en'
import de from './locales/de'
const i18n = createI18n({
legacy: false,
locale: 'en',
fallbackLocale: 'en',
messages: {
en,
de // Add the new language
}
})
Language Switching
Language Selector Component
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { locale, availableLocales } = useI18n()
const languages = computed(() => [
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'de', name: 'Deutsch', flag: '🇩🇪' },
// Add more languages as needed
])
function changeLanguage(langCode: string) {
locale.value = langCode
// Optionally save to localStorage
localStorage.setItem('preferred-language', langCode)
}
// Load saved language preference
const savedLanguage = localStorage.getItem('preferred-language')
if (savedLanguage && availableLocales.includes(savedLanguage)) {
locale.value = savedLanguage
}
</script>
<template>
<div class="language-selector">
<select
:value="locale"
@change="changeLanguage($event.target.value)"
class="language-select"
>
<option
v-for="lang in languages"
:key="lang.code"
:value="lang.code"
>
{{ lang.flag }} {{ lang.name }}
</option>
</select>
</div>
</template>
Best Practices
1. Translation Key Naming
Use descriptive, hierarchical naming:
// Good
'mcpServers.deployment.credentials.title'
'common.validation.passwordTooShort'
'dashboard.widgets.serverStatus.healthy'
// Avoid
'title'
'error1'
'msg'
2. Parameterized Messages
Use parameters for dynamic content:
// Translation file
export default {
messages: {
welcomeUser: 'Welcome back, {userName}!',
itemsCount: 'You have {count} {count, plural, one {item} other {items}}',
deploymentTime: 'Deployed {timeAgo} ago'
}
}
// Usage
t('messages.welcomeUser', { userName: 'John' })
t('messages.itemsCount', { count: 5 })
3. Context-Aware Translations
Group related translations together:
export default {
serverStatus: {
running: {
label: 'Running',
description: 'Server is operating normally',
action: 'Stop Server'
},
stopped: {
label: 'Stopped',
description: 'Server is not running',
action: 'Start Server'
}
}
}
4. Handle Missing Translations
Always provide fallback values:
<template>
<!-- The fallback will be used if translation is missing -->
<h1>{{ $t('page.title', 'Default Page Title') }}</h1>
</template>
TypeScript Integration
Translation Key Types
Create type definitions for better IDE support:
// types/i18n.ts
export interface I18nMessages {
common: {
buttons: {
save: string
cancel: string
// ... other button translations
}
// ... other common translations
}
mcpServers: {
title: string
deployment: {
title: string
// ... other deployment translations
}
// ... other MCP server translations
}
}
// Extend the vue-i18n module
declare module 'vue-i18n' {
export interface DefineLocaleMessage extends I18nMessages {}
}
Typed Translation Function
import { useI18n } from 'vue-i18n'
import type { I18nMessages } from '@/types/i18n'
// Create a typed version of the translation function
export function useTypedI18n() {
const { t, ...rest } = useI18n<I18nMessages>()
return { t, ...rest }
}
Testing Translations
Testing Translation Keys
// tests/i18n.test.ts
import { describe, it, expect } from 'vitest'
import en from '@/i18n/locales/en'
describe('i18n translations', () => {
it('should have all required common translations', () => {
expect(en.common.buttons.save).toBeDefined()
expect(en.common.buttons.cancel).toBeDefined()
expect(en.common.validation.required).toBeDefined()
})
it('should have MCP server translations', () => {
expect(en.mcpServers.title).toBeDefined()
expect(en.mcpServers.deployment.title).toBeDefined()
})
})
Component Translation Testing
// tests/components/LanguageSelector.test.ts
import { mount } from '@vue/test-utils'
import { createI18n } from 'vue-i18n'
import LanguageSelector from '@/components/LanguageSelector.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: { test: 'Test' },
de: { test: 'Test' }
}
})
describe('LanguageSelector', () => {
it('should change language when selected', async () => {
const wrapper = mount(LanguageSelector, {
global: {
plugins: [i18n]
}
})
const select = wrapper.find('select')
await select.setValue('de')
expect(i18n.global.locale.value).toBe('de')
})
})
Performance Considerations
Lazy Loading Translations
For large applications, consider lazy loading translation files:
// src/i18n/index.ts
import { createI18n } from 'vue-i18n'
const i18n = createI18n({
legacy: false,
locale: 'en',
fallbackLocale: 'en',
messages: {}
})
// Lazy load function
async function loadLocaleMessages(locale: string) {
if (i18n.global.availableLocales.includes(locale)) {
return
}
try {
const messages = await import(`./locales/${locale}/index.ts`)
i18n.global.setLocaleMessage(locale, messages.default)
} catch (error) {
console.error(`Failed to load locale ${locale}:`, error)
}
}
// Usage in components
export async function setLanguage(locale: string) {
await loadLocaleMessages(locale)
i18n.global.locale.value = locale
}
Translation Caching
Implement caching for frequently used translations:
// utils/translationCache.ts
const cache = new Map<string, string>()
export function getCachedTranslation(key: string, params?: any): string | null {
const cacheKey = `${key}-${JSON.stringify(params || {})}`
return cache.get(cacheKey) || null
}
export function setCachedTranslation(key: string, params: any, value: string): void {
const cacheKey = `${key}-${JSON.stringify(params || {})}`
cache.set(cacheKey, value)
}
Common Patterns
Form Validation Messages
// i18n/locales/en/validation.ts
export default {
serverName: {
required: 'Server name is required',
minLength: 'Server name must be at least 3 characters',
maxLength: 'Server name cannot exceed 50 characters',
invalidChars: 'Server name can only contain letters, numbers, and hyphens'
},
deployment: {
region: {
required: 'Please select a deployment region',
invalid: 'Selected region is not available'
},
credentials: {
apiKey: {
required: 'API key is required',
invalid: 'Invalid API key format'
}
}
}
}
Status and Notification Messages
// i18n/locales/en/notifications.ts
export default {
success: {
serverDeployed: 'MCP server deployed successfully',
configurationSaved: 'Configuration saved',
credentialsUpdated: 'Credentials updated securely'
},
error: {
deploymentFailed: 'Deployment failed: {reason}',
networkError: 'Network connection error. Please try again.',
unauthorized: 'You are not authorized to perform this action',
serverNotFound: 'Server not found or has been deleted'
},
warning: {
unsavedChanges: 'You have unsaved changes. Are you sure you want to leave?',
serverRestarting: 'Server is restarting. This may take a few minutes.',
quotaExceeded: 'You have reached your deployment quota'
}
}
Navigation and Menu Items
// i18n/locales/en/navigation.ts
export default {
main: {
dashboard: 'Dashboard',
catalog: 'MCP Catalog',
deployments: 'My Deployments',
teams: 'Teams',
settings: 'Settings'
},
breadcrumbs: {
home: 'Home',
mcpServers: 'MCP Servers',
deployment: 'Deployment',
configuration: 'Configuration'
},
actions: {
deploy: 'Deploy Server',
configure: 'Configure',
manage: 'Manage',
monitor: 'Monitor',
scale: 'Scale'
}
}
Internationalization Checklist
Before Adding New Features
- Plan translation structure for new components
- Identify reusable common translations
- Consider context and parameterization needs
- Plan for pluralization if needed
During Development
- Use translation keys instead of hardcoded text
- Add proper TypeScript types for new translations
- Test with different languages if available
- Consider text length variations between languages
Before Release
- Ensure all user-facing text is translatable
- Test language switching functionality
- Verify fallback translations work
- Check for missing translation keys
- Test form validation messages in different languages
Maintenance
Regular Translation Updates
- Review translation completeness for each supported language
- Update outdated translations when features change
- Add missing translations for new features
- Remove unused translation keys to keep files clean
Translation File Organization
Keep translation files organized and maintainable:
# Good organization
src/i18n/locales/en/
├── common.ts # Shared across app
├── navigation.ts # Navigation items
├── validation.ts # Form validation
├── notifications.ts # Success/error messages
├── features/
│ ├── mcp-servers.ts
│ ├── deployment.ts
│ └── dashboard.ts
Version Control Best Practices
- Keep translation files in version control
- Use meaningful commit messages for translation changes
- Consider separate PRs for translation updates
- Document translation conventions in your project
This comprehensive i18n setup ensures your DeployStack frontend can grow to support multiple languages while maintaining clean, organized, and maintainable translation files.