@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.
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
package/README.md CHANGED
@@ -952,6 +952,122 @@ const ChatExample = () => {
952
952
  };
953
953
  ```
954
954
 
955
+ ### useTranslation
956
+
957
+ Internationalization (i18n) support built on i18next:
958
+
959
+ ```typescript
960
+ import { useTranslation } from "@rimori/client";
961
+
962
+ const TranslatedComponent = () => {
963
+ const { t, ready } = useTranslation();
964
+
965
+ if (!ready) {
966
+ return <div>Loading translations...</div>;
967
+ }
968
+
969
+ return (
970
+ <div>
971
+ <h1>{t('discussion.title')}</h1>
972
+ <p>{t('discussion.whatToTalkAbout')}</p>
973
+ </div>
974
+ );
975
+ };
976
+ ```
977
+
978
+ ## Translation Feature
979
+
980
+ Rimori includes a comprehensive internationalization (i18n) system built on i18next that allows plugins to support multiple languages with minimal developer effort.
981
+
982
+ ### How it works
983
+
984
+ - **Developer Focus**: Developers only need to ensure their interface works in English
985
+ - **Automatic Translations**: With every release, translations for all other languages are generated automatically
986
+ - **Local Testing**: For local development, you can test translations by:
987
+ 1. Setting your user language to a non-English locale (e.g., German)
988
+ 2. Creating a local translation file with "local-" prefix (e.g., `local-de.json`) in the `public/locales/` directory
989
+ 3. The translator will automatically use the local translation file in development mode
990
+ - **Manual Translations**: If developers want to manually translate files, they should place the language file manually in the `public/locales/` folder with the language code as filename (e.g., `de.json`, `fr.json`)
991
+
992
+ ### Usage
993
+
994
+ #### Using the Hook (Recommended)
995
+
996
+ ```typescript
997
+ import { useTranslation } from "@rimori/client";
998
+
999
+ function MyComponent() {
1000
+ const { t, ready } = useTranslation();
1001
+
1002
+ if (!ready) {
1003
+ return <div>Loading translations...</div>;
1004
+ }
1005
+
1006
+ return (
1007
+ <div>
1008
+ <h1>{t('discussion.title')}</h1>
1009
+ <p>{t('discussion.whatToTalkAbout')}</p>
1010
+ </div>
1011
+ );
1012
+ }
1013
+ ```
1014
+
1015
+ #### Using the Translator Instance
1016
+
1017
+ ```typescript
1018
+ import { useRimori } from "@rimori/client";
1019
+
1020
+ const { plugin } = useRimori();
1021
+ const translator = await plugin.getTranslator()
1022
+
1023
+ const translatedText = translator.t("discussion.title");
1024
+ ```
1025
+
1026
+ ### Translation File Structure
1027
+
1028
+ - **Location**: `public/locales/`
1029
+ - **Production Files**: Must be named `{language}.json` (e.g., `en.json`, `de.json`, `fr.json`)
1030
+ - **Local Development Files**: Must be named `local-{language}.json` (e.g., `local-de.json`, `local-fr.json`)
1031
+ - **Format**: Standard JSON with nested objects for organization
1032
+ - **English Requirement**: `en.json` is required as the base language
1033
+ - **Release Process**: Files starting with "local-" are ignored during the release process
1034
+
1035
+ Example translation file structure:
1036
+
1037
+ ```json
1038
+ {
1039
+ "discussion": {
1040
+ "title": "Discussion",
1041
+ "whatToTalkAbout": "What do you want to talk about?",
1042
+ "topics": {
1043
+ "everyday": {
1044
+ "title": "Everyday Conversations",
1045
+ "description": "Ordering coffee, asking for directions, etc."
1046
+ }
1047
+ }
1048
+ }
1049
+ }
1050
+ ```
1051
+
1052
+ ### Features
1053
+
1054
+ - **I18next Support**: All i18next features work with these translations including:
1055
+ - Variable interpolation: `{{name}}`
1056
+ - Pluralization
1057
+ - Fallback mechanisms
1058
+ - **Automatic Fallback**: If a translation is missing, it falls back to English
1059
+ - **Development Mode**: Local translation files are prioritized in development
1060
+ - **Production Ready**: Automatic translation generation for production releases
1061
+
1062
+ ### Limitations
1063
+
1064
+ - Only one translation file per language is allowed
1065
+ - Namespaces are not supported
1066
+ - Production translation files must be named `{language}.json` and placed in `public/locales/`
1067
+ - Local development files must be named `local-{language}.json` and placed in `public/locales/`
1068
+ - English (`en.json`) is required as the base language
1069
+ - Local files (prefixed with "local-") are ignored during the release process
1070
+
955
1071
  ## Utilities
956
1072
 
957
1073
  ### difficultyConverter
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Detect available translation languages from public/locales directory
3
+ * @returns Promise<string[]> Array of language codes found in the locales directory
4
+ */
5
+ export declare function detectTranslationLanguages(): Promise<string[]>;
@@ -0,0 +1,43 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import fs from 'fs';
11
+ /**
12
+ * Detect available translation languages from public/locales directory
13
+ * @returns Promise<string[]> Array of language codes found in the locales directory
14
+ */
15
+ export function detectTranslationLanguages() {
16
+ return __awaiter(this, void 0, void 0, function* () {
17
+ const localesPath = './public/locales';
18
+ try {
19
+ yield fs.promises.access(localesPath);
20
+ }
21
+ catch (e) {
22
+ console.log('⚠️ No locales directory found, no translations available');
23
+ return [];
24
+ }
25
+ try {
26
+ const files = yield fs.promises.readdir(localesPath);
27
+ // Filter out local- files and only include .json files
28
+ const translationFiles = files.filter((file) => file.endsWith('.json') && !file.startsWith('local-'));
29
+ if (translationFiles.length === 0) {
30
+ console.log('⚠️ No translation files found (excluding local- files)');
31
+ return [];
32
+ }
33
+ // Extract language codes from filenames (e.g., en.json -> en)
34
+ const languages = translationFiles.map((file) => file.replace('.json', ''));
35
+ console.log(`🌐 Found ${languages.length} translation languages: ${languages.join(', ')}`);
36
+ return languages;
37
+ }
38
+ catch (error) {
39
+ console.error(`❌ Error reading locales directory:`, error.message);
40
+ return [];
41
+ }
42
+ });
43
+ }
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import fs from 'fs';
11
11
  import path from 'path';
12
12
  import ts from 'typescript';
13
+ import { detectTranslationLanguages } from './detect-translation-languages.js';
13
14
  /**
14
15
  * Read and send the rimori configuration to the release endpoint
15
16
  * @param config - Configuration object
@@ -54,6 +55,8 @@ export function sendConfiguration(config) {
54
55
  if (!configObject) {
55
56
  throw new Error('Configuration object is empty or undefined');
56
57
  }
58
+ // Detect available translation languages
59
+ const availableLanguages = yield detectTranslationLanguages();
57
60
  console.log(`🚀 Sending configuration...`);
58
61
  const requestBody = {
59
62
  config: configObject,
@@ -61,6 +64,7 @@ export function sendConfiguration(config) {
61
64
  plugin_id: config.plugin_id,
62
65
  release_channel: config.release_channel,
63
66
  rimori_client_version: config.rimori_client_version,
67
+ provided_languages: availableLanguages.join(','),
64
68
  };
65
69
  try {
66
70
  const response = yield fetch(`${config.domain}/release`, {
@@ -0,0 +1,6 @@
1
+ import { Config } from './release.js';
2
+ /**
3
+ * Upload translation files to the release function
4
+ * @param config - Configuration object
5
+ */
6
+ export declare function uploadTranslations(config: Config, release_id: string): Promise<void>;
@@ -0,0 +1,87 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ /**
13
+ * Upload translation files to the release function
14
+ * @param config - Configuration object
15
+ */
16
+ export function uploadTranslations(config, release_id) {
17
+ return __awaiter(this, void 0, void 0, function* () {
18
+ const localesPath = './public/locales';
19
+ console.log(`📁 Checking for translation files...`);
20
+ // Check if locales directory exists
21
+ try {
22
+ yield fs.promises.access(localesPath);
23
+ }
24
+ catch (e) {
25
+ console.log('⚠️ No locales directory found at public/locales, skipping translation upload');
26
+ return;
27
+ }
28
+ // Read all files in the locales directory
29
+ const files = yield fs.promises.readdir(localesPath);
30
+ // Filter out local- files and only include .json files
31
+ const translationFiles = files.filter(file => file.endsWith('.json') &&
32
+ !file.startsWith('local-'));
33
+ if (translationFiles.length === 0) {
34
+ console.log('⚠️ No translation files found (excluding local- files), skipping translation upload');
35
+ return;
36
+ }
37
+ console.log(`🌐 Found ${translationFiles.length} translation files: ${translationFiles.join(', ')}`);
38
+ // Create FormData
39
+ const formData = new FormData();
40
+ // Add version and release channel data
41
+ formData.append('version', config.version);
42
+ formData.append('release_channel', config.release_channel);
43
+ formData.append('plugin_id', config.plugin_id);
44
+ // Create path mapping with ID as key
45
+ const pathMapping = {};
46
+ try {
47
+ // Process each translation file
48
+ for (let i = 0; i < translationFiles.length; i++) {
49
+ const fileName = translationFiles[i];
50
+ const filePath = path.join(localesPath, fileName);
51
+ console.log(`📄 Processing ${fileName}...`);
52
+ const fileContent = yield fs.promises.readFile(filePath);
53
+ const contentType = 'application/json';
54
+ // Generate unique ID for this file
55
+ const fileId = `file_${i}`;
56
+ // Add to path mapping using ID as key
57
+ pathMapping[fileId] = fileName;
58
+ // Create a Blob with the file content and content type
59
+ const blob = new Blob([new Uint8Array(fileContent)], { type: contentType });
60
+ // Add file to FormData with ID_filename format
61
+ const formFileName = `${fileId}_${fileName}`;
62
+ formData.append('files', blob, formFileName);
63
+ }
64
+ }
65
+ catch (error) {
66
+ console.error(`❌ Error reading translation files:`, error.message);
67
+ throw error;
68
+ }
69
+ // Add path mapping to FormData
70
+ formData.append('path_mapping', JSON.stringify(pathMapping));
71
+ // Upload to the release endpoint
72
+ const response = yield fetch(`${config.domain}/release/${release_id}/translations`, {
73
+ method: 'POST',
74
+ headers: { Authorization: `Bearer ${config.token}` },
75
+ body: formData,
76
+ });
77
+ if (response.ok) {
78
+ console.log('✅ Translation files uploaded successfully!');
79
+ }
80
+ else {
81
+ const errorText = yield response.text();
82
+ console.log('❌ Translation upload failed!');
83
+ console.log('Response:', errorText);
84
+ throw new Error(`Translation upload failed with status ${response.status}`);
85
+ }
86
+ });
87
+ }
@@ -4,7 +4,7 @@
4
4
  * rimori-release <release_channel>
5
5
  *
6
6
  * Environment variables required:
7
- * RIMORI_TOKEN - Your Rimori token
7
+ * RIMORI_TOKEN - Your Rimori token
8
8
  * RIMORI_PLUGIN - Your plugin ID
9
9
  *
10
10
  * Make sure to install dependencies:
@@ -4,7 +4,7 @@
4
4
  * rimori-release <release_channel>
5
5
  *
6
6
  * Environment variables required:
7
- * RIMORI_TOKEN - Your Rimori token
7
+ * RIMORI_TOKEN - Your Rimori token
8
8
  * RIMORI_PLUGIN - Your plugin ID
9
9
  *
10
10
  * Make sure to install dependencies:
@@ -29,15 +29,22 @@ import { releasePlugin, sendConfiguration } from './release-config-upload.js';
29
29
  const packageJson = JSON.parse(fs.readFileSync(path.resolve('./package.json'), 'utf8'));
30
30
  const { version, r_id: pluginId } = packageJson;
31
31
  const RIMORI_TOKEN = process.env.RIMORI_TOKEN;
32
- if (!RIMORI_TOKEN)
33
- throw new Error('RIMORI_TOKEN is not set');
34
- if (!pluginId)
35
- throw new Error('The plugin id (r_id) is not set in package.json');
32
+ if (!RIMORI_TOKEN) {
33
+ console.error('Error: RIMORI_TOKEN is not set');
34
+ process.exit(1);
35
+ }
36
+ if (!pluginId) {
37
+ console.error('Error: The plugin id (r_id) is not set in package.json');
38
+ process.exit(1);
39
+ }
36
40
  const [releaseChannel] = process.argv.slice(2);
37
41
  if (!releaseChannel) {
38
42
  console.error('Usage: rimori-release <release_channel>');
39
43
  process.exit(1);
40
44
  }
45
+ if (process.env.RIMORI_BACKEND_URL) {
46
+ console.info('Using backend url:', process.env.RIMORI_BACKEND_URL);
47
+ }
41
48
  const config = {
42
49
  version,
43
50
  release_channel: releaseChannel,
@@ -60,6 +67,8 @@ function releaseProcess() {
60
67
  yield uploadDirectory(config, release_id);
61
68
  // Then release the plugin
62
69
  yield releasePlugin(config, release_id);
70
+ // Inform user about translation processing
71
+ console.log('🌐 Hint: The plugin is released but it might take some time until all translations are being processed.');
63
72
  }
64
73
  catch (error) {
65
74
  console.log('❌ Error:', error.message);
@@ -20,9 +20,9 @@ export class MessageSender {
20
20
  this.voice = voice;
21
21
  }
22
22
  getCompletedSentences(currentText, isLoading) {
23
- // Split the text based on the following characters: .,?!
23
+ // Split the text based on the following characters: .?!
24
24
  // Only split on : when followed by a space
25
- const pattern = /(.+?[,.?!]|.+?:\s+|.+?\n+)/g;
25
+ const pattern = /(.+?[.?!]|.+?:\s+|.+?\n+)/g;
26
26
  const result = [];
27
27
  let match;
28
28
  while ((match = pattern.exec(currentText)) !== null) {
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
@@ -1,6 +1,5 @@
1
1
  import { SupabaseClient } from '@supabase/supabase-js';
2
2
  import { LanguageLevel } from '../../utils/difficultyConverter';
3
- import { Language } from '../../utils/Language';
4
3
  import { Guild } from '../core';
5
4
  export interface Buddy {
6
5
  id: string;
@@ -10,6 +9,13 @@ export interface Buddy {
10
9
  voiceId: string;
11
10
  aiPersonality: string;
12
11
  }
12
+ export interface Language {
13
+ code: string;
14
+ name: string;
15
+ native: string;
16
+ capitalized: string;
17
+ uppercase: string;
18
+ }
13
19
  export interface UserInfo {
14
20
  skill_level_reading: LanguageLevel;
15
21
  skill_level_writing: LanguageLevel;
@@ -2,11 +2,10 @@ export * from '../fromRimori/PluginTypes';
2
2
  export * from '../plugin/PluginController';
3
3
  export * from '../plugin/RimoriClient';
4
4
  export * from '../utils/difficultyConverter';
5
- export * from '../utils/Language';
6
5
  export * from '../utils/PluginUtils';
7
6
  export * from '../worker/WorkerSetup';
8
7
  export { EventBusMessage } from '../fromRimori/EventBus';
9
- export { Buddy, UserInfo } from './controller/SettingsController';
8
+ export { Buddy, UserInfo, Language } from './controller/SettingsController';
10
9
  export { SharedContent } from './controller/SharedContentController';
11
10
  export { Exercise, TriggerAction } from './controller/ExerciseController';
12
11
  export { Message, OnLLMResponse, ToolInvocation } from './controller/AIController';
package/dist/core/core.js CHANGED
@@ -3,6 +3,5 @@ export * from '../fromRimori/PluginTypes';
3
3
  export * from '../plugin/PluginController';
4
4
  export * from '../plugin/RimoriClient';
5
5
  export * from '../utils/difficultyConverter';
6
- export * from '../utils/Language';
7
6
  export * from '../utils/PluginUtils';
8
7
  export * from '../worker/WorkerSetup';
@@ -12,18 +12,18 @@ export class EventBusHandler {
12
12
  this.listeners = new Map();
13
13
  this.responseResolvers = new Map();
14
14
  this.debugEnabled = false;
15
- this.evName = '';
15
+ this.evName = "";
16
16
  //private constructor
17
17
  }
18
18
  static getInstance(name) {
19
19
  if (!EventBusHandler.instance) {
20
20
  EventBusHandler.instance = new EventBusHandler();
21
- EventBusHandler.instance.on('global.system.requestDebug', () => {
21
+ EventBusHandler.instance.on("global.system.requestDebug", () => {
22
22
  EventBusHandler.instance.debugEnabled = true;
23
23
  console.log(`[${EventBusHandler.instance.evName}] Debug mode enabled. Make sure debugging messages are enabled in the browser console.`);
24
24
  });
25
25
  }
26
- if (name && EventBusHandler.instance.evName === '') {
26
+ if (name && EventBusHandler.instance.evName === "") {
27
27
  EventBusHandler.instance.evName = name;
28
28
  }
29
29
  return EventBusHandler.instance;
@@ -67,7 +67,7 @@ export class EventBusHandler {
67
67
  }
68
68
  const event = this.createEvent(sender, topic, data, eventId);
69
69
  const handlers = this.getMatchingHandlers(event.topic);
70
- handlers.forEach((handler) => {
70
+ handlers.forEach(handler => {
71
71
  if (handler.ignoreSender && handler.ignoreSender.includes(sender)) {
72
72
  // console.log("ignore event as its in the ignoreSender list", { event, ignoreList: handler.ignoreSender });
73
73
  return;
@@ -93,7 +93,7 @@ export class EventBusHandler {
93
93
  * @returns An EventListener object containing an off() method to unsubscribe the listeners.
94
94
  */
95
95
  on(topics, handler, ignoreSender = []) {
96
- const ids = this.toArray(topics).map((topic) => {
96
+ const ids = this.toArray(topics).map(topic => {
97
97
  this.logIfDebug(`Subscribing to ` + topic, { ignoreSender });
98
98
  if (!this.validateTopic(topic)) {
99
99
  this.logAndThrowError(true, `Invalid topic: ` + topic);
@@ -109,7 +109,7 @@ export class EventBusHandler {
109
109
  return btoa(JSON.stringify({ topic, id }));
110
110
  });
111
111
  return {
112
- off: () => this.off(ids),
112
+ off: () => this.off(ids)
113
113
  };
114
114
  }
115
115
  /**
@@ -121,10 +121,10 @@ export class EventBusHandler {
121
121
  */
122
122
  respond(sender, topic, handler) {
123
123
  const topics = Array.isArray(topic) ? topic : [topic];
124
- const listeners = topics.map((topic) => {
124
+ const listeners = topics.map(topic => {
125
125
  const blackListedEventIds = [];
126
126
  //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
127
- const finalIgnoreSender = !topic.startsWith('self.') ? [sender] : [];
127
+ const finalIgnoreSender = !topic.startsWith("self.") ? [sender] : [];
128
128
  const listener = this.on(topic, (data) => __awaiter(this, void 0, void 0, function* () {
129
129
  if (blackListedEventIds.includes(data.eventId)) {
130
130
  // console.log("BLACKLISTED EVENT ID", data.eventId);
@@ -134,16 +134,16 @@ export class EventBusHandler {
134
134
  if (blackListedEventIds.length > 20) {
135
135
  blackListedEventIds.shift();
136
136
  }
137
- const response = typeof handler === 'function' ? yield handler(data) : handler;
137
+ const response = typeof handler === "function" ? yield handler(data) : handler;
138
138
  this.emit(sender, topic, response, data.eventId);
139
139
  }), finalIgnoreSender);
140
- this.logIfDebug(`Added respond listener ` + sender + ' to topic ' + topic, { listener, sender });
140
+ this.logIfDebug(`Added respond listener ` + sender + " to topic " + topic, { listener, sender });
141
141
  return {
142
- off: () => listener.off(),
142
+ off: () => listener.off()
143
143
  };
144
144
  });
145
145
  return {
146
- off: () => listeners.forEach((listener) => listener.off()),
146
+ off: () => listeners.forEach(listener => listener.off())
147
147
  };
148
148
  }
149
149
  /**
@@ -169,10 +169,10 @@ export class EventBusHandler {
169
169
  * @param listenerIds - The ids of the listeners to unsubscribe from.
170
170
  */
171
171
  off(listenerIds) {
172
- this.toArray(listenerIds).forEach((fullId) => {
172
+ this.toArray(listenerIds).forEach(fullId => {
173
173
  const { topic, id } = JSON.parse(atob(fullId));
174
174
  const listeners = this.listeners.get(topic) || new Set();
175
- listeners.forEach((listener) => {
175
+ listeners.forEach(listener => {
176
176
  if (listener.id === Number(id)) {
177
177
  listeners.delete(listener);
178
178
  this.logIfDebug(`Removed listener ` + fullId, { topic, listenerId: id });
@@ -197,7 +197,7 @@ export class EventBusHandler {
197
197
  }
198
198
  const event = this.createEvent(sender, topic, data || {});
199
199
  this.logIfDebug(`Requesting data from ` + topic, { event });
200
- return new Promise((resolve) => {
200
+ return new Promise(resolve => {
201
201
  this.responseResolvers.set(event.eventId, (value) => resolve(value));
202
202
  this.emitInternal(sender, topic, data || {}, event.eventId, true);
203
203
  });
@@ -212,7 +212,7 @@ export class EventBusHandler {
212
212
  const exact = this.listeners.get(topic) || new Set();
213
213
  // Find wildcard matches
214
214
  const wildcard = [...this.listeners.entries()]
215
- .filter(([key]) => key.endsWith('*') && topic.startsWith(key.slice(0, -1)))
215
+ .filter(([key]) => key.endsWith("*") && topic.startsWith(key.slice(0, -1)))
216
216
  .flatMap(([_, handlers]) => [...handlers]);
217
217
  return new Set([...exact, ...wildcard]);
218
218
  }
@@ -223,27 +223,27 @@ export class EventBusHandler {
223
223
  */
224
224
  validateTopic(topic) {
225
225
  // Split event type into parts
226
- const parts = topic.split('.');
226
+ const parts = topic.split(".");
227
227
  const [plugin, area, action] = parts;
228
228
  if (parts.length !== 3) {
229
- if (parts.length === 1 && plugin === '*') {
229
+ if (parts.length === 1 && plugin === "*") {
230
230
  return true;
231
231
  }
232
- if (parts.length === 2 && plugin !== '*' && area === '*') {
232
+ if (parts.length === 2 && plugin !== "*" && area === "*") {
233
233
  return true;
234
234
  }
235
235
  this.logAndThrowError(false, `Event type must have 3 parts separated by dots. Received: ` + topic);
236
236
  return false;
237
237
  }
238
- if (action === '*') {
238
+ if (action === "*") {
239
239
  return true;
240
240
  }
241
241
  // Validate action part
242
- const validActions = ['request', 'create', 'update', 'delete', 'trigger'];
243
- if (validActions.some((a) => action.startsWith(a))) {
242
+ const validActions = ["request", "create", "update", "delete", "trigger"];
243
+ if (validActions.some(a => action.startsWith(a))) {
244
244
  return true;
245
245
  }
246
- this.logAndThrowError(false, `Invalid event topic name. The action: ` + action + '. Must be or start with one of: ' + validActions.join(', '));
246
+ this.logAndThrowError(false, `Invalid event topic name. The action: ` + action + ". Must be or start with one of: " + validActions.join(", "));
247
247
  return false;
248
248
  }
249
249
  logIfDebug(...args) {
@@ -3,7 +3,7 @@ export type Plugin<T extends {} = {}> = Omit<RimoriPluginConfig<T>, 'context_men
3
3
  endpoint: string;
4
4
  assetEndpoint: string;
5
5
  context_menu_actions: MenuEntry[];
6
- release_channel: 'alpha' | 'beta' | 'stable';
6
+ release_channel: "alpha" | "beta" | "stable";
7
7
  };
8
8
  export type ActivePlugin = Plugin<{
9
9
  active?: boolean;
@@ -14,7 +14,7 @@ export interface PluginPage {
14
14
  url: string;
15
15
  show: boolean;
16
16
  description: string;
17
- root: 'vocabulary' | 'grammar' | 'reading' | 'listening' | 'watching' | 'writing' | 'speaking' | 'other' | 'community';
17
+ root: "vocabulary" | "grammar" | "reading" | "listening" | "watching" | "writing" | "speaking" | "other" | "community";
18
18
  action?: {
19
19
  key: string;
20
20
  parameters: ObjectTool;
@@ -79,7 +79,7 @@ export interface RimoriPluginConfig<T extends {} = {}> {
79
79
  /**
80
80
  * Context menu actions that the plugin registers to appear in right-click menus throughout the application.
81
81
  */
82
- context_menu_actions: Omit<MenuEntry, 'plugin_id'>[];
82
+ context_menu_actions: Omit<MenuEntry, "plugin_id">[];
83
83
  /**
84
84
  * Documentation paths for different types of plugin documentation.
85
85
  */
@@ -107,7 +107,7 @@ export interface Tool {
107
107
  parameters: {
108
108
  name: string;
109
109
  description: string;
110
- type: 'string' | 'number' | 'boolean';
110
+ type: "string" | "number" | "boolean";
111
111
  }[];
112
112
  execute?: (args: Record<string, any>) => Promise<unknown> | unknown | void;
113
113
  }
@@ -0,0 +1,11 @@
1
+ import { TOptions } from 'i18next';
2
+ type TranslatorFn = (key: string, options?: TOptions) => string;
3
+ /**
4
+ * Custom useTranslation hook that provides a translation function and indicates readiness
5
+ * @returns An object containing the translation function (`t`) and a boolean (`ready`) indicating if the translator is initialized.
6
+ */
7
+ export declare function useTranslation(): {
8
+ t: TranslatorFn;
9
+ ready: boolean;
10
+ };
11
+ export {};
@@ -0,0 +1,25 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useRimori } from '../providers/PluginProvider';
3
+ /**
4
+ * Custom useTranslation hook that provides a translation function and indicates readiness
5
+ * @returns An object containing the translation function (`t`) and a boolean (`ready`) indicating if the translator is initialized.
6
+ */
7
+ export function useTranslation() {
8
+ const { plugin } = useRimori();
9
+ const [translatorInstance, setTranslatorInstance] = useState(null);
10
+ useEffect(() => {
11
+ void plugin.getTranslator().then(setTranslatorInstance);
12
+ }, [plugin]);
13
+ const safeT = (key, options) => {
14
+ // return zero-width space if translator is not initialized to keep text space occupied
15
+ if (!translatorInstance)
16
+ return '\u200B'; // zero-width space
17
+ const result = translatorInstance.t(key, options);
18
+ if (!result) {
19
+ console.error(`Translation key not found: ${key}`);
20
+ return '\u200B'; // zero-width space
21
+ }
22
+ return result;
23
+ };
24
+ return { t: safeT, ready: translatorInstance !== null };
25
+ }
@@ -0,0 +1,11 @@
1
+ import { TOptions } from 'i18next';
2
+ type TranslatorFn = (key: string, options?: TOptions) => string;
3
+ /**
4
+ * Custom useTranslation hook that provides a translation function and indicates readiness
5
+ * @returns An object containing the translation function (`t`) and a boolean (`ready`) indicating if the translator is initialized.
6
+ */
7
+ export declare function useTranslation(): {
8
+ t: TranslatorFn;
9
+ ready: boolean;
10
+ };
11
+ export {};