ridho-arachman-portfolio 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ridho-arachman-portfolio",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Personal portfolio web app (Next.js). Published automatically on every push to main.",
5
5
  "homepage": "https://ridho-arachman.vercel.app",
6
6
  "repository": {
@@ -0,0 +1,273 @@
1
+ // auto-translate.test.ts
2
+ // Auto-translate wiring at the route boundary, for every entity that has an
3
+ // Indonesian override column: projects, experience, certificates, categories.
4
+ //
5
+ // Three guarantees, checked for all eight handlers (POST + PUT each):
6
+ // 1. blank `id` override + working translator -> filled from the English base
7
+ // 2. blank `id` override + broken translator -> stays blank, save still
8
+ // succeeds (never a 500), so
9
+ // the resolver inherits English
10
+ // 3. non-blank `id` override -> kept verbatim, translator
11
+ // never asked for that field
12
+ //
13
+ // One table instead of four per-entity suites: the contract is identical for
14
+ // every entity, so testing it eight times in one place is what keeps the five
15
+ // sibling route files from drifting apart.
16
+ import { beforeEach, describe, expect, it, vi } from "vitest";
17
+
18
+ const { writes } = vi.hoisted(() => ({
19
+ writes: [] as Array<Record<string, unknown>>,
20
+ }));
21
+
22
+ vi.mock("next/cache", () => ({
23
+ revalidatePath: vi.fn(),
24
+ revalidateTag: vi.fn(),
25
+ }));
26
+
27
+ vi.mock("@/lib/session", () => ({
28
+ requireAdminSession: vi.fn(),
29
+ }));
30
+
31
+ vi.mock("@/lib/rate-limit", () => ({
32
+ applyRateLimit: vi.fn(),
33
+ }));
34
+
35
+ vi.mock("@/lib/translate", () => ({
36
+ translateTexts: vi.fn(async (texts: string[]) =>
37
+ texts.map((text) => `AUTO:${text}`),
38
+ ),
39
+ }));
40
+
41
+ vi.mock("@/lib/prisma", () => {
42
+ const model = () => ({
43
+ findMany: vi.fn(async () => []),
44
+ count: vi.fn(async () => 0),
45
+ findUnique: vi.fn(async () => ({ id: "row-1", slug: "stable-slug" })),
46
+ create: vi.fn(async (args: { data: Record<string, unknown> }) => {
47
+ writes.push(args.data);
48
+ return { id: "new-1", slug: "stable-slug" };
49
+ }),
50
+ update: vi.fn(async (args: { data: Record<string, unknown> }) => {
51
+ writes.push(args.data);
52
+ return { id: "row-1", slug: "stable-slug" };
53
+ }),
54
+ });
55
+
56
+ return {
57
+ default: {
58
+ project: model(),
59
+ experience: model(),
60
+ certificate: model(),
61
+ category: model(),
62
+ rateLimit: { findUnique: vi.fn(), create: vi.fn(), update: vi.fn() },
63
+ },
64
+ };
65
+ });
66
+
67
+ import { applyRateLimit } from "@/lib/rate-limit";
68
+ import { requireAdminSession } from "@/lib/session";
69
+ import { translateTexts } from "@/lib/translate";
70
+ import { POST as postProject } from "./projects/route";
71
+ import { PUT as putProject } from "./projects/[id]/route";
72
+ import { POST as postExperience } from "./experience/route";
73
+ import { PUT as putExperience } from "./experience/[id]/route";
74
+ import { POST as postCertificate } from "./certificates/route";
75
+ import { PUT as putCertificate } from "./certificates/[id]/route";
76
+ import { POST as postCategory } from "./categories/route";
77
+ import { PUT as putCategory } from "./categories/[id]/route";
78
+
79
+ type Body = Record<string, unknown>;
80
+
81
+ interface Entity {
82
+ name: string;
83
+ /** One translatable field, used as the override target in every scenario. */
84
+ field: string;
85
+ /** English base value written by the admin form. */
86
+ english: string;
87
+ create: Body;
88
+ update: Body;
89
+ post: (body: Body) => Promise<Response>;
90
+ put: (body: Body) => Promise<Response>;
91
+ }
92
+
93
+ function request(url: string, body: Body): Request {
94
+ return new Request(url, {
95
+ method: "POST",
96
+ headers: { "content-type": "application/json" },
97
+ body: JSON.stringify(body),
98
+ }) as Request;
99
+ }
100
+
101
+ const putCtx = { params: Promise.resolve({ id: "row-1" }) };
102
+
103
+ const entities: Entity[] = [
104
+ {
105
+ name: "project",
106
+ field: "title",
107
+ english: "English Project Title",
108
+ create: {
109
+ title: "English Project Title",
110
+ description: "English project description long enough.",
111
+ thumbnail: "https://example.com/thumb.png",
112
+ technologies: [],
113
+ gallery: [],
114
+ highlights: [],
115
+ isPublished: true,
116
+ order: 0,
117
+ },
118
+ update: {
119
+ title: "English Project Title",
120
+ description: "English project description long enough.",
121
+ },
122
+ post: (body) => postProject(request("http://x/api/admin/projects", body)),
123
+ put: (body) =>
124
+ putProject(request("http://x/api/admin/projects/row-1", body), putCtx),
125
+ },
126
+ {
127
+ name: "experience",
128
+ field: "title",
129
+ english: "English Role Title",
130
+ create: {
131
+ title: "English Role Title",
132
+ company: "Acme",
133
+ type: "WORK",
134
+ location: "Jakarta",
135
+ startDate: "2020-01-01",
136
+ isCurrent: false,
137
+ description: ["English achievement bullet"],
138
+ gallery: [],
139
+ isPublished: true,
140
+ order: 0,
141
+ },
142
+ update: {
143
+ title: "English Role Title",
144
+ description: ["English achievement bullet"],
145
+ },
146
+ post: (body) =>
147
+ postExperience(request("http://x/api/admin/experience", body)),
148
+ put: (body) =>
149
+ putExperience(request("http://x/api/admin/experience/row-1", body), putCtx),
150
+ },
151
+ {
152
+ name: "certificate",
153
+ field: "title",
154
+ english: "English Certificate Title",
155
+ create: {
156
+ title: "English Certificate Title",
157
+ issuer: "Issuer Inc",
158
+ issueDate: "2024-01-01",
159
+ skills: [],
160
+ summary: ["English summary point"],
161
+ isPublished: true,
162
+ order: 0,
163
+ },
164
+ update: {
165
+ title: "English Certificate Title",
166
+ summary: ["English summary point"],
167
+ },
168
+ post: (body) =>
169
+ postCertificate(request("http://x/api/admin/certificates", body)),
170
+ put: (body) =>
171
+ putCertificate(
172
+ request("http://x/api/admin/certificates/row-1", body),
173
+ putCtx,
174
+ ),
175
+ },
176
+ {
177
+ name: "category",
178
+ field: "name",
179
+ english: "English Category",
180
+ create: {
181
+ name: "English Category",
182
+ description: "English category description",
183
+ order: 0,
184
+ },
185
+ update: {
186
+ name: "English Category",
187
+ description: "English category description",
188
+ },
189
+ post: (body) => postCategory(request("http://x/api/admin/categories", body)),
190
+ put: (body) =>
191
+ putCategory(request("http://x/api/admin/categories/row-1", body), putCtx),
192
+ },
193
+ ];
194
+
195
+ function storedIdOverride(field: string): string {
196
+ const translations = writes.at(-1)?.translations as
197
+ | { id?: Record<string, string> }
198
+ | undefined;
199
+ return translations?.id?.[field] ?? "";
200
+ }
201
+
202
+ function translateCalls(): string[] {
203
+ return vi.mocked(translateTexts).mock.calls.flatMap(([texts]) => texts);
204
+ }
205
+
206
+ describe.each(entities)("$name auto-translate", (entity) => {
207
+ const cases = [
208
+ { verb: "POST", run: entity.post, base: entity.create, ok: 201 },
209
+ { verb: "PUT", run: entity.put, base: entity.update, ok: 200 },
210
+ ] as const;
211
+
212
+ beforeEach(() => {
213
+ writes.length = 0;
214
+ vi.mocked(translateTexts).mockClear();
215
+ vi.mocked(translateTexts).mockImplementation(async (texts) =>
216
+ texts.map((text) => `AUTO:${text}`),
217
+ );
218
+ vi.mocked(requireAdminSession).mockResolvedValue({
219
+ user: { id: "admin-1" },
220
+ } as Awaited<ReturnType<typeof requireAdminSession>>);
221
+ vi.mocked(applyRateLimit).mockResolvedValue({
222
+ allowed: true,
223
+ headers: {},
224
+ } as Awaited<ReturnType<typeof applyRateLimit>>);
225
+ });
226
+
227
+ it.each(cases)(
228
+ "$verb fills a blank id override from the English base",
229
+ async ({ run, base, ok }) => {
230
+ const res = await run({
231
+ ...base,
232
+ translations: { id: { [entity.field]: "" } },
233
+ });
234
+
235
+ expect(res.status).toBe(ok);
236
+ expect(storedIdOverride(entity.field)).toBe(`AUTO:${entity.english}`);
237
+ },
238
+ );
239
+
240
+ it.each(cases)(
241
+ "$verb saves successfully with a blank id override when the translator fails",
242
+ async ({ run, base, ok }) => {
243
+ // translateOne swallows network errors, timeouts and MyMemory error
244
+ // statuses alike and resolves to "", so this one mock covers them all.
245
+ vi.mocked(translateTexts).mockImplementation(async (texts) =>
246
+ texts.map(() => ""),
247
+ );
248
+
249
+ const res = await run({
250
+ ...base,
251
+ translations: { id: { [entity.field]: "" } },
252
+ });
253
+
254
+ expect(res.status).toBe(ok);
255
+ expect(writes).toHaveLength(1);
256
+ expect(storedIdOverride(entity.field)).toBe("");
257
+ },
258
+ );
259
+
260
+ it.each(cases)(
261
+ "$verb never overwrites a manual id override",
262
+ async ({ run, base, ok }) => {
263
+ const res = await run({
264
+ ...base,
265
+ translations: { id: { [entity.field]: "Judul Manual" } },
266
+ });
267
+
268
+ expect(res.status).toBe(ok);
269
+ expect(storedIdOverride(entity.field)).toBe("Judul Manual");
270
+ expect(translateCalls()).not.toContain("Judul Manual");
271
+ },
272
+ );
273
+ });
@@ -4,6 +4,11 @@ import prisma from "@/lib/prisma";
4
4
  import { notDeleted, slugReserved } from "@/lib/soft-delete";
5
5
  import { slugify } from "@/utils/slug";
6
6
  import { categoryUpdateSchema } from "@/schema/category";
7
+ import { CATEGORY_TRANSLATABLE_FIELDS } from "@/schema/content-translations";
8
+ import {
9
+ translatableBase,
10
+ withAutoIdTranslations,
11
+ } from "@/lib/content-i18n";
7
12
  import { requireAdminSession } from "@/lib/session";
8
13
  import { applyRateLimit } from "@/lib/rate-limit";
9
14
  import {successResponse, errorResponse, errorResponseFrom } from "@/lib/api-helpers";
@@ -58,6 +63,21 @@ export async function PUT(
58
63
  const slug =
59
64
  data.slug || (data.name ? slugify(data.name) : undefined);
60
65
 
66
+ // Auto-fill blank Indonesian overrides; translator failure keeps blanks.
67
+ // Only when the translations key is present: an absent key leaves the
68
+ // stored column untouched so existing manual overrides are never lost.
69
+ const translations =
70
+ data.translations !== undefined
71
+ ? await withAutoIdTranslations(
72
+ translatableBase(
73
+ { ...existing, ...data },
74
+ CATEGORY_TRANSLATABLE_FIELDS,
75
+ ),
76
+ data.translations,
77
+ CATEGORY_TRANSLATABLE_FIELDS,
78
+ )
79
+ : undefined;
80
+
61
81
  const category = await prisma.category.update({
62
82
  where: { id },
63
83
  data: {
@@ -69,7 +89,7 @@ export async function PUT(
69
89
  ...(data.order !== undefined && { order: data.order }),
70
90
  // Spread seperti di POST: aman terhadap client yang belum di-generate ulang.
71
91
  ...(data.translations !== undefined && {
72
- translations: data.translations ?? Prisma.DbNull,
92
+ translations: translations ?? Prisma.DbNull,
73
93
  }),
74
94
  },
75
95
  });
@@ -10,6 +10,11 @@ import {successResponse,
10
10
  paginatedResponse,
11
11
  parsePagination, errorResponseFrom } from "@/lib/api-helpers";
12
12
  import { categoryCreateSchema } from "@/schema/category";
13
+ import { CATEGORY_TRANSLATABLE_FIELDS } from "@/schema/content-translations";
14
+ import {
15
+ translatableBase,
16
+ withAutoIdTranslations,
17
+ } from "@/lib/content-i18n";
13
18
 
14
19
  export const dynamic = "force-dynamic";
15
20
 
@@ -62,6 +67,13 @@ export async function POST(req: Request) {
62
67
  const data = parsed.data;
63
68
  const slug = data.slug || slugify(data.name);
64
69
 
70
+ // Auto-fill blank Indonesian overrides; translator failure keeps blanks.
71
+ const translations = await withAutoIdTranslations(
72
+ translatableBase({ ...data }, CATEGORY_TRANSLATABLE_FIELDS),
73
+ data.translations,
74
+ CATEGORY_TRANSLATABLE_FIELDS,
75
+ );
76
+
65
77
  const category = await prisma.category.create({
66
78
  data: {
67
79
  name: data.name,
@@ -73,7 +85,7 @@ export async function POST(req: Request) {
73
85
  // kena excess-property check, jadi baris ini kompilasi sebelum
74
86
  // maupun sesudah generate. Pola kondisinya sama dengan PUT project.
75
87
  ...(data.translations !== undefined && {
76
- translations: data.translations ?? Prisma.DbNull,
88
+ translations: translations ?? Prisma.DbNull,
77
89
  }),
78
90
  },
79
91
  });
@@ -4,6 +4,11 @@ import prisma from "@/lib/prisma";
4
4
  import { notDeleted, slugReserved } from "@/lib/soft-delete";
5
5
  import { slugify } from "@/utils/slug";
6
6
  import { certificateUpdateSchema } from "@/schema/certificate";
7
+ import { CERTIFICATE_TRANSLATABLE_FIELDS } from "@/schema/content-translations";
8
+ import {
9
+ translatableBase,
10
+ withAutoIdTranslations,
11
+ } from "@/lib/content-i18n";
7
12
  import { requireAdminSession } from "@/lib/session";
8
13
  import { applyRateLimit } from "@/lib/rate-limit";
9
14
  import {successResponse, errorResponse, errorResponseFrom } from "@/lib/api-helpers";
@@ -58,6 +63,21 @@ export async function PUT(
58
63
  const slug =
59
64
  data.slug || (data.title ? slugify(data.title) : undefined);
60
65
 
66
+ // Auto-fill blank Indonesian overrides; translator failure keeps blanks.
67
+ // Only when the translations key is present: an absent key leaves the
68
+ // stored column untouched so existing manual overrides are never lost.
69
+ const translations =
70
+ data.translations !== undefined
71
+ ? await withAutoIdTranslations(
72
+ translatableBase(
73
+ { ...existing, ...data },
74
+ CERTIFICATE_TRANSLATABLE_FIELDS,
75
+ ),
76
+ data.translations,
77
+ CERTIFICATE_TRANSLATABLE_FIELDS,
78
+ )
79
+ : undefined;
80
+
61
81
  const certificate = await prisma.certificate.update({
62
82
  where: { id },
63
83
  data: {
@@ -87,7 +107,7 @@ export async function PUT(
87
107
  }),
88
108
  ...(data.order !== undefined && { order: data.order }),
89
109
  ...(data.translations !== undefined && {
90
- translations: data.translations ?? Prisma.DbNull,
110
+ translations: translations ?? Prisma.DbNull,
91
111
  }),
92
112
  },
93
113
  });
@@ -10,6 +10,11 @@ import {successResponse,
10
10
  paginatedResponse,
11
11
  parsePagination, errorResponseFrom } from "@/lib/api-helpers";
12
12
  import { certificateCreateSchema } from "@/schema/certificate";
13
+ import { CERTIFICATE_TRANSLATABLE_FIELDS } from "@/schema/content-translations";
14
+ import {
15
+ translatableBase,
16
+ withAutoIdTranslations,
17
+ } from "@/lib/content-i18n";
13
18
 
14
19
  export const dynamic = "force-dynamic";
15
20
 
@@ -67,6 +72,13 @@ export async function POST(req: Request) {
67
72
  const data = parsed.data;
68
73
  const slug = data.slug || slugify(data.title);
69
74
 
75
+ // Auto-fill blank Indonesian overrides; translator failure keeps blanks.
76
+ const translations = await withAutoIdTranslations(
77
+ translatableBase({ ...data }, CERTIFICATE_TRANSLATABLE_FIELDS),
78
+ data.translations,
79
+ CERTIFICATE_TRANSLATABLE_FIELDS,
80
+ );
81
+
70
82
  const certificate = await prisma.certificate.create({
71
83
  data: {
72
84
  slug,
@@ -82,7 +94,7 @@ export async function POST(req: Request) {
82
94
  summary: data.summary,
83
95
  isPublished: data.isPublished,
84
96
  order: data.order,
85
- translations: data.translations ?? Prisma.DbNull,
97
+ translations: translations ?? Prisma.DbNull,
86
98
  },
87
99
  });
88
100
 
@@ -3,6 +3,11 @@ import { Prisma } from "@/generated/prisma/client";
3
3
  import prisma from "@/lib/prisma";
4
4
  import { notDeleted, slugReserved } from "@/lib/soft-delete";
5
5
  import { experienceUpdateSchema } from "@/schema/experience";
6
+ import { EXPERIENCE_TRANSLATABLE_FIELDS } from "@/schema/content-translations";
7
+ import {
8
+ translatableBase,
9
+ withAutoIdTranslations,
10
+ } from "@/lib/content-i18n";
6
11
  import { requireAdminSession } from "@/lib/session";
7
12
  import { applyRateLimit } from "@/lib/rate-limit";
8
13
  import {successResponse, errorResponse, errorResponseFrom } from "@/lib/api-helpers";
@@ -55,6 +60,21 @@ export async function PUT(
55
60
  return errorResponse("Experience not found", 404);
56
61
  }
57
62
 
63
+ // Auto-fill blank Indonesian overrides; translator failure keeps blanks.
64
+ // Only when the translations key is present: an absent key leaves the
65
+ // stored column untouched so existing manual overrides are never lost.
66
+ const translations =
67
+ data.translations !== undefined
68
+ ? await withAutoIdTranslations(
69
+ translatableBase(
70
+ { ...existing, ...data },
71
+ EXPERIENCE_TRANSLATABLE_FIELDS,
72
+ ),
73
+ data.translations,
74
+ EXPERIENCE_TRANSLATABLE_FIELDS,
75
+ )
76
+ : undefined;
77
+
58
78
  const experience = await prisma.experience.update({
59
79
  where: { id },
60
80
  data: {
@@ -80,7 +100,7 @@ export async function PUT(
80
100
  ...(data.gallery !== undefined && { gallery: data.gallery }),
81
101
  ...(data.order !== undefined && { order: data.order }),
82
102
  ...(data.translations !== undefined && {
83
- translations: data.translations ?? Prisma.DbNull,
103
+ translations: translations ?? Prisma.DbNull,
84
104
  }),
85
105
  },
86
106
  });
@@ -9,6 +9,11 @@ import {successResponse,
9
9
  paginatedResponse,
10
10
  parsePagination, errorResponseFrom } from "@/lib/api-helpers";
11
11
  import { experienceCreateSchema } from "@/schema/experience";
12
+ import { EXPERIENCE_TRANSLATABLE_FIELDS } from "@/schema/content-translations";
13
+ import {
14
+ translatableBase,
15
+ withAutoIdTranslations,
16
+ } from "@/lib/content-i18n";
12
17
 
13
18
  export const dynamic = "force-dynamic";
14
19
 
@@ -67,6 +72,13 @@ export async function POST(req: Request) {
67
72
  const data = parsed.data;
68
73
  const slug = data.slug ?? slugify(data.title);
69
74
 
75
+ // Auto-fill blank Indonesian overrides; translator failure keeps blanks.
76
+ const translations = await withAutoIdTranslations(
77
+ translatableBase({ ...data }, EXPERIENCE_TRANSLATABLE_FIELDS),
78
+ data.translations,
79
+ EXPERIENCE_TRANSLATABLE_FIELDS,
80
+ );
81
+
70
82
  const experience = await prisma.experience.create({
71
83
  data: {
72
84
  ...data,
@@ -75,6 +87,7 @@ export async function POST(req: Request) {
75
87
  endDate: data.endDate ? new Date(data.endDate) : null,
76
88
  description: data.description ?? [],
77
89
  gallery: data.gallery ?? [],
90
+ translations: translations ?? undefined,
78
91
  },
79
92
  });
80
93
 
@@ -7,6 +7,11 @@ import { requireAdminSession } from "@/lib/session";
7
7
  import { applyRateLimit } from "@/lib/rate-limit";
8
8
  import {successResponse, errorResponse, errorResponseFrom } from "@/lib/api-helpers";
9
9
  import { projectUpdateSchema } from "@/schema/project";
10
+ import { PROJECT_TRANSLATABLE_FIELDS } from "@/schema/content-translations";
11
+ import {
12
+ translatableBase,
13
+ withAutoIdTranslations,
14
+ } from "@/lib/content-i18n";
10
15
 
11
16
  export const dynamic = "force-dynamic";
12
17
 
@@ -74,6 +79,22 @@ export async function PUT(
74
79
  const slug =
75
80
  data.slug || (data.title ? slugify(data.title) : undefined);
76
81
 
82
+ // Auto-fill blank Indonesian overrides; translator failure keeps blanks.
83
+ // Only when the translations key is present: an absent key leaves the
84
+ // stored column untouched so existing manual overrides are never lost.
85
+ // Base falls back to the stored row for fields missing in partial updates.
86
+ const translations =
87
+ data.translations !== undefined
88
+ ? await withAutoIdTranslations(
89
+ translatableBase(
90
+ { ...existing, ...data },
91
+ PROJECT_TRANSLATABLE_FIELDS,
92
+ ),
93
+ data.translations,
94
+ PROJECT_TRANSLATABLE_FIELDS,
95
+ )
96
+ : undefined;
97
+
77
98
  const project = await prisma.project.update({
78
99
  where: { id },
79
100
  data: {
@@ -101,7 +122,7 @@ export async function PUT(
101
122
  categoryId: data.categoryId || null,
102
123
  }),
103
124
  ...(data.translations !== undefined && {
104
- translations: data.translations ?? Prisma.DbNull,
125
+ translations: translations ?? Prisma.DbNull,
105
126
  }),
106
127
  },
107
128
  });
@@ -31,6 +31,12 @@ vi.mock("@/lib/prisma", () => ({
31
31
  },
32
32
  }));
33
33
 
34
+ // POST auto-fills blank Indonesian overrides, which reaches MyMemory unless the
35
+ // translator is stubbed. Keep this suite off the network.
36
+ vi.mock("@/lib/translate", () => ({
37
+ translateTexts: vi.fn(async (texts: string[]) => texts.map(() => "")),
38
+ }));
39
+
34
40
  import { revalidatePath, revalidateTag } from "next/cache";
35
41
  import prisma from "@/lib/prisma";
36
42
  import { requireAdminSession } from "@/lib/session";
@@ -4,6 +4,11 @@ import prisma from "@/lib/prisma";
4
4
  import { notDeleted, trashedOnly, slugReserved } from "@/lib/soft-delete";
5
5
  import { slugify } from "@/utils/slug";
6
6
  import { projectCreateSchema } from "@/schema/project";
7
+ import { PROJECT_TRANSLATABLE_FIELDS } from "@/schema/content-translations";
8
+ import {
9
+ translatableBase,
10
+ withAutoIdTranslations,
11
+ } from "@/lib/content-i18n";
7
12
  import { requireAdminSession } from "@/lib/session";
8
13
  import { applyRateLimit } from "@/lib/rate-limit";
9
14
  import {successResponse,
@@ -62,6 +67,13 @@ export async function POST(req: Request) {
62
67
  const data = parsed.data;
63
68
  const slug = data.slug || slugify(data.title);
64
69
 
70
+ // Auto-fill blank Indonesian overrides; translator failure keeps blanks.
71
+ const translations = await withAutoIdTranslations(
72
+ translatableBase({ ...data }, PROJECT_TRANSLATABLE_FIELDS),
73
+ data.translations,
74
+ PROJECT_TRANSLATABLE_FIELDS,
75
+ );
76
+
65
77
  const project = await prisma.project.create({
66
78
  data: {
67
79
  title: data.title,
@@ -79,7 +91,7 @@ export async function POST(req: Request) {
79
91
  isPublished: data.isPublished,
80
92
  order: data.order,
81
93
  categoryId: data.categoryId || null,
82
- translations: data.translations ?? Prisma.DbNull,
94
+ translations: translations ?? Prisma.DbNull,
83
95
  },
84
96
  });
85
97
 
@@ -39,7 +39,7 @@ export const ADMIN_CATEGORIES = {
39
39
  fieldOrder: "Order",
40
40
  fieldOrderHint: "Lower values appear first.",
41
41
  idSectionLabel: "Bahasa Indonesia (opsional)",
42
- idSectionNote: "Kosongkan field yang tidak perlu diterjemahkan — otomatis pakai versi Inggris.",
42
+ idSectionNote: "Leave blank to auto-translate from English.",
43
43
  fieldIdName: "Name (ID)",
44
44
  fieldIdNamePlaceholder: "e.g. Pengembangan Web",
45
45
  fieldIdDescription: "Description (ID)",
@@ -50,7 +50,7 @@ export const ADMIN_CERTIFICATES = {
50
50
  fieldOrder: "Order",
51
51
  fieldOrderHint: "Lower values appear first.",
52
52
  idSectionLabel: "Bahasa Indonesia (opsional)",
53
- idSectionNote: "Kosongkan field yang tidak perlu diterjemahkan — otomatis pakai versi Inggris.",
53
+ idSectionNote: "Leave blank to auto-translate from English.",
54
54
  fieldIdTitle: "Title (ID)",
55
55
  fieldIdTitlePlaceholder: "e.g. AWS Certified Cloud Practitioner",
56
56
  fieldIdSummary: "Summary (ID, satu poin per baris)",
@@ -73,7 +73,7 @@ export const ADMIN_EXPERIENCE = {
73
73
  orderLabel: "Display Order",
74
74
  orderPlaceholder: "0",
75
75
  idSectionLabel: "Bahasa Indonesia (opsional)",
76
- idSectionNote: "Kosongkan field yang tidak perlu diterjemahkan — otomatis pakai versi Inggris.",
76
+ idSectionNote: "Leave blank to auto-translate from English.",
77
77
  idTitleLabel: "Role (ID)",
78
78
  idTitlePlaceholder: "e.g. Frontend Developer",
79
79
  idDescriptionLabel: "Description (ID, satu poin per baris)",
@@ -47,7 +47,7 @@ export const ADMIN_PROJECTS = {
47
47
  fieldOrder: "Order",
48
48
  fieldOrderHint: "Lower values appear first.",
49
49
  idSectionLabel: "Bahasa Indonesia (opsional)",
50
- idSectionNote: "Kosongkan field yang tidak perlu diterjemahkan — otomatis pakai versi Inggris.",
50
+ idSectionNote: "Leave blank to auto-translate from English.",
51
51
  fieldIdTitle: "Title (ID)",
52
52
  fieldIdTitlePlaceholder: "e.g. Platform Portfolio Web3",
53
53
  fieldIdDescription: "Description (ID)",