@kokimoki/app 2.1.0 → 3.0.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 (38) hide show
  1. package/README.md +72 -0
  2. package/dist/core/kokimoki-client.d.ts +75 -15
  3. package/dist/core/kokimoki-client.js +137 -22
  4. package/dist/index.d.ts +6 -1
  5. package/dist/index.js +4 -0
  6. package/dist/kokimoki.min.d.ts +322 -2
  7. package/dist/kokimoki.min.js +1884 -72
  8. package/dist/kokimoki.min.js.map +1 -1
  9. package/dist/llms.txt +6 -0
  10. package/dist/protocol/ws-message/reader.d.ts +1 -1
  11. package/dist/services/index.d.ts +1 -0
  12. package/dist/services/index.js +1 -0
  13. package/dist/services/kokimoki-ai.d.ts +185 -122
  14. package/dist/services/kokimoki-ai.js +201 -109
  15. package/dist/services/kokimoki-i18n.d.ts +259 -0
  16. package/dist/services/kokimoki-i18n.js +325 -0
  17. package/dist/stores/kokimoki-local-store.d.ts +1 -1
  18. package/dist/types/common.d.ts +9 -0
  19. package/dist/types/env.d.ts +36 -0
  20. package/dist/types/env.js +1 -0
  21. package/dist/types/index.d.ts +1 -0
  22. package/dist/types/index.js +1 -0
  23. package/dist/utils/kokimoki-client.d.ts +31 -0
  24. package/dist/utils/kokimoki-client.js +38 -0
  25. package/dist/utils/kokimoki-dev.d.ts +30 -0
  26. package/dist/utils/kokimoki-dev.js +75 -0
  27. package/dist/utils/kokimoki-env.d.ts +20 -0
  28. package/dist/utils/kokimoki-env.js +30 -0
  29. package/dist/version.d.ts +1 -1
  30. package/dist/version.js +1 -1
  31. package/docs/kokimoki-ai.instructions.md +316 -0
  32. package/docs/kokimoki-dynamic-stores.instructions.md +439 -0
  33. package/docs/kokimoki-i18n.instructions.md +285 -0
  34. package/docs/kokimoki-leaderboard.instructions.md +189 -0
  35. package/docs/kokimoki-sdk.instructions.md +221 -0
  36. package/docs/kokimoki-storage.instructions.md +162 -0
  37. package/llms.txt +43 -0
  38. package/package.json +9 -13
@@ -0,0 +1,439 @@
1
+ ---
2
+ description: "Dynamic stores for room-based state management with Kokimoki SDK"
3
+ applyTo: "**/*.ts,**/*.tsx"
4
+ ---
5
+
6
+ # Dynamic Stores
7
+
8
+ Dynamic stores enable creation of isolated, room-based state management. This is useful for scenarios where subsets of players need to share state without affecting the global store.
9
+
10
+ For core SDK concepts, see [Kokimoki SDK](./kokimoki-sdk.instructions.md).
11
+
12
+ ## When to Use Dynamic Stores
13
+
14
+ **Use for:**
15
+
16
+ - **Room-based isolation**: Chat rooms, team spaces, breakout rooms
17
+ - **Player grouping**: Teams, squads, parties that share private state
18
+ - **Multi-instance features**: Multiple parallel games or activities
19
+ - **Scoped collaboration**: Players working together on specific tasks
20
+
21
+ **Do NOT use for:**
22
+
23
+ - Global game state (use `kmClient.store()` instead)
24
+ - Player-specific data (use `kmClient.localStore()` instead)
25
+ - Single shared space for all players
26
+
27
+ ## General Best Practices
28
+
29
+ - **Unique room names**: Use descriptive prefixes (e.g., `"chat-room-"`, `"team-"`)
30
+ - **Key remounting**: Use `key={roomCode}` on components to force remount when switching rooms
31
+ - **Check isConnected**: Wait for connection before showing/updating content
32
+ - **Defensive state checks**: Always verify properties exist before accessing (e.g., `if (!state.players)` before using)
33
+ - **UseEffect guards**: Check for undefined/null in useEffect conditions that depend on dynamic store state
34
+ - **Cleanup on leave**: Remove player-specific data when leaving a room
35
+ - **Use actions**: Never inline `kmClient.transact` calls in components
36
+ - **Use Records, not Arrays**: Store collections as `Record<string, T>` with timestamp keys for automatic sorting and better sync performance
37
+ - **Atomic transactions**: Combine related state updates in single transaction when possible
38
+
39
+ ## Creating a useDynamicStore Hook
40
+
41
+ Since dynamic stores require lifecycle management (join/leave), create a custom hook that wraps the store lifecycle:
42
+
43
+ **Hook implementation** (`src/hooks/useDynamicStore.ts`):
44
+
45
+ ```typescript
46
+ import { useState, useEffect, useRef } from "react";
47
+ import { getKmClient, KokimokiStore } from "@kokimoki/app";
48
+
49
+ const kmClient = getKmClient();
50
+
51
+ // Global store cache for reference counting
52
+ const storeCache = new Map<
53
+ string,
54
+ { store: KokimokiStore<any>; refCount: number }
55
+ >();
56
+
57
+ export function useDynamicStore<T extends object>(
58
+ roomName: string,
59
+ initialState: T
60
+ ) {
61
+ const [isConnecting, setIsConnecting] = useState(true);
62
+ const [isConnected, setIsConnected] = useState(false);
63
+ const storeRef = useRef<KokimokiStore<T> | null>(null);
64
+
65
+ // Get or create the store
66
+ if (!storeRef.current) {
67
+ const cached = storeCache.get(roomName);
68
+ if (cached) {
69
+ storeRef.current = cached.store;
70
+ cached.refCount++;
71
+ } else {
72
+ const store = kmClient.store<T>(roomName, initialState);
73
+ storeRef.current = store;
74
+ storeCache.set(roomName, { store, refCount: 1 });
75
+ }
76
+ }
77
+
78
+ useEffect(() => {
79
+ const store = storeRef.current!;
80
+ let mounted = true;
81
+
82
+ const joinStore = async () => {
83
+ try {
84
+ await kmClient.join(store);
85
+ if (mounted) {
86
+ setIsConnected(true);
87
+ setIsConnecting(false);
88
+ }
89
+ } catch (error) {
90
+ console.error(`Failed to join store "${roomName}":`, error);
91
+ if (mounted) {
92
+ setIsConnecting(false);
93
+ }
94
+ }
95
+ };
96
+
97
+ joinStore();
98
+
99
+ return () => {
100
+ mounted = false;
101
+ const cached = storeCache.get(roomName);
102
+ if (cached) {
103
+ cached.refCount--;
104
+ if (cached.refCount <= 0) {
105
+ kmClient.leave(store);
106
+ storeCache.delete(roomName);
107
+ }
108
+ }
109
+ };
110
+ }, [roomName]);
111
+
112
+ return {
113
+ store: storeRef.current!,
114
+ isConnected,
115
+ isConnecting,
116
+ };
117
+ }
118
+ ```
119
+
120
+ ## Hook API
121
+
122
+ **Parameters:**
123
+
124
+ - **roomName**: `string` - Unique identifier for the store (e.g., `"chat-room-abc123"`)
125
+ - **initialState**: `T` - Default state structure with initial values
126
+
127
+ **Return Values:**
128
+
129
+ - **store**: `KokimokiStore<T>` - The Kokimoki store instance
130
+ - **isConnected**: `boolean` - Whether successfully joined the store
131
+ - **isConnecting**: `boolean` - Whether currently joining the store
132
+
133
+ ## Basic Usage
134
+
135
+ ```tsx
136
+ import { useSnapshot } from "valtio";
137
+ import { useDynamicStore } from "@/hooks/useDynamicStore";
138
+
139
+ interface RoomState {
140
+ messages: Record<string, string>; // key: timestamp, value: message
141
+ count: number;
142
+ }
143
+
144
+ const initialState: RoomState = {
145
+ messages: {},
146
+ count: 0,
147
+ };
148
+
149
+ const MyComponent = ({ roomCode }: { roomCode: string }) => {
150
+ const { store, isConnected } = useDynamicStore<RoomState>(
151
+ `my-room-${roomCode}`,
152
+ initialState
153
+ );
154
+
155
+ const roomState = useSnapshot(store.proxy);
156
+
157
+ // Update state
158
+ const addMessage = async (message: string) => {
159
+ await kmClient.transact([store], ([state]) => {
160
+ const timestamp = kmClient.serverTimestamp().toString();
161
+ state.messages[timestamp] = message;
162
+ });
163
+ };
164
+
165
+ if (!isConnected) {
166
+ return <div>Connecting...</div>;
167
+ }
168
+
169
+ return <div>Messages: {Object.keys(roomState.messages).length}</div>;
170
+ };
171
+
172
+ // Use key to remount component on roomCode change
173
+ <MyComponent key={roomCode} roomCode={roomCode} />;
174
+ ```
175
+
176
+ ## Common Patterns
177
+
178
+ ### Example: Team-based State
179
+
180
+ Players can be grouped into teams with private team state:
181
+
182
+ ```tsx
183
+ interface TeamState {
184
+ teamName: string;
185
+ players: Record<string, { name: string; role: string }>;
186
+ score: number;
187
+ strategy: string;
188
+ }
189
+
190
+ const TeamView = ({ teamId }: { teamId: string }) => {
191
+ const { name } = useSnapshot(playerStore.proxy);
192
+ const { store: teamStore, isConnected } = useDynamicStore<TeamState>(
193
+ `team-${teamId}`,
194
+ {
195
+ teamName: `Team ${teamId}`,
196
+ players: {},
197
+ score: 0,
198
+ strategy: "",
199
+ }
200
+ );
201
+
202
+ const teamState = useSnapshot(teamStore.proxy);
203
+
204
+ // Join team on connect
205
+ useEffect(() => {
206
+ if (isConnected) {
207
+ kmClient.transact([teamStore], ([state]) => {
208
+ state.players[kmClient.id] = { name, role: "member" };
209
+ });
210
+ }
211
+ }, [isConnected]);
212
+
213
+ // Update team strategy
214
+ const updateStrategy = async (strategy: string) => {
215
+ await kmClient.transact([teamStore], ([state]) => {
216
+ state.strategy = strategy;
217
+ });
218
+ };
219
+
220
+ return (
221
+ <div>
222
+ <h2>{teamState.teamName}</h2>
223
+ <p>Score: {teamState.score}</p>
224
+ <p>Players: {Object.keys(teamState.players || {}).length}</p>
225
+ </div>
226
+ );
227
+ };
228
+ ```
229
+
230
+ ### Example: Chat Rooms
231
+
232
+ Multiple independent chat rooms:
233
+
234
+ ```tsx
235
+ interface ChatMessage {
236
+ clientId: string;
237
+ playerName: string;
238
+ text: string;
239
+ timestamp: number;
240
+ }
241
+
242
+ interface ChatState {
243
+ messages: Record<string, ChatMessage>; // key: timestamp as string
244
+ }
245
+
246
+ const ChatRoom = ({ roomCode }: { roomCode: string }) => {
247
+ const { store, isConnected } = useDynamicStore<ChatState>(
248
+ `chat-${roomCode}`,
249
+ { messages: {} }
250
+ );
251
+
252
+ const { messages } = useSnapshot(store.proxy);
253
+
254
+ const sendMessage = async (playerName: string, text: string) => {
255
+ await kmClient.transact([store], ([state]) => {
256
+ const timestamp = kmClient.serverTimestamp();
257
+ state.messages[timestamp.toString()] = {
258
+ clientId: kmClient.id,
259
+ playerName,
260
+ text,
261
+ timestamp,
262
+ };
263
+ });
264
+ };
265
+
266
+ // Get sorted messages
267
+ const sortedMessages = Object.entries(messages || {})
268
+ .sort(([a], [b]) => Number(a) - Number(b))
269
+ .map(([, msg]) => msg);
270
+
271
+ return <div>{sortedMessages.length} messages</div>;
272
+ };
273
+ ```
274
+
275
+ ### Example: Breakout Rooms
276
+
277
+ Small group discussions or activities:
278
+
279
+ ```tsx
280
+ interface BreakoutRoomState {
281
+ topic: string;
282
+ participants: Record<string, { joinedAt: number }>; // key: clientId
283
+ responses: Record<string, string>; // key: clientId
284
+ completed: boolean;
285
+ }
286
+
287
+ const BreakoutRoom = ({ roomId }: { roomId: string }) => {
288
+ const { store, isConnected } = useDynamicStore<BreakoutRoomState>(
289
+ `breakout-${roomId}`,
290
+ {
291
+ topic: "",
292
+ participants: {},
293
+ responses: {},
294
+ completed: false,
295
+ }
296
+ );
297
+
298
+ const roomState = useSnapshot(store.proxy);
299
+
300
+ // Join room on connect
301
+ useEffect(() => {
302
+ if (isConnected) {
303
+ kmClient.transact([store], ([state]) => {
304
+ if (!state.participants[kmClient.id]) {
305
+ state.participants[kmClient.id] = {
306
+ joinedAt: kmClient.serverTimestamp(),
307
+ };
308
+ }
309
+ });
310
+ }
311
+ }, [isConnected]);
312
+
313
+ const submitResponse = async (response: string) => {
314
+ await kmClient.transact([store], ([state]) => {
315
+ state.responses[kmClient.id] = response;
316
+ });
317
+ };
318
+
319
+ return (
320
+ <div>
321
+ <h3>{roomState.topic}</h3>
322
+ <p>Participants: {Object.keys(roomState.participants || {}).length}</p>
323
+ </div>
324
+ );
325
+ };
326
+ ```
327
+
328
+ ## Key Behaviors
329
+
330
+ **Store Lifecycle:**
331
+
332
+ - Stores are **created on first use** and **cached** globally
333
+ - Multiple components using the same `roomName` share the same store instance
334
+ - Reference counting ensures cleanup only when all components unmount
335
+
336
+ **State Synchronization:**
337
+
338
+ - State syncs in real-time across all clients joined to the same room
339
+ - Use transactions to update state atomically
340
+ - **CRITICAL**: Always check if state properties exist before accessing them (e.g., `if (state.players)` before `Object.keys(state.players)`)
341
+ - Initial state may be `undefined` during connection/sync - add defensive checks
342
+
343
+ **Connection Management:**
344
+
345
+ - Component automatically joins the store on mount
346
+ - Component automatically leaves the store on unmount
347
+ - `isConnected` indicates successful join
348
+ - `isConnecting` indicates join in progress
349
+ - **Store state may not be fully initialized immediately after `isConnected` becomes true**
350
+
351
+ ## File Organization
352
+
353
+ **ALWAYS** organize dynamic store code using the standard store/actions pattern:
354
+
355
+ **State definition** (`src/state/chat-store.ts`):
356
+
357
+ ```typescript
358
+ // Export message interface
359
+ export interface ChatMessage {
360
+ clientId: string;
361
+ playerName: string;
362
+ message: string;
363
+ timestamp: number;
364
+ }
365
+
366
+ // Export state interface
367
+ export interface ChatState {
368
+ messages: Record<string, ChatMessage>; // key: timestamp as string
369
+ }
370
+
371
+ // Export function to generate initial state
372
+ export function createChatState(): ChatState {
373
+ return {
374
+ messages: {},
375
+ };
376
+ }
377
+ ```
378
+
379
+ **Actions** (`src/state/actions/chat-actions.ts`):
380
+
381
+ ```typescript
382
+ import type { KokimokiStore } from "@kokimoki/app";
383
+ import { getKmClient } from "@kokimoki/app";
384
+ import type { ChatState, ChatMessage } from "../chat-store";
385
+
386
+ const kmClient = getKmClient();
387
+
388
+ export const chatActions = {
389
+ async sendMessage(
390
+ store: KokimokiStore<ChatState>,
391
+ playerName: string,
392
+ message: string
393
+ ) {
394
+ await kmClient.transact([store], ([state]) => {
395
+ const timestamp = kmClient.serverTimestamp();
396
+ const newMessage: ChatMessage = {
397
+ clientId: kmClient.id,
398
+ playerName,
399
+ message,
400
+ timestamp,
401
+ };
402
+ state.messages[timestamp.toString()] = newMessage;
403
+ });
404
+ },
405
+ };
406
+ ```
407
+
408
+ **Usage in component** (`src/views/chat-view.tsx`):
409
+
410
+ ```tsx
411
+ import { useSnapshot } from "valtio";
412
+ import { useDynamicStore } from "@/hooks/useDynamicStore";
413
+ import { createChatState, type ChatState } from "@/state/chat-store";
414
+ import { chatActions } from "@/state/actions/chat-actions";
415
+
416
+ const ChatRoom = ({ roomCode }: { roomCode: string }) => {
417
+ const { store, isConnected } = useDynamicStore<ChatState>(
418
+ `chat-${roomCode}`,
419
+ createChatState()
420
+ );
421
+
422
+ const { messages } = useSnapshot(store.proxy);
423
+
424
+ // Get sorted messages
425
+ const sortedMessages = Object.entries(messages || {})
426
+ .sort(([a], [b]) => Number(a) - Number(b))
427
+ .map(([, msg]) => msg);
428
+
429
+ const handleSend = async (message: string) => {
430
+ await chatActions.sendMessage(store, playerName, message);
431
+ };
432
+
433
+ if (!isConnected) {
434
+ return <div>Connecting...</div>;
435
+ }
436
+
437
+ return <div>{sortedMessages.length} messages</div>;
438
+ };
439
+ ```
@@ -0,0 +1,285 @@
1
+ ---
2
+ description: "Internationalization with AI-powered translations in Kokimoki SDK"
3
+ applyTo: "**/*.ts,**/*.tsx"
4
+ ---
5
+
6
+ # Internationalization (i18n)
7
+
8
+ Kokimoki SDK provides built-in internationalization support powered by i18next with AI-powered translation capabilities. No manual translation files needed for additional languages.
9
+
10
+ For core SDK concepts, see [Kokimoki SDK](./kokimoki-sdk.instructions.md).
11
+
12
+ Access i18n API via `kmClient.i18n`.
13
+
14
+ ## Key Features
15
+
16
+ - Pre-configured i18next instance creation
17
+ - Consistent HTTP-based loading in dev and production
18
+ - AI-powered translation requests with polling support
19
+ - Automatic namespace management
20
+
21
+ ## Setup
22
+
23
+ ```typescript
24
+ // src/i18n.ts
25
+ import { initReactI18next } from "react-i18next";
26
+ import { getKmClient } from "@kokimoki/app";
27
+
28
+ const kmClient = getKmClient();
29
+
30
+ // Create i18next instance with React integration
31
+ export const i18n = kmClient.i18n.createI18n({
32
+ use: [initReactI18next],
33
+ fallbackLng: "en",
34
+ defaultNS: "game",
35
+ });
36
+
37
+ // Initialize with primary language
38
+ await kmClient.i18n.init("en");
39
+ ```
40
+
41
+ ## API Methods
42
+
43
+ ### i18n.createI18n(options?): i18n
44
+
45
+ Create and configure an i18next instance. Call `init()` to initialize with a specific language.
46
+
47
+ **Parameters:**
48
+
49
+ - **options.use**: `Plugin[]` Array of i18next plugins (e.g., `initReactI18next`)
50
+ - **options.fallbackLng**: `string` Fallback language code (default: same as `lng` passed to init)
51
+ - **options.defaultNS**: `string` Default namespace (default: first namespace in config)
52
+
53
+ **Example:**
54
+
55
+ ```typescript
56
+ import { initReactI18next } from "react-i18next";
57
+
58
+ export const i18n = kmClient.i18n.createI18n({
59
+ use: [initReactI18next],
60
+ fallbackLng: "en",
61
+ });
62
+ ```
63
+
64
+ ### i18n.init(lng): Promise<void>
65
+
66
+ Initialize the i18next instance with a specific language. Must call `createI18n()` first.
67
+
68
+ **Parameters:**
69
+
70
+ - **lng**: `string` Language code to initialize with (e.g., 'en', 'de')
71
+
72
+ **Example:**
73
+
74
+ ```typescript
75
+ await kmClient.i18n.init("en");
76
+ ```
77
+
78
+ ### i18n.getNamespaces(): string[]
79
+
80
+ Get the list of available namespaces configured in @kokimoki/kit.
81
+
82
+ ```typescript
83
+ const namespaces = kmClient.i18n.getNamespaces();
84
+ // ['game', 'ui', 'common']
85
+ ```
86
+
87
+ ### i18n.getLanguages(): string[]
88
+
89
+ Get the list of available languages configured in @kokimoki/kit.
90
+
91
+ ```typescript
92
+ const languages = kmClient.i18n.getLanguages();
93
+ // ['en', 'et']
94
+ ```
95
+
96
+ ### i18n.getNamespaceUrl(lng, ns): string
97
+
98
+ Get the URL for a translation namespace file.
99
+
100
+ **Parameters:**
101
+
102
+ - **lng**: `string` Language code
103
+ - **ns**: `string` Namespace name
104
+
105
+ ```typescript
106
+ const url = kmClient.i18n.getNamespaceUrl("en", "game");
107
+ // Dev: "/__kokimoki/i18n/en/game.json"
108
+ // Prod: "https://cdn.kokimoki.com/build-123/km-i18n/en/game.json"
109
+ ```
110
+
111
+ ## AI Translation API
112
+
113
+ ### i18n.getAllLanguagesStatus(): Promise<AllLanguagesStatus>
114
+
115
+ Get the status of all languages that have been requested for AI translation.
116
+
117
+ ```typescript
118
+ interface AllLanguagesStatus {
119
+ languages: { lng: string; status: LanguageStatus }[];
120
+ }
121
+
122
+ type LanguageStatus = "available" | "processing" | "failed" | "partial";
123
+ ```
124
+
125
+ **Example:**
126
+
127
+ ```typescript
128
+ const { languages } = await kmClient.i18n.getAllLanguagesStatus();
129
+ // [{ lng: 'de', status: 'available' }, { lng: 'fr', status: 'processing' }]
130
+ ```
131
+
132
+ ### i18n.getTranslationStatus(lng): Promise<TranslationStatus>
133
+
134
+ Get the translation status for a specific language.
135
+
136
+ ```typescript
137
+ interface TranslationStatus {
138
+ status: "available" | "processing" | "not_available";
139
+ namespaces: Record<string, NamespaceStatus>;
140
+ }
141
+
142
+ type NamespaceStatus = "available" | "processing" | "failed" | "not_available";
143
+ ```
144
+
145
+ **Example:**
146
+
147
+ ```typescript
148
+ const status = await kmClient.i18n.getTranslationStatus("de");
149
+
150
+ if (status.status === "available") {
151
+ i18next.changeLanguage("de");
152
+ } else if (status.status === "processing") {
153
+ // Show loading indicator
154
+ } else {
155
+ // Request translation
156
+ await kmClient.i18n.requestTranslation("de");
157
+ }
158
+ ```
159
+
160
+ ### i18n.requestTranslation(lng): Promise<RequestTranslationResult>
161
+
162
+ Request AI translation for a target language. Triggers background AI translation jobs for all namespaces.
163
+
164
+ ```typescript
165
+ interface RequestTranslationResult {
166
+ lng: string;
167
+ status: "started" | "already_processing" | "already_available";
168
+ }
169
+ ```
170
+
171
+ **Example:**
172
+
173
+ ```typescript
174
+ const result = await kmClient.i18n.requestTranslation("de");
175
+
176
+ if (result.status === "already_available") {
177
+ // Already translated, switch immediately
178
+ i18next.changeLanguage("de");
179
+ } else {
180
+ // Poll until ready
181
+ await kmClient.i18n.pollTranslation("de");
182
+ i18next.changeLanguage("de");
183
+ }
184
+ ```
185
+
186
+ ### i18n.pollTranslation(lng, options?): Promise<void>
187
+
188
+ Poll a translation request until all namespaces are available.
189
+
190
+ **Parameters:**
191
+
192
+ - **lng**: `string` Language code to poll for
193
+ - **options.pollInterval**: `number` Polling interval in ms (default: 2000, min: 1000)
194
+ - **options.timeout**: `number` Timeout in ms (default: 120000)
195
+ - **options.onProgress**: `(status: TranslationStatus) => void` Progress callback
196
+
197
+ **Example:**
198
+
199
+ ```typescript
200
+ await kmClient.i18n.pollTranslation("de", {
201
+ timeout: 60000,
202
+ onProgress: (status) => {
203
+ console.log("Overall:", status.status);
204
+ console.log("Namespaces:", status.namespaces);
205
+ },
206
+ });
207
+
208
+ // Now safe to switch
209
+ i18next.changeLanguage("de");
210
+ ```
211
+
212
+ ## Common Patterns
213
+
214
+ ### Example: Language Switcher with AI Translation
215
+
216
+ ```tsx
217
+ import { useState } from "react";
218
+ import { useTranslation } from "react-i18next";
219
+ import { getKmClient } from "@kokimoki/app";
220
+
221
+ const kmClient = getKmClient();
222
+
223
+ const LanguageSwitcher = () => {
224
+ const { i18n } = useTranslation();
225
+ const [loading, setLoading] = useState(false);
226
+
227
+ const switchLanguage = async (lng: string) => {
228
+ // Check if translation is available
229
+ const status = await kmClient.i18n.getTranslationStatus(lng);
230
+
231
+ if (status.status === "available") {
232
+ i18n.changeLanguage(lng);
233
+ return;
234
+ }
235
+
236
+ // Request and wait for AI translation
237
+ setLoading(true);
238
+ await kmClient.i18n.requestTranslation(lng);
239
+ await kmClient.i18n.pollTranslation(lng, {
240
+ onProgress: (s) => console.log(`Translating: ${s.status}`),
241
+ });
242
+ setLoading(false);
243
+
244
+ i18n.changeLanguage(lng);
245
+ };
246
+
247
+ return (
248
+ <select
249
+ value={i18n.language}
250
+ onChange={(e) => switchLanguage(e.target.value)}
251
+ disabled={loading}
252
+ >
253
+ <option value="en">English</option>
254
+ <option value="de">Deutsch</option>
255
+ <option value="fr">Français</option>
256
+ </select>
257
+ );
258
+ };
259
+ ```
260
+
261
+ ### Example: Using Translations in Components
262
+
263
+ ```tsx
264
+ import { useTranslation } from "react-i18next";
265
+
266
+ const GameUI = () => {
267
+ const { t } = useTranslation("game");
268
+
269
+ return (
270
+ <div>
271
+ <h1>{t("title")}</h1>
272
+ <button>{t("startButton")}</button>
273
+ <p>{t("score", { points: 100 })}</p>
274
+ </div>
275
+ );
276
+ };
277
+ ```
278
+
279
+ ## Key Points
280
+
281
+ - **Setup**: Call `createI18n()` first, then `init()` with the primary language
282
+ - **React Integration**: Pass `initReactI18next` to `use` option for React hooks support
283
+ - **AI Translation**: Request translations for any language, AI translates from your primary language
284
+ - **Polling**: Use `pollTranslation()` to wait for AI translation to complete
285
+ - **Namespaces**: Translations are organized by namespaces configured in @kokimoki/kit