@rimori/client 1.4.4 → 1.4.6
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/README.md +116 -0
- package/dist/cli/scripts/release/detect-translation-languages.d.ts +5 -0
- package/dist/cli/scripts/release/detect-translation-languages.js +43 -0
- package/dist/cli/scripts/release/release-config-upload.js +4 -0
- package/dist/cli/scripts/release/release-translation-upload.d.ts +6 -0
- package/dist/cli/scripts/release/release-translation-upload.js +87 -0
- package/dist/cli/scripts/release/release.d.ts +1 -1
- package/dist/cli/scripts/release/release.js +14 -5
- package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.js +2 -2
- package/dist/core/controller/EnhancedUserInfo.d.ts +0 -0
- package/dist/core/controller/EnhancedUserInfo.js +1 -0
- package/dist/core/controller/SettingsController.d.ts +7 -1
- package/dist/core/core.d.ts +1 -2
- package/dist/core/core.js +0 -1
- package/dist/fromRimori/EventBus.js +23 -23
- package/dist/fromRimori/PluginTypes.d.ts +4 -4
- package/dist/hooks/I18nHooks.d.ts +11 -0
- package/dist/hooks/I18nHooks.js +25 -0
- package/dist/i18n/I18nHooks.d.ts +11 -0
- package/dist/i18n/I18nHooks.js +25 -0
- package/dist/i18n/Translator.d.ts +43 -0
- package/dist/i18n/Translator.js +118 -0
- package/dist/i18n/config.d.ts +7 -0
- package/dist/i18n/config.js +20 -0
- package/dist/i18n/createI18nInstance.d.ts +7 -0
- package/dist/i18n/createI18nInstance.js +31 -0
- package/dist/i18n/hooks.d.ts +11 -0
- package/dist/i18n/hooks.js +25 -0
- package/dist/i18n/index.d.ts +4 -0
- package/dist/i18n/index.js +4 -0
- package/dist/i18n/types.d.ts +7 -0
- package/dist/i18n/types.js +1 -0
- package/dist/i18n/useRimoriI18n.d.ts +11 -0
- package/dist/i18n/useRimoriI18n.js +41 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin/RimoriClient.d.ts +3 -0
- package/dist/plugin/RimoriClient.js +6 -0
- package/dist/plugin/TranslationController.d.ts +38 -0
- package/dist/plugin/TranslationController.js +105 -0
- package/dist/plugin/Translator.d.ts +38 -0
- package/dist/plugin/Translator.js +101 -0
- package/dist/utils/LanguageClass.d.ts +36 -0
- package/dist/utils/LanguageClass.example.d.ts +0 -0
- package/dist/utils/LanguageClass.example.js +1 -0
- package/dist/utils/LanguageClass.js +50 -0
- package/dist/utils/LanguageClass.test.d.ts +0 -0
- package/dist/utils/LanguageClass.test.js +1 -0
- package/package.json +12 -14
- package/prettier.config.js +1 -1
- package/src/cli/scripts/release/detect-translation-languages.ts +37 -0
- package/src/cli/scripts/release/release-config-upload.ts +5 -0
- package/src/cli/scripts/release/release.ts +20 -4
- package/src/cli/types/DatabaseTypes.ts +10 -2
- package/src/components/ai/EmbeddedAssistent/TTS/MessageSender.ts +2 -2
- package/src/core/controller/SettingsController.ts +8 -1
- package/src/core/core.ts +1 -2
- package/src/fromRimori/EventBus.ts +47 -76
- package/src/fromRimori/PluginTypes.ts +17 -26
- package/src/hooks/I18nHooks.ts +33 -0
- package/src/index.ts +1 -1
- package/src/plugin/RimoriClient.ts +9 -1
- package/src/plugin/TranslationController.ts +105 -0
- 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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
190
|
-
|
|
191
|
-
const listener = this.on(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
blackListedEventIds.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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>>(
|
|
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(
|
|
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 !==
|
|
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 = [
|
|
305
|
+
const validActions = ["request", "create", "update", "delete", "trigger"];
|
|
332
306
|
|
|
333
|
-
if (validActions.some(
|
|
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:
|
|
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,
|
|
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:
|
|
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 }
|
|
208
|
-
| [{ [key: string]: ToolParameter }];
|
|
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
|
+
}
|
package/src/utils/Language.ts
DELETED
|
@@ -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
|
-
}
|