@codaijs/keel 0.1.0

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 (116) hide show
  1. package/dist/__tests__/cli.test.d.ts +2 -0
  2. package/dist/__tests__/cli.test.d.ts.map +1 -0
  3. package/dist/__tests__/cli.test.js +173 -0
  4. package/dist/__tests__/cli.test.js.map +1 -0
  5. package/dist/__tests__/registry.test.d.ts +2 -0
  6. package/dist/__tests__/registry.test.d.ts.map +1 -0
  7. package/dist/__tests__/registry.test.js +86 -0
  8. package/dist/__tests__/registry.test.js.map +1 -0
  9. package/dist/__tests__/sail-installer.test.d.ts +2 -0
  10. package/dist/__tests__/sail-installer.test.d.ts.map +1 -0
  11. package/dist/__tests__/sail-installer.test.js +158 -0
  12. package/dist/__tests__/sail-installer.test.js.map +1 -0
  13. package/dist/create-runner.d.ts +11 -0
  14. package/dist/create-runner.d.ts.map +1 -0
  15. package/dist/create-runner.js +63 -0
  16. package/dist/create-runner.js.map +1 -0
  17. package/dist/create.d.ts +10 -0
  18. package/dist/create.d.ts.map +1 -0
  19. package/dist/create.js +15 -0
  20. package/dist/create.js.map +1 -0
  21. package/dist/manage.d.ts +24 -0
  22. package/dist/manage.d.ts.map +1 -0
  23. package/dist/manage.js +1461 -0
  24. package/dist/manage.js.map +1 -0
  25. package/dist/prompts.d.ts +36 -0
  26. package/dist/prompts.d.ts.map +1 -0
  27. package/dist/prompts.js +208 -0
  28. package/dist/prompts.js.map +1 -0
  29. package/dist/sail-installer.d.ts +37 -0
  30. package/dist/sail-installer.d.ts.map +1 -0
  31. package/dist/sail-installer.js +935 -0
  32. package/dist/sail-installer.js.map +1 -0
  33. package/dist/scaffold.d.ts +10 -0
  34. package/dist/scaffold.d.ts.map +1 -0
  35. package/dist/scaffold.js +297 -0
  36. package/dist/scaffold.js.map +1 -0
  37. package/package.json +57 -0
  38. package/sails/_template/addon.json +20 -0
  39. package/sails/_template/install.ts +402 -0
  40. package/sails/admin-dashboard/README.md +117 -0
  41. package/sails/admin-dashboard/addon.json +28 -0
  42. package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -0
  43. package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -0
  44. package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -0
  45. package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -0
  46. package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -0
  47. package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -0
  48. package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -0
  49. package/sails/admin-dashboard/install.ts +305 -0
  50. package/sails/analytics/README.md +178 -0
  51. package/sails/analytics/addon.json +27 -0
  52. package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -0
  53. package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -0
  54. package/sails/analytics/files/frontend/lib/analytics.ts +103 -0
  55. package/sails/analytics/install.ts +297 -0
  56. package/sails/file-uploads/README.md +191 -0
  57. package/sails/file-uploads/addon.json +30 -0
  58. package/sails/file-uploads/files/backend/routes/files.ts +198 -0
  59. package/sails/file-uploads/files/backend/schema/files.ts +36 -0
  60. package/sails/file-uploads/files/backend/services/file-storage.ts +128 -0
  61. package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -0
  62. package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -0
  63. package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -0
  64. package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -0
  65. package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -0
  66. package/sails/file-uploads/install.ts +466 -0
  67. package/sails/gdpr/README.md +174 -0
  68. package/sails/gdpr/addon.json +27 -0
  69. package/sails/gdpr/files/backend/routes/gdpr.ts +140 -0
  70. package/sails/gdpr/files/backend/services/gdpr.ts +293 -0
  71. package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -0
  72. package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -0
  73. package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -0
  74. package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -0
  75. package/sails/gdpr/install.ts +756 -0
  76. package/sails/google-oauth/README.md +121 -0
  77. package/sails/google-oauth/addon.json +22 -0
  78. package/sails/google-oauth/files/GoogleButton.tsx +50 -0
  79. package/sails/google-oauth/install.ts +252 -0
  80. package/sails/i18n/README.md +193 -0
  81. package/sails/i18n/addon.json +30 -0
  82. package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -0
  83. package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -0
  84. package/sails/i18n/files/frontend/lib/i18n.ts +32 -0
  85. package/sails/i18n/files/frontend/locales/de/common.json +44 -0
  86. package/sails/i18n/files/frontend/locales/en/common.json +44 -0
  87. package/sails/i18n/install.ts +407 -0
  88. package/sails/push-notifications/README.md +163 -0
  89. package/sails/push-notifications/addon.json +31 -0
  90. package/sails/push-notifications/files/backend/routes/notifications.ts +153 -0
  91. package/sails/push-notifications/files/backend/schema/notifications.ts +31 -0
  92. package/sails/push-notifications/files/backend/services/notifications.ts +117 -0
  93. package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -0
  94. package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -0
  95. package/sails/push-notifications/install.ts +384 -0
  96. package/sails/r2-storage/README.md +101 -0
  97. package/sails/r2-storage/addon.json +29 -0
  98. package/sails/r2-storage/files/backend/services/storage.ts +71 -0
  99. package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -0
  100. package/sails/r2-storage/install.ts +412 -0
  101. package/sails/rate-limiting/README.md +145 -0
  102. package/sails/rate-limiting/addon.json +20 -0
  103. package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -0
  104. package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -0
  105. package/sails/rate-limiting/install.ts +300 -0
  106. package/sails/registry.json +107 -0
  107. package/sails/stripe/README.md +214 -0
  108. package/sails/stripe/addon.json +24 -0
  109. package/sails/stripe/files/backend/routes/stripe.ts +154 -0
  110. package/sails/stripe/files/backend/schema/stripe.ts +74 -0
  111. package/sails/stripe/files/backend/services/stripe.ts +224 -0
  112. package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -0
  113. package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -0
  114. package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -0
  115. package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -0
  116. package/sails/stripe/install.ts +378 -0
@@ -0,0 +1,108 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import { useLanguage } from "@/hooks/useLanguage.js";
3
+
4
+ export default function LanguageSwitcher() {
5
+ const { currentLanguage, changeLanguage, availableLanguages } = useLanguage();
6
+ const [isOpen, setIsOpen] = useState(false);
7
+ const containerRef = useRef<HTMLDivElement>(null);
8
+
9
+ const currentLabel =
10
+ availableLanguages.find((l) => l.code === currentLanguage)?.label ??
11
+ currentLanguage.toUpperCase();
12
+
13
+ // Close dropdown when clicking outside
14
+ useEffect(() => {
15
+ function handleClickOutside(event: MouseEvent) {
16
+ if (
17
+ containerRef.current &&
18
+ !containerRef.current.contains(event.target as Node)
19
+ ) {
20
+ setIsOpen(false);
21
+ }
22
+ }
23
+
24
+ if (isOpen) {
25
+ document.addEventListener("mousedown", handleClickOutside);
26
+ }
27
+ return () => {
28
+ document.removeEventListener("mousedown", handleClickOutside);
29
+ };
30
+ }, [isOpen]);
31
+
32
+ return (
33
+ <div ref={containerRef} className="relative">
34
+ <button
35
+ onClick={() => setIsOpen(!isOpen)}
36
+ className="flex items-center gap-1.5 rounded-lg border border-keel-gray-800 px-2.5 py-1.5 text-sm font-medium text-keel-gray-400 transition-colors hover:border-keel-gray-400 hover:text-white"
37
+ aria-label="Switch language"
38
+ >
39
+ <svg
40
+ className="h-4 w-4"
41
+ fill="none"
42
+ viewBox="0 0 24 24"
43
+ stroke="currentColor"
44
+ >
45
+ <path
46
+ strokeLinecap="round"
47
+ strokeLinejoin="round"
48
+ strokeWidth={2}
49
+ d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
50
+ />
51
+ </svg>
52
+ <span>{currentLanguage.toUpperCase()}</span>
53
+ <svg
54
+ className={`h-3 w-3 transition-transform ${isOpen ? "rotate-180" : ""}`}
55
+ fill="none"
56
+ viewBox="0 0 24 24"
57
+ stroke="currentColor"
58
+ >
59
+ <path
60
+ strokeLinecap="round"
61
+ strokeLinejoin="round"
62
+ strokeWidth={2}
63
+ d="M19 9l-7 7-7-7"
64
+ />
65
+ </svg>
66
+ </button>
67
+
68
+ {isOpen && (
69
+ <div className="absolute right-0 z-20 mt-1.5 w-40 rounded-lg border border-keel-gray-800 bg-keel-gray-900 py-1 shadow-lg">
70
+ {availableLanguages.map((lang) => (
71
+ <button
72
+ key={lang.code}
73
+ onClick={() => {
74
+ changeLanguage(lang.code);
75
+ setIsOpen(false);
76
+ }}
77
+ className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors hover:bg-keel-gray-800 ${
78
+ currentLanguage === lang.code
79
+ ? "font-medium text-keel-blue"
80
+ : "text-keel-gray-100"
81
+ }`}
82
+ >
83
+ <span className="w-6 text-center text-xs uppercase text-keel-gray-400">
84
+ {lang.code}
85
+ </span>
86
+ <span>{lang.label}</span>
87
+ {currentLanguage === lang.code && (
88
+ <svg
89
+ className="ml-auto h-4 w-4 text-keel-blue"
90
+ fill="none"
91
+ viewBox="0 0 24 24"
92
+ stroke="currentColor"
93
+ >
94
+ <path
95
+ strokeLinecap="round"
96
+ strokeLinejoin="round"
97
+ strokeWidth={2}
98
+ d="M5 13l4 4L19 7"
99
+ />
100
+ </svg>
101
+ )}
102
+ </button>
103
+ ))}
104
+ </div>
105
+ )}
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,31 @@
1
+ import { useTranslation } from "react-i18next";
2
+ import { useCallback } from "react";
3
+
4
+ export interface Language {
5
+ code: string;
6
+ label: string;
7
+ }
8
+
9
+ const availableLanguages: Language[] = [
10
+ { code: "en", label: "English" },
11
+ { code: "de", label: "Deutsch" },
12
+ ];
13
+
14
+ export function useLanguage() {
15
+ const { i18n } = useTranslation();
16
+
17
+ const currentLanguage = i18n.language?.split("-")[0] ?? "en";
18
+
19
+ const changeLanguage = useCallback(
20
+ async (code: string) => {
21
+ await i18n.changeLanguage(code);
22
+ },
23
+ [i18n],
24
+ );
25
+
26
+ return {
27
+ currentLanguage,
28
+ changeLanguage,
29
+ availableLanguages,
30
+ };
31
+ }
@@ -0,0 +1,32 @@
1
+ import i18n from "i18next";
2
+ import { initReactI18next } from "react-i18next";
3
+ import LanguageDetector from "i18next-browser-languagedetector";
4
+
5
+ // Import locale files
6
+ import enCommon from "@/locales/en/common.json";
7
+ import deCommon from "@/locales/de/common.json";
8
+
9
+ const resources = {
10
+ en: { common: enCommon },
11
+ de: { common: deCommon },
12
+ };
13
+
14
+ i18n
15
+ .use(LanguageDetector)
16
+ .use(initReactI18next)
17
+ .init({
18
+ resources,
19
+ defaultNS: "common",
20
+ ns: ["common"],
21
+ fallbackLng: "en",
22
+ interpolation: {
23
+ escapeValue: false, // React already escapes
24
+ },
25
+ detection: {
26
+ order: ["localStorage", "navigator", "htmlTag"],
27
+ caches: ["localStorage"],
28
+ lookupLocalStorage: "i18nextLng",
29
+ },
30
+ });
31
+
32
+ export default i18n;
@@ -0,0 +1,44 @@
1
+ {
2
+ "nav": {
3
+ "home": "Startseite",
4
+ "profile": "Profil",
5
+ "settings": "Einstellungen",
6
+ "login": "Anmelden",
7
+ "signup": "Registrieren",
8
+ "logout": "Abmelden"
9
+ },
10
+ "auth": {
11
+ "loginTitle": "Willkommen zurueck",
12
+ "signupTitle": "Konto erstellen",
13
+ "email": "E-Mail",
14
+ "password": "Passwort",
15
+ "confirmPassword": "Passwort bestaetigen",
16
+ "name": "Name",
17
+ "forgotPassword": "Passwort vergessen?",
18
+ "noAccount": "Noch kein Konto?",
19
+ "hasAccount": "Bereits ein Konto?",
20
+ "resetPassword": "Passwort zuruecksetzen",
21
+ "loginButton": "Anmelden",
22
+ "signupButton": "Registrieren",
23
+ "resetButton": "Link senden"
24
+ },
25
+ "profile": {
26
+ "title": "Profil",
27
+ "editProfile": "Profil bearbeiten",
28
+ "name": "Name",
29
+ "email": "E-Mail",
30
+ "memberSince": "Mitglied seit"
31
+ },
32
+ "common": {
33
+ "save": "Speichern",
34
+ "cancel": "Abbrechen",
35
+ "delete": "Loeschen",
36
+ "loading": "Laden...",
37
+ "error": "Etwas ist schiefgelaufen",
38
+ "success": "Erfolgreich",
39
+ "confirm": "Bestaetigen",
40
+ "back": "Zurueck",
41
+ "search": "Suche",
42
+ "noResults": "Keine Ergebnisse gefunden"
43
+ }
44
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "nav": {
3
+ "home": "Home",
4
+ "profile": "Profile",
5
+ "settings": "Settings",
6
+ "login": "Log in",
7
+ "signup": "Sign up",
8
+ "logout": "Log out"
9
+ },
10
+ "auth": {
11
+ "loginTitle": "Welcome back",
12
+ "signupTitle": "Create your account",
13
+ "email": "Email",
14
+ "password": "Password",
15
+ "confirmPassword": "Confirm password",
16
+ "name": "Name",
17
+ "forgotPassword": "Forgot password?",
18
+ "noAccount": "Don't have an account?",
19
+ "hasAccount": "Already have an account?",
20
+ "resetPassword": "Reset password",
21
+ "loginButton": "Log in",
22
+ "signupButton": "Sign up",
23
+ "resetButton": "Send reset link"
24
+ },
25
+ "profile": {
26
+ "title": "Profile",
27
+ "editProfile": "Edit profile",
28
+ "name": "Name",
29
+ "email": "Email",
30
+ "memberSince": "Member since"
31
+ },
32
+ "common": {
33
+ "save": "Save",
34
+ "cancel": "Cancel",
35
+ "delete": "Delete",
36
+ "loading": "Loading...",
37
+ "error": "Something went wrong",
38
+ "success": "Success",
39
+ "confirm": "Confirm",
40
+ "back": "Back",
41
+ "search": "Search",
42
+ "noResults": "No results found"
43
+ }
44
+ }
@@ -0,0 +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
+ });