@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,121 @@
1
+ # Google OAuth Sail
2
+
3
+ Adds Google sign-in to your keel application using BetterAuth's social provider system.
4
+
5
+ ## Prerequisites
6
+
7
+ - A Google Cloud project
8
+ - OAuth 2.0 credentials (Client ID and Client Secret)
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npx tsx sails/google-oauth/install.ts
14
+ ```
15
+
16
+ The installer will prompt for your Google OAuth credentials and configure everything automatically.
17
+
18
+ ## Manual Setup: Google Cloud Console
19
+
20
+ ### 1. Create or Select a Project
21
+
22
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
23
+ 2. Click the project dropdown in the top bar
24
+ 3. Click **New Project** or select an existing one
25
+
26
+ ### 2. Enable the Google+ API (if not enabled)
27
+
28
+ 1. Go to **APIs & Services** > **Library**
29
+ 2. Search for "Google+ API" or "Google Identity"
30
+ 3. Click **Enable**
31
+
32
+ ### 3. Configure the OAuth Consent Screen
33
+
34
+ 1. Go to **APIs & Services** > **OAuth consent screen**
35
+ 2. Select **External** (or **Internal** for Google Workspace orgs)
36
+ 3. Fill in the required fields:
37
+ - **App name**: Your application name
38
+ - **User support email**: Your email
39
+ - **Developer contact information**: Your email
40
+ 4. Add scopes: `email`, `profile`, `openid`
41
+ 5. Add test users if in "Testing" mode
42
+
43
+ ### 4. Create OAuth 2.0 Credentials
44
+
45
+ 1. Go to **APIs & Services** > **Credentials**
46
+ 2. Click **Create Credentials** > **OAuth client ID**
47
+ 3. Select **Web application**
48
+ 4. Set the name (e.g., "My App - Web")
49
+ 5. Add **Authorized JavaScript origins**:
50
+ - `http://localhost:5173` (Vite dev server)
51
+ - `https://yourdomain.com` (production)
52
+ 6. Add **Authorized redirect URIs**:
53
+ - `http://localhost:3000/api/auth/callback/google` (development)
54
+ - `https://yourdomain.com/api/auth/callback/google` (production)
55
+ 7. Click **Create**
56
+ 8. Copy the **Client ID** and **Client Secret**
57
+
58
+ ### 5. Configure Environment Variables
59
+
60
+ Add to your `.env` file:
61
+
62
+ ```env
63
+ GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
64
+ GOOGLE_CLIENT_SECRET=your-client-secret
65
+ ```
66
+
67
+ ## How It Works
68
+
69
+ ### Authentication Flow
70
+
71
+ 1. User clicks "Continue with Google" button
72
+ 2. BetterAuth redirects to Google's OAuth consent screen
73
+ 3. User authorizes the application
74
+ 4. Google redirects back to `/api/auth/callback/google`
75
+ 5. BetterAuth creates or links the user account
76
+ 6. User is redirected to the dashboard
77
+
78
+ ### Files Modified
79
+
80
+ **Backend:**
81
+ - `src/auth/index.ts` -- Google added as a social provider in BetterAuth config
82
+ - `src/env.ts` -- Environment variable validation for Google credentials
83
+
84
+ **Frontend:**
85
+ - `src/components/auth/LoginForm.tsx` -- Google sign-in button added
86
+ - `src/components/auth/SignupForm.tsx` -- Google sign-in button added
87
+
88
+ ### Files Added
89
+
90
+ **Frontend:**
91
+ - `src/components/auth/GoogleButton.tsx` -- Styled Google sign-in button component
92
+
93
+ ## Capacitor / Native Apps
94
+
95
+ For native mobile apps using Capacitor, Google OAuth works through the system browser (in-app browser tab). The redirect URI remains the same since the native app loads the web app in a WebView.
96
+
97
+ If you need native Google Sign-In (using the Google SDK directly), you will need to:
98
+
99
+ 1. Create separate OAuth credentials for iOS and Android
100
+ 2. Use `@capacitor/google-auth` plugin
101
+ 3. Pass the token to BetterAuth for session creation
102
+
103
+ ## Troubleshooting
104
+
105
+ ### "redirect_uri_mismatch" Error
106
+
107
+ Make sure your redirect URI in Google Cloud Console exactly matches:
108
+ - Development: `http://localhost:3000/api/auth/callback/google`
109
+ - Production: `https://yourdomain.com/api/auth/callback/google`
110
+
111
+ The port must match your backend server port, and the path is determined by BetterAuth.
112
+
113
+ ### "Access blocked: app has not completed verification"
114
+
115
+ Your OAuth consent screen is in "Testing" mode. Either:
116
+ - Add test users in the consent screen settings, or
117
+ - Submit your app for verification (required for production)
118
+
119
+ ### Users Not Being Linked
120
+
121
+ If a user signs up with email/password and later tries Google OAuth with the same email, BetterAuth will attempt to link the accounts. Make sure `account linking` is enabled in your BetterAuth configuration.
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "google-oauth",
3
+ "displayName": "Google OAuth",
4
+ "description": "Adds Google OAuth sign-in via BetterAuth social provider",
5
+ "version": "1.0.0",
6
+ "compatibility": ">=1.0.0",
7
+ "requiredEnvVars": [
8
+ { "key": "GOOGLE_CLIENT_ID", "description": "Google OAuth Client ID from Google Cloud Console" },
9
+ { "key": "GOOGLE_CLIENT_SECRET", "description": "Google OAuth Client Secret" }
10
+ ],
11
+ "dependencies": {
12
+ "backend": {},
13
+ "frontend": {}
14
+ },
15
+ "modifies": {
16
+ "backend": ["src/auth/index.ts", "src/env.ts"],
17
+ "frontend": ["src/components/auth/LoginForm.tsx", "src/components/auth/SignupForm.tsx"]
18
+ },
19
+ "adds": {
20
+ "frontend": ["src/components/auth/GoogleButton.tsx"]
21
+ }
22
+ }
@@ -0,0 +1,50 @@
1
+ import { authClient } from "@/lib/auth-client";
2
+
3
+ /**
4
+ * Google sign-in button component.
5
+ *
6
+ * Uses BetterAuth's social sign-in to redirect the user to Google's OAuth
7
+ * consent screen. After authorization, Google redirects back to the app and
8
+ * BetterAuth completes the session creation automatically.
9
+ */
10
+ export function GoogleButton() {
11
+ const handleGoogleSignIn = async () => {
12
+ await authClient.signIn.social({
13
+ provider: "google",
14
+ callbackURL: "/dashboard",
15
+ });
16
+ };
17
+
18
+ return (
19
+ <button
20
+ type="button"
21
+ onClick={handleGoogleSignIn}
22
+ className="flex w-full items-center justify-center gap-3 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
23
+ >
24
+ {/* Google "G" logo */}
25
+ <svg
26
+ className="h-5 w-5"
27
+ viewBox="0 0 24 24"
28
+ xmlns="http://www.w3.org/2000/svg"
29
+ >
30
+ <path
31
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
32
+ fill="#4285F4"
33
+ />
34
+ <path
35
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
36
+ fill="#34A853"
37
+ />
38
+ <path
39
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
40
+ fill="#FBBC05"
41
+ />
42
+ <path
43
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
44
+ fill="#EA4335"
45
+ />
46
+ </svg>
47
+ Continue with Google
48
+ </button>
49
+ );
50
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Google OAuth Sail Installer
3
+ *
4
+ * Adds Google OAuth sign-in via BetterAuth social provider.
5
+ * Features a full interactive setup wizard that guides the user through
6
+ * Google Cloud project configuration, credential collection, and installation.
7
+ *
8
+ * Usage:
9
+ * npx tsx sails/google-oauth/install.ts
10
+ */
11
+
12
+ import {
13
+ readFileSync,
14
+ writeFileSync,
15
+ copyFileSync,
16
+ existsSync,
17
+ mkdirSync,
18
+ } from "node:fs";
19
+ import { resolve, dirname, join } from "node:path";
20
+ import { input, confirm } 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 BACKEND_ROOT = join(PROJECT_ROOT, "packages/backend");
29
+ const FRONTEND_ROOT = join(PROJECT_ROOT, "packages/frontend");
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Helpers
33
+ // ---------------------------------------------------------------------------
34
+
35
+ interface SailManifest {
36
+ name: string;
37
+ displayName: string;
38
+ version: string;
39
+ requiredEnvVars: { key: string; description: string }[];
40
+ }
41
+
42
+ function loadManifest(): SailManifest {
43
+ return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
44
+ }
45
+
46
+ function insertAtMarker(filePath: string, marker: string, code: string): void {
47
+ if (!existsSync(filePath)) {
48
+ console.warn(` Warning: File not found: ${filePath}`);
49
+ return;
50
+ }
51
+ let content = readFileSync(filePath, "utf-8");
52
+ if (!content.includes(marker)) {
53
+ console.warn(` Warning: Marker "${marker}" not found in ${filePath}`);
54
+ return;
55
+ }
56
+ if (content.includes(code.trim())) {
57
+ console.log(` Skipped (already present) -> ${filePath}`);
58
+ return;
59
+ }
60
+ content = content.replace(marker, `${marker}\n${code}`);
61
+ writeFileSync(filePath, content, "utf-8");
62
+ console.log(` Modified -> ${filePath}`);
63
+ }
64
+
65
+ function appendToEnvExample(entries: Record<string, string>): void {
66
+ const envPath = join(PROJECT_ROOT, ".env.example");
67
+ if (!existsSync(envPath)) {
68
+ console.warn(" Warning: .env.example not found");
69
+ return;
70
+ }
71
+ let content = readFileSync(envPath, "utf-8");
72
+ const lines: string[] = [];
73
+ for (const [key, val] of Object.entries(entries)) {
74
+ if (!content.includes(key)) {
75
+ lines.push(`${key}=${val}`);
76
+ }
77
+ }
78
+ if (lines.length > 0) {
79
+ content += `\n# Google OAuth\n${lines.join("\n")}\n`;
80
+ writeFileSync(envPath, content, "utf-8");
81
+ console.log(" Updated .env.example");
82
+ }
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Main
87
+ // ---------------------------------------------------------------------------
88
+
89
+ async function main(): Promise<void> {
90
+ const manifest = loadManifest();
91
+
92
+ // -- Step 1: Welcome message -------------------------------------------------
93
+ console.log("\n------------------------------------------------------------");
94
+ console.log(` Google OAuth Sail Installer (v${manifest.version})`);
95
+ console.log("------------------------------------------------------------");
96
+ console.log();
97
+ console.log(" This sail integrates Google sign-in into your project");
98
+ console.log(" using BetterAuth's social provider system. After installation,");
99
+ console.log(" users will be able to sign in with their Google account via a");
100
+ console.log(' "Sign in with Google" button on your login and signup pages.');
101
+ console.log();
102
+
103
+ const pkgPath = join(PROJECT_ROOT, "package.json");
104
+ if (existsSync(pkgPath)) {
105
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
106
+ console.log(` Template version: ${pkg.version ?? "unknown"}`);
107
+ console.log();
108
+ }
109
+
110
+ // -- Step 2: Prerequisites check ---------------------------------------------
111
+ const hasProject = await confirm({
112
+ message: "Do you already have a Google Cloud project with OAuth configured?",
113
+ default: false,
114
+ });
115
+
116
+ if (!hasProject) {
117
+ console.log();
118
+ console.log(" Follow these steps to create a Google OAuth client:");
119
+ console.log();
120
+ console.log(" 1. Go to https://console.cloud.google.com/");
121
+ console.log(" 2. Create a new project (or select an existing one)");
122
+ console.log(" 3. Go to APIs & Services > Credentials");
123
+ console.log(' 4. Click "Create Credentials" > "OAuth client ID"');
124
+ console.log(' 5. Set application type to "Web application"');
125
+ console.log(" 6. Add authorized redirect URI:");
126
+ console.log(" {BACKEND_URL}/api/auth/callback/google");
127
+ console.log(" For local development, use:");
128
+ console.log(" http://localhost:3000/api/auth/callback/google");
129
+ console.log(" 7. Copy the Client ID and Client Secret");
130
+ console.log();
131
+
132
+ await confirm({
133
+ message: "I have completed the steps above and have my credentials ready",
134
+ default: false,
135
+ });
136
+ }
137
+
138
+ const googleClientId = await input({
139
+ message: "Google OAuth Client ID:",
140
+ validate: (value) => {
141
+ if (!value || value.trim().length === 0) return "Client ID is required.";
142
+ return true;
143
+ },
144
+ });
145
+
146
+ const googleClientSecret = await input({
147
+ message: "Google OAuth Client Secret:",
148
+ validate: (value) => {
149
+ if (!value || value.trim().length === 0) return "Client Secret is required.";
150
+ return true;
151
+ },
152
+ });
153
+
154
+ // -- Step 5: Show summary ----------------------------------------------------
155
+ console.log();
156
+ console.log(" Summary of changes:");
157
+ console.log(" -------------------");
158
+ console.log(" Files to copy:");
159
+ console.log(" + packages/frontend/src/components/auth/GoogleButton.tsx");
160
+ console.log();
161
+ console.log(" Files to modify:");
162
+ console.log(" ~ packages/backend/src/auth/index.ts (add Google provider)");
163
+ console.log(" ~ packages/backend/src/env.ts (add env validation)");
164
+ console.log(" ~ packages/frontend/src/components/auth/LoginForm.tsx");
165
+ console.log(" ~ packages/frontend/src/components/auth/SignupForm.tsx");
166
+ console.log(" ~ .env.example");
167
+ console.log(" ~ .env (if exists)");
168
+ console.log();
169
+ console.log(" Environment variables:");
170
+ console.log(` GOOGLE_CLIENT_ID=${googleClientId}`);
171
+ console.log(` GOOGLE_CLIENT_SECRET=${googleClientSecret.slice(0, 8)}...`);
172
+ console.log();
173
+
174
+ const proceed = await confirm({ message: "Proceed with installation?", default: true });
175
+ if (!proceed) { console.log("\n Installation cancelled.\n"); process.exit(0); }
176
+
177
+ // -- Step 7: Execute installation --------------------------------------------
178
+ console.log();
179
+ console.log(" Installing...");
180
+ console.log();
181
+
182
+ console.log(" Copying files...");
183
+ const srcFile = join(SAIL_DIR, "files/GoogleButton.tsx");
184
+ const destDir = join(FRONTEND_ROOT, "src/components/auth");
185
+ mkdirSync(destDir, { recursive: true });
186
+ copyFileSync(srcFile, join(destDir, "GoogleButton.tsx"));
187
+ console.log(" Copied -> src/components/auth/GoogleButton.tsx");
188
+
189
+ console.log();
190
+ console.log(" Modifying backend files...");
191
+
192
+ insertAtMarker(
193
+ join(BACKEND_ROOT, "src/auth/index.ts"),
194
+ "// [SAIL_SOCIAL_PROVIDERS]",
195
+ ` google: {\n clientId: process.env.GOOGLE_CLIENT_ID!,\n clientSecret: process.env.GOOGLE_CLIENT_SECRET!,\n },`
196
+ );
197
+
198
+ insertAtMarker(
199
+ join(BACKEND_ROOT, "src/env.ts"),
200
+ "// [SAIL_ENV_VARS]",
201
+ ` GOOGLE_CLIENT_ID: z.string().min(1, "GOOGLE_CLIENT_ID is required"),\n GOOGLE_CLIENT_SECRET: z.string().min(1, "GOOGLE_CLIENT_SECRET is required"),`
202
+ );
203
+
204
+ console.log();
205
+ console.log(" Modifying frontend files...");
206
+
207
+ for (const form of ["LoginForm.tsx", "SignupForm.tsx"]) {
208
+ const formPath = join(FRONTEND_ROOT, "src/components/auth", form);
209
+ insertAtMarker(formPath, "// [SAIL_IMPORTS]", 'import { GoogleButton } from "./GoogleButton";');
210
+ insertAtMarker(formPath, "{/* [SAIL_SOCIAL_BUTTONS] */}", " <GoogleButton />");
211
+ }
212
+
213
+ console.log();
214
+ console.log(" Updating environment files...");
215
+ appendToEnvExample({ GOOGLE_CLIENT_ID: googleClientId, GOOGLE_CLIENT_SECRET: googleClientSecret });
216
+
217
+ const dotEnvPath = join(PROJECT_ROOT, ".env");
218
+ if (existsSync(dotEnvPath)) {
219
+ let dotEnv = readFileSync(dotEnvPath, "utf-8");
220
+ if (!dotEnv.includes("GOOGLE_CLIENT_ID")) {
221
+ dotEnv += `\n# Google OAuth\nGOOGLE_CLIENT_ID=${googleClientId}\nGOOGLE_CLIENT_SECRET=${googleClientSecret}\n`;
222
+ writeFileSync(dotEnvPath, dotEnv, "utf-8");
223
+ console.log(" Updated .env");
224
+ }
225
+ }
226
+
227
+ console.log();
228
+ console.log("------------------------------------------------------------");
229
+ console.log(" Google OAuth installed successfully!");
230
+ console.log("------------------------------------------------------------");
231
+ console.log();
232
+ console.log(" Next steps:");
233
+ console.log();
234
+ console.log(" 1. Verify your Google Cloud Console settings:");
235
+ console.log(" https://console.cloud.google.com/apis/credentials");
236
+ console.log();
237
+ console.log(" 2. Ensure these redirect URIs are configured:");
238
+ console.log(" Development: http://localhost:3000/api/auth/callback/google");
239
+ console.log(" Production: https://yourdomain.com/api/auth/callback/google");
240
+ console.log();
241
+ console.log(" 3. Restart your dev server:");
242
+ console.log(" npm run dev");
243
+ console.log();
244
+ console.log(" Testing:");
245
+ console.log(' - Visit your login page and click "Sign in with Google"');
246
+ console.log(" - You should be redirected to Google's consent screen");
247
+ console.log(" - After authorizing, you should be redirected back and signed in");
248
+ console.log(" - Check the users table in your database for the new account");
249
+ console.log();
250
+ }
251
+
252
+ main().catch((err) => { console.error("Installation failed:", err); process.exit(1); });
@@ -0,0 +1,193 @@
1
+ # Internationalization (i18n) Sail
2
+
3
+ Adds multi-language support to your keel application using i18next, react-i18next, and automatic browser language detection.
4
+
5
+ ## Features
6
+
7
+ - i18next translation framework
8
+ - react-i18next for seamless React integration
9
+ - Automatic browser language detection (via `i18next-browser-languagedetector`)
10
+ - Language switcher dropdown component (dark Keel theme)
11
+ - Translation files for English, German, French, and Spanish
12
+ - Language preference persisted in localStorage
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npx tsx sails/i18n/install.ts
18
+ ```
19
+
20
+ The installer will prompt you to select which languages to include and will configure everything automatically.
21
+
22
+ ## Usage
23
+
24
+ ### Using Translations in Components
25
+
26
+ ```tsx
27
+ import { useTranslation } from "react-i18next";
28
+
29
+ function MyComponent() {
30
+ const { t } = useTranslation();
31
+
32
+ return (
33
+ <div>
34
+ <h1>{t("nav.home")}</h1>
35
+ <p>{t("common.loading")}</p>
36
+ <button>{t("common.save")}</button>
37
+ </div>
38
+ );
39
+ }
40
+ ```
41
+
42
+ ### Using the Language Hook
43
+
44
+ ```tsx
45
+ import { useLanguage } from "@/hooks/useLanguage";
46
+
47
+ function SettingsPage() {
48
+ const { currentLanguage, changeLanguage, availableLanguages } = useLanguage();
49
+
50
+ return (
51
+ <select
52
+ value={currentLanguage}
53
+ onChange={(e) => changeLanguage(e.target.value)}
54
+ >
55
+ {availableLanguages.map((lang) => (
56
+ <option key={lang.code} value={lang.code}>
57
+ {lang.label}
58
+ </option>
59
+ ))}
60
+ </select>
61
+ );
62
+ }
63
+ ```
64
+
65
+ ### Translation Keys with Interpolation
66
+
67
+ ```json
68
+ {
69
+ "greeting": "Hello, {{name}}!"
70
+ }
71
+ ```
72
+
73
+ ```tsx
74
+ t("greeting", { name: user.name })
75
+ ```
76
+
77
+ ### Pluralization
78
+
79
+ ```json
80
+ {
81
+ "items_one": "{{count}} item",
82
+ "items_other": "{{count}} items"
83
+ }
84
+ ```
85
+
86
+ ```tsx
87
+ t("items", { count: 5 }) // "5 items"
88
+ t("items", { count: 1 }) // "1 item"
89
+ ```
90
+
91
+ ## Adding a New Language
92
+
93
+ 1. Create the locale file:
94
+
95
+ ```bash
96
+ mkdir -p packages/frontend/src/locales/ja
97
+ ```
98
+
99
+ 2. Create `packages/frontend/src/locales/ja/common.json` with all translation keys (copy from `en/common.json` as a template).
100
+
101
+ 3. Update `packages/frontend/src/lib/i18n.ts`:
102
+
103
+ ```ts
104
+ import jaCommon from "@/locales/ja/common.json";
105
+
106
+ const resources = {
107
+ en: { common: enCommon },
108
+ de: { common: deCommon },
109
+ ja: { common: jaCommon }, // Add this
110
+ };
111
+ ```
112
+
113
+ 4. Update `packages/frontend/src/hooks/useLanguage.ts`:
114
+
115
+ ```ts
116
+ const availableLanguages: Language[] = [
117
+ { code: "en", label: "English" },
118
+ { code: "de", label: "Deutsch" },
119
+ { code: "ja", label: "Japanese" }, // Add this
120
+ ];
121
+ ```
122
+
123
+ ## Adding New Translation Namespaces
124
+
125
+ For larger applications, split translations into namespaces (e.g., per page):
126
+
127
+ 1. Create `packages/frontend/src/locales/en/dashboard.json`
128
+ 2. Import in `i18n.ts` and add to resources:
129
+
130
+ ```ts
131
+ import enDashboard from "@/locales/en/dashboard.json";
132
+
133
+ const resources = {
134
+ en: { common: enCommon, dashboard: enDashboard },
135
+ // ...
136
+ };
137
+ ```
138
+
139
+ 3. Update the `ns` array in `i18n.init()`:
140
+
141
+ ```ts
142
+ ns: ["common", "dashboard"],
143
+ ```
144
+
145
+ 4. Use in components:
146
+
147
+ ```tsx
148
+ const { t } = useTranslation("dashboard");
149
+ return <h1>{t("title")}</h1>;
150
+ ```
151
+
152
+ ## Language Detection
153
+
154
+ The language detector checks in this order:
155
+
156
+ 1. `localStorage` key `i18nextLng`
157
+ 2. Browser navigator language
158
+ 3. HTML `lang` attribute
159
+
160
+ The selected language is automatically persisted to `localStorage`.
161
+
162
+ ## Architecture
163
+
164
+ | File | Purpose |
165
+ |------|---------|
166
+ | `src/lib/i18n.ts` | i18next initialization and configuration |
167
+ | `src/locales/<lang>/common.json` | Translation strings per language |
168
+ | `src/hooks/useLanguage.ts` | Hook for language switching |
169
+ | `src/components/LanguageSwitcher.tsx` | Dropdown UI component |
170
+
171
+ ## Configuration
172
+
173
+ ### Changing the Default Language
174
+
175
+ Edit `src/lib/i18n.ts`:
176
+
177
+ ```ts
178
+ fallbackLng: "de", // Change from "en" to desired default
179
+ ```
180
+
181
+ ### Disabling Language Detection
182
+
183
+ Remove the `LanguageDetector` plugin from `i18n.ts`:
184
+
185
+ ```ts
186
+ i18n
187
+ // .use(LanguageDetector) // Remove this line
188
+ .use(initReactI18next)
189
+ .init({
190
+ lng: "en", // Set a fixed language
191
+ // ...
192
+ });
193
+ ```
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "i18n",
3
+ "displayName": "Internationalization (i18n)",
4
+ "description": "Multi-language support with i18next, react-i18next, and automatic language detection",
5
+ "version": "1.0.0",
6
+ "compatibility": ">=1.0.0",
7
+ "requiredEnvVars": [],
8
+ "dependencies": {
9
+ "backend": {},
10
+ "frontend": {
11
+ "i18next": "^24.0.0",
12
+ "react-i18next": "^15.0.0",
13
+ "i18next-browser-languagedetector": "^8.0.0"
14
+ }
15
+ },
16
+ "modifies": {
17
+ "backend": [],
18
+ "frontend": ["src/main.tsx", "src/components/layout/Header.tsx"]
19
+ },
20
+ "adds": {
21
+ "backend": [],
22
+ "frontend": [
23
+ "src/lib/i18n.ts",
24
+ "src/locales/en/common.json",
25
+ "src/locales/de/common.json",
26
+ "src/hooks/useLanguage.ts",
27
+ "src/components/LanguageSwitcher.tsx"
28
+ ]
29
+ }
30
+ }