Skip to main content

Overview

Portal Ciudadano Manta supports three languages using Vue I18n 9.14+:

Español

Spanish (default)

English

English

Quechua

Kichwa/Quechua

Configuration

The i18n instance is configured with custom settings to handle special characters:
src/i18n/index.ts
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

{
  "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"
    }
  }
}

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

Best Practices

{
  "navbar": {
    "toolsMenu": {
      "report": "Reportar Problema"
    }
  }
}
Access with: $t('navbar.toolsMenu.report')
Ensure all keys exist in all language files. Missing keys will fall back to Spanish (es).
{
  "messages": {
    "welcome": "Welcome, {name}!"
  }
}
<p>{{ $t('messages.welcome', { name: userName }) }}</p>
{
  "reports": {
    "count": "no reports | 1 report | {count} reports"
  }
}
<span>{{ $t('reports.count', count, count) }}</span>
const changeLanguage = (lang: string) => {
  locale.value = lang;
  localStorage.setItem('userLanguage', lang);
  document.documentElement.lang = lang; // For accessibility
};

Adding New Translations

1

Add keys to all language files

Add the same key structure to es.json, en.json, and qu.json:
es.json
{
  "newFeature": {
    "title": "Nueva Característica",
    "description": "Descripción en español"
  }
}
2

Use in components

<h2>{{ $t('newFeature.title') }}</h2>
<p>{{ $t('newFeature.description') }}</p>
3

Test all languages

Switch between languages to ensure all translations display correctly.

TypeScript Support

For type-safe translations, you can create a type definition:
src/types/i18n.d.ts
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');

Locale-Specific Formatting

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

Build docs developers (and LLMs) love