How We Translated Shipi18n.com... Using Shipi18n
We built a translation API, then realized we needed to translate our own website. Here's how we dogfooded Shipi18n to ship 11 languages in one afternoon.
Key takeaway
We built a translation API, then realized we needed to translate our own website. Here's how we dogfooded Shipi18n to ship 11 languages in one afternoon.
We built a translation API. Then we looked at our own website.
It was English-only.
The irony wasn't lost on us.
The setup
Shipi18n is an i18n tool for developers: it translates locale files and checks them for broken strings before you ship. Our marketing site? One language. English.
It's the classic "cobbler's children have no shoes" problem. We were so busy building translation tools that we forgot to translate ourselves.
So we did what any self-respecting developer would do: we used our own product.
Here's exactly how it went down.
What we were working with
Our frontend is an Astro site with i18next for internationalization. The translation strings live in JSON namespace files:
src/locales/
├── en/
│ ├── common.json # Nav, footer, language names
│ ├── home.json # Homepage content
│ ├── pricing.json # Pricing page
│ ├── security.json # Security page
│ ├── integrations.json
│ ├── api.json
│ └── blog.json
The English files contained around 200+ translation keys across 7 namespaces. Headlines, feature descriptions, CTAs, meta tags—the works.
Target: 11 languages. English plus Spanish, French, German, Japanese, Chinese (Simplified), Russian, Italian, Korean, Dutch, and Arabic.
That's 10 languages × 7 files = 70 translation files to generate.
Manually? That's a week of mind-numbing copy-paste work.
With Shipi18n CLI? About 15 minutes.
Step 1: Install the CLI
First, we installed our own CLI tool. Yes, it felt weird.
npm install -g @shipi18n/cli
The CLI is on npm, free and Apache-2.0 licensed. It wraps the open-source translation engine with a developer-friendly interface for batch translations. There are no language or key limits — you bring your own LLM key and pay your provider directly.
Step 2: Set up the API key
We set our Anthropic key as an environment variable — the same one any user would bring:
export ANTHROPIC_API_KEY=sk-ant-...
For CI/CD, this would go in your GitHub secrets. For local dev, a .env file works.
Step 3: Translate each namespace
Here's where the magic happened. For each namespace file, we ran:
npx @shipi18n/cli translate src/locales/en/home.json \
--target es,fr,de,ja,zh-CN,ru,it,ko,nl,ar \
--output src/locales
The CLI:
- Reads the English source file
- Extracts all string values (preserving JSON structure)
- Sends them to our API
- Writes translated files to the output directory
One command, 10 languages. Each file took about 30-45 seconds to process.
We ran this for each namespace:
# Homepage
npx @shipi18n/cli translate src/locales/en/home.json \
--target es,fr,de,ja,zh-CN,ru,it,ko,nl,ar \
--output src/locales
# Common strings (nav, footer)
npx @shipi18n/cli translate src/locales/en/common.json \
--target es,fr,de,ja,zh-CN,ru,it,ko,nl,ar \
--output src/locales
# Pricing
npx @shipi18n/cli translate src/locales/en/pricing.json \
--target es,fr,de,ja,zh-CN,ru,it,ko,nl,ar \
--output src/locales
# ... and so on for security, integrations, api, blog
Could we have scripted this into a single command? Sure. But we wanted to review each batch as it came through.
The tricky parts
Placeholders
Our homepage has strings like:
{
"footer": {
"copyright": "© {{year}} shipi18n. Fast, smart localization for modern apps."
}
}
That {{year}} placeholder needs to survive translation intact. If it becomes {{año}} or {{Jahr}}, the app breaks.
Shipi18n automatically detects and preserves these. Here's what the Japanese translation looks like:
{
"footer": {
"copyright": "© {{year}} shipi18n。最新アプリ向けの高速でスマートなローカリゼーション。"
}
}
{{year}} stayed exactly as-is. The same placeholder preservation works for {name}, %s, <0>, and other common formats.
HTML in strings
Some of our marketing copy has inline HTML:
{
"hero": {
"subtitle": "...get translations in <strong>100+ languages</strong> with preserved placeholders..."
}
}
The <strong> tags also survive translation:
{
"hero": {
"subtitle": "...Übersetzungen in <strong>über 100 Sprachen</strong> mit beibehaltenen Platzhaltern..."
}
}
No manual intervention needed.
RTL language (Arabic)
Arabic is right-to-left. The text direction is handled by CSS, but the translations themselves need to be accurate.
English:
{
"nav": {
"home": "Home",
"docs": "Docs",
"pricing": "Pricing"
}
}
Arabic:
{
"nav": {
"home": "بيت",
"docs": "المستندات",
"pricing": "التسعير"
}
}
The API handled Arabic, Japanese, Korean, Chinese, and Russian—all different scripts—without any special configuration.
The results
After about 15 minutes of CLI commands:
- 77 translation files generated (7 namespaces × 11 languages)
- 2,000+ translated strings across all files
- Zero placeholder corruption
- Zero manual copy-paste
Here's a before/after of our project structure:
Before:
src/locales/
└── en/
├── common.json
├── home.json
└── ... (5 more files)
After:
src/locales/
├── en/
├── es/
├── fr/
├── de/
├── ja/
├── zh-CN/
├── ru/
├── it/
├── ko/
├── nl/
└── ar/
├── common.json
├── home.json
└── ... (5 more files each)
What we learned
1. Dogfooding is humbling
Using your own product exposes every rough edge. We found a few CLI UX improvements to make (better progress indicators, clearer error messages). Those are now on our roadmap.
2. Translation Memory is clutch
About 30% of our strings were similar across pages ("Learn more", "Get started", "Free trial"). Translation Memory cached these, so we didn't pay to translate the same phrase twice.
3. Review is still important
Machine translation is good, not perfect. We did a quick review of each language with native speakers where possible. A few tweaks here and there, but nothing major.
4. i18next structure matters
Having clean, organized namespace files made this process smooth. If your i18n setup is messy, you'll fight the tooling. Invest in good key naming and namespace organization upfront.
Would we do it again?
Absolutely.
The alternative was:
- Copy 200+ strings into Google Translate
- Paste back into 70 separate files
- Manually verify placeholders didn't break
- Repeat for every content update
That's easily a week of work. We did it in an afternoon.
The site now automatically detects browser language preferences and serves the appropriate translation. Visitors from Tokyo see Japanese. Visitors from Berlin see German. Visitors from São Paulo see... well, we should probably add Portuguese.
Try it yourself
Want to translate your own app the same way we did?
-
Install the CLI:
npm install -g @shipi18n/cli -
Get an LLM key: from Anthropic or OpenAI — there is no Shipi18n account
-
Translate:
npx @shipi18n/cli translate src/locales/en.json --target es,fr,de
That's it. Your JSON files, translated, with placeholders preserved.
We built it. We used it. It worked.
Does your CI check your translations?
One command, no account, no API key — the structural check runs anywhere. Apache-2.0, bring your own LLM for the semantic pass.
Star on GitHub →