@codaijs/keel 0.2.2 → 0.2.4

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 (80) hide show
  1. package/dist/__tests__/sail-installer.test.js +25 -25
  2. package/dist/sail-installer.js +174 -174
  3. package/dist/scaffold.js +68 -68
  4. package/package.json +58 -58
  5. package/sails/_template/addon.json +20 -20
  6. package/sails/_template/install.ts +402 -402
  7. package/sails/admin-dashboard/README.md +117 -117
  8. package/sails/admin-dashboard/addon.json +28 -28
  9. package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
  10. package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
  11. package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
  12. package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
  13. package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
  14. package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
  15. package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
  16. package/sails/admin-dashboard/install.ts +305 -305
  17. package/sails/analytics/README.md +178 -178
  18. package/sails/analytics/addon.json +27 -27
  19. package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
  20. package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
  21. package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
  22. package/sails/analytics/install.ts +297 -297
  23. package/sails/file-uploads/addon.json +30 -30
  24. package/sails/file-uploads/files/backend/routes/files.ts +198 -198
  25. package/sails/file-uploads/files/backend/schema/files.ts +36 -36
  26. package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
  27. package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
  28. package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
  29. package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
  30. package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
  31. package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
  32. package/sails/file-uploads/install.ts +466 -466
  33. package/sails/gdpr/README.md +174 -174
  34. package/sails/gdpr/addon.json +27 -27
  35. package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
  36. package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
  37. package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
  38. package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
  39. package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
  40. package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
  41. package/sails/gdpr/install.ts +756 -756
  42. package/sails/google-oauth/README.md +121 -121
  43. package/sails/google-oauth/addon.json +22 -22
  44. package/sails/google-oauth/files/GoogleButton.tsx +50 -50
  45. package/sails/google-oauth/install.ts +252 -252
  46. package/sails/i18n/README.md +193 -193
  47. package/sails/i18n/addon.json +30 -30
  48. package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
  49. package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
  50. package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
  51. package/sails/i18n/files/frontend/locales/de/common.json +44 -44
  52. package/sails/i18n/files/frontend/locales/en/common.json +44 -44
  53. package/sails/i18n/install.ts +407 -407
  54. package/sails/push-notifications/README.md +163 -163
  55. package/sails/push-notifications/addon.json +31 -31
  56. package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
  57. package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
  58. package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
  59. package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
  60. package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
  61. package/sails/push-notifications/install.ts +384 -384
  62. package/sails/r2-storage/addon.json +29 -29
  63. package/sails/r2-storage/files/backend/services/storage.ts +71 -71
  64. package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
  65. package/sails/r2-storage/install.ts +412 -412
  66. package/sails/rate-limiting/addon.json +20 -20
  67. package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
  68. package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
  69. package/sails/rate-limiting/install.ts +300 -300
  70. package/sails/registry.json +107 -107
  71. package/sails/stripe/README.md +214 -214
  72. package/sails/stripe/addon.json +24 -24
  73. package/sails/stripe/files/backend/routes/stripe.ts +154 -154
  74. package/sails/stripe/files/backend/schema/stripe.ts +74 -74
  75. package/sails/stripe/files/backend/services/stripe.ts +224 -224
  76. package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
  77. package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
  78. package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
  79. package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
  80. package/sails/stripe/install.ts +378 -378
@@ -1,407 +1,407 @@
1
- /**
2
- * Internationalization (i18n) Sail Installer
3
- *
4
- * Adds multi-language support with i18next, react-i18next,
5
- * and automatic browser language detection.
6
- *
7
- * Usage:
8
- * npx tsx sails/i18n/install.ts
9
- */
10
-
11
- import {
12
- readFileSync,
13
- writeFileSync,
14
- copyFileSync,
15
- existsSync,
16
- mkdirSync,
17
- } from "node:fs";
18
- import { resolve, dirname, join } from "node:path";
19
- import { execSync } from "node:child_process";
20
- import { confirm, checkbox } from "@inquirer/prompts";
21
-
22
- // ---------------------------------------------------------------------------
23
- // Paths
24
- // ---------------------------------------------------------------------------
25
-
26
- const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
27
- const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
28
- const FRONTEND_ROOT = join(PROJECT_ROOT, "packages/frontend");
29
-
30
- // ---------------------------------------------------------------------------
31
- // Helpers
32
- // ---------------------------------------------------------------------------
33
-
34
- interface SailManifest {
35
- name: string;
36
- displayName: string;
37
- version: string;
38
- requiredEnvVars: { key: string; description: string }[];
39
- dependencies: { backend: Record<string, string>; frontend: Record<string, string> };
40
- }
41
-
42
- function loadManifest(): SailManifest {
43
- return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
44
- }
45
-
46
- function copyFile(src: string, dest: string, label: string): void {
47
- mkdirSync(dirname(dest), { recursive: true });
48
- copyFileSync(src, dest);
49
- console.log(` Copied -> ${label}`);
50
- }
51
-
52
- function installDeps(deps: Record<string, string>, workspace: string): void {
53
- const entries = Object.entries(deps);
54
- if (entries.length === 0) return;
55
- const packages = entries.map(([n, v]) => `${n}@${v}`).join(" ");
56
- const cmd = `npm install ${packages} --workspace=${workspace}`;
57
- console.log(` Running: ${cmd}`);
58
- execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
59
- }
60
-
61
- // ---------------------------------------------------------------------------
62
- // Language definitions
63
- // ---------------------------------------------------------------------------
64
-
65
- interface LanguageDef {
66
- code: string;
67
- label: string;
68
- nativeLabel: string;
69
- hasBuiltinLocale: boolean;
70
- }
71
-
72
- const AVAILABLE_LANGUAGES: LanguageDef[] = [
73
- { code: "en", label: "English", nativeLabel: "English", hasBuiltinLocale: true },
74
- { code: "de", label: "German", nativeLabel: "Deutsch", hasBuiltinLocale: true },
75
- { code: "fr", label: "French", nativeLabel: "Francais", hasBuiltinLocale: false },
76
- { code: "es", label: "Spanish", nativeLabel: "Espanol", hasBuiltinLocale: false },
77
- ];
78
-
79
- // Stub translations for French
80
- const FR_COMMON = {
81
- nav: { home: "Accueil", profile: "Profil", settings: "Parametres", login: "Se connecter", signup: "S'inscrire", logout: "Se deconnecter" },
82
- auth: { loginTitle: "Bon retour", signupTitle: "Creer un compte", email: "E-mail", password: "Mot de passe", confirmPassword: "Confirmer le mot de passe", name: "Nom", forgotPassword: "Mot de passe oublie?", noAccount: "Pas encore de compte?", hasAccount: "Deja un compte?", resetPassword: "Reinitialiser le mot de passe", loginButton: "Se connecter", signupButton: "S'inscrire", resetButton: "Envoyer le lien" },
83
- profile: { title: "Profil", editProfile: "Modifier le profil", name: "Nom", email: "E-mail", memberSince: "Membre depuis" },
84
- common: { save: "Enregistrer", cancel: "Annuler", delete: "Supprimer", loading: "Chargement...", error: "Une erreur est survenue", success: "Succes", confirm: "Confirmer", back: "Retour", search: "Rechercher", noResults: "Aucun resultat" },
85
- };
86
-
87
- // Stub translations for Spanish
88
- const ES_COMMON = {
89
- nav: { home: "Inicio", profile: "Perfil", settings: "Configuracion", login: "Iniciar sesion", signup: "Registrarse", logout: "Cerrar sesion" },
90
- auth: { loginTitle: "Bienvenido de nuevo", signupTitle: "Crear cuenta", email: "Correo electronico", password: "Contrasena", confirmPassword: "Confirmar contrasena", name: "Nombre", forgotPassword: "Olvidaste tu contrasena?", noAccount: "No tienes cuenta?", hasAccount: "Ya tienes cuenta?", resetPassword: "Restablecer contrasena", loginButton: "Iniciar sesion", signupButton: "Registrarse", resetButton: "Enviar enlace" },
91
- profile: { title: "Perfil", editProfile: "Editar perfil", name: "Nombre", email: "Correo electronico", memberSince: "Miembro desde" },
92
- common: { save: "Guardar", cancel: "Cancelar", delete: "Eliminar", loading: "Cargando...", error: "Algo salio mal", success: "Exito", confirm: "Confirmar", back: "Volver", search: "Buscar", noResults: "Sin resultados" },
93
- };
94
-
95
- // ---------------------------------------------------------------------------
96
- // Main
97
- // ---------------------------------------------------------------------------
98
-
99
- async function main(): Promise<void> {
100
- const manifest = loadManifest();
101
-
102
- // -- Step 1: Welcome --------------------------------------------------------
103
- console.log("\n------------------------------------------------------------");
104
- console.log(` Internationalization Sail Installer (v${manifest.version})`);
105
- console.log("------------------------------------------------------------");
106
- console.log();
107
- console.log(" This sail adds multi-language support to your project:");
108
- console.log(" - i18next for translation management");
109
- console.log(" - react-i18next for React integration");
110
- console.log(" - Automatic browser language detection");
111
- console.log(" - Language switcher component");
112
- console.log(" - Translation files for selected languages");
113
- console.log();
114
-
115
- const pkgPath = join(PROJECT_ROOT, "package.json");
116
- if (existsSync(pkgPath)) {
117
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
118
- console.log(` Template version: ${pkg.version ?? "unknown"}`);
119
- console.log();
120
- }
121
-
122
- // -- Step 2: Select languages -----------------------------------------------
123
- const selectedCodes = await checkbox({
124
- message: "Which languages would you like to include?",
125
- choices: AVAILABLE_LANGUAGES.map((lang) => ({
126
- name: `${lang.label} (${lang.nativeLabel})`,
127
- value: lang.code,
128
- checked: lang.code === "en" || lang.code === "de",
129
- })),
130
- validate: (values) => {
131
- if (!values.includes("en")) {
132
- return "English must be selected as the fallback language.";
133
- }
134
- if (values.length < 2) {
135
- return "Please select at least two languages.";
136
- }
137
- return true;
138
- },
139
- });
140
-
141
- const selectedLanguages = AVAILABLE_LANGUAGES.filter((l) =>
142
- selectedCodes.includes(l.code),
143
- );
144
-
145
- console.log();
146
- console.log(
147
- ` Selected languages: ${selectedLanguages.map((l) => l.label).join(", ")}`,
148
- );
149
- console.log();
150
-
151
- // -- Step 3: Summary --------------------------------------------------------
152
- console.log(" Summary of changes:");
153
- console.log(" -------------------");
154
- console.log(" Files to create:");
155
- console.log(" + packages/frontend/src/lib/i18n.ts");
156
- console.log(" + packages/frontend/src/hooks/useLanguage.ts");
157
- console.log(" + packages/frontend/src/components/LanguageSwitcher.tsx");
158
- for (const lang of selectedLanguages) {
159
- console.log(` + packages/frontend/src/locales/${lang.code}/common.json`);
160
- }
161
- console.log();
162
- console.log(" Files to modify:");
163
- console.log(" ~ packages/frontend/src/main.tsx (import i18n)");
164
- console.log(" ~ packages/frontend/src/components/layout/Header.tsx (add LanguageSwitcher)");
165
- console.log();
166
- console.log(" Dependencies to install:");
167
- console.log(" i18next, react-i18next, i18next-browser-languagedetector");
168
- console.log();
169
-
170
- // -- Step 4: Confirm --------------------------------------------------------
171
- const proceed = await confirm({ message: "Proceed with installation?", default: true });
172
- if (!proceed) {
173
- console.log("\n Installation cancelled.\n");
174
- process.exit(0);
175
- }
176
-
177
- console.log();
178
- console.log(" Installing...");
179
- console.log();
180
-
181
- // -- Step 5: Copy locale files ----------------------------------------------
182
- console.log(" Copying translation files...");
183
-
184
- // English and German have built-in files
185
- for (const lang of selectedLanguages) {
186
- const localeDestDir = join(FRONTEND_ROOT, `src/locales/${lang.code}`);
187
- mkdirSync(localeDestDir, { recursive: true });
188
-
189
- if (lang.hasBuiltinLocale) {
190
- copyFile(
191
- join(SAIL_DIR, `files/frontend/locales/${lang.code}/common.json`),
192
- join(localeDestDir, "common.json"),
193
- `src/locales/${lang.code}/common.json`,
194
- );
195
- } else {
196
- // Generate locale file from inline data
197
- let data: Record<string, unknown>;
198
- switch (lang.code) {
199
- case "fr":
200
- data = FR_COMMON;
201
- break;
202
- case "es":
203
- data = ES_COMMON;
204
- break;
205
- default:
206
- data = {};
207
- }
208
- const destPath = join(localeDestDir, "common.json");
209
- writeFileSync(destPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
210
- console.log(` Created -> src/locales/${lang.code}/common.json`);
211
- }
212
- }
213
-
214
- console.log();
215
- console.log(" Copying source files...");
216
-
217
- // Copy core files
218
- copyFile(
219
- join(SAIL_DIR, "files/frontend/hooks/useLanguage.ts"),
220
- join(FRONTEND_ROOT, "src/hooks/useLanguage.ts"),
221
- "src/hooks/useLanguage.ts",
222
- );
223
- copyFile(
224
- join(SAIL_DIR, "files/frontend/components/LanguageSwitcher.tsx"),
225
- join(FRONTEND_ROOT, "src/components/LanguageSwitcher.tsx"),
226
- "src/components/LanguageSwitcher.tsx",
227
- );
228
-
229
- // -- Step 6: Generate i18n.ts with selected languages -----------------------
230
- console.log();
231
- console.log(" Generating i18n configuration...");
232
-
233
- const importLines = selectedLanguages
234
- .map((l) => `import ${l.code}Common from "@/locales/${l.code}/common.json";`)
235
- .join("\n");
236
-
237
- const resourceLines = selectedLanguages
238
- .map((l) => ` ${l.code}: { common: ${l.code}Common },`)
239
- .join("\n");
240
-
241
- const i18nContent = `import i18n from "i18next";
242
- import { initReactI18next } from "react-i18next";
243
- import LanguageDetector from "i18next-browser-languagedetector";
244
-
245
- // Import locale files
246
- ${importLines}
247
-
248
- const resources = {
249
- ${resourceLines}
250
- };
251
-
252
- i18n
253
- .use(LanguageDetector)
254
- .use(initReactI18next)
255
- .init({
256
- resources,
257
- defaultNS: "common",
258
- ns: ["common"],
259
- fallbackLng: "en",
260
- interpolation: {
261
- escapeValue: false, // React already escapes
262
- },
263
- detection: {
264
- order: ["localStorage", "navigator", "htmlTag"],
265
- caches: ["localStorage"],
266
- lookupLocalStorage: "i18nextLng",
267
- },
268
- });
269
-
270
- export default i18n;
271
- `;
272
-
273
- const i18nDest = join(FRONTEND_ROOT, "src/lib/i18n.ts");
274
- mkdirSync(dirname(i18nDest), { recursive: true });
275
- writeFileSync(i18nDest, i18nContent, "utf-8");
276
- console.log(" Created -> src/lib/i18n.ts");
277
-
278
- // Update useLanguage.ts with the selected languages
279
- const languageEntries = selectedLanguages
280
- .map((l) => ` { code: "${l.code}", label: "${l.nativeLabel}" },`)
281
- .join("\n");
282
-
283
- const useLanguageContent = `import { useTranslation } from "react-i18next";
284
- import { useCallback } from "react";
285
-
286
- export interface Language {
287
- code: string;
288
- label: string;
289
- }
290
-
291
- const availableLanguages: Language[] = [
292
- ${languageEntries}
293
- ];
294
-
295
- export function useLanguage() {
296
- const { i18n } = useTranslation();
297
-
298
- const currentLanguage = i18n.language?.split("-")[0] ?? "en";
299
-
300
- const changeLanguage = useCallback(
301
- async (code: string) => {
302
- await i18n.changeLanguage(code);
303
- },
304
- [i18n],
305
- );
306
-
307
- return {
308
- currentLanguage,
309
- changeLanguage,
310
- availableLanguages,
311
- };
312
- }
313
- `;
314
-
315
- writeFileSync(join(FRONTEND_ROOT, "src/hooks/useLanguage.ts"), useLanguageContent, "utf-8");
316
- console.log(" Updated -> src/hooks/useLanguage.ts");
317
-
318
- // -- Step 7: Modify main.tsx — import i18n ----------------------------------
319
- console.log();
320
- console.log(" Modifying frontend files...");
321
-
322
- const mainPath = join(FRONTEND_ROOT, "src/main.tsx");
323
- if (existsSync(mainPath)) {
324
- let mainContent = readFileSync(mainPath, "utf-8");
325
-
326
- if (!mainContent.includes("./lib/i18n")) {
327
- // Add i18n import at the top (after existing imports)
328
- mainContent = `import "./lib/i18n.js";\n${mainContent}`;
329
- writeFileSync(mainPath, mainContent, "utf-8");
330
- console.log(" Modified -> src/main.tsx (added i18n import)");
331
- } else {
332
- console.log(" Skipped (already present) -> src/main.tsx");
333
- }
334
- }
335
-
336
- // -- Step 8: Add LanguageSwitcher to Header ---------------------------------
337
- const headerPath = join(FRONTEND_ROOT, "src/components/layout/Header.tsx");
338
- if (existsSync(headerPath)) {
339
- let headerContent = readFileSync(headerPath, "utf-8");
340
-
341
- if (!headerContent.includes("LanguageSwitcher")) {
342
- // Add import
343
- headerContent = headerContent.replace(
344
- 'import { useAuth } from "@/hooks/useAuth";',
345
- 'import { useAuth } from "@/hooks/useAuth";\nimport LanguageSwitcher from "@/components/LanguageSwitcher";',
346
- );
347
-
348
- // Add LanguageSwitcher in the desktop nav, before the user section
349
- headerContent = headerContent.replace(
350
- "{/* Desktop Nav */}\n <nav className=\"hidden items-center gap-6 md:flex\">",
351
- "{/* Desktop Nav */}\n <nav className=\"hidden items-center gap-6 md:flex\">\n <LanguageSwitcher />",
352
- );
353
-
354
- writeFileSync(headerPath, headerContent, "utf-8");
355
- console.log(" Modified -> src/components/layout/Header.tsx (added LanguageSwitcher)");
356
- } else {
357
- console.log(" Skipped (already present) -> Header.tsx");
358
- }
359
- }
360
-
361
- // -- Step 9: Install dependencies -------------------------------------------
362
- console.log();
363
- console.log(" Installing dependencies...");
364
- installDeps(manifest.dependencies.frontend, "packages/frontend");
365
-
366
- // -- Step 10: Next steps ----------------------------------------------------
367
- console.log();
368
- console.log("------------------------------------------------------------");
369
- console.log(" Internationalization installed successfully!");
370
- console.log("------------------------------------------------------------");
371
- console.log();
372
- console.log(" Next steps:");
373
- console.log();
374
- console.log(" 1. Start your dev server:");
375
- console.log(" npm run dev");
376
- console.log();
377
- console.log(" 2. Use the language switcher in the header to change languages");
378
- console.log();
379
- console.log(" 3. Use translations in your components:");
380
- console.log();
381
- console.log(' import { useTranslation } from "react-i18next";');
382
- console.log();
383
- console.log(" function MyComponent() {");
384
- console.log(' const { t } = useTranslation();');
385
- console.log(' return <h1>{t("nav.home")}</h1>;');
386
- console.log(" }");
387
- console.log();
388
- console.log(" 4. To add a new language:");
389
- console.log(" a. Create src/locales/<code>/common.json");
390
- console.log(" b. Import it in src/lib/i18n.ts and add to resources");
391
- console.log(" c. Add the language to availableLanguages in src/hooks/useLanguage.ts");
392
- console.log();
393
- console.log(" 5. To add new translation keys:");
394
- console.log(" a. Add the key to ALL locale files (en, de, etc.)");
395
- console.log(' b. Use t("section.key") in your components');
396
- console.log();
397
- console.log(" 6. For namespaced translations (e.g., per-page):");
398
- console.log(" a. Create a new JSON file: src/locales/en/dashboard.json");
399
- console.log(" b. Import and add it in i18n.ts resources");
400
- console.log(' c. Use: const { t } = useTranslation("dashboard");');
401
- console.log();
402
- }
403
-
404
- main().catch((err) => {
405
- console.error("Installation failed:", err);
406
- process.exit(1);
407
- });
1
+ /**
2
+ * Internationalization (i18n) Sail Installer
3
+ *
4
+ * Adds multi-language support with i18next, react-i18next,
5
+ * and automatic browser language detection.
6
+ *
7
+ * Usage:
8
+ * npx tsx sails/i18n/install.ts
9
+ */
10
+
11
+ import {
12
+ readFileSync,
13
+ writeFileSync,
14
+ copyFileSync,
15
+ existsSync,
16
+ mkdirSync,
17
+ } from "node:fs";
18
+ import { resolve, dirname, join } from "node:path";
19
+ import { execSync } from "node:child_process";
20
+ import { confirm, checkbox } from "@inquirer/prompts";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Paths
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
27
+ const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
28
+ const FRONTEND_ROOT = join(PROJECT_ROOT, "packages/frontend");
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Helpers
32
+ // ---------------------------------------------------------------------------
33
+
34
+ interface SailManifest {
35
+ name: string;
36
+ displayName: string;
37
+ version: string;
38
+ requiredEnvVars: { key: string; description: string }[];
39
+ dependencies: { backend: Record<string, string>; frontend: Record<string, string> };
40
+ }
41
+
42
+ function loadManifest(): SailManifest {
43
+ return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
44
+ }
45
+
46
+ function copyFile(src: string, dest: string, label: string): void {
47
+ mkdirSync(dirname(dest), { recursive: true });
48
+ copyFileSync(src, dest);
49
+ console.log(` Copied -> ${label}`);
50
+ }
51
+
52
+ function installDeps(deps: Record<string, string>, workspace: string): void {
53
+ const entries = Object.entries(deps);
54
+ if (entries.length === 0) return;
55
+ const packages = entries.map(([n, v]) => `${n}@${v}`).join(" ");
56
+ const cmd = `npm install ${packages} --workspace=${workspace}`;
57
+ console.log(` Running: ${cmd}`);
58
+ execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Language definitions
63
+ // ---------------------------------------------------------------------------
64
+
65
+ interface LanguageDef {
66
+ code: string;
67
+ label: string;
68
+ nativeLabel: string;
69
+ hasBuiltinLocale: boolean;
70
+ }
71
+
72
+ const AVAILABLE_LANGUAGES: LanguageDef[] = [
73
+ { code: "en", label: "English", nativeLabel: "English", hasBuiltinLocale: true },
74
+ { code: "de", label: "German", nativeLabel: "Deutsch", hasBuiltinLocale: true },
75
+ { code: "fr", label: "French", nativeLabel: "Francais", hasBuiltinLocale: false },
76
+ { code: "es", label: "Spanish", nativeLabel: "Espanol", hasBuiltinLocale: false },
77
+ ];
78
+
79
+ // Stub translations for French
80
+ const FR_COMMON = {
81
+ nav: { home: "Accueil", profile: "Profil", settings: "Parametres", login: "Se connecter", signup: "S'inscrire", logout: "Se deconnecter" },
82
+ auth: { loginTitle: "Bon retour", signupTitle: "Creer un compte", email: "E-mail", password: "Mot de passe", confirmPassword: "Confirmer le mot de passe", name: "Nom", forgotPassword: "Mot de passe oublie?", noAccount: "Pas encore de compte?", hasAccount: "Deja un compte?", resetPassword: "Reinitialiser le mot de passe", loginButton: "Se connecter", signupButton: "S'inscrire", resetButton: "Envoyer le lien" },
83
+ profile: { title: "Profil", editProfile: "Modifier le profil", name: "Nom", email: "E-mail", memberSince: "Membre depuis" },
84
+ common: { save: "Enregistrer", cancel: "Annuler", delete: "Supprimer", loading: "Chargement...", error: "Une erreur est survenue", success: "Succes", confirm: "Confirmer", back: "Retour", search: "Rechercher", noResults: "Aucun resultat" },
85
+ };
86
+
87
+ // Stub translations for Spanish
88
+ const ES_COMMON = {
89
+ nav: { home: "Inicio", profile: "Perfil", settings: "Configuracion", login: "Iniciar sesion", signup: "Registrarse", logout: "Cerrar sesion" },
90
+ auth: { loginTitle: "Bienvenido de nuevo", signupTitle: "Crear cuenta", email: "Correo electronico", password: "Contrasena", confirmPassword: "Confirmar contrasena", name: "Nombre", forgotPassword: "Olvidaste tu contrasena?", noAccount: "No tienes cuenta?", hasAccount: "Ya tienes cuenta?", resetPassword: "Restablecer contrasena", loginButton: "Iniciar sesion", signupButton: "Registrarse", resetButton: "Enviar enlace" },
91
+ profile: { title: "Perfil", editProfile: "Editar perfil", name: "Nombre", email: "Correo electronico", memberSince: "Miembro desde" },
92
+ common: { save: "Guardar", cancel: "Cancelar", delete: "Eliminar", loading: "Cargando...", error: "Algo salio mal", success: "Exito", confirm: "Confirmar", back: "Volver", search: "Buscar", noResults: "Sin resultados" },
93
+ };
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Main
97
+ // ---------------------------------------------------------------------------
98
+
99
+ async function main(): Promise<void> {
100
+ const manifest = loadManifest();
101
+
102
+ // -- Step 1: Welcome --------------------------------------------------------
103
+ console.log("\n------------------------------------------------------------");
104
+ console.log(` Internationalization Sail Installer (v${manifest.version})`);
105
+ console.log("------------------------------------------------------------");
106
+ console.log();
107
+ console.log(" This sail adds multi-language support to your project:");
108
+ console.log(" - i18next for translation management");
109
+ console.log(" - react-i18next for React integration");
110
+ console.log(" - Automatic browser language detection");
111
+ console.log(" - Language switcher component");
112
+ console.log(" - Translation files for selected languages");
113
+ console.log();
114
+
115
+ const pkgPath = join(PROJECT_ROOT, "package.json");
116
+ if (existsSync(pkgPath)) {
117
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
118
+ console.log(` Template version: ${pkg.version ?? "unknown"}`);
119
+ console.log();
120
+ }
121
+
122
+ // -- Step 2: Select languages -----------------------------------------------
123
+ const selectedCodes = await checkbox({
124
+ message: "Which languages would you like to include?",
125
+ choices: AVAILABLE_LANGUAGES.map((lang) => ({
126
+ name: `${lang.label} (${lang.nativeLabel})`,
127
+ value: lang.code,
128
+ checked: lang.code === "en" || lang.code === "de",
129
+ })),
130
+ validate: (values) => {
131
+ if (!values.includes("en")) {
132
+ return "English must be selected as the fallback language.";
133
+ }
134
+ if (values.length < 2) {
135
+ return "Please select at least two languages.";
136
+ }
137
+ return true;
138
+ },
139
+ });
140
+
141
+ const selectedLanguages = AVAILABLE_LANGUAGES.filter((l) =>
142
+ selectedCodes.includes(l.code),
143
+ );
144
+
145
+ console.log();
146
+ console.log(
147
+ ` Selected languages: ${selectedLanguages.map((l) => l.label).join(", ")}`,
148
+ );
149
+ console.log();
150
+
151
+ // -- Step 3: Summary --------------------------------------------------------
152
+ console.log(" Summary of changes:");
153
+ console.log(" -------------------");
154
+ console.log(" Files to create:");
155
+ console.log(" + packages/frontend/src/lib/i18n.ts");
156
+ console.log(" + packages/frontend/src/hooks/useLanguage.ts");
157
+ console.log(" + packages/frontend/src/components/LanguageSwitcher.tsx");
158
+ for (const lang of selectedLanguages) {
159
+ console.log(` + packages/frontend/src/locales/${lang.code}/common.json`);
160
+ }
161
+ console.log();
162
+ console.log(" Files to modify:");
163
+ console.log(" ~ packages/frontend/src/main.tsx (import i18n)");
164
+ console.log(" ~ packages/frontend/src/components/layout/Header.tsx (add LanguageSwitcher)");
165
+ console.log();
166
+ console.log(" Dependencies to install:");
167
+ console.log(" i18next, react-i18next, i18next-browser-languagedetector");
168
+ console.log();
169
+
170
+ // -- Step 4: Confirm --------------------------------------------------------
171
+ const proceed = await confirm({ message: "Proceed with installation?", default: true });
172
+ if (!proceed) {
173
+ console.log("\n Installation cancelled.\n");
174
+ process.exit(0);
175
+ }
176
+
177
+ console.log();
178
+ console.log(" Installing...");
179
+ console.log();
180
+
181
+ // -- Step 5: Copy locale files ----------------------------------------------
182
+ console.log(" Copying translation files...");
183
+
184
+ // English and German have built-in files
185
+ for (const lang of selectedLanguages) {
186
+ const localeDestDir = join(FRONTEND_ROOT, `src/locales/${lang.code}`);
187
+ mkdirSync(localeDestDir, { recursive: true });
188
+
189
+ if (lang.hasBuiltinLocale) {
190
+ copyFile(
191
+ join(SAIL_DIR, `files/frontend/locales/${lang.code}/common.json`),
192
+ join(localeDestDir, "common.json"),
193
+ `src/locales/${lang.code}/common.json`,
194
+ );
195
+ } else {
196
+ // Generate locale file from inline data
197
+ let data: Record<string, unknown>;
198
+ switch (lang.code) {
199
+ case "fr":
200
+ data = FR_COMMON;
201
+ break;
202
+ case "es":
203
+ data = ES_COMMON;
204
+ break;
205
+ default:
206
+ data = {};
207
+ }
208
+ const destPath = join(localeDestDir, "common.json");
209
+ writeFileSync(destPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
210
+ console.log(` Created -> src/locales/${lang.code}/common.json`);
211
+ }
212
+ }
213
+
214
+ console.log();
215
+ console.log(" Copying source files...");
216
+
217
+ // Copy core files
218
+ copyFile(
219
+ join(SAIL_DIR, "files/frontend/hooks/useLanguage.ts"),
220
+ join(FRONTEND_ROOT, "src/hooks/useLanguage.ts"),
221
+ "src/hooks/useLanguage.ts",
222
+ );
223
+ copyFile(
224
+ join(SAIL_DIR, "files/frontend/components/LanguageSwitcher.tsx"),
225
+ join(FRONTEND_ROOT, "src/components/LanguageSwitcher.tsx"),
226
+ "src/components/LanguageSwitcher.tsx",
227
+ );
228
+
229
+ // -- Step 6: Generate i18n.ts with selected languages -----------------------
230
+ console.log();
231
+ console.log(" Generating i18n configuration...");
232
+
233
+ const importLines = selectedLanguages
234
+ .map((l) => `import ${l.code}Common from "@/locales/${l.code}/common.json";`)
235
+ .join("\n");
236
+
237
+ const resourceLines = selectedLanguages
238
+ .map((l) => ` ${l.code}: { common: ${l.code}Common },`)
239
+ .join("\n");
240
+
241
+ const i18nContent = `import i18n from "i18next";
242
+ import { initReactI18next } from "react-i18next";
243
+ import LanguageDetector from "i18next-browser-languagedetector";
244
+
245
+ // Import locale files
246
+ ${importLines}
247
+
248
+ const resources = {
249
+ ${resourceLines}
250
+ };
251
+
252
+ i18n
253
+ .use(LanguageDetector)
254
+ .use(initReactI18next)
255
+ .init({
256
+ resources,
257
+ defaultNS: "common",
258
+ ns: ["common"],
259
+ fallbackLng: "en",
260
+ interpolation: {
261
+ escapeValue: false, // React already escapes
262
+ },
263
+ detection: {
264
+ order: ["localStorage", "navigator", "htmlTag"],
265
+ caches: ["localStorage"],
266
+ lookupLocalStorage: "i18nextLng",
267
+ },
268
+ });
269
+
270
+ export default i18n;
271
+ `;
272
+
273
+ const i18nDest = join(FRONTEND_ROOT, "src/lib/i18n.ts");
274
+ mkdirSync(dirname(i18nDest), { recursive: true });
275
+ writeFileSync(i18nDest, i18nContent, "utf-8");
276
+ console.log(" Created -> src/lib/i18n.ts");
277
+
278
+ // Update useLanguage.ts with the selected languages
279
+ const languageEntries = selectedLanguages
280
+ .map((l) => ` { code: "${l.code}", label: "${l.nativeLabel}" },`)
281
+ .join("\n");
282
+
283
+ const useLanguageContent = `import { useTranslation } from "react-i18next";
284
+ import { useCallback } from "react";
285
+
286
+ export interface Language {
287
+ code: string;
288
+ label: string;
289
+ }
290
+
291
+ const availableLanguages: Language[] = [
292
+ ${languageEntries}
293
+ ];
294
+
295
+ export function useLanguage() {
296
+ const { i18n } = useTranslation();
297
+
298
+ const currentLanguage = i18n.language?.split("-")[0] ?? "en";
299
+
300
+ const changeLanguage = useCallback(
301
+ async (code: string) => {
302
+ await i18n.changeLanguage(code);
303
+ },
304
+ [i18n],
305
+ );
306
+
307
+ return {
308
+ currentLanguage,
309
+ changeLanguage,
310
+ availableLanguages,
311
+ };
312
+ }
313
+ `;
314
+
315
+ writeFileSync(join(FRONTEND_ROOT, "src/hooks/useLanguage.ts"), useLanguageContent, "utf-8");
316
+ console.log(" Updated -> src/hooks/useLanguage.ts");
317
+
318
+ // -- Step 7: Modify main.tsx — import i18n ----------------------------------
319
+ console.log();
320
+ console.log(" Modifying frontend files...");
321
+
322
+ const mainPath = join(FRONTEND_ROOT, "src/main.tsx");
323
+ if (existsSync(mainPath)) {
324
+ let mainContent = readFileSync(mainPath, "utf-8");
325
+
326
+ if (!mainContent.includes("./lib/i18n")) {
327
+ // Add i18n import at the top (after existing imports)
328
+ mainContent = `import "./lib/i18n.js";\n${mainContent}`;
329
+ writeFileSync(mainPath, mainContent, "utf-8");
330
+ console.log(" Modified -> src/main.tsx (added i18n import)");
331
+ } else {
332
+ console.log(" Skipped (already present) -> src/main.tsx");
333
+ }
334
+ }
335
+
336
+ // -- Step 8: Add LanguageSwitcher to Header ---------------------------------
337
+ const headerPath = join(FRONTEND_ROOT, "src/components/layout/Header.tsx");
338
+ if (existsSync(headerPath)) {
339
+ let headerContent = readFileSync(headerPath, "utf-8");
340
+
341
+ if (!headerContent.includes("LanguageSwitcher")) {
342
+ // Add import
343
+ headerContent = headerContent.replace(
344
+ 'import { useAuth } from "@/hooks/useAuth";',
345
+ 'import { useAuth } from "@/hooks/useAuth";\nimport LanguageSwitcher from "@/components/LanguageSwitcher";',
346
+ );
347
+
348
+ // Add LanguageSwitcher in the desktop nav, before the user section
349
+ headerContent = headerContent.replace(
350
+ "{/* Desktop Nav */}\n <nav className=\"hidden items-center gap-6 md:flex\">",
351
+ "{/* Desktop Nav */}\n <nav className=\"hidden items-center gap-6 md:flex\">\n <LanguageSwitcher />",
352
+ );
353
+
354
+ writeFileSync(headerPath, headerContent, "utf-8");
355
+ console.log(" Modified -> src/components/layout/Header.tsx (added LanguageSwitcher)");
356
+ } else {
357
+ console.log(" Skipped (already present) -> Header.tsx");
358
+ }
359
+ }
360
+
361
+ // -- Step 9: Install dependencies -------------------------------------------
362
+ console.log();
363
+ console.log(" Installing dependencies...");
364
+ installDeps(manifest.dependencies.frontend, "packages/frontend");
365
+
366
+ // -- Step 10: Next steps ----------------------------------------------------
367
+ console.log();
368
+ console.log("------------------------------------------------------------");
369
+ console.log(" Internationalization installed successfully!");
370
+ console.log("------------------------------------------------------------");
371
+ console.log();
372
+ console.log(" Next steps:");
373
+ console.log();
374
+ console.log(" 1. Start your dev server:");
375
+ console.log(" npm run dev");
376
+ console.log();
377
+ console.log(" 2. Use the language switcher in the header to change languages");
378
+ console.log();
379
+ console.log(" 3. Use translations in your components:");
380
+ console.log();
381
+ console.log(' import { useTranslation } from "react-i18next";');
382
+ console.log();
383
+ console.log(" function MyComponent() {");
384
+ console.log(' const { t } = useTranslation();');
385
+ console.log(' return <h1>{t("nav.home")}</h1>;');
386
+ console.log(" }");
387
+ console.log();
388
+ console.log(" 4. To add a new language:");
389
+ console.log(" a. Create src/locales/<code>/common.json");
390
+ console.log(" b. Import it in src/lib/i18n.ts and add to resources");
391
+ console.log(" c. Add the language to availableLanguages in src/hooks/useLanguage.ts");
392
+ console.log();
393
+ console.log(" 5. To add new translation keys:");
394
+ console.log(" a. Add the key to ALL locale files (en, de, etc.)");
395
+ console.log(' b. Use t("section.key") in your components');
396
+ console.log();
397
+ console.log(" 6. For namespaced translations (e.g., per-page):");
398
+ console.log(" a. Create a new JSON file: src/locales/en/dashboard.json");
399
+ console.log(" b. Import and add it in i18n.ts resources");
400
+ console.log(' c. Use: const { t } = useTranslation("dashboard");');
401
+ console.log();
402
+ }
403
+
404
+ main().catch((err) => {
405
+ console.error("Installation failed:", err);
406
+ process.exit(1);
407
+ });