@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.
- package/dist/__tests__/cli.test.d.ts +2 -0
- package/dist/__tests__/cli.test.d.ts.map +1 -0
- package/dist/__tests__/cli.test.js +173 -0
- package/dist/__tests__/cli.test.js.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/registry.test.js +86 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/sail-installer.test.d.ts +2 -0
- package/dist/__tests__/sail-installer.test.d.ts.map +1 -0
- package/dist/__tests__/sail-installer.test.js +158 -0
- package/dist/__tests__/sail-installer.test.js.map +1 -0
- package/dist/create-runner.d.ts +11 -0
- package/dist/create-runner.d.ts.map +1 -0
- package/dist/create-runner.js +63 -0
- package/dist/create-runner.js.map +1 -0
- package/dist/create.d.ts +10 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +15 -0
- package/dist/create.js.map +1 -0
- package/dist/manage.d.ts +24 -0
- package/dist/manage.d.ts.map +1 -0
- package/dist/manage.js +1461 -0
- package/dist/manage.js.map +1 -0
- package/dist/prompts.d.ts +36 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +208 -0
- package/dist/prompts.js.map +1 -0
- package/dist/sail-installer.d.ts +37 -0
- package/dist/sail-installer.d.ts.map +1 -0
- package/dist/sail-installer.js +935 -0
- package/dist/sail-installer.js.map +1 -0
- package/dist/scaffold.d.ts +10 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +297 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +57 -0
- package/sails/_template/addon.json +20 -0
- package/sails/_template/install.ts +402 -0
- package/sails/admin-dashboard/README.md +117 -0
- package/sails/admin-dashboard/addon.json +28 -0
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -0
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -0
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -0
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -0
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -0
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -0
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -0
- package/sails/admin-dashboard/install.ts +305 -0
- package/sails/analytics/README.md +178 -0
- package/sails/analytics/addon.json +27 -0
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -0
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -0
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -0
- package/sails/analytics/install.ts +297 -0
- package/sails/file-uploads/README.md +191 -0
- package/sails/file-uploads/addon.json +30 -0
- package/sails/file-uploads/files/backend/routes/files.ts +198 -0
- package/sails/file-uploads/files/backend/schema/files.ts +36 -0
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -0
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -0
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -0
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -0
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -0
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -0
- package/sails/file-uploads/install.ts +466 -0
- package/sails/gdpr/README.md +174 -0
- package/sails/gdpr/addon.json +27 -0
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -0
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -0
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -0
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -0
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -0
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -0
- package/sails/gdpr/install.ts +756 -0
- package/sails/google-oauth/README.md +121 -0
- package/sails/google-oauth/addon.json +22 -0
- package/sails/google-oauth/files/GoogleButton.tsx +50 -0
- package/sails/google-oauth/install.ts +252 -0
- package/sails/i18n/README.md +193 -0
- package/sails/i18n/addon.json +30 -0
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -0
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -0
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -0
- package/sails/i18n/files/frontend/locales/de/common.json +44 -0
- package/sails/i18n/files/frontend/locales/en/common.json +44 -0
- package/sails/i18n/install.ts +407 -0
- package/sails/push-notifications/README.md +163 -0
- package/sails/push-notifications/addon.json +31 -0
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -0
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -0
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -0
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -0
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -0
- package/sails/push-notifications/install.ts +384 -0
- package/sails/r2-storage/README.md +101 -0
- package/sails/r2-storage/addon.json +29 -0
- package/sails/r2-storage/files/backend/services/storage.ts +71 -0
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -0
- package/sails/r2-storage/install.ts +412 -0
- package/sails/rate-limiting/README.md +145 -0
- package/sails/rate-limiting/addon.json +20 -0
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -0
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -0
- package/sails/rate-limiting/install.ts +300 -0
- package/sails/registry.json +107 -0
- package/sails/stripe/README.md +214 -0
- package/sails/stripe/addon.json +24 -0
- package/sails/stripe/files/backend/routes/stripe.ts +154 -0
- package/sails/stripe/files/backend/schema/stripe.ts +74 -0
- package/sails/stripe/files/backend/services/stripe.ts +224 -0
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -0
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -0
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -0
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -0
- 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
|
+
});
|