Testing i18n
Testing i18n bukan hanya cek apakah terjemahan muncul. Kita harus test layout RTL, string expansion, format angka/tanggal, dan edge cases karakter Unicode.
Pseudo-localization
Pseudo-localization mengubah string English menjadi "pseudo" characters yang masih bisa dibaca tapi menguji rendering. Ini mendeteksi hardcoded strings, string expansion, dan character encoding issues.
// "Settings" → "[Šéttîñgš !!!]"
// Brackets [] = menandai string yang melalui i18n system
// Accented chars = test character rendering
// Padding !!! = test string expansion (30-40% lebih panjang)
// i18next pseudo-localization
import i18next from "i18next";
import { pseudoLocalize } from "i18next-pseudo";
i18next.init({
lng: "pseudo",
resources: {
pseudo: {
translation: pseudoLocalize(enTranslations)
}
}
});
// Atau manual
function pseudoLocalize(str) {
const map = {
a: "à", b: "ƀ", c: "ç", d: "ð", e: "é", f: "ƒ",
g: "ĝ", h: "ĥ", i: "î", j: "ĵ", k: "ĸ", l: "ĺ",
m: "ɱ", n: "ñ", o: "ö", p: "þ", r: "ŗ", s: "š",
t: "ţ", u: "û", v: "ṽ", w: "ŵ", x: "ẋ", y: "ÿ", z: "ž"
};
const pseudo = str.replace(/[a-z]/gi, (c) => {
const lower = c.toLowerCase();
return map[lower] ? (c === lower ? map[lower] : map[lower].toUpperCase()) : c;
});
return `[${pseudo} !!!]`;
}
Unit Test: Translation Keys
// Jest: pastikan semua key ada di semua locale
describe("Translation completeness", () => {
const en = require("../locales/en/common.json");
const id = require("../locales/id/common.json");
const ja = require("../locales/ja/common.json");
const getAllKeys = (obj, prefix = "") =>
Object.entries(obj).flatMap(([k, v]) => {
const key = prefix ? `${prefix}.${k}` : k;
return typeof v === "object" ? getAllKeys(v, key) : [key];
});
const enKeys = getAllKeys(en);
test("id has all English keys", () => {
const idKeys = getAllKeys(id);
const missing = enKeys.filter((k) => !idKeys.includes(k));
expect(missing).toEqual([]);
});
test("ja has all English keys", () => {
const jaKeys = getAllKeys(ja);
const missing = enKeys.filter((k) => !jaKeys.includes(k));
expect(missing).toEqual([]);
});
});
Component Test dengan Locale
// React Testing Library + i18next
import { render, screen } from "@testing-library/react";
import { I18nextProvider } from "react-i18next";
import i18n from "../test-i18n"; // test config
function renderWithI18n(component, locale = "id") {
i18n.changeLanguage(locale);
return render(
<I18nextProvider i18n={i18n}>{component}</I18nextProvider>
);
}
test("shows greeting in Indonesian", () => {
renderWithI18n(<Greeting name="Ahmad" />, "id");
expect(screen.getByText("Halo, Ahmad!")).toBeInTheDocument();
});
test("shows greeting in English", () => {
renderWithI18n(<Greeting name="Ahmad" />, "en");
expect(screen.getByText("Hello, Ahmad!")).toBeInTheDocument();
});
Visual Regression Test
// Playwright: screenshot per locale
import { test, expect } from "@playwright/test";
const locales = ["en", "id", "ar", "ja"];
for (const locale of locales) {
test(`homepage renders correctly in ${locale}`, async ({ page }) => {
await page.goto(`http://localhost:3000/${locale}`);
await expect(page).toHaveScreenshot(`homepage-${locale}.png`, {
fullPage: true,
maxDiffPixelRatio: 0.01,
});
});
}
// RTL-specific test
test("Arabic layout is mirrored", async ({ page }) => {
await page.goto("http://localhost:3000/ar");
const html = page.locator("html");
await expect(html).toHaveAttribute("dir", "rtl");
// Sidebar harus di kanan
const sidebar = page.locator("[data-testid=sidebar]");
const box = await sidebar.boundingBox();
const viewport = page.viewportSize();
expect(box.x).toBeGreaterThan(viewport.width / 2);
});