Analytics for Translation and Localization Platforms
Multi-locale routing breaks standard pageview attribution. Here's how to instrument i18n sites correctly — locale-aware events, hreflang correlation, and cross-region funnels that don't double-count.
Three months ago I watched a SaaS founder stare at her analytics dashboard, genuinely confused. Her German traffic had supposedly tripled overnight. Her French conversion rate had dropped to zero. Neither thing had actually happened.
What happened: she'd just shipped i18n support with locale subfolders. Every page now existed at /en/pricing, /de/pricing, /fr/pricing. Her analytics tool — GA4, in this case — was treating each URL as a separate page. The German "surge" was just existing visitors hitting the new /de/ routes. The French "drop" was a tracking script that failed to load on one locale because of a Content-Security-Policy header someone forgot to update.
This is the tutorial I wish I'd had when I first started instrumenting multi-locale sites. We're going to wire up analytics that actually work across locales — pageviews that attribute correctly, custom events that carry language context, and funnels that don't fall apart when someone switches from English to Spanish mid-checkout.
Prerequisites
Before we start:
- A site with i18n already working (subfolders like
/en/,/de/or subdomains likeen.example.com) - Basic familiarity with your i18n library (Next.js i18n, i18next, or whatever you're using)
- A JustAnalytics account (free tier works fine for this)
- About 30 minutes
The patterns here work with any analytics tool, but the code examples use JustAnalytics because that's what I've tested most thoroughly. If you're on Plausible or Fathom, the concepts translate — you'll just need to adjust the API calls.
My strong opinion: most i18n implementations get analytics completely wrong, and the default behavior of every major analytics tool makes it worse. GA4 is particularly bad at this.
Step 1: Install the tracking script with locale awareness
The basic script installation is the same as any site. Drop this in your root layout or _document:
<script
src="https://cdn.justanalytics.app/script.js"
data-site="your-site-id"
data-auto-pageview="false"
defer
></script>
Notice data-auto-pageview="false". We're disabling automatic pageviews because we need to send custom dimensions with every pageview — specifically, the current locale and the canonical page identifier.
Why this matters: without it, /en/pricing and /de/pricing are completely separate entries in your reports. You can't answer "how many people viewed the pricing page?" without manually summing across locales. And if you've got 12 locales, that gets old fast.
Step 2: Fire pageviews with locale metadata
Here's a React hook that handles this. Drop it in a client component that mounts on every page:
// components/LocaleAnalytics.tsx
"use client";
import { usePathname } from "next/navigation";
import { useEffect } from "react";
export function LocaleAnalytics({ locale }: { locale: string }) {
const pathname = usePathname();
useEffect(() => {
if (typeof window === "undefined") return;
// Strip the locale prefix to get the canonical page
const canonicalPath = pathname.replace(/^\/(en|de|fr|es|ja|zh)\//, "/");
window.ja?.("pageview", {
url: pathname,
locale: locale,
canonical_page: canonicalPath,
});
}, [pathname, locale]);
return null;
}
Mount it in your layout:
// app/[locale]/layout.tsx
import { LocaleAnalytics } from "@/components/LocaleAnalytics";
export default function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: string };
}) {
return (
<>
<LocaleAnalytics locale={params.locale} />
{children}
</>
);
}
Now every pageview carries three pieces of data: the full URL (for debugging), the locale (for filtering), and the canonical page (for cross-locale aggregation). You can group by canonical_page to see total traffic to /pricing regardless of language, or filter by locale to see just your German visitors.
I burned a weekend figuring this out the first time. Not my proudest moment. The trick is sending both the full path AND the canonical path — if you only send the canonical, you lose the ability to debug locale-specific issues. Ask me how I know.
Step 3: Track locale switches as events (not pageviews)
When someone clicks the language switcher, don't fire a pageview. Fire a custom event.
// components/LocaleSwitcher.tsx
"use client";
import { useRouter, usePathname } from "next/navigation";
export function LocaleSwitcher({ currentLocale }: { currentLocale: string }) {
const router = useRouter();
const pathname = usePathname();
function switchLocale(newLocale: string) {
// Track the switch
window.ja?.("event", "locale_switched", {
from_locale: currentLocale,
to_locale: newLocale,
page: pathname,
});
// Navigate
const newPath = pathname.replace(`/${currentLocale}/`, `/${newLocale}/`);
router.push(newPath);
}
return (
<select
value={currentLocale}
onChange={(e) => switchLocale(e.target.value)}
>
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
<option value="es">Español</option>
</select>
);
}
Why not a pageview? Because the user isn't consuming new content — they're reading the same page in a different language. Counting it as a pageview inflates your numbers and wrecks your average session duration. I've seen sites report 8 pageviews per session that were actually 2 pages viewed in 4 different languages each. Honestly, this is one of those things that seems obvious in hindsight but I got wrong for months.
Plus, tracking switches as events gives you data you can't get otherwise. You can answer questions like: "Which locales do users switch away from most?" (Maybe your German translations need work.) "Do users who switch locales have higher or lower conversion rates?" (Often higher — they're engaged enough to try multiple languages.)
Step 4: Handle subdomain i18n setups
If you're using subdomains (en.example.com, de.example.com) instead of subfolders, there's an extra step. By default, most analytics tools treat each subdomain as a separate site. Same visitor on en.example.com and then de.example.com gets counted twice.
In JustAnalytics, enable cross-subdomain tracking in your site settings:
- Go to Site Settings → Tracking
- Enable "Cross-subdomain tracking"
- Add your root domain (
example.com)
The script will then use a first-party identifier that persists across subdomains. One visitor, one session, even if they bounce between en. and de..
If you're doing this with cookies (on other analytics tools), you'll need to set the cookie domain to .example.com (note the leading dot) so it's readable from all subdomains. JustAnalytics is cookieless, so this isn't an issue — the fingerprint works across subdomains automatically.
Step 5: Build cross-locale conversion funnels
Here's where the canonical page identifier pays off. Let's say your signup funnel is:
- Landing page (
/) - Pricing page (
/pricing) - Signup form (
/signup) - Onboarding (
/onboarding)
Without locale normalization, you straight up can't build this funnel. A German user who goes /de/ → /de/pricing → /de/signup → /de/onboarding lives in a completely different funnel than an English user going through /en/ equivalents. Your funnel visualization becomes useless.
With the canonical_page dimension we set up earlier, you build one funnel using canonical paths:
Funnel: Signup Flow
Step 1: canonical_page = "/"
Step 2: canonical_page = "/pricing"
Step 3: canonical_page = "/signup"
Step 4: canonical_page = "/onboarding"
Then you can break it down by locale to compare: "Are German users dropping off at pricing more than English users?" That's actually useful. Without normalization, you'd need to build 12 separate funnels and compare them manually. Nobody does that. I tried once. Once.
Step 6: Correlate with hreflang for SEO debugging
This one's specific to localization platforms and translation management systems like Phrase, Lokalise, or Crowdin. If you're publishing translated content and using hreflang tags, you want to know whether Google is actually indexing the right locales.
Add hreflang data to your pageview events:
useEffect(() => {
if (typeof window === "undefined") return;
// Grab hreflang links from the DOM
const hreflangs = Array.from(
document.querySelectorAll('link[rel="alternate"][hreflang]')
).map((link) => ({
lang: link.getAttribute("hreflang"),
href: link.getAttribute("href"),
}));
const canonicalPath = pathname.replace(/^\/(en|de|fr|es|ja|zh)\//, "/");
window.ja?.("pageview", {
url: pathname,
locale: locale,
canonical_page: canonicalPath,
hreflang_count: hreflangs.length,
has_x_default: hreflangs.some((h) => h.lang === "x-default"),
});
}, [pathname, locale]);
Now you can filter for pages where hreflang_count is unexpectedly low (maybe a locale didn't get published) or where has_x_default is false (which can confuse Google). If you're managing translations through Weglot or similar, this catches sync issues before they tank your international SEO.
Common errors and how to fix them
"My bounce rate dropped to 5% after adding i18n"
You're probably double-counting pageviews. Check that data-auto-pageview="false" is set on the script tag and that your custom pageview handler isn't firing twice per navigation. React Strict Mode in development will call effects twice — that's normal in dev, but if you're seeing it in production, you've got a duplicate mount somewhere.
"Cross-subdomain tracking isn't working"
Three things to check: (1) Is the root domain configured correctly in your site settings? (2) Are both subdomains loading the same site ID? (3) Is there a CSP header blocking the tracking script on one subdomain but not the other? That last one got me once — we'd set up CSP per-subdomain and forgot to add the analytics domain to the German site.
"Locale switches show as direct traffic in my funnel"
This happens when the referrer isn't passed across locale navigation. It's rare with subfolder setups but common with subdomains if your redirect logic strips the referrer. Check your redirect implementation — a 301 should preserve the referrer, but some proxy configurations don't.
"My translation platform shows different pageview counts than analytics"
Tools like Phrase or Lokalise track "string views" or API calls, not actual page loads. They're measuring how often their snippet renders translated content, which can fire multiple times per page if you have dynamic content. Your analytics tool measures actual pageviews. The numbers won't match, and that's fine — they're measuring different things.
Look, I'll be honest: i18n analytics is annoying. There's no getting around that. But getting it wrong means flying blind on your international expansion, and that's worse.
Next steps
Once you've got the basics working, there's more you can do. If you're tracking errors alongside analytics (which you should be — JustAnalytics bundles error tracking with analytics in the same script), you can filter errors by locale to catch translation-related bugs. A missing translation key that throws in production will show up in your error feed with the locale attached.
For teams managing click fraud alongside international campaigns, the locale data pairs well with ClickzProtect — certain regions are notorious for low-quality traffic, and having locale on your conversion events helps you spot patterns. We've written about protecting multi-region ad spend if you're running paid acquisition internationally.
And if you're testing across multiple browser profiles for different locales (common for QA on translation platforms), JustBrowser makes it easier to maintain separate sessions per locale without constantly clearing cookies. Though honestly, if you're doing QA across 12+ locales, you've got bigger problems than browser profiles.
Frequently Asked Questions
Why do my locale subfolders show as separate pages in analytics?
Most analytics tools treat /en/pricing and /de/pricing as completely different URLs. To see them as the same page across locales, you need to send a normalized page identifier (like 'pricing') as a custom dimension alongside the full URL. JustAnalytics lets you attach custom dimensions to every pageview, so you can group by canonical page while still filtering by locale when needed.
Should I track locale switches as pageviews or custom events?
Track them as custom events, not pageviews. A locale switch doesn't represent new content consumption — the user is reading the same page in a different language. Firing a pageview inflates your numbers and breaks session duration metrics. Use a custom event like 'locale_switched' with properties for the source and target locale.
How do I attribute conversions across locale switches?
The key is maintaining a consistent visitor identifier across locale subdomains or subfolders. JustAnalytics uses a cookieless fingerprint that persists across /en/ and /de/ subfolders automatically. For subdomain setups (en.example.com, de.example.com), configure cross-subdomain tracking in your site settings so the same visitor isn't counted twice.
Do I need separate analytics properties for each locale?
No, and you shouldn't create them. Running separate properties per locale fragments your data and makes cross-locale analysis impossible. Use a single property with locale as a dimension. You can still create filtered views or segments for regional teams who only care about their market.
Try JustAnalytics
All-in-one observability in one under-5KB script: cookieless analytics + error tracking + APM + session replay + uptime + structured logs. Replaces GA4 + Sentry + Datadog + Pingdom + LogRocket. Free tier (100K events/mo), Pro $49/month ($39 annual).
Author at JustAnalytics.