Overview
Portal Ciudadano Manta supports three languages using Vue I18n 9.14+ :
Configuration
The i18n instance is configured with custom settings to handle special characters:
import { createI18n } from "vue-i18n" ;
import enJson from "./locales/en.json" ;
import esJson from "./locales/es.json" ;
import quJson from "./locales/qu.json" ;
const en = enJson as Record < string , any >;
const es = esJson as Record < string , any >;
const qu = quJson as Record < string , any >;
// Get saved language safely
const getSavedLanguage = () => {
if ( typeof window !== "undefined" && window . localStorage ) {
return localStorage . getItem ( "userLanguage" ) || "es" ;
}
return "es" ;
};
const i18n = createI18n ({
legacy: false , // Use Composition API
globalInjection: true , // Allow $t in templates
locale: getSavedLanguage (), // Initial language
fallbackLocale: "es" ,
// Custom message resolver to prevent issues with @ symbols
messageResolver : ( obj : unknown , path : string ) => {
const keys = path . split ( '.' );
let result : any = obj ;
for ( const key of keys ) {
if ( result && typeof result === 'object' && key in result ) {
result = result [ key ];
} else {
return null ;
}
}
return result ;
},
// Disable modifiers and warnings
modifiers: {},
warnHtmlMessage: 'off' ,
missingWarn: false ,
fallbackWarn: false ,
warnHtmlInMessage: 'off' ,
messages: { es , en , qu },
});
export default i18n ;
The custom messageResolver is configured to handle email addresses and special characters like @ without triggering i18n parsing errors.
Translation Files Structure
All translations are stored in JSON files under src/i18n/locales/:
src/i18n/locales/
├── es.json # Spanish (default)
├── en.json # English
└── qu.json # Quechua/Kichwa
Example Translation Structure
Spanish (es.json)
English (en.json)
Quechua (qu.json)
{
"navbar" : {
"title" : "Portal Ciudadano Manta" ,
"subtitle" : "Sistema de Gestión Ciudadana" ,
"home" : "Inicio" ,
"about" : "Sobre Nosotros" ,
"login" : "Iniciar Sesión" ,
"register" : "Registrarse" ,
"dashboard" : "Dashboard" ,
"profile" : "Perfil" ,
"logout" : "Cerrar sesión" ,
"toolsMenu" : {
"report" : "Reportar Problema" ,
"surveys" : "Encuestas" ,
"news" : "Noticias"
}
},
"home" : {
"search" : {
"placeholder" : "Buscar servicios, trámites..."
},
"hero" : {
"title" : "Portal Ciudadano Manta" ,
"subtitle" : "Donde la participación ciudadana se encuentra con la mejora continua"
}
}
}
{
"navbar" : {
"title" : "Manta Citizen Portal" ,
"subtitle" : "Citizen Management System" ,
"home" : "Home" ,
"about" : "About Us" ,
"login" : "Sign In" ,
"register" : "Sign Up" ,
"dashboard" : "Dashboard" ,
"profile" : "Profile" ,
"logout" : "Sign Out" ,
"toolsMenu" : {
"report" : "Report Problem" ,
"surveys" : "Surveys" ,
"news" : "News"
}
},
"home" : {
"search" : {
"placeholder" : "Search services, procedures..."
},
"hero" : {
"title" : "Manta Citizen Portal" ,
"subtitle" : "Where citizen participation meets continuous improvement"
}
}
}
{
"navbar" : {
"title" : "Manta Llaqta Punku" ,
"subtitle" : "Llaqta Kamachiy Sistema" ,
"home" : "Qallarina" ,
"about" : "Ñuqaykumanta" ,
"login" : "Yaykuy" ,
"register" : "Qillqakuy"
}
}
Usage in Components
Template Usage
< template >
< div >
<!-- Simple translation -->
< h1 > {{ $t ( 'navbar.title' ) }} </ h1 >
<!-- Nested keys -->
< button > {{ $t ( 'navbar.toolsMenu.report' ) }} </ button >
<!-- With parameters -->
< p > {{ $t ( 'messages.welcome' , { name: userName }) }} </ p >
<!-- Pluralization -->
< span > {{ $t ( 'reports.count' , { count: reportCount }, reportCount ) }} </ span >
</ div >
</ template >
Script Setup Usage
< script setup lang = "ts" >
import { useI18n } from 'vue-i18n' ;
const { t , locale } = useI18n ();
// Use translation in script
const pageTitle = t ( 'navbar.title' );
// Change language
const changeLanguage = ( lang : 'es' | 'en' | 'qu' ) => {
locale . value = lang ;
localStorage . setItem ( 'userLanguage' , lang );
};
// Dynamic translation
const getMessage = ( key : string ) => {
return t ( `messages. ${ key } ` );
};
</ script >
Language Switching
The application provides a useLanguage composable for language management:
src/composables/useLanguage.ts
import { computed } from 'vue' ;
import { useI18n } from 'vue-i18n' ;
export const useLanguage = () => {
const { locale , t } = useI18n ();
const currentLanguage = computed (() => locale . value );
const availableLanguages = [
{ code: 'es' , name: 'Español' , flag: '🇪🇸' },
{ code: 'en' , name: 'English' , flag: '🇬🇧' },
{ code: 'qu' , name: 'Kichwa' , flag: '🇵🇪' },
];
const changeLanguage = ( lang : string ) => {
locale . value = lang ;
localStorage . setItem ( 'userLanguage' , lang );
document . documentElement . lang = lang ;
};
return {
currentLanguage ,
availableLanguages ,
changeLanguage ,
t ,
};
};
Language Selector Component
< script setup lang = "ts" >
import { useLanguage } from '@/composables/useLanguage' ;
const { currentLanguage , availableLanguages , changeLanguage } = useLanguage ();
</ script >
< template >
< div class = "language-selector" >
< select
: value = " currentLanguage "
@ change = " changeLanguage ( $event . target . value ) "
>
< option
v-for = " lang in availableLanguages "
: key = " lang . code "
: value = " lang . code "
>
{{ lang . flag }} {{ lang . name }}
</ option >
</ select >
</ div >
</ template >
Translation Keys Reference
Common Keys
Navigation
Home Page
Reports
Forms
// Navbar
navbar . title
navbar . subtitle
navbar . home
navbar . about
navbar . login
navbar . register
navbar . dashboard
navbar . adminPanel
navbar . profile
navbar . logout
// Tools menu
navbar . toolsMenu . report
navbar . toolsMenu . surveys
navbar . toolsMenu . news
// Quick access
navbar . quick . report
navbar . quick . surveys
navbar . quick . news
// Search
home . search . placeholder
home . search . options . report . title
home . search . options . report . description
home . search . options . surveys . title
home . search . options . news . title
// Hero section
home . hero . title
home . hero . subtitle
home . hero . cta
home . hero . description
// Services
home . services . title
home . services . report . title
home . services . report . description
home . services . surveys . title
home . services . news . title
// Report page
reportar . title
reportar . subtitle
reportar . categoria
reportar . descripcion
reportar . ubicacion
reportar . imagen
reportar . submit
// Categories
reportar . categorias . alumbrado
reportar . categorias . baches
reportar . categorias . limpieza
reportar . categorias . agua
reportar . categorias . alcantarillado
// States
reportar . estados . pendiente
reportar . estados . en_revision
reportar . estados . en_proceso
reportar . estados . resuelto
reportar . estados . rechazado
// Common form labels
forms . email
forms . password
forms . confirmPassword
forms . nombres
forms . apellidos
forms . cedula
forms . parroquia
forms . barrio
// Buttons
forms . submit
forms . cancel
forms . save
forms . delete
forms . edit
// Validation
forms . required
forms . invalid
forms . minLength
forms . maxLength
Best Practices
Use nested keys for organization
{
"navbar" : {
"toolsMenu" : {
"report" : "Reportar Problema"
}
}
}
Access with: $t('navbar.toolsMenu.report')
Always provide fallback translations
Ensure all keys exist in all language files. Missing keys will fall back to Spanish (es).
Use parameters for dynamic content
{
"messages" : {
"welcome" : "Welcome, {name}!"
}
}
< p > {{ $t('messages.welcome', { name: userName }) }} </ p >
Handle pluralization properly
{
"reports" : {
"count" : "no reports | 1 report | {count} reports"
}
}
< span > {{ $t('reports.count', count, count) }} </ span >
Store language preference
const changeLanguage = ( lang : string ) => {
locale . value = lang ;
localStorage . setItem ( 'userLanguage' , lang );
document . documentElement . lang = lang ; // For accessibility
};
Adding New Translations
Add keys to all language files
Add the same key structure to es.json, en.json, and qu.json: {
"newFeature" : {
"title" : "Nueva Característica" ,
"description" : "Descripción en español"
}
}
Use in components
< h2 > {{ $t('newFeature.title') }} </ h2 >
< p > {{ $t('newFeature.description') }} </ p >
Test all languages
Switch between languages to ensure all translations display correctly.
TypeScript Support
For type-safe translations, you can create a type definition:
import 'vue-i18n' ;
import type es from '@/i18n/locales/es.json' ;
type MessageSchema = typeof es ;
declare module 'vue-i18n' {
export interface DefineLocaleMessage extends MessageSchema {}
}
This provides autocomplete for translation keys:
// ✅ Autocomplete works
const title = t ( 'navbar.title' );
// ❌ TypeScript error if key doesn't exist
const invalid = t ( 'navbar.nonexistent' );
Dates
< script setup lang = "ts" >
import { useI18n } from 'vue-i18n' ;
const { d } = useI18n ();
const date = new Date ();
</ script >
< template >
< p > {{ d ( date , 'long' ) }} </ p >
</ template >
Numbers
< script setup lang = "ts" >
import { useI18n } from 'vue-i18n' ;
const { n } = useI18n ();
const amount = 1234.56 ;
</ script >
< template >
< p > {{ n ( amount , 'currency' ) }} </ p >
</ template >
Accessibility Considerations
Always update the lang attribute on the <html> element when changing languages for proper screen reader support.
const changeLanguage = ( lang : string ) => {
locale . value = lang ;
document . documentElement . lang = lang ; // Important for accessibility
localStorage . setItem ( 'userLanguage' , lang );
};
Next Steps
Architecture Learn about the application architecture
Core Features Explore core platform features