Skip to content

Repository files navigation

Maizzle Starter

Internationalization (i18n) Starter

Version Build Downloads License

Internationalization (i18n)

Maizzle 6 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.

Getting started

Create a new Maizzle project using this starter:

npx maizzle new maizzle/starter-i18n

Install the dependencies:

cd starter-i18n
npm install

What's in this starter

Locale files

Translations live in locales/, one JSON file per language:

locales/
├── en.json
└── fr.json

locales/en.json (excerpt):

{
  "greeting": "Hello there!",
  "body": "We're happy to have you on board! ...",
  "button": { "label": "Verify email" }
}

locales/fr.json (excerpt):

{
  "greeting": "Bonjour !",
  "body": "Nous sommes heureux de vous compter parmi nous ! ...",
  "button": { "label": "Vérifier l’e-mail" }
}

Configuration

maizzle.config.ts registers vue-i18n as a Vue plugin.

Locale files are read instead of being imported, so edits show up in dev through HMR:

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

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 },
      }),
    ],
  },
})

A few things worth noting about the config above:

  • plugins is a factory (() => [...]), not a plain array. vue-i18n is stateful — setting locale.value in one template mutates the shared instance and would leak into the next render. The factory form gives each template a fresh i18n instance.
  • readFileSync avoids Node's ES module import cache, so editing locales/*.json while the dev server runs takes effect on the next render. A plain import en from './locales/en.json' would be cached and require a server restart.
  • fallbackLocale: 'en' means missing keys in other languages fall back to English instead of rendering the raw key.

Templates

The starter ships one template per locale under emails/:

emails/
├── en/
│   └── welcome.vue
└── fr/
    └── welcome.vue

emails/en/welcome.vue has no <script setup> — it relies on the default locale ('en') from the config and uses $t() directly in the template:

<template>
  <Layout>
    <Heading level="1">{{ $t('greeting') }}</Heading>
    <Text>{{ $t('body') }}</Text>
    <Button href="https://maizzle.com">{{ $t('button.label') }}</Button>
  </Layout>
</template>

emails/fr/welcome.vue overrides the locale per-template via useI18n():

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

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

<template>
  <Layout>
    <Heading level="1">{{ $t('greeting') }}</Heading>
    <Text>{{ $t('body') }}</Text>
    <Button href="https://maizzle.com">{{ $t('button.label') }}</Button>
  </Layout>
</template>

Both files share the same translation keys but render into one HTML file per language.

Tip: When using useI18n(), you can destructure t and use {{ t('greeting') }} instead of {{ $t('greeting') }}. Both work the same — $t() is globally available, t() requires the import.

Building the same template in multiple languages

You'll notice both emails/en/welcome.vue and emails/fr/welcome.vue duplicate the entire markup — only the locale differs. If you don't need them to diverge, extract the shared markup into a component and keep the per-locale files as thin wrappers.

Reorganize like this:

emails/
└── welcome/
    ├── en.vue
    └── fr.vue
components/
└── Welcome.vue

components/Welcome.vue holds the markup and translation keys:

<!-- components/Welcome.vue -->
<template>
  <Layout>
    <Heading level="1">{{ $t('greeting') }}</Heading>
    <Text>{{ $t('body') }}</Text>
    <Button href="https://maizzle.com">{{ $t('button.label') }}</Button>
  </Layout>
</template>

Each wrapper just sets the locale and renders the shared component:

<!-- emails/welcome/en.vue -->
<script setup>
import { useI18n } from 'vue-i18n'

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

<template>
  <Welcome />
</template>
<!-- emails/welcome/fr.vue -->
<script setup>
import { useI18n } from 'vue-i18n'

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

<template>
  <Welcome />
</template>

You still get one HTML file per language in the output, but the markup lives in a single place.

Adding a new language

  1. Create a new locale file, e.g. locales/ro.json.
  2. Add the import + entry to maizzle.config.ts:
const ro = JSON.parse(readFileSync('./locales/ro.json', 'utf-8'))

// ...
messages: { en, fr, ro },
  1. Add a wrapper template under emails/ro/welcome.vue that sets locale.value = 'ro' in <script setup> and renders the same markup.

Live-reloading

Maizzle watches the locales directory by default and re-renders templates when any locale file changes — no extra config needed. If your locale files live somewhere else, point Maizzle at them via server.watch:

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

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

Issues

Please open all issues in the framework repository.

License

The Maizzle framework is open-sourced software licensed under the MIT license.

About

i18n starter for Maizzle 6

Resources

Stars

Watchers

Forks

Contributors

Languages