@codaijs/keel 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/sail-installer.test.js +25 -25
- package/dist/sail-installer.js +174 -174
- package/dist/scaffold.js +68 -68
- package/package.json +58 -58
- package/sails/_template/addon.json +20 -20
- package/sails/_template/install.ts +402 -402
- package/sails/admin-dashboard/README.md +117 -117
- package/sails/admin-dashboard/addon.json +28 -28
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
- package/sails/admin-dashboard/install.ts +305 -305
- package/sails/analytics/README.md +178 -178
- package/sails/analytics/addon.json +27 -27
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
- package/sails/analytics/install.ts +297 -297
- package/sails/file-uploads/addon.json +30 -30
- package/sails/file-uploads/files/backend/routes/files.ts +198 -198
- package/sails/file-uploads/files/backend/schema/files.ts +36 -36
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
- package/sails/file-uploads/install.ts +466 -466
- package/sails/gdpr/README.md +174 -174
- package/sails/gdpr/addon.json +27 -27
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
- package/sails/gdpr/install.ts +756 -756
- package/sails/google-oauth/README.md +121 -121
- package/sails/google-oauth/addon.json +22 -22
- package/sails/google-oauth/files/GoogleButton.tsx +50 -50
- package/sails/google-oauth/install.ts +252 -252
- package/sails/i18n/README.md +193 -193
- package/sails/i18n/addon.json +30 -30
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
- package/sails/i18n/files/frontend/locales/de/common.json +44 -44
- package/sails/i18n/files/frontend/locales/en/common.json +44 -44
- package/sails/i18n/install.ts +407 -407
- package/sails/push-notifications/README.md +163 -163
- package/sails/push-notifications/addon.json +31 -31
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
- package/sails/push-notifications/install.ts +384 -384
- package/sails/r2-storage/addon.json +29 -29
- package/sails/r2-storage/files/backend/services/storage.ts +71 -71
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
- package/sails/r2-storage/install.ts +412 -412
- package/sails/rate-limiting/addon.json +20 -20
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
- package/sails/rate-limiting/install.ts +300 -300
- package/sails/registry.json +107 -107
- package/sails/stripe/README.md +214 -214
- package/sails/stripe/addon.json +24 -24
- package/sails/stripe/files/backend/routes/stripe.ts +154 -154
- package/sails/stripe/files/backend/schema/stripe.ts +74 -74
- package/sails/stripe/files/backend/services/stripe.ts +224 -224
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
- package/sails/stripe/install.ts +378 -378
package/sails/i18n/README.md
CHANGED
|
@@ -1,193 +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
|
-
```
|
|
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
|
+
```
|
package/sails/i18n/addon.json
CHANGED
|
@@ -1,30 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,108 +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
|
-
}
|
|
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
|
+
}
|