Set Up i18n on Day One, Even If You Only Have One Language
TL;DR
My team built a pilot in Bahasa Indonesia using Vue 2 and Express. Because it was only a pilot, we wrote text directly in components and stored content in one language.
A year later, we needed English as soon as possible. By then, the app had hundreds of components and a database full of content written in a WYSIWYG editor.
We shipped it, but the work included a full interface audit, a schema migration, and manual translation of every article. Most of that cost came from one early decision: we treated text as a literal instead of a lookup.
How the Pilot Grew
At the start, international visitors were not on anyone's radar. The scope was tight, and nobody wanted to build a language switcher for a product that might not survive the pilot.
So every menu, heading, subtitle, and button label went straight into the template in Bahasa Indonesia.
We had no t() calls and no locale files.
That choice felt reasonable while the app was small. The problem was that the app did not stay small.
Features kept shipping, the component count kept growing, and writers kept publishing formatted content directly into the database. Then the business wanted to reach visitors outside Indonesia.
Suddenly, multilingual support was urgent.
The Options We Considered
Before changing anything, we discussed several approaches. Each one solved a different part of the problem, and none removed the work already waiting for us.
For the interface, we considered extracting every literal into locale files and using vue-i18n.
This was the clearest option because it matched our Vue stack and gave us one place to manage interface text.
For content, we considered adding columns such as title_en and body_en.
This fit our existing queries, but every future language would require more columns and another migration.
We also considered storing each translation in a JSON object keyed by locale. That would scale better to more languages, but it required larger changes to code that expected plain database columns.
In the end, we combined the first two approaches.
We used vue-i18n for interface text and altered the database tables to hold English content in separate columns.
What Retrofitting Cost
The interface work was simple but enormous.
We opened nearly every Vue component, replaced each literal with a vue-i18n key, and created Indonesian and English locale files.
AI coding tools were not as popular or capable then as they are now. We searched for strings and updated components mostly by hand.
We never kept a clean count of the strings. We worked through the codebase file by file until there were none left.
Content was harder because code could not solve it for us. Each article contained Indonesian text and formatting created by a writer, so someone had to translate it and recreate that formatting by hand.
It was the safer choice under pressure, but it tied every new locale to another schema change. The tools mattered less than the timing because every option still required us to revisit a year of work.
What I Would Do Now
I would configure vue-i18n with one locale before writing the first component.
Every visible string would use t() from the beginning, even if a second language never arrived.
For content, I would make locale part of the data model from day one.
For this app, I would use an object such as { "id": {...} }, then add en later without changing the schema.
That storage choice is not right for every system. The important part is giving content a locale boundary before writers fill the database.
This may sound like early abstraction. I used to think so too.
i18n is different from building a plugin system nobody has asked for. It decides where text lives and how it moves through the app.
Writing t('nav.home') instead of "Beranda" costs almost nothing when the component is new.
Making that change across hundreds of components costs an audit.
The Lesson
Not every pilot needs several translations or a polished language switcher. It only needs the wiring that says interface text is a lookup and content belongs to a locale.
That wiring is cheap when the codebase is new. A year later, it can mean a migration, a full interface audit, and asking writers to redo every article.
Set it up early. Register one locale. Then move on.