Aug 22, 2025Ā·8 min read

Vue 3 i18n workflow for 500+ keys without prod surprises

A practical Vue 3 i18n workflow for large apps: key naming, plurals, QA checks, and release steps to avoid missing translations in production.

Vue 3 i18n workflow for 500+ keys without prod surprises

What goes wrong at 500+ i18n keys

Once your app passes a few hundred strings, the first thing that breaks usually isn't Vue I18n. It's consistency. People add keys in different styles, duplicate the same idea under different names, and no one is sure which messages are safe to delete.

Missing translations also stop being rare. They show up in normal user paths, especially in less-used screens like settings, error states, empty states, and notifications.

When a translation is missing, users usually see one of three failures: a blank UI (a button with no label), raw keys (like checkout.pay_now), or a strange fallback where part of the page switches language. None of those feel like a small bug. They make the app look broken.

That’s why a Vue 3 i18n workflow matters more than the specific library. The library will do what you ask. At scale, teams often don’t agree on what ā€œdoneā€ looks like.

A common example: a developer ships a new "Invite teammate" flow with 40 new strings. The English file is updated, but the French file isn’t. In staging, everything looks fine because the tester uses English. In production, French users see a mix of translated and untranslated UI, and support gets screenshots of raw keys.

The fix is defining what "done" means for translated UI. It can't be only "strings added". A practical definition of done usually includes: keys follow naming rules, locales build without missing-key warnings, plurals and variables render correctly with real data, at least one non-default locale was checked, and copy changes are tracked so old keys don’t linger.

At 500+ keys, you win by treating localization like a release process, not a last-minute file edit.

Set a few rules before you add more strings

After a few hundred strings, translation work isn’t the messy part. Consistency is. A small set of rules makes your Vue 3 i18n workflow predictable, even when multiple people touch copy every week.

Start by deciding what a ā€œconceptā€ is and keeping one source of truth for it. If the same UI idea shows up in five places (for example, ā€œSave changesā€), you want one key, not five variations like save, saveChanges, save_update, and saveBtn. Duplicate keys drift in meaning over time, and users feel that inconsistency.

Next, decide where formatting lives. Teams often split this by accident: some messages include punctuation and casing, others rely on code to add it. Pick one approach and stick to it.

A practical default:

  • Put grammar, punctuation, and user-facing formatting (like ā€œ(optional)ā€) in the message.
  • Keep pure data formatting in code (dates, currency, units), then pass the result into i18n.
  • Use placeholders for names and counts, not string concatenation.
  • Treat HTML in messages as a special case with a clear rule (allowed or not allowed).

Then define ownership. Decide who can add new keys, who reviews the base-language copy, and who approves other locales. Without this, strings get added in a hurry and never get reviewed.

Finally, choose and document a fallback strategy. If a key is missing, what should users see: the key name, the default-locale text, or a safe generic message? In production, many teams prefer falling back to the default locale plus logging, so users aren’t blocked while you still get a signal that something is wrong.

If you build Vue 3 apps with a generator like AppMaster (Vue3 web UI plus real backend code), these rules still apply. Treat translations like product content, not "just dev text", and you’ll avoid most late surprises.

Key naming conventions that stay readable

Past a few hundred strings, consistency is the biggest multiplier. Pick one key style (most teams use dot paths like billing.invoice.title) and make it a rule. Mixing dots, slashes, snake_case, and random casing makes search and reviews slow.

Use stable keys that survive copy changes. A key like "Please enter your email" will break the moment marketing tweaks the sentence. Prefer intent-based names like auth.email.required or auth.email.invalid.

Group keys by product area or UI surface first, then by purpose. Think in the same buckets your app already has: auth, billing, settings, support, dashboard. This keeps locale files easier to scan and reduces duplicates when two screens need the same idea.

Within each area, keep a small set of patterns for common UI pieces:

  • Buttons: *.actions.save, *.actions.cancel
  • Labels: *.fields.email.label, *.fields.password.label
  • Hints/help text: *.fields.email.hint
  • Errors/validation: *.errors.required, *.errors.invalidFormat
  • Notifications/toasts: *.notices.saved, *.notices.failed

Dynamic messages should say what changes, not how. Name the message by intent and use parameters for the variable parts. For example, billing.invoice.dueInDays with {days} is clearer than billing.invoice.dueIn3Days.

Example (works nicely in a Vue 3 i18n workflow): orders.summary.itemsCount with {count} for the number, and orders.summary.total with {amount} for money. When someone reads the key in code, they should know where it belongs and why it exists, even if the final copy changes later.

Plural rules and message formatting without surprises

Plural text breaks quietly when you treat every language like English. Decide early when you’ll use ICU message syntax and when a simple placeholder is enough.

Use simple replacements for labels and short UI text that never changes by number (for example, "Welcome, {name}"). Switch to ICU for anything count-based, because it keeps all forms in one place and makes the rules explicit.

{
  "notifications.count": "{count, plural, =0 {No notifications} one {# notification} other {# notifications}}"
}

Write count messages so they’re easy to translate. Prefer a full sentence and keep the number placeholder (#) close to the noun. Avoid clever shortcuts like reusing the same key for ā€œ1 itemā€ and ā€œ2 itemsā€ in code. Translators need to see the whole message, not guess how it will be stitched together.

Plan for =0, one, and other at a minimum, and document what you expect for 0. Some products want ā€œ0 itemsā€, others want ā€œNo itemsā€. Pick one style and stick to it so the UI feels consistent.

Also watch for languages with more plural categories than you expect. Many languages don’t follow ā€œone vs manyā€. If you add a new locale later, a message that only has one and other may be grammatically wrong even if it renders.

Before shipping, test plurals with real counts in the UI, not only by staring at JSON. A quick check that catches most issues is 0, 1, 2, 5, and 21.

If you build a Vue3 web app (for example, inside AppMaster), do this test on the actual screen where the text appears. Layout issues, truncated text, and awkward phrasing show up there first.

Organizing locale files for growth

Design data-driven UI text
Model PostgreSQL data visually, then reflect those fields consistently in labels and errors.
Design data

After a few hundred strings, one big en.json becomes a bottleneck. People touch the same file, merge conflicts grow, and you lose track of where copy lives. A good structure keeps your Vue 3 i18n workflow steady even when the product keeps changing.

Suggested structures

For 2 to 5 locales, splitting by feature is usually enough. Keep the same file layout in every locale so adding a key is a predictable edit.

  • locales/en/common.json, locales/en/auth.json, locales/en/billing.json
  • locales/es/common.json, locales/es/auth.json, locales/es/billing.json
  • locales/index.ts (loads and merges messages)

For 20+ locales, scale the same idea but make it harder to drift. Treat English as the source of truth and enforce that every locale mirrors the same folder and file names. If a new domain appears (for example, notifications), it should exist for every locale even if the text is temporary.

Splitting by domain reduces merge conflicts because two people can add strings to different files without stepping on each other. Domains should match how your app is built: common, navigation, errors, settings, reports, plus feature folders for larger areas.

Keeping keys consistent

Within each file, keep the same key shape across locales: same nesting, same key names, different text. Avoid ā€œcreativeā€ keys per language, even if a phrase is hard to translate. If English needs billing.invoice.status.paid, every locale must have that exact key.

Centralize only what truly repeats everywhere: button labels, generic validation errors, and global navigation. Keep feature-specific copy close to the feature domain, even if it feels reusable. ā€œSaveā€ belongs in common. ā€œSave payment methodā€ belongs in billing.

Long-form content

Long help text, onboarding steps, and email templates get messy fast. A few rules help:

  • Put long-form strings in their own domain (for example, help or onboarding) and avoid deep nesting.
  • Prefer short paragraphs over one massive string, so translators can work safely.
  • If marketing or support edits text often, keep those messages in a dedicated file to reduce churn elsewhere.
  • For emails, store subject and body separately and keep placeholders consistent (names, dates, amounts).

This setup makes it easier to review changes, translate steadily, and avoid surprise gaps right before release.

A step-by-step workflow for adding and shipping strings

A stable Vue 3 i18n workflow is less about tools and more about repeating the same small steps every time. New UI text shouldn’t reach production without a key, a default message, and a clear translation status.

Start by adding the key to your base locale (often en). Write the default text like real copy, not a placeholder. This gives product and QA something readable to review, and it prevents ā€œmystery stringsā€ later.

When you use the key in a component, include params and plural logic from day one so translators see the full shape of the message.

// simple param
$t('billing.invoiceDue', { date: formattedDate })

// plural
$t('files.selected', count, { count })

If translations aren’t ready, don’t leave keys missing. Add placeholder translations in other locales or mark them as pending in a consistent way (for example, prefix the value with TODO:). The important part is that the app renders predictably and reviewers can spot unfinished copy.

Before merging, run quick automated checks. Keep them boring and strict: missing keys across locales, unused keys, placeholder mismatch (missing {count}, {date}, or wrong braces), invalid plural forms for supported languages, and accidental overrides.

Finally, do a short UI pass in at least one non-base locale. Pick a language with longer strings (often German or French) to catch overflow, clipped buttons, and awkward line breaks. If your Vue 3 UI is generated or maintained alongside other parts of a product (for example, a Vue3 web app produced by AppMaster), this step still matters because layouts change as features evolve.

Treat these steps as your definition of done for any feature that adds text.

Common mistakes teams keep repeating

Start with a key structure
Set up clean domains and key naming early, then reuse them across screens.
Create project

The fastest way to make localization painful is to treat i18n keys like ā€œjust strings.ā€ After a few hundred keys, small habits turn into production bugs.

Keys that drift, collide, or lie

Typos and casing differences are classic causes of missing text: checkout.title in one place, Checkout.title in another. It looks fine in code review, then your fallback language quietly ships.

Another common issue is reusing one key for different meanings. ā€œOpenā€ might mean ā€œOpen ticketā€ on a support screen and ā€œOpen nowā€ on a store page. If you reuse a single key, one of those screens will be wrong in other languages, and translators will guess.

A simple rule helps: one key equals one meaning. If the meaning changes, make a new key even if the English text is the same.

Layout bugs caused by ā€œstring-shapedā€ assumptions

Teams often hardcode punctuation, spacing, or bits of HTML into translations. That works until a language needs different punctuation, or your UI escapes or renders markup differently. Keep markup decisions in templates, and keep messages focused on text.

Mobile is where problems hide. A label that fits in English can wrap into three lines in German, or overflow in Thai. If you only test one screen size, you’ll miss it.

Watch for repeat offenders: assuming English word order when inserting variables (names, counts, dates), building messages by concatenating pieces instead of using a single message, forgetting to test long values (product names, addresses, error details), shipping with silent fallback enabled so missing keys look ā€œfineā€ in English, and copy-pasting keys while editing only the English value.

If you want a Vue 3 i18n workflow that stays calm at 500+ keys, treat keys as part of your API: stable, specific, and tested like everything else.

QA steps that catch missing translations early

Make system messages predictable
Use the Business Process Editor to standardize toasts, notifications, and error messages.
Build automations

Missing translations are easy to miss because the app still ā€œworks.ā€ It just falls back to the key, the wrong locale, or an empty string. Treat translation coverage like tests: you want fast feedback before anything reaches production.

Automated checks (run on every PR)

Start with checks that fail the build, not checks that print warnings nobody reads.

  • Scan the codebase for $t('...') and t('...') usage, then verify every used key exists in the base locale.
  • Compare key sets across locales and fail if any locale is missing keys (unless the key is on a small, reviewed exception list like ā€œen-only legal notesā€).
  • Flag orphan keys that exist in locale files but are never used.
  • Validate message syntax (placeholders, ICU/plural blocks). A single broken message can crash a page at runtime.
  • Treat duplicate keys or inconsistent casing as errors.

Keep the exception list short and owned by the team, not by ā€œwhatever passes CI.ā€

Runtime and visual checks (staging)

Even with CI, you still want a net in staging because real user paths trigger strings you forgot were dynamic.

Enable missing-translation logging in staging and include enough context to fix fast: locale, route, component name (if available), and the missing key. Make it noisy. If it’s easy to ignore, it will be ignored.

Add a pseudo-locale and use it for a quick UI pass. One simple approach is to transform each string (make it longer and add markers) so layout problems jump out, for example: [!!! š—§š—²š˜…š˜ š—²š˜…š—½š—®š—»š—±š—²š—± !!!]. This catches clipped buttons, broken table headers, and missing spacing before you ship.

Finally, do a short pre-release spot check of your highest-value flows in 2-3 locales: sign-in, checkout/payment, core settings, and the new feature you’re releasing. This is where you catch ā€œit was translated but the placeholder is wrongā€ bugs.

Handling new languages and ongoing copy changes

Adding a new language gets messy when you treat it as ā€œcopy workā€ instead of a small product release. The easiest way to keep your Vue 3 i18n workflow stable is to make the app build even when a locale is incomplete, while still making gaps obvious before users see them.

When you add a new locale, start by generating the same file shape as your source locale (often en). Don’t translate first, structure first.

  • Create the new locale folder/file with the full key set copied from the source.
  • Mark values as TODO placeholders so missing strings are visible in QA.
  • Add the locale to your language switcher only after the basics are covered.
  • Run one screen-by-screen pass to catch layout issues (longer words, wrapping).

Translations often arrive late, so decide in advance what ā€œpartialā€ means for your product. If a feature is customer-facing, consider a feature flag so the feature and its strings ship together. If you must ship without full translations, prefer a clear fallback (like English) over empty labels, but make it loud in staging.

Copy updates are where teams break keys. If you change wording, keep the key stable. Keys should describe intent, not the exact sentence. Save the key rename for when meaning changes, and even then, do it with a controlled migration.

To avoid ā€œzombie strings,ā€ deprecate keys on purpose: mark keys as deprecated with a date and replacement, keep them for one release cycle, then remove them only after you’ve confirmed there are zero references.

Keep translation notes close to the key. If your JSON format can’t hold comments, store notes in a small companion document or adjacent metadata file (for example, ā€œused on checkout success screenā€). This is especially helpful when your Vue 3 web app is generated from a tool like AppMaster and multiple people touch copy and UI.

Example: shipping a feature with 60 new strings

One workspace for all locales
Create backend, web UI, and mobile apps in one place and reduce i18n drift.
Start building

One sprint, your team ships three things at once: a new Settings page, a Billing screen, and three email templates (receipt, payment failed, trial ending). That’s about 60 new strings, and it’s where messy i18n usually starts.

The team agrees to group keys by feature, then by surface area. A new file is created for each feature, and keys follow the same pattern everywhere: feature.area.element.

// settings
"settings.profile.title": "Profile",
"settings.security.mfa.enable": "Enable two-factor authentication",

// billing
"billing.plan.current": "Current plan",
"billing.invoice.count": "{count} invoice | {count} invoices",

// email
"email.receipt.subject": "Your receipt",
"email.payment_failed.cta": "Update payment method"

Before translation work starts, plural strings get tested with real values, not guesses. For billing.invoice.count, QA checks 0, 1, 2, and 5 using seeded data (or a simple dev toggle). This catches awkward cases early, like "0 invoice".

QA then runs a focused flow that tends to reveal missing keys: open Settings and Billing and click every tab once, trigger each email template in staging with test accounts, run the app with a non-default locale active, and fail the build if any missing-translation warnings appear in logs.

In this sprint, QA finds two missing keys: one label on the Billing screen and one email subject. The team fixes them before release.

When deciding whether to block release or allow fallback, they use a simple rule: user-facing screens and transactional emails block the release; low-risk admin-only text can temporarily fall back to the default language, but only with a ticket and a clear deadline.

Next steps and a simple release checklist

A Vue 3 i18n workflow stays stable when you treat translations like code: add them the same way every time, test them, and keep score.

Release checklist (5 minutes before merge)

  • Keys: follow your naming pattern, and keep the scope clear (page, feature, component).
  • Plurals: confirm plural forms render correctly in at least one language with multiple plural rules.
  • Placeholders: verify variables are present, named the same everywhere, and look right with real data.
  • Fallbacks: confirm missing-key behavior matches your current policy.
  • Screens: spot-check the few screens most likely to break (tables, toasts, modals, empty states).

Decide what to measure (so problems show up early)

Pick a few simple numbers and review them regularly: missing-key rate in your test run, untranslated string count in non-default locales, and unused key count (strings that no longer appear anywhere). Track them per release if you can, so you see trends instead of one-off failures.

Write down the exact steps for adding a string, updating translations, and verifying the result. Keep it short, and include examples of key names and plural usage. New contributors should be able to follow it without asking.

If you build internal tools, AppMaster (appmaster.io) can be a practical way to keep UI copy and translation keys consistent across a Vue3 web app and companion mobile apps, since everything is managed in one place.

Schedule one small i18n health task each sprint: delete dead keys, fix inconsistent placeholders, and review recent misses. Small cleanups beat emergency translation hunts in production.

Easy to start
Create something amazing

Experiment with AppMaster with free plan.
When you will be ready you can choose the proper subscription.

Get Started