Testing i18n — i18n

Testing i18n Testing i18n bukan hanya cek apakah terjemahan muncul. Kita harus test layout RTL, string expansion, format angka/tanggal, dan edge cases karakter

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);
});

Yang akan kamu pelajari