@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.
- 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
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,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,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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 = /(.+?[
|
|
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;
|
package/dist/core/core.d.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 ===
|
|
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 +
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 !==
|
|
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 = [
|
|
243
|
-
if (validActions.some(
|
|
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 +
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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 {};
|