Internationalization

Maizzle supports internationalization through vue-i18n. This lets you create email templates in multiple languages using translation keys that resolve to localized strings at build time.

Starter project

The quickest way to get started is with maizzle/starter-i18n, which has a working example of the setup described in this guide.

bash
npx maizzle new maizzle/starter-i18n --install

Installation

To add internalization to an existing project, start by installing vue-i18n:

bash
npm install vue-i18n

Setup

Locale files

Create a locales directory in your project root with a JSON file for each language:

locales/
├── en.json
└── fr.json
{
  "greeting": "Hello, World!",
  "farewell": "Goodbye!"
}
{
  "greeting": "Bonjour, le monde !",
  "farewell": "Au revoir !"
}

Configuration

Import your locale files and register vue-i18n as a Vue plugin:

maizzle.config.ts
import { defineConfig } from '@maizzle/framework'
import { createI18n } from 'vue-i18n'
import en from './locales/en.json'
import fr from './locales/fr.json'

export default defineConfig({
  vue: {
    plugins: () => [
      createI18n({
        legacy: false,
        locale: 'en',
        fallbackLocale: 'en',
        messages: { en, fr },
      }),
    ],
  },
})

Usage

Global locale

With the config above, all templates render using the default locale ('en' in this example). Use $t() to output translated strings:

vue
<template>
  <Layout>
    <Heading level="1">{{ $t('greeting') }}</Heading>
    <Text>{{ $t('farewell') }}</Text>
  </Layout>
</template>

This outputs Hello, World! and Goodbye! because the default locale is set to 'en'.

Per-template locale

To render a specific template in a different language, use useI18n() in <script setup>:

vue
<script setup>
import { useI18n } from 'vue-i18n'

const { locale } = useI18n()
locale.value = 'fr'
</script>

<template>
  <Layout>
    <Heading level="1">{{ $t('greeting') }}</Heading>
  </Layout>
</template>

This outputs "Bonjour, le monde !" regardless of the default locale in the config.

Using t() instead of $t()

When using useI18n(), you can destructure the t function if you prefer:

vue
<script setup>
import { useI18n } from 'vue-i18n'

const { t } = useI18n()
</script>

<template>
  <Layout>
    <Heading level="1">{{ t('greeting') }}</Heading>
    <Text>{{ t('farewell') }}</Text>
  </Layout>
</template>

Both $t() and t() work the same way, only difference is that $t() is globally available while t() requires the useI18n() import.

Building the same template in multiple languages

A common pattern for email localization is to build the same template once per language.

To get started, create a wrapper template for each locale:

emails/
├── welcome/
│   ├── en.vue
│   └── fr.vue
<script setup>
import { useI18n } from 'vue-i18n'

const { locale } = useI18n()
locale.value = 'en'
</script>

<template>
  <Welcome />
</template>
<script setup>
import { useI18n } from 'vue-i18n'

const { locale } = useI18n()
locale.value = 'fr'
</script>

<template>
  <Welcome />
</template>

Then, create a shared components/Welcome.vue component that uses $t() for all translatable strings. Each wrapper sets the locale and renders the same component, producing one HTML file per language in your output.

components/Welcome.vue
<template>
  <Layout>
    <Heading level="1">{{ $t('greeting') }}</Heading>
    <Text>{{ $t('farewell') }}</Text>
  </Layout>
</template>

Adding a new language

  1. Create a new locale file, e.g. locales/ro.json
  2. Import it in maizzle.config.ts and add it to the messages object:
maizzle.config.ts
import en from './locales/en.json'
import fr from './locales/fr.json'
import ro from './locales/ro.json'
// ...
plugins: () => [
  createI18n({
    legacy: false,
    locale: 'en',
    fallbackLocale: 'en',
    messages: { en, fr, ro },  }),
],

The fallbackLocale option ensures that if a key is missing in ro.json, it falls back to the English translation instead of showing the raw key.

Preview locale changes in dev

Static imports like import en from './locales/en.json' are cached by Node.js, so editing a locale file during development won't update the preview.

To enable HMR when making updates to translations, use readFileSync instead so the files are read fresh each time the config is re-evaluated:

maizzle.config.ts
import { defineConfig } from '@maizzle/framework'
import { createI18n } from 'vue-i18n'
import { readFileSync } from 'node:fs'

const en = JSON.parse(readFileSync('./locales/en.json', 'utf-8'))
const fr = JSON.parse(readFileSync('./locales/fr.json', 'utf-8'))

export default defineConfig({
  vue: {
    plugins: () => [
      createI18n({
        legacy: false,
        locale: 'en',
        fallbackLocale: 'en',
        messages: { en, fr },
      }),
    ],
  },
})

Maizzle will automatically watch the locales directory in your project root for changes and re-render templates when a locale file is updated.

If you store locale files somewhere else, add the path to the watch array in the config:

maizzle.config.ts
import { defineConfig } from '@maizzle/framework'

export default defineConfig({
  server: {
    watch: ['./messages/**/*.json'],
  },
})