The best way to localize a Lovable app built on the classic Vite stack is to extract your hardcoded strings into committed Lingui PO catalogs, not to bolt on a runtime translation widget. Your Lovable app is a client-rendered React SPA, so there is no server render to hook into, and the durable option is translation files that live in your own repo and ship with your build. globalize.now is AI-powered localization infrastructure that does the extraction and keeps those catalogs in sync on every Git push, so you set it up once and never hand-manage locale files again.
Which stack is your Lovable app on, classic Vite or TanStack Start?
Open your file tree first, because Lovable ships two different stacks and each needs a different i18n setup. If you see src/pages/Index.tsx, src/main.tsx, and vite.config.ts, you are on the classic Vite plus React SPA stack, which is Lovable's default. TanStack Start projects instead have src/routes/, src/router.tsx, src/routeTree.gen.ts, and src/server.ts.
The stack is chosen when the project is created and cannot be switched afterward, so this decision is already made for you. The classic Vite stack renders entirely in the browser and does no server-side data fetching by default. This guide covers that classic Vite SPA. If your files match the TanStack Start layout, follow our TanStack Start i18n guide for Lovable instead, since server rendering changes how translations load.
Why do Vite SPA Lovable apps get skipped by i18n tooling?
Because the tooling attention moved to Lovable's newer TanStack Start plus SSR default and left the classic Vite SPA underserved, even though it remains the most common Lovable stack. Over the past months the localization tools that target Lovable, including Lovalingo, Intlayer, and Paraglide, all re-pointed their guides at TanStack Start with server-side rendering.
That leaves a gap. A client-rendered SPA has real constraints the SSR guides ignore: there is no server render to inject translated markup, the whole bundle loads in the browser, and any late-loading script paints over an English UI. The good news is that the classic stack has a clean, well-supported answer that most builders never hear about, because the loudest tools are busy chasing the other stack.
What is the difference between committed catalogs and a runtime translation widget?
Committed catalogs are translation files checked into your repository and bundled at build time, while a runtime widget is a third-party script that swaps text in the browser after your English UI has already loaded. That difference decides three things that matter for a Vite SPA.
First, flash and layout shift: a widget shows English, then rewrites it, so users see a visible swap and the layout can jump. Committed catalogs render the right language on first paint. Second, indexability: strings that ship inside your build are part of your source, while widget text is injected late and is harder for crawlers to read. Third, ownership: committed catalogs are your files in your repo, so nothing external has to stay online for your app to keep speaking German or Japanese.
How do you add i18n to a Lovable Vite app with Lingui?
Add Lingui as your i18n layer, wrap your strings, and let its Vite plugin compile the catalogs. Lingui is a lightweight i18n framework with a dedicated Vite plugin and a PO-based catalog format, which is exactly what a client-rendered SPA needs.
First, install the packages. Lingui splits its CLI, its Vite plugin, and its React runtime:
npm install --save-dev @lingui/cli @lingui/vite-plugin @rolldown/plugin-babel
npm install @lingui/core @lingui/reactNext, register the plugin in vite.config.ts and add a lingui.config.ts that points at your source and locales folder:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { lingui } from "@lingui/vite-plugin";
export default defineConfig({
plugins: [react(), lingui()],
});// lingui.config.ts
import { defineConfig } from "@lingui/cli";
export default defineConfig({
locales: ["en", "de", "fr"],
sourceLocale: "en",
catalogs: [
{
path: "src/locales/{locale}/messages",
include: ["src"],
},
],
});Then wrap your hardcoded JSX text with the Trans macro instead of leaving bare strings:
// before
<h1>Create your account</h1>
// after
import { Trans } from "@lingui/react/macro";
<h1><Trans>Create your account</Trans></h1>Now extract. The CLI scans your src directory and writes a PO catalog per locale, for example src/locales/en/messages.po:
npx lingui extractFill in the msgstr fields for each language, or hand that step to an automated layer (see the next section). You do not run a separate compile command, because the @lingui/vite-plugin compiles the PO catalogs on the fly during development and build.
Finally, add a language switcher by activating a locale at runtime:
import { i18n } from "@lingui/core";
i18n.activate("de");That is the whole loop for the classic Vite stack: install, wrap, extract, translate, switch. Everything except the translations themselves lives in your repo.
How does globalize.now automate this on every push?
globalize.now removes the manual extract-and-fill steps by watching your repo and doing them for you on every Git push. It finds newly hardcoded strings in your Lovable codebase, generates the keys and PO entries Lingui expects, produces the translations, and commits the updated catalogs back, so your src/locales folder is always current without you running commands.
Install it once:
npx skills add globalize-now/globalize-skillsInside Lovable specifically, add the lovable-i18n skill in your workspace, then prompt Lovable to set up i18n. The runtime stays Lingui, so nothing about how your app renders changes. What changes is that you stop babysitting locale files. This is the same set-it-up-once, sync-on-every-push model our vibe-coder workflow and developer setup are built around.
Pricing is straightforward for a solo builder: €20 per month per workspace, with no per-seat and no per-language charges, plus the translations you actually use. New workspaces get a €5 signup credit with no card, which is enough to translate a small app end to end.
Should you use a runtime widget instead? The Lovalingo lesson
Not if you want your translations to outlive any single vendor. A runtime translation widget is convenient at first, but it makes your app depend on someone else's server staying online, and that dependency has a real failure mode.
Lovalingo, a runtime widget popular with Lovable builders, announced it is closing on August 31, 2026, with no new signups accepted and no named successor. Its own migration guidance tells users to move to project-owned internationalization, which is precisely the committed-catalog approach in this guide. When a widget's vendor shuts down, the injected translations stop and the UI falls back to English. Files in your repo do not. If you are already on a widget, our Lovalingo shutdown migration guide walks through the move, and the broader translations-break-on-every-push problem is why committed catalogs win for AI-built apps.
globalize.now turns hardcoded app copy into translation-ready locale files and keeps them updated as you ship.
Try globalize.now free