scale-stack 0.0.1-alpha.2 → 0.0.2

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +67 -5
  3. package/dist/index.js +4081 -106
  4. package/package.json +7 -2
  5. package/templates/ai-chat/chat-panel.tsx.ejs +70 -0
  6. package/templates/ai-chat/layout.tsx.ejs +35 -0
  7. package/templates/ai-chat/page.tsx.ejs +37 -0
  8. package/templates/ai-chat/route.ts.ejs +68 -0
  9. package/templates/ai-chat/use-chat.ts.ejs +26 -0
  10. package/templates/analytics/analytics-provider.tsx.ejs +32 -0
  11. package/templates/analytics/analytics.ts.ejs +40 -0
  12. package/templates/auth/auth-client.ts.ejs +7 -0
  13. package/templates/auth/auth.ts.ejs +45 -0
  14. package/templates/auth/route.ts.ejs +4 -0
  15. package/templates/auth/sign-in-page.tsx.ejs +122 -0
  16. package/templates/auth/sign-up-page.tsx.ejs +137 -0
  17. package/templates/auth/unauthorized.tsx.ejs +28 -0
  18. package/templates/core/layout.tsx.ejs +46 -0
  19. package/templates/core/loading.tsx.ejs +7 -0
  20. package/templates/core/next.config.ts.ejs +33 -0
  21. package/templates/error-handling/catch-all-not-found-page.tsx.ejs +5 -0
  22. package/templates/error-handling/error.tsx.ejs +33 -0
  23. package/templates/error-handling/global-error.tsx.ejs +32 -0
  24. package/templates/error-handling/not-found.tsx.ejs +28 -0
  25. package/templates/eslint-prettier/.prettierignore.ejs +29 -0
  26. package/templates/eslint-prettier/eslint.config.mjs.ejs +31 -0
  27. package/templates/form-handling/dashboard-page.tsx.ejs +39 -0
  28. package/templates/form-handling/example-form-action.ts.ejs +50 -0
  29. package/templates/form-handling/example-form-schema.ts.ejs +87 -0
  30. package/templates/form-handling/example-form.tsx.ejs +428 -0
  31. package/templates/i18n/ar.json.ejs +77 -0
  32. package/templates/i18n/en.json.ejs +77 -0
  33. package/templates/i18n/locale-layout.tsx.ejs +81 -0
  34. package/templates/i18n/navigation.ts.ejs +5 -0
  35. package/templates/i18n/next-intl.d.ts.ejs +9 -0
  36. package/templates/i18n/request.ts.ejs +15 -0
  37. package/templates/i18n/routing.ts.ejs +7 -0
  38. package/templates/orm/prisma.config.ts.ejs +12 -0
  39. package/templates/orm/prisma.ts.ejs +17 -0
  40. package/templates/orm/schema.prisma.ejs +8 -0
  41. package/templates/pre-commit/prek.toml.ejs +35 -0
  42. package/templates/proxy/proxy.ts.ejs +81 -0
  43. package/templates/server-actions/safe-action.ts.ejs +51 -0
  44. package/templates/ui/client-side-wrappers.tsx.ejs +19 -0
  45. package/templates/ui/page.tsx.ejs +117 -0
  46. package/templates/utility-libs/date-fns.SKILL.md.ejs +34 -0
  47. package/templates/utility-libs/motion.SKILL.md.ejs +234 -0
  48. package/templates/utility-libs/ts-pattern.SKILL.md.ejs +44 -0
  49. package/templates/utility-libs/usehooks.SKILL.md.ejs +38 -0
@@ -0,0 +1,428 @@
1
+ "use client";
2
+
3
+ <% if (hasAuth) { %>import { useAction } from "next-safe-action/hooks";
4
+ <% } else { %>import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";
5
+ <% } %>
6
+ import { zodResolver } from "@hookform/resolvers/zod";
7
+ import { useEffect } from "react";
8
+ import { Controller, type FieldPath<% if (hasAuth) { %>, useForm<% } %> } from "react-hook-form";
9
+ import { Badge } from "@/components/ui/badge";
10
+ import { Button } from "@/components/ui/button";
11
+ import {
12
+ Card,
13
+ CardContent,
14
+ CardDescription,
15
+ CardFooter,
16
+ CardHeader,
17
+ CardTitle,
18
+ } from "@/components/ui/card";
19
+ import { Checkbox } from "@/components/ui/checkbox";
20
+ import {
21
+ Field,
22
+ FieldContent,
23
+ FieldDescription,
24
+ FieldError,
25
+ FieldGroup,
26
+ FieldLabel,
27
+ FieldLegend,
28
+ FieldSeparator,
29
+ FieldSet,
30
+ } from "@/components/ui/field";
31
+ import { Input } from "@/components/ui/input";
32
+ import {
33
+ Select,
34
+ SelectContent,
35
+ SelectItem,
36
+ SelectTrigger,
37
+ SelectValue,
38
+ } from "@/components/ui/select";
39
+ import { Switch } from "@/components/ui/switch";
40
+ import { Textarea } from "@/components/ui/textarea";
41
+ import { exampleFormAction } from "../_actions/example-form";
42
+ import {
43
+ exampleFormDefaultValues,
44
+ exampleFormSchema,
45
+ type ExampleFormValues,
46
+ notificationOptions,
47
+ planOptions,
48
+ } from "../_lib/example-form-schema";
49
+
50
+ export function ExampleForm() {
51
+ <% if (hasAuth) { %> const action = useAction(exampleFormAction, {
52
+ throwOnNavigation: true,
53
+ onSuccess({ data }) {
54
+ if (!data) {
55
+ return;
56
+ }
57
+
58
+ console.info("[safe-action] example form succeeded", {
59
+ submittedAt: data.submittedAt,
60
+ workspace: data.workspace,
61
+ });
62
+ },
63
+ onError({ error }) {
64
+ console.warn("[safe-action] example form failed", {
65
+ hasServerError: Boolean(error.serverError),
66
+ hasValidationErrors: Boolean(error.validationErrors),
67
+ });
68
+ },
69
+ });
70
+ const form = useForm<ExampleFormValues>({
71
+ defaultValues: exampleFormDefaultValues,
72
+ mode: "onBlur",
73
+ resolver: zodResolver(exampleFormSchema),
74
+ });
75
+ const handleSubmitWithAction = form.handleSubmit((values) => {
76
+ action.execute(values);
77
+ });
78
+ const resetFormAndAction = () => {
79
+ form.reset(exampleFormDefaultValues);
80
+ action.reset();
81
+ };
82
+ <% } else { %>
83
+ const { action, form, handleSubmitWithAction, resetFormAndAction } =
84
+ useHookFormAction(exampleFormAction, zodResolver(exampleFormSchema), {
85
+ actionProps: {
86
+ onSuccess({ data }) {
87
+ if (!data) {
88
+ return;
89
+ }
90
+
91
+ console.info("[safe-action] example form succeeded", {
92
+ submittedAt: data.submittedAt,
93
+ workspace: data.workspace,
94
+ });
95
+ },
96
+ onError({ error }) {
97
+ console.warn("[safe-action] example form failed", {
98
+ hasServerError: Boolean(error.serverError),
99
+ hasValidationErrors: Boolean(error.validationErrors),
100
+ });
101
+ },
102
+ },
103
+ errorMapProps: {
104
+ joinBy: "\n",
105
+ },
106
+ formProps: {
107
+ defaultValues: exampleFormDefaultValues,
108
+ mode: "onBlur",
109
+ },
110
+ });
111
+ <% } %>
112
+
113
+ const watchedValues = form.watch();
114
+ const serverFieldErrors = action.result.validationErrors?.fieldErrors;
115
+ const serverFormError =
116
+ action.result.validationErrors?.formErrors.join("\n") ?? null;
117
+
118
+ useEffect(() => {
119
+ if (!serverFieldErrors) {
120
+ return;
121
+ }
122
+
123
+ for (const [fieldName, messages] of Object.entries(
124
+ serverFieldErrors,
125
+ ) as Array<[FieldPath<ExampleFormValues>, string[] | undefined]>) {
126
+ if (!messages?.length) {
127
+ continue;
128
+ }
129
+
130
+ form.setError(fieldName, {
131
+ message: messages.join("\n"),
132
+ type: "server",
133
+ });
134
+ }
135
+ }, [form, serverFieldErrors]);
136
+
137
+ useEffect(() => {
138
+ if (action.isPending || action.hasSucceeded) {
139
+ form.clearErrors();
140
+ }
141
+ }, [action.hasSucceeded, action.isPending, form]);
142
+
143
+ return (
144
+ <Card>
145
+ <CardHeader className="border-b">
146
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
147
+ <div className="space-y-1">
148
+ <CardTitle>Full-stack form example</CardTitle>
149
+ <CardDescription>
150
+ One shared Zod schema drives React Hook Form validation, the
151
+ next-safe-action input parser, and the typed action result.
152
+ </CardDescription>
153
+ </div>
154
+ <Badge variant="secondary">Status: {action.status}</Badge>
155
+ </div>
156
+ </CardHeader>
157
+ <CardContent className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_18rem]">
158
+ <form
159
+ id="example-form"
160
+ className="space-y-6"
161
+ noValidate
162
+ onSubmit={handleSubmitWithAction}
163
+ >
164
+ <FieldGroup>
165
+ <Controller
166
+ control={form.control}
167
+ name="name"
168
+ render={({ field, fieldState }) => (
169
+ <Field data-invalid={fieldState.invalid}>
170
+ <FieldLabel htmlFor="example-form-name">Name</FieldLabel>
171
+ <Input
172
+ {...field}
173
+ id="example-form-name"
174
+ aria-invalid={fieldState.invalid}
175
+ autoComplete="name"
176
+ placeholder="Ada Lovelace"
177
+ />
178
+ <FieldDescription>
179
+ Client-side errors come from the shared Zod schema.
180
+ </FieldDescription>
181
+ {fieldState.invalid ? (
182
+ <FieldError errors={[fieldState.error]} />
183
+ ) : null}
184
+ </Field>
185
+ )}
186
+ />
187
+
188
+ <Controller
189
+ control={form.control}
190
+ name="email"
191
+ render={({ field, fieldState }) => (
192
+ <Field data-invalid={fieldState.invalid}>
193
+ <FieldLabel htmlFor="example-form-email">Email</FieldLabel>
194
+ <Input
195
+ {...field}
196
+ id="example-form-email"
197
+ aria-invalid={fieldState.invalid}
198
+ autoComplete="email"
199
+ placeholder="ada@example.dev"
200
+ type="email"
201
+ />
202
+ <FieldDescription>
203
+ The submitted object is inferred from the same schema on
204
+ both sides of the server action boundary.
205
+ </FieldDescription>
206
+ {fieldState.invalid ? (
207
+ <FieldError errors={[fieldState.error]} />
208
+ ) : null}
209
+ </Field>
210
+ )}
211
+ />
212
+
213
+ <Controller
214
+ control={form.control}
215
+ name="workspace"
216
+ render={({ field, fieldState }) => (
217
+ <Field data-invalid={fieldState.invalid}>
218
+ <FieldLabel htmlFor="example-form-workspace">
219
+ Workspace slug
220
+ </FieldLabel>
221
+ <Input
222
+ {...field}
223
+ id="example-form-workspace"
224
+ aria-invalid={fieldState.invalid}
225
+ placeholder="scale-stack-demo"
226
+ />
227
+ <FieldDescription>
228
+ Try <code>admin</code> to see a server-side validation error
229
+ mapped back into this field.
230
+ </FieldDescription>
231
+ {fieldState.invalid ? (
232
+ <FieldError errors={[fieldState.error]} />
233
+ ) : null}
234
+ </Field>
235
+ )}
236
+ />
237
+
238
+ <Controller
239
+ control={form.control}
240
+ name="plan"
241
+ render={({ field, fieldState }) => (
242
+ <Field data-invalid={fieldState.invalid}>
243
+ <FieldLabel htmlFor="example-form-plan">Plan</FieldLabel>
244
+ <Select
245
+ name={field.name}
246
+ value={field.value}
247
+ onValueChange={field.onChange}
248
+ >
249
+ <SelectTrigger
250
+ id="example-form-plan"
251
+ aria-invalid={fieldState.invalid}
252
+ >
253
+ <SelectValue placeholder="Choose a plan" />
254
+ </SelectTrigger>
255
+ <SelectContent>
256
+ {planOptions.map((option) => (
257
+ <SelectItem key={option.value} value={option.value}>
258
+ {option.label}
259
+ </SelectItem>
260
+ ))}
261
+ </SelectContent>
262
+ </Select>
263
+ <FieldDescription>
264
+ Enterprise requires a note, enforced by the server action.
265
+ </FieldDescription>
266
+ {fieldState.invalid ? (
267
+ <FieldError errors={[fieldState.error]} />
268
+ ) : null}
269
+ </Field>
270
+ )}
271
+ />
272
+
273
+ <Controller
274
+ control={form.control}
275
+ name="notifications"
276
+ render={({ field, fieldState }) => (
277
+ <FieldSet data-invalid={fieldState.invalid}>
278
+ <FieldLegend variant="label">Notifications</FieldLegend>
279
+ <FieldDescription>
280
+ Checkbox arrays stay typed as the schema enum values.
281
+ </FieldDescription>
282
+ <FieldGroup data-slot="checkbox-group">
283
+ {notificationOptions.map((option) => (
284
+ <Field key={option.value} orientation="horizontal">
285
+ <Checkbox
286
+ id={`example-form-notification-${option.value}`}
287
+ name={field.name}
288
+ checked={field.value.includes(option.value)}
289
+ onCheckedChange={(checked) => {
290
+ const nextValue =
291
+ checked === true
292
+ ? [...field.value, option.value]
293
+ : field.value.filter(
294
+ (value) => value !== option.value,
295
+ );
296
+
297
+ field.onChange(nextValue);
298
+ }}
299
+ />
300
+ <FieldContent>
301
+ <FieldLabel
302
+ className="font-normal"
303
+ htmlFor={`example-form-notification-${option.value}`}
304
+ >
305
+ {option.label}
306
+ </FieldLabel>
307
+ <FieldDescription>
308
+ {option.description}
309
+ </FieldDescription>
310
+ </FieldContent>
311
+ </Field>
312
+ ))}
313
+ </FieldGroup>
314
+ {fieldState.invalid ? (
315
+ <FieldError errors={[fieldState.error]} />
316
+ ) : null}
317
+ </FieldSet>
318
+ )}
319
+ />
320
+
321
+ <Controller
322
+ control={form.control}
323
+ name="notes"
324
+ render={({ field, fieldState }) => (
325
+ <Field data-invalid={fieldState.invalid}>
326
+ <FieldLabel htmlFor="example-form-notes">Notes</FieldLabel>
327
+ <Textarea
328
+ {...field}
329
+ id="example-form-notes"
330
+ aria-invalid={fieldState.invalid}
331
+ placeholder="What should the generated app demonstrate?"
332
+ />
333
+ <FieldDescription>
334
+ Server rules can return field errors through
335
+ returnValidationErrors.
336
+ </FieldDescription>
337
+ {fieldState.invalid ? (
338
+ <FieldError errors={[fieldState.error]} />
339
+ ) : null}
340
+ </Field>
341
+ )}
342
+ />
343
+
344
+ <Controller
345
+ control={form.control}
346
+ name="acceptTerms"
347
+ render={({ field, fieldState }) => (
348
+ <Field
349
+ orientation="horizontal"
350
+ data-invalid={fieldState.invalid}
351
+ >
352
+ <Switch
353
+ id="example-form-accept-terms"
354
+ name={field.name}
355
+ checked={field.value}
356
+ onCheckedChange={field.onChange}
357
+ aria-invalid={fieldState.invalid}
358
+ />
359
+ <FieldContent>
360
+ <FieldLabel htmlFor="example-form-accept-terms">
361
+ Accept demo terms
362
+ </FieldLabel>
363
+ <FieldDescription>
364
+ Boolean fields use controlled components without losing
365
+ type inference.
366
+ </FieldDescription>
367
+ {fieldState.invalid ? (
368
+ <FieldError errors={[fieldState.error]} />
369
+ ) : null}
370
+ </FieldContent>
371
+ </Field>
372
+ )}
373
+ />
374
+ </FieldGroup>
375
+ </form>
376
+
377
+ <aside className="space-y-4 rounded-lg border bg-muted/30 p-4">
378
+ <div className="space-y-1">
379
+ <p className="text-sm font-medium">Typed form state</p>
380
+ <p className="text-sm text-muted-foreground">
381
+ This preview is typed from the shared schema, not duplicated
382
+ client state.
383
+ </p>
384
+ </div>
385
+ <pre className="max-h-72 overflow-auto rounded-md bg-background p-3 text-xs">
386
+ {JSON.stringify(watchedValues, null, 2)}
387
+ </pre>
388
+ <FieldSeparator />
389
+ {action.hasSucceeded ? (
390
+ <div className="space-y-1 rounded-md border bg-background p-3">
391
+ <p className="text-sm font-medium">
392
+ {action.result.data.message}
393
+ </p>
394
+ <p className="text-sm text-muted-foreground">
395
+ Workspace: {action.result.data.workspace}
396
+ </p>
397
+ <p className="text-sm text-muted-foreground">
398
+ Notifications: {action.result.data.notificationCount}
399
+ </p>
400
+ <p className="text-xs text-muted-foreground">
401
+ Submitted at {action.result.data.submittedAt}
402
+ </p>
403
+ </div>
404
+ ) : null}
405
+ {action.hasErrored && action.result.serverError ? (
406
+ <FieldError>{action.result.serverError}</FieldError>
407
+ ) : null}
408
+ {action.hasErrored && serverFormError ? (
409
+ <FieldError>{serverFormError}</FieldError>
410
+ ) : null}
411
+ </aside>
412
+ </CardContent>
413
+ <CardFooter className="flex flex-col gap-3 border-t sm:flex-row sm:justify-between">
414
+ <Button
415
+ type="button"
416
+ variant="outline"
417
+ onClick={resetFormAndAction}
418
+ disabled={action.isPending}
419
+ >
420
+ Reset form and action
421
+ </Button>
422
+ <Button type="submit" form="example-form" disabled={action.isPending}>
423
+ {action.isPending ? "Submitting..." : "Submit typed action"}
424
+ </Button>
425
+ </CardFooter>
426
+ </Card>
427
+ );
428
+ }
@@ -0,0 +1,77 @@
1
+ {
2
+ "Metadata": {
3
+ "title": "<%= projectName %>",
4
+ "description": "تم إنشاؤه باستخدام Scale Stack"
5
+ },
6
+ "Home": {
7
+ "eyebrow": "Scale Stack",
8
+ "description": "تم تجهيز تطبيقك باستخدام shadcn/ui و Tailwind CSS 4 وإجراءات خادم typed ونماذج جاهزة وقواعد Scale Stack التي تساعد الوكلاء على العمل بأمان.",
9
+ "openDashboard": "افتح لوحة التحكم",
10
+ "openChat": "افتح المحادثة",
11
+ "signIn": "تسجيل الدخول",
12
+ "signUp": "إنشاء حساب",
13
+ "availablePages": "الصفحات المتاحة",
14
+ "availablePagesDescription": "انتقل مباشرة إلى المسارات التي أنشأها هذا القالب للمكدس الذي اخترته.",
15
+ "dashboard": "لوحة التحكم",
16
+ "dashboardDescription": "نماذج وإجراءات خادم typed.",
17
+ "dashboardCardDescription": "جرّب مثال React Hook Form المدعوم بـ Zod وحقول shadcn/ui و next-safe-action.",
18
+ "aiChat": "محادثة الذكاء الاصطناعي",
19
+ "aiChatDescription": "AI Elements مع مسار تجريبي دون مزود.",
20
+ "aiChatCardDescription": "افتح تجربة AI Elements وشاهد عقد نقل AI SDK جاهزًا لخلفيتك.",
21
+ "signInDescription": "تدفق تسجيل دخول Better Auth مع Microsoft Entra OAuth.",
22
+ "signInCardDescription": "جرّب Better Auth مع Microsoft Entra OAuth.",
23
+ "signInStatefulCardDescription": "جرّب Better Auth مع Microsoft Entra OAuth وجلسات بريد وكلمة مرور مدعومة بـ Prisma.",
24
+ "signUpDescription": "إنشاء حساب.",
25
+ "signUpStatefulDescription": "إنشاء حساب مع بريد وكلمة مرور وجلسات مدعومة بـ Prisma.",
26
+ "signUpCardDescription": "أنشئ حسابًا من خلال إعداد Better Auth نفسه واترك مسار البدء جاهزًا للتخصيص.",
27
+ "explorePages": "استكشف الصفحات المنشأة",
28
+ "explorePagesDescription": "كل بطاقة ترتبط بمسار حقيقي في هذا القالب.",
29
+ "exploreForms": "استكشف النماذج"
30
+ },
31
+ "Dashboard": {
32
+ "eyebrow": "لوحة التحكم",
33
+ "title": "تم ربط React Hook Form و next-safe-action معًا.",
34
+ "description": "هذا المسار هو المثال الوحيد المنشأ للتعديلات: تعرض مكونات shadcn الواجهة، ويدير React Hook Form حالة العميل، ويتحقق Zod من إدخال العميل والخادم، وتعيد next-safe-action بيانات typed وأخطاء تحقق مهيكلة."
35
+ },
36
+ "Chat": {
37
+ "metadataTitle": "المحادثة",
38
+ "metadataDescription": "محادثة تجريبية متوافقة مع AI SDK",
39
+ "eyebrow": "محادثة الذكاء الاصطناعي",
40
+ "title": "تم ربط AI Elements و AI SDK بتجربة حتمية.",
41
+ "description": "الواجهة جاهزة لخلفية محادثة خارجية، لكن هذا القالب يبقى دون مزود حتى يعمل المسار قبل وجود بيانات الاعتماد."
42
+ },
43
+ "Auth": {
44
+ "signInTitle": "تسجيل الدخول",
45
+ "signInDescriptionStateful": "استخدم حساب Microsoft أو البريد وكلمة المرور للمتابعة.",
46
+ "signInDescriptionStateless": "استخدم حساب Microsoft للمتابعة.",
47
+ "continueWithMicrosoft": "المتابعة باستخدام Microsoft",
48
+ "email": "البريد الإلكتروني",
49
+ "password": "كلمة المرور",
50
+ "name": "الاسم",
51
+ "signInWithEmail": "تسجيل الدخول بالبريد",
52
+ "noAccount": "ليس لديك حساب؟",
53
+ "createOne": "أنشئ حسابًا",
54
+ "createAccountTitle": "إنشاء حساب",
55
+ "createAccountMicrosoftTitle": "إنشاء حساب باستخدام Microsoft",
56
+ "createAccountDescriptionStateful": "استخدم Microsoft أو البريد وكلمة المرور لإنشاء حسابك.",
57
+ "createAccountDescriptionStateless": "تتم إدارة إنشاء الحساب عبر Microsoft Entra في هذا الإعداد عديم الحالة.",
58
+ "alreadyHaveAccount": "لديك حساب بالفعل؟",
59
+ "microsoftSignInFailed": "فشل تسجيل الدخول عبر Microsoft.",
60
+ "emailSignInFailed": "فشل تسجيل الدخول بالبريد.",
61
+ "emailSignUpFailed": "فشل إنشاء الحساب بالبريد.",
62
+ "authenticationRequired": "المصادقة مطلوبة",
63
+ "signInToContinue": "سجّل الدخول للمتابعة",
64
+ "protectedAction": "هذا الإجراء محمي. سجّل الدخول ثم حاول مرة أخرى.",
65
+ "goToSignIn": "اذهب إلى تسجيل الدخول"
66
+ },
67
+ "Errors": {
68
+ "errorEyebrow": "خطأ",
69
+ "errorTitle": "حدث خطأ ما",
70
+ "unexpectedError": "حدث خطأ غير متوقع.",
71
+ "tryAgain": "حاول مرة أخرى",
72
+ "notFoundEyebrow": "Scale Stack",
73
+ "notFoundTitle": "الصفحة غير موجودة",
74
+ "notFoundDescription": "الصفحة التي تبحث عنها غير موجودة.",
75
+ "goHome": "اذهب إلى الرئيسية"
76
+ }
77
+ }
@@ -0,0 +1,77 @@
1
+ {
2
+ "Metadata": {
3
+ "title": "<%= projectName %>",
4
+ "description": "Generated with Scale Stack"
5
+ },
6
+ "Home": {
7
+ "eyebrow": "Scale Stack",
8
+ "description": "Your app is wired with shadcn/ui, Tailwind CSS 4, typed server actions, form handling, and the Scale Stack guardrails agents need to keep moving safely.",
9
+ "openDashboard": "Open dashboard",
10
+ "openChat": "Open chat",
11
+ "signIn": "Sign in",
12
+ "signUp": "Sign up",
13
+ "availablePages": "Available pages",
14
+ "availablePagesDescription": "Jump straight into the routes this scaffold generated for your selected stack.",
15
+ "dashboard": "Dashboard",
16
+ "dashboardDescription": "Form handling and typed server actions.",
17
+ "dashboardCardDescription": "Try the generated React Hook Form example backed by Zod, shadcn/ui fields, and next-safe-action.",
18
+ "aiChat": "AI chat",
19
+ "aiChatDescription": "AI Elements with a provider-free mock route.",
20
+ "aiChatCardDescription": "Open the provider-free AI Elements mock and see the AI SDK transport contract ready for your backend.",
21
+ "signInDescription": "Better Auth sign-in flow with Microsoft Entra OAuth.",
22
+ "signInCardDescription": "Exercise Better Auth with Microsoft Entra OAuth.",
23
+ "signInStatefulCardDescription": "Exercise Better Auth with Microsoft Entra OAuth and Prisma-backed email/password sessions.",
24
+ "signUpDescription": "Account creation.",
25
+ "signUpStatefulDescription": "Account creation with email/password and Prisma-backed sessions.",
26
+ "signUpCardDescription": "Create an account through the same Better Auth setup and keep the onboarding route ready for customization.",
27
+ "explorePages": "Explore generated pages",
28
+ "explorePagesDescription": "Each card links to a real route in this scaffold.",
29
+ "exploreForms": "Explore forms"
30
+ },
31
+ "Dashboard": {
32
+ "eyebrow": "Dashboard",
33
+ "title": "React Hook Form and next-safe-action are wired together.",
34
+ "description": "This route is the single generated example for mutations: shadcn field components render the UI, React Hook Form owns client state, Zod validates both client and server input, and next-safe-action returns typed data and mapped validation errors."
35
+ },
36
+ "Chat": {
37
+ "metadataTitle": "Chat",
38
+ "metadataDescription": "Deterministic AI SDK-compatible mock chat",
39
+ "eyebrow": "AI Chat",
40
+ "title": "AI Elements and AI SDK are wired to a deterministic mock.",
41
+ "description": "The UI is ready for an external chat backend, but this scaffold stays provider-free so the route works before credentials exist."
42
+ },
43
+ "Auth": {
44
+ "signInTitle": "Sign in",
45
+ "signInDescriptionStateful": "Use your Microsoft account or email and password to continue.",
46
+ "signInDescriptionStateless": "Use your Microsoft account to continue.",
47
+ "continueWithMicrosoft": "Continue with Microsoft",
48
+ "email": "Email",
49
+ "password": "Password",
50
+ "name": "Name",
51
+ "signInWithEmail": "Sign in with email",
52
+ "noAccount": "No account?",
53
+ "createOne": "Create one",
54
+ "createAccountTitle": "Create account",
55
+ "createAccountMicrosoftTitle": "Create account with Microsoft",
56
+ "createAccountDescriptionStateful": "Use Microsoft or email/password to create your account.",
57
+ "createAccountDescriptionStateless": "Account creation is handled by Microsoft Entra for this stateless setup.",
58
+ "alreadyHaveAccount": "Already have an account?",
59
+ "microsoftSignInFailed": "Microsoft sign-in failed.",
60
+ "emailSignInFailed": "Email sign-in failed.",
61
+ "emailSignUpFailed": "Email sign-up failed.",
62
+ "authenticationRequired": "Authentication required",
63
+ "signInToContinue": "Sign in to continue",
64
+ "protectedAction": "This action is protected. Sign in and then try again.",
65
+ "goToSignIn": "Go to sign in"
66
+ },
67
+ "Errors": {
68
+ "errorEyebrow": "Error",
69
+ "errorTitle": "Something went wrong",
70
+ "unexpectedError": "An unexpected error occurred.",
71
+ "tryAgain": "Try again",
72
+ "notFoundEyebrow": "Scale Stack",
73
+ "notFoundTitle": "Page not found",
74
+ "notFoundDescription": "The page you are looking for does not exist.",
75
+ "goHome": "Go home"
76
+ }
77
+ }
@@ -0,0 +1,81 @@
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import { notFound } from "next/navigation";
4
+ import { Suspense } from "react";
5
+ import { NextIntlClientProvider, hasLocale } from "next-intl";
6
+ import {
7
+ getMessages,
8
+ getTranslations,
9
+ setRequestLocale,
10
+ } from "next-intl/server";
11
+ import { routing } from "@/i18n/routing";
12
+ import "../globals.css";
13
+ import { ClientSideWrappers } from "../_providers/client-side-wrappers";
14
+
15
+ const geistSans = Geist({
16
+ variable: "--font-geist-sans",
17
+ subsets: ["latin"],
18
+ });
19
+
20
+ const geistMono = Geist_Mono({
21
+ variable: "--font-geist-mono",
22
+ subsets: ["latin"],
23
+ });
24
+
25
+ type LocaleLayoutProps = Readonly<{
26
+ children: React.ReactNode;
27
+ params: Promise<{ locale: string }>;
28
+ }>;
29
+
30
+ export function generateStaticParams() {
31
+ return routing.locales.map((locale) => ({ locale }));
32
+ }
33
+
34
+ export async function generateMetadata({
35
+ params,
36
+ }: Pick<LocaleLayoutProps, "params">): Promise<Metadata> {
37
+ const { locale } = await params;
38
+
39
+ if (!hasLocale(routing.locales, locale)) {
40
+ notFound();
41
+ }
42
+
43
+ const t = await getTranslations({ locale, namespace: "Metadata" });
44
+
45
+ return {
46
+ title: t("title"),
47
+ description: t("description"),
48
+ };
49
+ }
50
+
51
+ export default async function LocaleLayout({
52
+ children,
53
+ params,
54
+ }: LocaleLayoutProps) {
55
+ const { locale } = await params;
56
+
57
+ if (!hasLocale(routing.locales, locale)) {
58
+ notFound();
59
+ }
60
+
61
+ setRequestLocale(locale);
62
+
63
+ const messages = await getMessages();
64
+ const direction = locale === "ar" ? "rtl" : "ltr";
65
+
66
+ return (
67
+ <html
68
+ lang={locale}
69
+ dir={direction}
70
+ className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
71
+ >
72
+ <body className="min-h-full flex flex-col">
73
+ <NextIntlClientProvider locale={locale} messages={messages}>
74
+ <Suspense>
75
+ <ClientSideWrappers>{children}</ClientSideWrappers>
76
+ </Suspense>
77
+ </NextIntlClientProvider>
78
+ </body>
79
+ </html>
80
+ );
81
+ }
@@ -0,0 +1,5 @@
1
+ import { createNavigation } from "next-intl/navigation";
2
+ import { routing } from "./routing";
3
+
4
+ export const { Link, redirect, usePathname, useRouter, getPathname } =
5
+ createNavigation(routing);
@@ -0,0 +1,9 @@
1
+ import { routing } from "./routing";
2
+ import en from "../../messages/en.json";
3
+
4
+ declare module "next-intl" {
5
+ interface AppConfig {
6
+ Locale: (typeof routing.locales)[number];
7
+ Messages: typeof en;
8
+ }
9
+ }