feat(i18n): add language switcher
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { expect, type Page, test } from "@playwright/test"
|
||||
|
||||
async function setLocaleCookie(
|
||||
page: Page,
|
||||
locale: "en" | "es",
|
||||
baseURL?: string,
|
||||
) {
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: "stock-manager-locale",
|
||||
value: locale,
|
||||
url: baseURL ?? "http://127.0.0.1:3100",
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
async function expectLocaleCookie(page: Page, locale: "en" | "es") {
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const cookies = await page.context().cookies()
|
||||
return cookies.find((cookie) => cookie.name === "stock-manager-locale")
|
||||
?.value
|
||||
})
|
||||
.toBe(locale)
|
||||
}
|
||||
|
||||
async function signInAsAdmin(page: Page, baseURL?: string) {
|
||||
await setLocaleCookie(page, "en", baseURL)
|
||||
await page.goto("/login")
|
||||
await page.getByLabel("Username").fill("admin")
|
||||
await page.getByLabel("Password").fill("admin-password")
|
||||
await page.getByRole("button", { name: "Sign In" }).click()
|
||||
await expect(page).toHaveURL("/")
|
||||
}
|
||||
|
||||
test.describe("language switcher", () => {
|
||||
test("switches the login page language in place through the locale cookie", async ({
|
||||
baseURL,
|
||||
page,
|
||||
}) => {
|
||||
await setLocaleCookie(page, "en", baseURL)
|
||||
|
||||
await page.goto("/login")
|
||||
|
||||
await expect(page).toHaveURL(/\/login$/)
|
||||
await expect(page.locator("html")).toHaveAttribute("lang", "en")
|
||||
await expect(page.getByRole("heading", { name: "Sign In" })).toBeVisible()
|
||||
|
||||
await page.getByRole("button", { name: "Language: English" }).click()
|
||||
|
||||
const options = page.getByRole("menuitemradio")
|
||||
await expect(options).toHaveCount(2)
|
||||
await expect(
|
||||
page.getByRole("menuitemradio", { name: "English" }),
|
||||
).toBeVisible()
|
||||
await page.getByRole("menuitemradio", { name: "Spanish" }).click()
|
||||
|
||||
await expect(page).toHaveURL(/\/login$/)
|
||||
await expect(page.locator("html")).toHaveAttribute("lang", "es")
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Iniciar sesión" }),
|
||||
).toBeVisible()
|
||||
await expect(page.getByLabel("Usuario")).toBeVisible()
|
||||
await expect(page.getByLabel("Contraseña")).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Iniciar sesión" }),
|
||||
).toBeVisible()
|
||||
await expectLocaleCookie(page, "es")
|
||||
})
|
||||
|
||||
test("switches the authenticated dashboard language from the navbar", async ({
|
||||
baseURL,
|
||||
page,
|
||||
}) => {
|
||||
await signInAsAdmin(page, baseURL)
|
||||
|
||||
await expect(page.locator("html")).toHaveAttribute("lang", "en")
|
||||
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
|
||||
await expect(page.getByText("E2E Admin")).toBeVisible()
|
||||
|
||||
await page.getByRole("button", { name: "Language: English" }).click()
|
||||
await page.getByRole("menuitemradio", { name: "Spanish" }).click()
|
||||
|
||||
await expect(page).toHaveURL("/")
|
||||
await expect(page.locator("html")).toHaveAttribute("lang", "es")
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Panel de control" }),
|
||||
).toBeVisible()
|
||||
await expect(page.getByText("E2E Admin")).toBeVisible()
|
||||
await expect(page.getByText("admin@example.test")).toBeVisible()
|
||||
await expectLocaleCookie(page, "es")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
import { LOCALE_COOKIE_MAX_AGE_SECONDS } from "@/i18n/locales"
|
||||
|
||||
const headersMocks = vi.hoisted(() => ({
|
||||
cookieSet: vi.fn(),
|
||||
cookies: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("next/headers", () => ({
|
||||
cookies: headersMocks.cookies,
|
||||
}))
|
||||
|
||||
import { setLocaleAction } from "@/actions/i18n.actions"
|
||||
|
||||
describe("setLocaleAction", () => {
|
||||
beforeEach(() => {
|
||||
headersMocks.cookieSet.mockReset()
|
||||
headersMocks.cookies.mockReset()
|
||||
headersMocks.cookies.mockResolvedValue({
|
||||
set: headersMocks.cookieSet,
|
||||
})
|
||||
})
|
||||
|
||||
it("writes a validated supported locale to the locale cookie", async () => {
|
||||
const result = await setLocaleAction("es")
|
||||
|
||||
expect(result).toEqual({ success: true, locale: "es" })
|
||||
expect(headersMocks.cookies).toHaveBeenCalledOnce()
|
||||
expect(headersMocks.cookieSet).toHaveBeenCalledWith(
|
||||
"stock-manager-locale",
|
||||
"es",
|
||||
{
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
maxAge: LOCALE_COOKIE_MAX_AGE_SECONDS,
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it("rejects unsupported locales without writing a cookie", async () => {
|
||||
const result = await setLocaleAction("fr")
|
||||
|
||||
expect(result).toEqual({ success: false, error: "UNSUPPORTED_LOCALE" })
|
||||
expect(headersMocks.cookies).not.toHaveBeenCalled()
|
||||
expect(headersMocks.cookieSet).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -26,6 +26,24 @@ describe("i18n dictionaries", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("provides localized language switcher copy for English and Spanish", () => {
|
||||
expect(getDictionary("en").common.languageSwitcher).toEqual({
|
||||
label: "Language",
|
||||
options: {
|
||||
en: "English",
|
||||
es: "Spanish",
|
||||
},
|
||||
})
|
||||
|
||||
expect(getDictionary("es").common.languageSwitcher).toEqual({
|
||||
label: "Idioma",
|
||||
options: {
|
||||
en: "Inglés",
|
||||
es: "Español",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("keeps dashboard home dictionary keys aligned across locales", () => {
|
||||
expect(getDictionary("en").dashboardHome).toEqual({
|
||||
heading: "Dashboard",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DEFAULT_LOCALE_ENV_VAR,
|
||||
FALLBACK_LOCALE,
|
||||
isLocale,
|
||||
LOCALE_COOKIE_MAX_AGE_SECONDS,
|
||||
LOCALE_COOKIE_NAME,
|
||||
resolveDefaultLocale,
|
||||
resolveLocale,
|
||||
@@ -16,6 +17,7 @@ describe("i18n locales", () => {
|
||||
expect(FALLBACK_LOCALE).toBe("en")
|
||||
expect(DEFAULT_LOCALE_ENV_VAR).toBe("STOCK_MANAGER_DEFAULT_LOCALE")
|
||||
expect(LOCALE_COOKIE_NAME).toBe("stock-manager-locale")
|
||||
expect(LOCALE_COOKIE_MAX_AGE_SECONDS).toBe(60 * 60 * 24 * 365)
|
||||
})
|
||||
|
||||
it("accepts only exact supported locale codes", () => {
|
||||
|
||||
Reference in New Issue
Block a user