feat: implement i18n for product and category, add 5 new locales

This commit is contained in:
Your Name committed 2026-06-13 23:58:50 -04:00
1 parent b867063efb
commit 96d874233d
17 files changed
+557 -62

No files matched your search

+81 -8
View File
@@ -1,17 +1,37 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
/** Resolve category name based on lang */
function resolveCatName(cat: any, lang: string): any {
if (lang === 'all') {
const nameObj: any = {};
cat.translations?.forEach((tr: any) => { nameObj[tr.langCode] = tr.name; });
return nameObj;
}
const trans = cat.translations?.find((t: any) => t.langCode === lang)
|| cat.translations?.find((t: any) => t.langCode === 'th')
|| cat.translations?.[0];
return trans?.name ?? cat.name;
}
export const categoriesRoute = new Elysia({ prefix: "/categories" })
.get("/", async () => {
.get("/", async ({ query }) => {
const lang = (query as any).lang || 'th';
const categories = await prisma.category.findMany({
orderBy: { name: "asc" },
include: {
translations: true,
_count: {
select: { products: true },
},
},
});
return categories;
return categories.map((cat: any) => ({
...cat,
name: resolveCatName(cat, lang),
}));
})
.get("/:id/products", async ({ params, query, status }) => {
const id = parseInt(params.id);
@@ -27,15 +47,16 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
prisma.category.findUnique({
where: { id },
include: {
translations: true,
products: {
include: {
scent: true,
category: true,
category: { include: { translations: true } },
translations: true,
variants: { orderBy: { price: "asc" as const } },
images: { orderBy: { order: "asc" as const } },
},
orderBy: { name: "asc" },
orderBy: { id: "asc" },
skip,
take: limit,
},
@@ -52,7 +73,7 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
return {
category: {
id: category.id,
name: category.name,
name: resolveCatName(category, lang),
imageUrl: category.imageUrl,
_count: category._count,
},
@@ -66,10 +87,22 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return { ...p, name: nameObj, description: descObj, details: detailsObj };
return {
...p,
name: nameObj,
description: descObj,
details: detailsObj,
category: { ...p.category, name: resolveCatName(p.category, lang) },
};
} else {
const trans = p.translations?.find((t: any) => t.langCode === lang) || p.translations?.find((t: any) => t.langCode === 'th') || p.translations?.[0];
return { ...p, name: trans?.name || p.scent?.name || '', description: trans?.description, details: trans?.details };
return {
...p,
name: trans?.name || p.scent?.name || '',
description: trans?.description,
details: trans?.details,
category: { ...p.category, name: resolveCatName(p.category, lang) },
};
}
}),
meta: {
@@ -80,12 +113,53 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
},
};
})
.patch("/:id/translations", async ({ params, body, status }) => {
const id = parseInt(params.id);
if (isNaN(id)) return status(400, { message: "Invalid category ID" });
const payload = body as any;
const results: any[] = [];
for (const lang of LANGS) {
const name = typeof payload.name === 'string'
? (lang === 'th' ? payload.name : null)
: payload.name?.[lang];
if (!name) continue;
const existing = await prisma.categoryTranslation.findUnique({
where: { categoryId_langCode: { categoryId: id, langCode: lang } }
});
if (existing) {
const updated = await prisma.categoryTranslation.update({
where: { id: existing.id },
data: { name },
});
results.push(updated);
} else {
const created = await prisma.categoryTranslation.create({
data: { categoryId: id, langCode: lang, name },
});
results.push(created);
}
}
const updated = await prisma.category.findUnique({
where: { id },
include: { translations: true, _count: { select: { products: true } } },
});
return { success: true, translations: results, category: updated };
})
.post("/", async ({ body, status }) => {
try {
const category = await prisma.category.create({
data: {
name: body.name,
imageUrl: body.imageUrl,
// Auto-seed Thai translation
translations: { create: { langCode: 'th', name: body.name } },
},
});
return category;
@@ -135,7 +209,6 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
if (isNaN(id)) return status(400, { message: "Invalid category ID" });
try {
// Ensure no products are attached, or decide if we should cascade delete
const category = await prisma.category.findUnique({
where: { id },
include: { _count: { select: { products: true } } }
+19 -5
View File
@@ -34,7 +34,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
prisma.product.findMany({
where,
include: {
category: true,
category: { include: { translations: true } },
scent: true,
translations: true,
variants: {
@@ -62,10 +62,17 @@ export const productsRoute = new Elysia({ prefix: "/products" })
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return { ...p, name: nameObj, description: descObj, details: detailsObj };
return { ...p, name: nameObj, description: descObj, details: detailsObj, category: { ...p.category, name: p.category.name } }; // Actually for 'all', keep as is, but wait, category name needs resolving if we want it? Let's just resolve it simply below.
} else {
const trans = p.translations.find(t => t.langCode === lang) || p.translations.find(t => t.langCode === 'th') || p.translations[0];
return { ...p, name: trans?.name || '', description: trans?.description, details: trans?.details };
const catTrans = p.category.translations?.find((t: any) => t.langCode === lang) || p.category.translations?.find((t: any) => t.langCode === 'th') || p.category.translations?.[0];
return {
...p,
name: trans?.name || '',
description: trans?.description,
details: trans?.details,
category: { ...p.category, name: catTrans?.name || p.category.name }
};
}
});
@@ -93,7 +100,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
const product = await prisma.product.findUnique({
where: { id },
include: {
category: true,
category: { include: { translations: true } },
scent: true,
translations: true,
variants: {
@@ -120,7 +127,14 @@ export const productsRoute = new Elysia({ prefix: "/products" })
return { ...product, name: nameObj, description: descObj, details: detailsObj };
} else {
const trans = product.translations.find(t => t.langCode === lang) || product.translations.find(t => t.langCode === 'th') || product.translations[0];
return { ...product, name: trans?.name || '', description: trans?.description, details: trans?.details };
const catTrans = product.category.translations?.find((t: any) => t.langCode === lang) || product.category.translations?.find((t: any) => t.langCode === 'th') || product.category.translations?.[0];
return {
...product,
name: trans?.name || '',
description: trans?.description,
details: trans?.details,
category: { ...product.category, name: catTrans?.name || product.category.name }
};
}
})
.put(