diff --git a/src/app/(dashboard)/inventory/items/[itemId]/edit/page.tsx b/src/app/(dashboard)/inventory/items/[itemId]/edit/page.tsx index 616260c..a893e50 100644 --- a/src/app/(dashboard)/inventory/items/[itemId]/edit/page.tsx +++ b/src/app/(dashboard)/inventory/items/[itemId]/edit/page.tsx @@ -13,24 +13,26 @@ export default async function AddItem({ const categories = await CategoryService.findAll() const item = await ItemService.findByIdWithAssetCount(itemId) const { dictionary } = await getI18n() + const copy = dictionary.inventory.items if (!item) { - return
Item not found
+ return
{copy.edit.notFound}
} return (
{item?._count?.assets && item?._count.assets > 0 && (
-

{`This item has already assets assigned to it.`}

+

{copy.edit.hasAssetsWarning}

)}
-

Edit Item

+

{copy.edit.title}

diff --git a/src/app/(dashboard)/inventory/items/[itemId]/page.tsx b/src/app/(dashboard)/inventory/items/[itemId]/page.tsx index bbe1d7d..c403f46 100644 --- a/src/app/(dashboard)/inventory/items/[itemId]/page.tsx +++ b/src/app/(dashboard)/inventory/items/[itemId]/page.tsx @@ -1,4 +1,5 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { getI18n } from "@/i18n/server" import { AssetService } from "@/services/asset.service" import { ItemService } from "@/services/item.service" import { MovementService } from "@/services/movement.service" @@ -12,9 +13,11 @@ export default async function ItemPage({ const item = await ItemService.findByIdWithCategory(itemId) const assets = await AssetService.findByItemId(itemId) const movements = await MovementService.findAllByItemId(itemId) + const { dictionary } = await getI18n() + const copy = dictionary.inventory.items.detail if (!item) { - return
Item not found
+ return
{copy.notFound}
} return ( @@ -26,11 +29,11 @@ export default async function ItemPage({
- Category + {copy.labels.category} {item.category.name}
- Stock + {copy.labels.stock} {item.stock}
diff --git a/src/app/(dashboard)/inventory/items/_components/delete.item.button.tsx b/src/app/(dashboard)/inventory/items/_components/delete.item.button.tsx index c0da1ea..30cd1fa 100644 --- a/src/app/(dashboard)/inventory/items/_components/delete.item.button.tsx +++ b/src/app/(dashboard)/inventory/items/_components/delete.item.button.tsx @@ -7,7 +7,15 @@ import { toast } from "sonner" import { deleteItemAction } from "@/actions/item.actions" import { Button } from "@/components/ui/button" -export default function DeleteItemButton({ itemId }: { itemId: string }) { +import type { ItemDeleteCopy } from "./item.copy" + +export default function DeleteItemButton({ + itemId, + copy, +}: { + itemId: string + copy: ItemDeleteCopy +}) { const router = useRouter() const [isPending, startTransition] = useTransition() @@ -24,7 +32,7 @@ export default function DeleteItemButton({ itemId }: { itemId: string }) { toast.success(response.message) router.refresh() } else { - toast.error(response.message ?? "Unknown error") + toast.error(response.message ?? copy.unknownError) } }) } @@ -38,6 +46,7 @@ export default function DeleteItemButton({ itemId }: { itemId: string }) { size="icon" variant="outline" disabled={isPending} + aria-label={isPending ? copy.pending : copy.label} > diff --git a/src/app/(dashboard)/inventory/items/_components/item.copy.ts b/src/app/(dashboard)/inventory/items/_components/item.copy.ts new file mode 100644 index 0000000..0272dd3 --- /dev/null +++ b/src/app/(dashboard)/inventory/items/_components/item.copy.ts @@ -0,0 +1,6 @@ +import type { Dictionary } from "@/i18n/dictionaries" + +export type ItemListCopy = Dictionary["inventory"]["items"]["list"] +export type ItemDetailCopy = Dictionary["inventory"]["items"]["detail"] +export type ItemFormCopy = Dictionary["inventory"]["items"]["form"] +export type ItemDeleteCopy = Dictionary["inventory"]["items"]["delete"] diff --git a/src/app/(dashboard)/inventory/items/_components/new.item.form.tsx b/src/app/(dashboard)/inventory/items/_components/new.item.form.tsx index 284a312..8a8b55d 100644 --- a/src/app/(dashboard)/inventory/items/_components/new.item.form.tsx +++ b/src/app/(dashboard)/inventory/items/_components/new.item.form.tsx @@ -15,11 +15,15 @@ import { } from "@/schemas/item.schema" import type { CategorySummary } from "@/types" +import type { ItemFormCopy } from "./item.copy" + export default function NewItemForm({ categories, + formCopy, submitButtonCopy, }: { categories: CategorySummary[] + formCopy: ItemFormCopy submitButtonCopy: SubmitButtonCopy }) { const router = useRouter() @@ -61,12 +65,12 @@ export default function NewItemForm({
@@ -74,14 +78,14 @@ export default function NewItemForm({
- Create Item + {formCopy.createSubmit} ) diff --git a/src/app/(dashboard)/inventory/items/_components/update.item.form.tsx b/src/app/(dashboard)/inventory/items/_components/update.item.form.tsx index 9c12cbe..7933269 100644 --- a/src/app/(dashboard)/inventory/items/_components/update.item.form.tsx +++ b/src/app/(dashboard)/inventory/items/_components/update.item.form.tsx @@ -15,13 +15,17 @@ import { } from "@/schemas/item.schema" import type { CategorySummary, ItemWithAssetCount } from "@/types" +import type { ItemFormCopy } from "./item.copy" + export default function UpdateItemForm({ categories, item, + formCopy, submitButtonCopy, }: { categories: CategorySummary[] item: ItemWithAssetCount + formCopy: ItemFormCopy submitButtonCopy: SubmitButtonCopy }) { const router = useRouter() @@ -72,12 +76,12 @@ export default function UpdateItemForm({ {item?.id && }
@@ -85,7 +89,7 @@ export default function UpdateItemForm({
- Update Item + {formCopy.updateSubmit} ) diff --git a/src/app/(dashboard)/inventory/items/new/page.tsx b/src/app/(dashboard)/inventory/items/new/page.tsx index f53aa3b..7b81bdb 100644 --- a/src/app/(dashboard)/inventory/items/new/page.tsx +++ b/src/app/(dashboard)/inventory/items/new/page.tsx @@ -6,14 +6,16 @@ import NewItemForm from "../_components/new.item.form" export default async function NewItemPage() { const categories = await CategoryService.findAll() const { dictionary } = await getI18n() + const copy = dictionary.inventory.items return (
-

New Item

+

{copy.new.title}

diff --git a/src/app/(dashboard)/inventory/items/page.tsx b/src/app/(dashboard)/inventory/items/page.tsx index f136690..13ad0a2 100644 --- a/src/app/(dashboard)/inventory/items/page.tsx +++ b/src/app/(dashboard)/inventory/items/page.tsx @@ -4,6 +4,7 @@ import Link from "next/link" import PageHeader from "@/components/common/pageheader" import PaginationButtons from "@/components/common/pagination" import { Button } from "@/components/ui/button" +import { getI18n } from "@/i18n/server" import { ItemService } from "@/services/item.service" import DeleteItemButton from "./_components/delete.item.button" @@ -22,19 +23,22 @@ export default async function ItemsPage(props: { pageSize: 10, search, }) + const { dictionary } = await getI18n() + const copy = dictionary.inventory.items return (
{items.length === 0 && currentPage === 1 && (
- No items found. + {copy.list.empty}
)} @@ -44,19 +48,19 @@ export default async function ItemsPage(props: { - Name + {copy.list.columns.name} - Category + {copy.list.columns.category} - Assets + {copy.list.columns.assets} - Stock + {copy.list.columns.stock} - Actions + {copy.list.columns.actions} @@ -69,17 +73,25 @@ export default async function ItemsPage(props: { {item.stock} - - {item._count.assets === 0 && item.stock === 0 && ( - + )} diff --git a/src/i18n/dictionaries/en.ts b/src/i18n/dictionaries/en.ts index db8e711..c38f247 100644 --- a/src/i18n/dictionaries/en.ts +++ b/src/i18n/dictionaries/en.ts @@ -113,6 +113,75 @@ export const en = { idRequired: "ID is required", }, }, + items: { + list: { + title: "Items", + addLabel: "Add Item", + empty: "No items found.", + columns: { + name: "Name", + category: "Category", + assets: "Assets", + stock: "Stock", + actions: "Actions", + }, + actions: { + view: "View item", + edit: "Edit item", + delete: "Delete item", + }, + }, + detail: { + notFound: "Item not found", + labels: { + category: "Category", + stock: "Stock", + }, + }, + new: { + title: "New Item", + }, + edit: { + title: "Edit Item", + notFound: "Item not found", + hasAssetsWarning: "This item has already assets assigned to it.", + }, + form: { + nameLabel: "Name", + namePlaceholder: "Item name", + categoryLabel: "Category", + categoryPlaceholder: "Select a category", + stockLabel: "Stock", + stockPlaceholder: "0", + createSubmit: "Create Item", + updateSubmit: "Update Item", + }, + delete: { + label: "Delete item", + pending: "Deleting...", + unknownError: "Unknown error", + }, + actions: { + createSuccess: "Item created successfully!", + createFailure: "Error creating item", + updateSuccess: "Item updated successfully!", + updateFailure: "Failed to update item", + deleteSuccess: "Item deleted successfully!", + deleteFailure: "Failed to delete item", + duplicateName: "Item already exists", + notFound: "Item not found", + hasAssets: "Cannot delete item with assets", + hasStock: "Cannot delete item with stock", + invalidStock: "Invalid stock", + negativeStock: "Stock cannot be negative", + }, + schema: { + nameRequired: "Name is required", + categoryRequired: "Category is required", + stockRequired: "Stock is required", + itemRequired: "Item is required", + }, + }, }, login: { title: "Sign In", diff --git a/src/i18n/dictionaries/es.ts b/src/i18n/dictionaries/es.ts index a73ccd2..d3f6c67 100644 --- a/src/i18n/dictionaries/es.ts +++ b/src/i18n/dictionaries/es.ts @@ -116,6 +116,75 @@ export const es = { idRequired: "El ID es obligatorio", }, }, + items: { + list: { + title: "Artículos", + addLabel: "Agregar artículo", + empty: "No se encontraron artículos.", + columns: { + name: "Nombre", + category: "Categoría", + assets: "Activos", + stock: "Stock", + actions: "Acciones", + }, + actions: { + view: "Ver artículo", + edit: "Editar artículo", + delete: "Eliminar artículo", + }, + }, + detail: { + notFound: "Artículo no encontrado", + labels: { + category: "Categoría", + stock: "Stock", + }, + }, + new: { + title: "Nuevo artículo", + }, + edit: { + title: "Editar artículo", + notFound: "Artículo no encontrado", + hasAssetsWarning: "Este artículo ya tiene activos asignados.", + }, + form: { + nameLabel: "Nombre", + namePlaceholder: "Nombre del artículo", + categoryLabel: "Categoría", + categoryPlaceholder: "Selecciona una categoría", + stockLabel: "Stock", + stockPlaceholder: "0", + createSubmit: "Crear artículo", + updateSubmit: "Actualizar artículo", + }, + delete: { + label: "Eliminar artículo", + pending: "Eliminando...", + unknownError: "Error desconocido", + }, + actions: { + createSuccess: "Artículo creado correctamente", + createFailure: "Error al crear el artículo", + updateSuccess: "Artículo actualizado correctamente", + updateFailure: "Error al actualizar el artículo", + deleteSuccess: "Artículo eliminado correctamente", + deleteFailure: "Error al eliminar el artículo", + duplicateName: "El artículo ya existe", + notFound: "Artículo no encontrado", + hasAssets: "No se puede eliminar un artículo con activos", + hasStock: "No se puede eliminar un artículo con stock", + invalidStock: "Stock inválido", + negativeStock: "El stock no puede ser negativo", + }, + schema: { + nameRequired: "El nombre es obligatorio", + categoryRequired: "La categoría es obligatoria", + stockRequired: "El stock es obligatorio", + itemRequired: "El artículo es obligatorio", + }, + }, }, login: { title: "Iniciar sesión", diff --git a/tests/e2e/inventory-items.spec.ts b/tests/e2e/inventory-items.spec.ts new file mode 100644 index 0000000..edd82b9 --- /dev/null +++ b/tests/e2e/inventory-items.spec.ts @@ -0,0 +1,59 @@ +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 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("inventory items localization", () => { + test("renders item list and new form UI copy in Spanish", async ({ + baseURL, + page, + }) => { + await signInAsAdmin(page, baseURL) + await setLocaleCookie(page, "es", baseURL) + + await page.goto("/inventory/items") + + await expect(page.locator("html")).toHaveAttribute("lang", "es") + await expect(page.getByRole("heading", { name: "Artículos" })).toBeVisible() + await expect( + page.getByRole("link", { name: /Agregar artículo/ }), + ).toBeVisible() + await expect(page.getByText("No se encontraron artículos.")).toBeVisible() + + await page.goto("/inventory/items/new") + + await expect( + page.getByRole("heading", { name: "Nuevo artículo" }), + ).toBeVisible() + await expect(page.getByLabel("Nombre")).toBeVisible() + await expect(page.getByPlaceholder("Nombre del artículo")).toBeVisible() + await expect(page.getByLabel("Categoría")).toBeVisible() + await expect(page.locator("select#categoryId")).toContainText( + "Selecciona una categoría", + ) + await expect(page.getByLabel("Stock")).toBeVisible() + await expect( + page.getByRole("button", { name: "Crear artículo" }), + ).toBeVisible() + }) +}) diff --git a/tests/unit/i18n/dictionaries.test.ts b/tests/unit/i18n/dictionaries.test.ts index 24289b3..967e53f 100644 --- a/tests/unit/i18n/dictionaries.test.ts +++ b/tests/unit/i18n/dictionaries.test.ts @@ -266,6 +266,148 @@ describe("i18n dictionaries", () => { }) }) + it("provides localized inventory item copy for English and Spanish", () => { + expect(getDictionary("en").inventory.items).toEqual({ + list: { + title: "Items", + addLabel: "Add Item", + empty: "No items found.", + columns: { + name: "Name", + category: "Category", + assets: "Assets", + stock: "Stock", + actions: "Actions", + }, + actions: { + view: "View item", + edit: "Edit item", + delete: "Delete item", + }, + }, + detail: { + notFound: "Item not found", + labels: { + category: "Category", + stock: "Stock", + }, + }, + new: { + title: "New Item", + }, + edit: { + title: "Edit Item", + notFound: "Item not found", + hasAssetsWarning: "This item has already assets assigned to it.", + }, + form: { + nameLabel: "Name", + namePlaceholder: "Item name", + categoryLabel: "Category", + categoryPlaceholder: "Select a category", + stockLabel: "Stock", + stockPlaceholder: "0", + createSubmit: "Create Item", + updateSubmit: "Update Item", + }, + delete: { + label: "Delete item", + pending: "Deleting...", + unknownError: "Unknown error", + }, + actions: { + createSuccess: "Item created successfully!", + createFailure: "Error creating item", + updateSuccess: "Item updated successfully!", + updateFailure: "Failed to update item", + deleteSuccess: "Item deleted successfully!", + deleteFailure: "Failed to delete item", + duplicateName: "Item already exists", + notFound: "Item not found", + hasAssets: "Cannot delete item with assets", + hasStock: "Cannot delete item with stock", + invalidStock: "Invalid stock", + negativeStock: "Stock cannot be negative", + }, + schema: { + nameRequired: "Name is required", + categoryRequired: "Category is required", + stockRequired: "Stock is required", + itemRequired: "Item is required", + }, + }) + + expect(getDictionary("es").inventory.items).toEqual({ + list: { + title: "Artículos", + addLabel: "Agregar artículo", + empty: "No se encontraron artículos.", + columns: { + name: "Nombre", + category: "Categoría", + assets: "Activos", + stock: "Stock", + actions: "Acciones", + }, + actions: { + view: "Ver artículo", + edit: "Editar artículo", + delete: "Eliminar artículo", + }, + }, + detail: { + notFound: "Artículo no encontrado", + labels: { + category: "Categoría", + stock: "Stock", + }, + }, + new: { + title: "Nuevo artículo", + }, + edit: { + title: "Editar artículo", + notFound: "Artículo no encontrado", + hasAssetsWarning: "Este artículo ya tiene activos asignados.", + }, + form: { + nameLabel: "Nombre", + namePlaceholder: "Nombre del artículo", + categoryLabel: "Categoría", + categoryPlaceholder: "Selecciona una categoría", + stockLabel: "Stock", + stockPlaceholder: "0", + createSubmit: "Crear artículo", + updateSubmit: "Actualizar artículo", + }, + delete: { + label: "Eliminar artículo", + pending: "Eliminando...", + unknownError: "Error desconocido", + }, + actions: { + createSuccess: "Artículo creado correctamente", + createFailure: "Error al crear el artículo", + updateSuccess: "Artículo actualizado correctamente", + updateFailure: "Error al actualizar el artículo", + deleteSuccess: "Artículo eliminado correctamente", + deleteFailure: "Error al eliminar el artículo", + duplicateName: "El artículo ya existe", + notFound: "Artículo no encontrado", + hasAssets: "No se puede eliminar un artículo con activos", + hasStock: "No se puede eliminar un artículo con stock", + invalidStock: "Stock inválido", + negativeStock: "El stock no puede ser negativo", + }, + schema: { + nameRequired: "El nombre es obligatorio", + categoryRequired: "La categoría es obligatoria", + stockRequired: "El stock es obligatorio", + itemRequired: "El artículo es obligatorio", + }, + }) + }) + it("keeps dashboard home dictionary keys aligned across locales", () => { expect(getDictionary("en").dashboardHome).toEqual({ heading: "Dashboard",