Vue · vue-i18n
The JSON your build imports, reviewed and merged like code.
vue-i18n takes a plain object of messages per locale, which in practice is a JSON file you import — or several of them, once one file has more owners than it can hold. Mergua reads and writes those files in place, so your app keeps the setup it has and gains a branch, a review step and a pipeline that fills them in.
Vue + vue-i18n
/for/vue-i18n
locale files
src/locales
json shape
nested
The files you already have
Mergua reads what is in your repo. Nothing is renamed, nothing moves, and no package is added to your project — the sync script is fetched with curl when the pipeline runs.
src/locales/
├── en.json ← source locale
├── de.json
└── fr.json{
"cart": {
"summary": {
"title": "Your cart",
"items": "{count} items",
"checkout": "Checkout"
}
}
}How vue-i18n reads them
This is vue-i18n's own setup, not Mergua's — if you already have it, skip the panel. It is here so the page is a working path from empty project to translated screen rather than a page about a product.
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
defineProps<{ count: number }>();
const { t } = useI18n();
</script>
<template>
<section>
<h1>{{ t('cart.summary.title') }}</h1>
<p>{{ t('cart.summary.items', { count }) }}</p>
</section>
</template>src/i18n.ts
import { createI18n } from 'vue-i18n';
import en from './locales/en.json';
import de from './locales/de.json';
import fr from './locales/fr.json';
export const i18n = createI18n({
legacy: false,
locale: 'en',
fallbackLocale: 'en',
messages: { en, de, fr },
});src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { i18n } from './i18n';
createApp(App).use(i18n).mount('#app');src/i18n.ts
// vue-i18n has no word for a second file per language: you
// compose the messages object yourself, so it is one import
// per file. Lay them out language folder first and Mergua
// reads them as they are:
//
// src/locales/en/common.json
// src/locales/en/checkout.json
import common from './locales/en/common.json';
import checkout from './locales/en/checkout.json';
export const i18n = createI18n({
legacy: false,
locale: 'en',
messages: { en: { common, checkout } },
});
// Nesting them this way makes the file name the first segment
// of the key: t('checkout.summary.title').npm i vue-i18nBack into the build
One job, and the locale files in src/locales are the ones your build imports. Keys you added in code go up, finished translations come back down as a commit on the branch the job is running on.
The job runs on the branch it is already on, so a feature branch syncs against the matching Mergua branch and main against main. That is the part no other translation tool does.
And you do not have to run it on a timer: a project can POST to a URL of yours when translations change or a branch is merged, so the job above starts on the change. The delivery is signed, it is filtered to the branch you name, and an import of four hundred keys arrives as one call.
# --path, because the CLI defaults to Angular's src/assets/i18n.
# --format stays nested, which is the shape createI18n expects.
curl -sf https://api.mergua.com/v1/mergua.sh | sh -s -- sync --path src/locales.gitlab-ci.yml
mergua-sync:
stage: .pre
image: alpine:latest
before_script:
- apk add --no-cache curl jq git
script:
- curl -sf https://api.mergua.com/v1/mergua.sh | sh -s -- sync --path src/locales# If you would rather own the commit yourself, pull and
# commit in your own step:
curl -sf https://api.mergua.com/v1/mergua.sh | sh -s -- pull --path src/localesPlaceholders
vue-i18n writes a variable as {count}. Mergua treats it as part of the string: if a translation comes back without it, the validator says so on the way out, and the AI translator is told to carry it through.
"items": "{count} items"t('cart.summary.items', { count: 3 })Worth knowing on Vue
Single braces, and that matters
vue-i18n names a variable {count}. Transloco and i18next both write {{count}}. A file copied from one to the other renders the braces as text, which is exactly the class of mistake Mergua's placeholder check catches on the way out.
Bundle the messages, do not fetch them
createI18n takes the messages up front, so the JSON is imported and bundled rather than requested at runtime. Nothing in the sync changes — the CLI writes the files before the build reads them, which is why the job belongs in an early stage.
Several files per language, whenever you want them
A Mergua project holds more than one file per language, and vue-i18n has no opinion about it — you build the messages object, so a second file is a second import. It reads a folder per language with the files inside, src/locales/en/checkout.json, and the file name is what the keys are filed under: checkout.json becomes checkout, while a plain en.json is read as the language. The "split" tab above is that setup.
Splitting a language across several filesVue I18n messages can be functions — those do not sync
A message compiled to a function in a .ts file is code, not data, and Mergua reads JSON. Keep the strings that need translating in the locale files and reserve functions for formatting you do not want a translator editing.
A different stack?
It is JSON in and JSON out, so anything that reads a locale file works. These are the three with a page of their own.
Run it on one branch.
Upload the src/locales/en.json you already have and see the whole loop on the free tier.
Create your account