feat: implement i18n for product and category, add 5 new locales
This commit is contained in:
1 parent
b867063efb
commit
96d874233d
17 files changed
+557
-62
No files matched your search
@@ -0,0 +1,22 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "CategoryTranslation" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"categoryId" INTEGER NOT NULL,
|
||||
"langCode" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "CategoryTranslation_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "CategoryTranslation_categoryId_langCode_key" ON "CategoryTranslation"("categoryId", "langCode");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CategoryTranslation" ADD CONSTRAINT "CategoryTranslation_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- Seed Thai translations from existing category names
|
||||
INSERT INTO "CategoryTranslation" ("categoryId", "langCode", "name", "updatedAt")
|
||||
SELECT id, 'th', name, NOW() FROM "Category"
|
||||
ON CONFLICT ("categoryId", "langCode") DO NOTHING;
|
||||
@@ -18,12 +18,25 @@ model Scent {
|
||||
}
|
||||
|
||||
model Category {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
imageUrl String?
|
||||
products Product[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
imageUrl String?
|
||||
products Product[]
|
||||
translations CategoryTranslation[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model CategoryTranslation {
|
||||
id Int @id @default(autoincrement())
|
||||
categoryId Int
|
||||
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
||||
langCode String
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([categoryId, langCode])
|
||||
}
|
||||
|
||||
model Product {
|
||||
|
||||
@@ -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 } } }
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user