@rimori/client 1.4.5 → 1.4.8

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 (64) hide show
  1. package/README.md +116 -0
  2. package/dist/cli/scripts/release/detect-translation-languages.d.ts +5 -0
  3. package/dist/cli/scripts/release/detect-translation-languages.js +43 -0
  4. package/dist/cli/scripts/release/release-config-upload.js +4 -0
  5. package/dist/cli/scripts/release/release-translation-upload.d.ts +6 -0
  6. package/dist/cli/scripts/release/release-translation-upload.js +87 -0
  7. package/dist/cli/scripts/release/release.d.ts +1 -1
  8. package/dist/cli/scripts/release/release.js +14 -5
  9. package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.js +2 -2
  10. package/dist/core/controller/EnhancedUserInfo.d.ts +0 -0
  11. package/dist/core/controller/EnhancedUserInfo.js +1 -0
  12. package/dist/core/controller/SettingsController.d.ts +7 -1
  13. package/dist/core/core.d.ts +1 -2
  14. package/dist/core/core.js +0 -1
  15. package/dist/fromRimori/EventBus.js +23 -23
  16. package/dist/fromRimori/PluginTypes.d.ts +4 -4
  17. package/dist/hooks/I18nHooks.d.ts +11 -0
  18. package/dist/hooks/I18nHooks.js +25 -0
  19. package/dist/i18n/I18nHooks.d.ts +11 -0
  20. package/dist/i18n/I18nHooks.js +25 -0
  21. package/dist/i18n/Translator.d.ts +43 -0
  22. package/dist/i18n/Translator.js +118 -0
  23. package/dist/i18n/config.d.ts +7 -0
  24. package/dist/i18n/config.js +20 -0
  25. package/dist/i18n/createI18nInstance.d.ts +7 -0
  26. package/dist/i18n/createI18nInstance.js +31 -0
  27. package/dist/i18n/hooks.d.ts +11 -0
  28. package/dist/i18n/hooks.js +25 -0
  29. package/dist/i18n/index.d.ts +4 -0
  30. package/dist/i18n/index.js +4 -0
  31. package/dist/i18n/types.d.ts +7 -0
  32. package/dist/i18n/types.js +1 -0
  33. package/dist/i18n/useRimoriI18n.d.ts +11 -0
  34. package/dist/i18n/useRimoriI18n.js +41 -0
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/plugin/RimoriClient.d.ts +3 -0
  38. package/dist/plugin/RimoriClient.js +6 -0
  39. package/dist/plugin/TranslationController.d.ts +38 -0
  40. package/dist/plugin/TranslationController.js +105 -0
  41. package/dist/plugin/Translator.d.ts +38 -0
  42. package/dist/plugin/Translator.js +101 -0
  43. package/dist/utils/LanguageClass.d.ts +36 -0
  44. package/dist/utils/LanguageClass.example.d.ts +0 -0
  45. package/dist/utils/LanguageClass.example.js +1 -0
  46. package/dist/utils/LanguageClass.js +50 -0
  47. package/dist/utils/LanguageClass.test.d.ts +0 -0
  48. package/dist/utils/LanguageClass.test.js +1 -0
  49. package/package.json +12 -14
  50. package/prettier.config.js +1 -1
  51. package/src/cli/scripts/release/detect-translation-languages.ts +37 -0
  52. package/src/cli/scripts/release/release-config-upload.ts +5 -0
  53. package/src/cli/scripts/release/release.ts +20 -4
  54. package/src/cli/types/DatabaseTypes.ts +10 -2
  55. package/src/components/ai/EmbeddedAssistent/TTS/MessageSender.ts +2 -2
  56. package/src/core/controller/SettingsController.ts +8 -1
  57. package/src/core/core.ts +1 -2
  58. package/src/fromRimori/EventBus.ts +47 -76
  59. package/src/fromRimori/PluginTypes.ts +17 -26
  60. package/src/hooks/I18nHooks.ts +33 -0
  61. package/src/index.ts +1 -1
  62. package/src/plugin/RimoriClient.ts +9 -1
  63. package/src/plugin/TranslationController.ts +105 -0
  64. package/src/utils/Language.ts +0 -72
@@ -3,9 +3,9 @@ export type EventPayload = Record<string, any>;
3
3
 
4
4
  /**
5
5
  * Interface representing a message sent through the EventBus
6
- *
6
+ *
7
7
  * Debug capabilities:
8
- * - System-wide debugging: Send an event to "global.system.requestDebug"
8
+ * - System-wide debugging: Send an event to "global.system.requestDebug"
9
9
  * Example: `EventBus.emit("yourPluginId", "global.system.requestDebug");`
10
10
  */
11
11
  export interface EventBusMessage<T = EventPayload> {
@@ -40,7 +40,7 @@ export class EventBusHandler {
40
40
  private responseResolvers: Map<number, (value: EventBusMessage<unknown>) => void> = new Map();
41
41
  private static instance: EventBusHandler | null = null;
42
42
  private debugEnabled: boolean = false;
43
- private evName: string = '';
43
+ private evName: string = "";
44
44
 
45
45
  private constructor() {
46
46
  //private constructor
@@ -50,14 +50,12 @@ export class EventBusHandler {
50
50
  if (!EventBusHandler.instance) {
51
51
  EventBusHandler.instance = new EventBusHandler();
52
52
 
53
- EventBusHandler.instance.on('global.system.requestDebug', () => {
53
+ EventBusHandler.instance.on("global.system.requestDebug", () => {
54
54
  EventBusHandler.instance!.debugEnabled = true;
55
- console.log(
56
- `[${EventBusHandler.instance!.evName}] Debug mode enabled. Make sure debugging messages are enabled in the browser console.`,
57
- );
55
+ console.log(`[${EventBusHandler.instance!.evName}] Debug mode enabled. Make sure debugging messages are enabled in the browser console.`);
58
56
  });
59
57
  }
60
- if (name && EventBusHandler.instance.evName === '') {
58
+ if (name && EventBusHandler.instance.evName === "") {
61
59
  EventBusHandler.instance.evName = name;
62
60
  }
63
61
  return EventBusHandler.instance;
@@ -82,9 +80,9 @@ export class EventBusHandler {
82
80
  * @param topic - The topic of the event.
83
81
  * @param data - The data of the event.
84
82
  * @param eventId - The event id of the event.
85
- *
83
+ *
86
84
  * The topic format is: **pluginId.area.action**
87
- *
85
+ *
88
86
  * Example topics:
89
87
  * - pl1234.card.requestHard
90
88
  * - pl1234.card.requestNew
@@ -98,13 +96,7 @@ export class EventBusHandler {
98
96
  this.emitInternal(sender, topic, data || {}, eventId);
99
97
  }
100
98
 
101
- private emitInternal(
102
- sender: string,
103
- topic: string,
104
- data: EventPayload,
105
- eventId?: number,
106
- skipResponseTrigger = false,
107
- ): void {
99
+ private emitInternal(sender: string, topic: string, data: EventPayload, eventId?: number, skipResponseTrigger = false): void {
108
100
  if (!this.validateTopic(topic)) {
109
101
  this.logAndThrowError(false, `Invalid topic: ` + topic);
110
102
  return;
@@ -113,7 +105,7 @@ export class EventBusHandler {
113
105
  const event = this.createEvent(sender, topic, data, eventId);
114
106
 
115
107
  const handlers = this.getMatchingHandlers(event.topic);
116
- handlers.forEach((handler) => {
108
+ handlers.forEach(handler => {
117
109
  if (handler.ignoreSender && handler.ignoreSender.includes(sender)) {
118
110
  // console.log("ignore event as its in the ignoreSender list", { event, ignoreList: handler.ignoreSender });
119
111
  return;
@@ -140,12 +132,8 @@ export class EventBusHandler {
140
132
  * @param ignoreSender - The senders to ignore.
141
133
  * @returns An EventListener object containing an off() method to unsubscribe the listeners.
142
134
  */
143
- public on<T = EventPayload>(
144
- topics: string | string[],
145
- handler: EventHandler<T>,
146
- ignoreSender: string[] = [],
147
- ): EventListener {
148
- const ids = this.toArray(topics).map((topic) => {
135
+ public on<T = EventPayload>(topics: string | string[], handler: EventHandler<T>, ignoreSender: string[] = []): EventListener {
136
+ const ids = this.toArray(topics).map(topic => {
149
137
  this.logIfDebug(`Subscribing to ` + topic, { ignoreSender });
150
138
  if (!this.validateTopic(topic)) {
151
139
  this.logAndThrowError(true, `Invalid topic: ` + topic);
@@ -166,7 +154,7 @@ export class EventBusHandler {
166
154
  });
167
155
 
168
156
  return {
169
- off: () => this.off(ids),
157
+ off: () => this.off(ids)
170
158
  };
171
159
  }
172
160
 
@@ -177,41 +165,33 @@ export class EventBusHandler {
177
165
  * @param handler - The handler to be called when the event is received. The handler returns the data to be emitted. Can be a static object or a function.
178
166
  * @returns An EventListener object containing an off() method to unsubscribe the listeners.
179
167
  */
180
- public respond(
181
- sender: string,
182
- topic: string | string[],
183
- handler: EventPayload | ((data: EventBusMessage) => EventPayload | Promise<EventPayload>),
184
- ): EventListener {
168
+ public respond(sender: string, topic: string | string[], handler: EventPayload | ((data: EventBusMessage) => EventPayload | Promise<EventPayload>)): EventListener {
185
169
  const topics = Array.isArray(topic) ? topic : [topic];
186
- const listeners = topics.map((topic) => {
170
+ const listeners = topics.map(topic => {
187
171
  const blackListedEventIds: number[] = [];
188
172
  //To allow event communication inside the same plugin the sender needs to be ignored but the events still need to be checked for the same event just reaching the subscriber to prevent infinite loops
189
- const finalIgnoreSender = !topic.startsWith('self.') ? [sender] : [];
190
-
191
- const listener = this.on(
192
- topic,
193
- async (data: EventBusMessage) => {
194
- if (blackListedEventIds.includes(data.eventId)) {
195
- // console.log("BLACKLISTED EVENT ID", data.eventId);
196
- return;
197
- }
198
- blackListedEventIds.push(data.eventId);
199
- if (blackListedEventIds.length > 20) {
200
- blackListedEventIds.shift();
201
- }
202
- const response = typeof handler === 'function' ? await handler(data) : handler;
203
- this.emit(sender, topic, response, data.eventId);
204
- },
205
- finalIgnoreSender,
206
- );
207
-
208
- this.logIfDebug(`Added respond listener ` + sender + ' to topic ' + topic, { listener, sender });
173
+ const finalIgnoreSender = !topic.startsWith("self.") ? [sender] : [];
174
+
175
+ const listener = this.on(topic, async (data: EventBusMessage) => {
176
+ if (blackListedEventIds.includes(data.eventId)) {
177
+ // console.log("BLACKLISTED EVENT ID", data.eventId);
178
+ return;
179
+ }
180
+ blackListedEventIds.push(data.eventId);
181
+ if (blackListedEventIds.length > 20) {
182
+ blackListedEventIds.shift();
183
+ }
184
+ const response = typeof handler === "function" ? await handler(data) : handler;
185
+ this.emit(sender, topic, response, data.eventId);
186
+ }, finalIgnoreSender);
187
+
188
+ this.logIfDebug(`Added respond listener ` + sender + " to topic " + topic, { listener, sender });
209
189
  return {
210
- off: () => listener.off(),
190
+ off: () => listener.off()
211
191
  };
212
192
  });
213
193
  return {
214
- off: () => listeners.forEach((listener) => listener.off()),
194
+ off: () => listeners.forEach(listener => listener.off())
215
195
  };
216
196
  }
217
197
 
@@ -241,12 +221,12 @@ export class EventBusHandler {
241
221
  * @param listenerIds - The ids of the listeners to unsubscribe from.
242
222
  */
243
223
  private off(listenerIds: string | string[]): void {
244
- this.toArray(listenerIds).forEach((fullId) => {
224
+ this.toArray(listenerIds).forEach(fullId => {
245
225
  const { topic, id } = JSON.parse(atob(fullId));
246
226
 
247
227
  const listeners = this.listeners.get(topic) || new Set();
248
228
 
249
- listeners.forEach((listener) => {
229
+ listeners.forEach(listener => {
250
230
  if (listener.id === Number(id)) {
251
231
  listeners.delete(listener);
252
232
  this.logIfDebug(`Removed listener ` + fullId, { topic, listenerId: id });
@@ -266,11 +246,7 @@ export class EventBusHandler {
266
246
  * @param data - The data of the event.
267
247
  * @returns A promise that resolves to the event.
268
248
  */
269
- public async request<T = EventPayload>(
270
- sender: string,
271
- topic: string,
272
- data?: EventPayload,
273
- ): Promise<EventBusMessage<T>> {
249
+ public async request<T = EventPayload>(sender: string, topic: string, data?: EventPayload): Promise<EventBusMessage<T>> {
274
250
  if (!this.validateTopic(topic)) {
275
251
  this.logAndThrowError(true, `Invalid topic: ` + topic);
276
252
  }
@@ -279,10 +255,8 @@ export class EventBusHandler {
279
255
 
280
256
  this.logIfDebug(`Requesting data from ` + topic, { event });
281
257
 
282
- return new Promise<EventBusMessage<T>>((resolve) => {
283
- this.responseResolvers.set(event.eventId, (value: EventBusMessage<unknown>) =>
284
- resolve(value as EventBusMessage<T>),
285
- );
258
+ return new Promise<EventBusMessage<T>>(resolve => {
259
+ this.responseResolvers.set(event.eventId, (value: EventBusMessage<unknown>) => resolve(value as EventBusMessage<T>));
286
260
  this.emitInternal(sender, topic, data || {}, event.eventId, true);
287
261
  });
288
262
  }
@@ -297,7 +271,7 @@ export class EventBusHandler {
297
271
 
298
272
  // Find wildcard matches
299
273
  const wildcard = [...this.listeners.entries()]
300
- .filter(([key]) => key.endsWith('*') && topic.startsWith(key.slice(0, -1)))
274
+ .filter(([key]) => key.endsWith("*") && topic.startsWith(key.slice(0, -1)))
301
275
  .flatMap(([_, handlers]) => [...handlers]);
302
276
  return new Set([...exact, ...wildcard]);
303
277
  }
@@ -309,35 +283,32 @@ export class EventBusHandler {
309
283
  */
310
284
  private validateTopic(topic: string): boolean {
311
285
  // Split event type into parts
312
- const parts = topic.split('.');
286
+ const parts = topic.split(".");
313
287
  const [plugin, area, action] = parts;
314
288
 
315
289
  if (parts.length !== 3) {
316
- if (parts.length === 1 && plugin === '*') {
290
+ if (parts.length === 1 && plugin === "*") {
317
291
  return true;
318
292
  }
319
- if (parts.length === 2 && plugin !== '*' && area === '*') {
293
+ if (parts.length === 2 && plugin !== "*" && area === "*") {
320
294
  return true;
321
295
  }
322
296
  this.logAndThrowError(false, `Event type must have 3 parts separated by dots. Received: ` + topic);
323
297
  return false;
324
298
  }
325
299
 
326
- if (action === '*') {
300
+ if (action === "*") {
327
301
  return true;
328
302
  }
329
303
 
330
304
  // Validate action part
331
- const validActions = ['request', 'create', 'update', 'delete', 'trigger'];
305
+ const validActions = ["request", "create", "update", "delete", "trigger"];
332
306
 
333
- if (validActions.some((a) => action.startsWith(a))) {
307
+ if (validActions.some(a => action.startsWith(a))) {
334
308
  return true;
335
309
  }
336
310
 
337
- this.logAndThrowError(
338
- false,
339
- `Invalid event topic name. The action: ` + action + '. Must be or start with one of: ' + validActions.join(', '),
340
- );
311
+ this.logAndThrowError(false, `Invalid event topic name. The action: ` + action + ". Must be or start with one of: " + validActions.join(", "));
341
312
  return false;
342
313
  }
343
314
 
@@ -356,4 +327,4 @@ export class EventBusHandler {
356
327
  }
357
328
  }
358
329
 
359
- export const EventBus = EventBusHandler.getInstance();
330
+ export const EventBus = EventBusHandler.getInstance();
@@ -4,10 +4,10 @@ export type Plugin<T extends {} = {}> = Omit<RimoriPluginConfig<T>, 'context_men
4
4
  endpoint: string;
5
5
  assetEndpoint: string;
6
6
  context_menu_actions: MenuEntry[];
7
- release_channel: 'alpha' | 'beta' | 'stable';
8
- };
7
+ release_channel: "alpha" | "beta" | "stable";
8
+ }
9
9
 
10
- export type ActivePlugin = Plugin<{ active?: boolean }>;
10
+ export type ActivePlugin = Plugin<{ active?: boolean }>
11
11
 
12
12
  // browsable page of a plugin
13
13
  export interface PluginPage {
@@ -17,22 +17,13 @@ export interface PluginPage {
17
17
  // Whether the page should be shown in the navbar
18
18
  show: boolean;
19
19
  description: string;
20
- root:
21
- | 'vocabulary'
22
- | 'grammar'
23
- | 'reading'
24
- | 'listening'
25
- | 'watching'
26
- | 'writing'
27
- | 'speaking'
28
- | 'other'
29
- | 'community';
20
+ root: "vocabulary" | "grammar" | "reading" | "listening" | "watching" | "writing" | "speaking" | "other" | "community";
30
21
  // The actions that can be triggered in the plugin
31
22
  // The key is the action key. The other entries are additional properties needed when triggering the action
32
23
  action?: {
33
24
  key: string;
34
25
  parameters: ObjectTool;
35
- };
26
+ }
36
27
  }
37
28
 
38
29
  // a sidebar page of a plugin
@@ -74,7 +65,7 @@ export interface ContextMenuAction {
74
65
  // id of the plugin that the action belongs to
75
66
  plugin_id: string;
76
67
  // key of the action. Used to know which action to trigger when clicking on the context menu
77
- action_key: string;
68
+ action_key: string
78
69
  }
79
70
 
80
71
  /**
@@ -95,7 +86,7 @@ export interface RimoriPluginConfig<T extends {} = {}> {
95
86
  logo: string;
96
87
  /** Optional website URL for the plugin's homepage or link to plugins owner for contributions */
97
88
  website?: string;
98
- };
89
+ }
99
90
  /**
100
91
  * Configuration for different types of pages.
101
92
  */
@@ -110,11 +101,11 @@ export interface RimoriPluginConfig<T extends {} = {}> {
110
101
  settings?: string;
111
102
  /** Optional array of event topics the plugin pages can listen to for cross-plugin communication */
112
103
  topics?: string[];
113
- };
104
+ }
114
105
  /**
115
106
  * Context menu actions that the plugin registers to appear in right-click menus throughout the application.
116
107
  */
117
- context_menu_actions: Omit<MenuEntry, 'plugin_id'>[];
108
+ context_menu_actions: Omit<MenuEntry, "plugin_id">[];
118
109
  /**
119
110
  * Documentation paths for different types of plugin documentation.
120
111
  */
@@ -125,7 +116,7 @@ export interface RimoriPluginConfig<T extends {} = {}> {
125
116
  user_path: string;
126
117
  /** Path to developer documentation for plugin development */
127
118
  developer_path: string;
128
- };
119
+ }
129
120
  /**
130
121
  * Configuration for the plugin's web worker if it uses background processing or exposes actions to other plugins.
131
122
  */
@@ -145,7 +136,7 @@ export interface Tool {
145
136
  parameters: {
146
137
  name: string;
147
138
  description: string;
148
- type: 'string' | 'number' | 'boolean';
139
+ type: "string" | "number" | "boolean";
149
140
  }[];
150
141
  execute?: (args: Record<string, any>) => Promise<unknown> | unknown | void;
151
142
  }
@@ -154,7 +145,7 @@ export interface Tool {
154
145
  * The tool definition structure is used for LLM function calling and plugin action parameters.
155
146
  * It defines the schema for tools that can be used by Language Learning Models (LLMs)
156
147
  * and plugin actions.
157
- *
148
+ *
158
149
  * @example
159
150
  * ```typescript
160
151
  * const flashcardTool: Tool = {
@@ -164,13 +155,13 @@ export interface Tool {
164
155
  * description: 'Number of flashcards to practice'
165
156
  * },
166
157
  * deck: {
167
- * type: 'string',
158
+ * type: 'string',
168
159
  * enum: ['latest', 'random', 'oldest', 'mix', 'best_known'],
169
160
  * description: 'Type of deck to practice'
170
161
  * }
171
162
  * };
172
163
  * ```
173
- *
164
+ *
174
165
  */
175
166
  export type ObjectTool = {
176
167
  [key: string]: ToolParameter;
@@ -197,15 +188,15 @@ interface ToolParameter {
197
188
  * Supports primitive types, nested objects for complex data structures,
198
189
  * and arrays of objects for collections. The tuple notation [{}] indicates
199
190
  * arrays of objects with a specific structure.
200
- *
191
+ *
201
192
  * @example Primitive: 'string' | 'number' | 'boolean'
202
193
  * @example Nested object: { name: { type: 'string' }, age: { type: 'number' } }
203
194
  * @example Array of objects: [{ id: { type: 'string' }, value: { type: 'number' } }]
204
195
  */
205
196
  type ToolParameterType =
206
197
  | PrimitiveType
207
- | { [key: string]: ToolParameter } // for nested objects
208
- | [{ [key: string]: ToolParameter }]; // for arrays of objects (notice the tuple type)
198
+ | { [key: string]: ToolParameter } // for nested objects
199
+ | [{ [key: string]: ToolParameter }]; // for arrays of objects (notice the tuple type)
209
200
 
210
201
  /**
211
202
  * Primitive data types supported by the LLM tool system.
@@ -0,0 +1,33 @@
1
+ import { TOptions } from 'i18next';
2
+ import { useEffect, useState } from 'react';
3
+ import { Translator } from '../plugin/TranslationController';
4
+ import { useRimori } from '../providers/PluginProvider';
5
+
6
+ type TranslatorFn = (key: string, options?: TOptions) => string;
7
+
8
+ /**
9
+ * Custom useTranslation hook that provides a translation function and indicates readiness
10
+ * @returns An object containing the translation function (`t`) and a boolean (`ready`) indicating if the translator is initialized.
11
+ */
12
+ export function useTranslation(): { t: TranslatorFn; ready: boolean } {
13
+ const { plugin } = useRimori();
14
+ const [translatorInstance, setTranslatorInstance] = useState<Translator | null>(null);
15
+
16
+ useEffect(() => {
17
+ void plugin.getTranslator().then(setTranslatorInstance);
18
+ }, [plugin]);
19
+
20
+ const safeT = (key: string, options?: TOptions): string => {
21
+ // return zero-width space if translator is not initialized to keep text space occupied
22
+ if (!translatorInstance) return '\u200B'; // zero-width space
23
+
24
+ const result = translatorInstance.t(key, options);
25
+ if (!result) {
26
+ console.error(`Translation key not found: ${key}`);
27
+ return '\u200B'; // zero-width space
28
+ }
29
+ return result;
30
+ };
31
+
32
+ return { t: safeT, ready: translatorInstance !== null };
33
+ }
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@ export * from './providers/PluginProvider';
6
6
  export * from './cli/types/DatabaseTypes';
7
7
  export * from './utils/difficultyConverter';
8
8
  export * from './utils/PluginUtils';
9
- export * from './utils/Language';
10
9
  export * from './fromRimori/PluginTypes';
11
10
  export { FirstMessages } from './components/ai/utils';
12
11
  export { AudioController } from './plugin/AudioController';
12
+ export { useTranslation } from './hooks/I18nHooks';
@@ -16,6 +16,7 @@ import { EventBus, EventBusMessage, EventHandler, EventPayload } from '../fromRi
16
16
  import { ActivePlugin, MainPanelAction, Plugin, Tool } from '../fromRimori/PluginTypes';
17
17
  import { AccomplishmentHandler, AccomplishmentPayload } from './AccomplishmentHandler';
18
18
  import { PluginController, RimoriInfo } from './PluginController';
19
+ import { Translator } from './TranslationController';
19
20
 
20
21
  interface Db {
21
22
  from: {
@@ -73,6 +74,7 @@ interface PluginInterface {
73
74
  sidePanelPlugin?: ActivePlugin;
74
75
  };
75
76
  getUserInfo: () => UserInfo;
77
+ getTranslator: () => Promise<Translator>;
76
78
  }
77
79
 
78
80
  export class RimoriClient {
@@ -84,6 +86,7 @@ export class RimoriClient {
84
86
  private exerciseController: ExerciseController;
85
87
  private accomplishmentHandler: AccomplishmentHandler;
86
88
  private rimoriInfo: RimoriInfo;
89
+ private translator: Translator;
87
90
  public plugin: PluginInterface;
88
91
  public db: Db;
89
92
 
@@ -95,6 +98,7 @@ export class RimoriClient {
95
98
  this.sharedContentController = new SharedContentController(this.superbase, this);
96
99
  this.exerciseController = new ExerciseController(supabase, pluginController);
97
100
  this.accomplishmentHandler = new AccomplishmentHandler(info.pluginId);
101
+ this.translator = new Translator(info.profile.mother_tongue.code);
98
102
 
99
103
  this.from = this.from.bind(this);
100
104
  this.getTableName = this.getTableName.bind(this);
@@ -108,7 +112,7 @@ export class RimoriClient {
108
112
  };
109
113
  this.plugin = {
110
114
  pluginId: info.pluginId,
111
- setSettings: async (settings: any) => {
115
+ setSettings: async (settings: any): Promise<void> => {
112
116
  await this.settingsController.setSettings(settings);
113
117
  },
114
118
  getSettings: async <T extends object>(defaultSettings: T): Promise<T> => {
@@ -124,6 +128,10 @@ export class RimoriClient {
124
128
  sidePanelPlugin: this.rimoriInfo.sidePanelPlugin,
125
129
  };
126
130
  },
131
+ getTranslator: async (): Promise<Translator> => {
132
+ await this.translator.initialize();
133
+ return this.translator;
134
+ },
127
135
  };
128
136
  }
129
137
 
@@ -0,0 +1,105 @@
1
+ import { createInstance, ThirdPartyModule, TOptions, i18n as i18nType } from 'i18next';
2
+
3
+ /**
4
+ * Translator class for handling internationalization
5
+ */
6
+ export class Translator {
7
+ private currentLanguage: string;
8
+ private isInitialized: boolean;
9
+ private i18n: i18nType | undefined;
10
+
11
+ constructor(initialLanguage: string) {
12
+ this.isInitialized = false;
13
+ this.currentLanguage = initialLanguage;
14
+ }
15
+
16
+ /**
17
+ * Initialize translator with user's language
18
+ * @param userLanguage - Language code from user info
19
+ */
20
+ async initialize(): Promise<void> {
21
+ if (this.isInitialized) return;
22
+
23
+ const translations = await this.fetchTranslations(this.currentLanguage);
24
+
25
+ const instance = createInstance({
26
+ lng: this.currentLanguage,
27
+ resources: {
28
+ [this.currentLanguage]: {
29
+ translation: translations,
30
+ },
31
+ },
32
+ debug: window.location.hostname === 'localhost',
33
+ });
34
+
35
+ await instance.init();
36
+ this.i18n = instance;
37
+ this.isInitialized = true;
38
+ }
39
+
40
+ private getTranslationUrl(language: string): string {
41
+ // For localhost development, use local- prefix for non-English languages
42
+ const isLocalhost = window.location.hostname === 'localhost';
43
+ const isEnglish = language === 'en';
44
+ const filename = isLocalhost && !isEnglish ? `local-${language}` : language;
45
+
46
+ return `${window.location.origin}/locales/${filename}.json`;
47
+ }
48
+
49
+ public usePlugin(plugin: ThirdPartyModule): void {
50
+ if (!this.i18n) {
51
+ throw new Error('Translator is not initialized');
52
+ }
53
+ this.i18n.use(plugin);
54
+ }
55
+ /**
56
+ * Fetch translations manually from the current domain
57
+ * @param language - Language code to fetch
58
+ * @returns Promise with translation data
59
+ */
60
+ private async fetchTranslations(language: string): Promise<Record<string, string>> {
61
+ try {
62
+ const response = await fetch(this.getTranslationUrl(language));
63
+ if (!response.ok) {
64
+ throw new Error(`Failed to fetch translations for ${language}`);
65
+ }
66
+ return (await response.json()) as Record<string, string>;
67
+ } catch (error) {
68
+ console.warn(`Failed to fetch translations for ${language}:`, error);
69
+ if (language === 'en') return {};
70
+
71
+ // Fallback to English
72
+ return this.fetchTranslations('en').catch((error) => {
73
+ console.error('Failed to fetch fallback translations:', error);
74
+ return {};
75
+ });
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Get translation for a key
81
+ * @param key - Translation key
82
+ * @param options - Translation options
83
+ * @returns Translated string
84
+ */
85
+ t(key: string, options?: TOptions): string {
86
+ if (!this.i18n) {
87
+ throw new Error('Translator is not initialized');
88
+ }
89
+ return this.i18n.t(key, options) as string;
90
+ }
91
+
92
+ /**
93
+ * Get current language
94
+ */
95
+ getCurrentLanguage(): string {
96
+ return this.currentLanguage;
97
+ }
98
+
99
+ /**
100
+ * Check if translator is initialized
101
+ */
102
+ isReady(): boolean {
103
+ return this.isInitialized;
104
+ }
105
+ }
@@ -1,72 +0,0 @@
1
- export const languageKeys = {
2
- sq: 'albanian',
3
- ar: 'arabic',
4
- hy: 'armenian',
5
- az: 'azerbaijani',
6
- bn: 'bengali',
7
- bs: 'bosnian',
8
- bg: 'bulgarian',
9
- ca: 'catalan',
10
- zh: 'chinese',
11
- hr: 'croatian',
12
- cs: 'czech',
13
- da: 'danish',
14
- nl: 'dutch',
15
- en: 'english',
16
- et: 'estonian',
17
- fi: 'finnish',
18
- fr: 'french',
19
- gl: 'galician',
20
- de: 'german',
21
- el: 'greek',
22
- he: 'hebrew',
23
- hi: 'hindi',
24
- hu: 'hungarian',
25
- is: 'icelandic',
26
- id: 'indonesian',
27
- it: 'italian',
28
- ja: 'japanese',
29
- kn: 'kannada',
30
- kk: 'kazakh',
31
- ko: 'korean',
32
- lv: 'latvian',
33
- lt: 'lithuanian',
34
- mk: 'macedonian',
35
- ms: 'malay',
36
- mr: 'marathi',
37
- mi: 'maori',
38
- ne: 'nepali',
39
- no: 'norwegian',
40
- fa: 'persian',
41
- pl: 'polish',
42
- pt: 'portuguese',
43
- ro: 'romanian',
44
- ru: 'russian',
45
- sr: 'serbian',
46
- sk: 'slovak',
47
- sl: 'slovenian',
48
- es: 'spanish',
49
- sw: 'swahili',
50
- sv: 'swedish',
51
- tl: 'filipino',
52
- ta: 'tamil',
53
- th: 'thai',
54
- tr: 'turkish',
55
- uk: 'ukrainian',
56
- ur: 'urdu',
57
- vi: 'vietnamese',
58
- cy: 'welsh',
59
- } as const;
60
-
61
- export type Language = keyof typeof languageKeys;
62
-
63
- /**
64
- * Get the language name from the language code
65
- * @param languageCode The code of the language
66
- * @param capitalize Whether to capitalize the first letter of the language name
67
- * @returns The language name
68
- */
69
- export function getLanguageName(languageCode: Language, capitalize: boolean = false): string {
70
- const lang = languageKeys[languageCode];
71
- return capitalize ? lang.charAt(0).toUpperCase() + lang.slice(1) : lang;
72
- }