@rimori/client 2.5.32 → 2.5.33
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/dist/cli/scripts/init/dev-registration.js +118 -136
- package/dist/cli/scripts/init/main.js +116 -127
- package/dist/cli/scripts/init/package-setup.js +15 -2
- package/dist/cli/scripts/release/detect-translation-languages.js +24 -35
- package/dist/cli/scripts/release/release-config-upload.js +87 -100
- package/dist/cli/scripts/release/release-db-update.js +70 -81
- package/dist/cli/scripts/release/release-file-upload.js +75 -91
- package/dist/cli/scripts/release/release-prompts-upload.js +60 -72
- package/dist/cli/scripts/release/release.js +20 -31
- package/dist/controller/AccomplishmentController.js +12 -12
- package/dist/controller/AudioController.js +15 -33
- package/dist/controller/TranslationController.js +108 -118
- package/dist/fromRimori/EventBus.js +20 -31
- package/dist/plugin/CommunicationHandler.js +73 -81
- package/dist/plugin/Logger.js +71 -83
- package/dist/plugin/RimoriClient.js +31 -31
- package/dist/plugin/StandaloneClient.js +81 -98
- package/dist/plugin/TTS/ChunkedAudioPlayer.js +31 -41
- package/dist/plugin/TTS/MessageSender.js +28 -37
- package/dist/plugin/module/AIModule.js +215 -237
- package/dist/plugin/module/DbModule.js +22 -31
- package/dist/plugin/module/EventModule.js +23 -32
- package/dist/plugin/module/ExerciseModule.js +42 -56
- package/dist/plugin/module/PluginModule.js +97 -106
- package/dist/plugin/module/SharedContentController.js +170 -207
- package/dist/plugin/module/StorageModule.js +18 -29
- package/dist/worker/WorkerSetup.js +23 -34
- package/package.json +1 -1
|
@@ -1,12 +1,3 @@
|
|
|
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
1
|
import fs from 'fs';
|
|
11
2
|
import path from 'path';
|
|
12
3
|
import ts from 'typescript';
|
|
@@ -16,73 +7,70 @@ import ts from 'typescript';
|
|
|
16
7
|
* @param config - Configuration object
|
|
17
8
|
* @param release_id - The release ID
|
|
18
9
|
*/
|
|
19
|
-
export default function promptsUpload(config, release_id) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
10
|
+
export default async function promptsUpload(config, release_id) {
|
|
11
|
+
const promptsConfigPath = path.resolve('./rimori/prompts.config.ts');
|
|
12
|
+
// Check if prompts config file exists — optional, skip if not present
|
|
13
|
+
try {
|
|
14
|
+
await fs.promises.access(promptsConfigPath);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
return; // No prompts.config.ts — silently skip
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
// Use TypeScript compiler to transpile and load
|
|
21
|
+
const promptsContent = await fs.promises.readFile(promptsConfigPath, 'utf8');
|
|
22
|
+
// Transpile TypeScript to JavaScript
|
|
23
|
+
const result = ts.transpile(promptsContent, {
|
|
24
|
+
target: ts.ScriptTarget.ES2020,
|
|
25
|
+
module: ts.ModuleKind.ES2020,
|
|
26
|
+
});
|
|
27
|
+
// Create a temporary file to import the transpiled code
|
|
28
|
+
const tempFile = path.join(process.cwd(), 'temp_prompts_config.js');
|
|
29
|
+
await fs.promises.writeFile(tempFile, result);
|
|
30
|
+
let prompts;
|
|
24
31
|
try {
|
|
25
|
-
|
|
32
|
+
const promptsModule = await import(`file://${tempFile}`);
|
|
33
|
+
// Collect all named exports as individual prompt definitions
|
|
34
|
+
prompts = Object.values(promptsModule);
|
|
35
|
+
await fs.promises.unlink(tempFile);
|
|
26
36
|
}
|
|
27
|
-
catch (
|
|
28
|
-
return; // No prompts.config.ts — silently skip
|
|
29
|
-
}
|
|
30
|
-
try {
|
|
31
|
-
// Use TypeScript compiler to transpile and load
|
|
32
|
-
const promptsContent = yield fs.promises.readFile(promptsConfigPath, 'utf8');
|
|
33
|
-
// Transpile TypeScript to JavaScript
|
|
34
|
-
const result = ts.transpile(promptsContent, {
|
|
35
|
-
target: ts.ScriptTarget.ES2020,
|
|
36
|
-
module: ts.ModuleKind.ES2020,
|
|
37
|
-
});
|
|
38
|
-
// Create a temporary file to import the transpiled code
|
|
39
|
-
const tempFile = path.join(process.cwd(), 'temp_prompts_config.js');
|
|
40
|
-
yield fs.promises.writeFile(tempFile, result);
|
|
41
|
-
let prompts;
|
|
37
|
+
catch (error) {
|
|
42
38
|
try {
|
|
43
|
-
|
|
44
|
-
// Collect all named exports as individual prompt definitions
|
|
45
|
-
prompts = Object.values(promptsModule);
|
|
46
|
-
yield fs.promises.unlink(tempFile);
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
try {
|
|
50
|
-
yield fs.promises.unlink(tempFile);
|
|
51
|
-
}
|
|
52
|
-
catch (e) { }
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
if (!Array.isArray(prompts) || prompts.length === 0) {
|
|
56
|
-
console.warn('⚠️ prompts.config.ts has no exports. Skipping.');
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
console.log(`📝 Sending ${prompts.length} prompt definitions...`);
|
|
60
|
-
const requestBody = {
|
|
61
|
-
prompts,
|
|
62
|
-
version: config.version,
|
|
63
|
-
release_channel: config.release_channel,
|
|
64
|
-
plugin_id: config.plugin_id,
|
|
65
|
-
};
|
|
66
|
-
const response = yield fetch(`${config.domain}/release/${release_id}/prompts`, {
|
|
67
|
-
method: 'POST',
|
|
68
|
-
headers: {
|
|
69
|
-
'Content-Type': 'application/json',
|
|
70
|
-
Authorization: `Bearer ${config.token}`,
|
|
71
|
-
},
|
|
72
|
-
body: JSON.stringify(requestBody),
|
|
73
|
-
});
|
|
74
|
-
if (response.ok) {
|
|
75
|
-
const data = yield response.json();
|
|
76
|
-
console.log(`✅ Prompts uploaded: ${(_b = (_a = data.prompt_names) === null || _a === void 0 ? void 0 : _a.join(', ')) !== null && _b !== void 0 ? _b : 'ok'}`);
|
|
39
|
+
await fs.promises.unlink(tempFile);
|
|
77
40
|
}
|
|
78
|
-
|
|
79
|
-
const text = yield response.text().catch(() => 'unknown error');
|
|
80
|
-
throw new Error(`Failed to upload prompts: ${text}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
console.error('❌ Error uploading prompts:', error.message);
|
|
41
|
+
catch (e) { }
|
|
85
42
|
throw error;
|
|
86
43
|
}
|
|
87
|
-
|
|
44
|
+
if (!Array.isArray(prompts) || prompts.length === 0) {
|
|
45
|
+
console.warn('⚠️ prompts.config.ts has no exports. Skipping.');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
console.log(`📝 Sending ${prompts.length} prompt definitions...`);
|
|
49
|
+
const requestBody = {
|
|
50
|
+
prompts,
|
|
51
|
+
version: config.version,
|
|
52
|
+
release_channel: config.release_channel,
|
|
53
|
+
plugin_id: config.plugin_id,
|
|
54
|
+
};
|
|
55
|
+
const response = await fetch(`${config.domain}/release/${release_id}/prompts`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
Authorization: `Bearer ${config.token}`,
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(requestBody),
|
|
62
|
+
});
|
|
63
|
+
if (response.ok) {
|
|
64
|
+
const data = await response.json();
|
|
65
|
+
console.log(`✅ Prompts uploaded: ${data.prompt_names?.join(', ') ?? 'ok'}`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const text = await response.text().catch(() => 'unknown error');
|
|
69
|
+
throw new Error(`Failed to upload prompts: ${text}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
console.error('❌ Error uploading prompts:', error.message);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
88
76
|
}
|
|
@@ -10,15 +10,6 @@
|
|
|
10
10
|
* Make sure to install dependencies:
|
|
11
11
|
* npm install node-fetch form-data ts-node typescript
|
|
12
12
|
*/
|
|
13
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
16
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
17
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
18
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
19
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
20
|
-
});
|
|
21
|
-
};
|
|
22
13
|
import 'dotenv/config';
|
|
23
14
|
import fs from 'fs';
|
|
24
15
|
import path from 'path';
|
|
@@ -54,27 +45,25 @@ const config = {
|
|
|
54
45
|
/**
|
|
55
46
|
* Main release process
|
|
56
47
|
*/
|
|
57
|
-
function releaseProcess() {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
});
|
|
48
|
+
async function releaseProcess() {
|
|
49
|
+
try {
|
|
50
|
+
console.log(`🚀 Releasing ${config.plugin_id} to ${config.release_channel}...`);
|
|
51
|
+
console.log(`📡 Deploying to: ${config.domain}`);
|
|
52
|
+
// First send the configuration
|
|
53
|
+
const release_id = await sendConfiguration(config);
|
|
54
|
+
// Upload prompts (if prompts.config.ts exists)
|
|
55
|
+
await promptsUpload(config, release_id);
|
|
56
|
+
await dbUpdate(config, release_id);
|
|
57
|
+
// Then upload the files
|
|
58
|
+
await uploadDirectory(config, release_id);
|
|
59
|
+
// Then release the plugin
|
|
60
|
+
await releasePlugin(config, release_id);
|
|
61
|
+
// Inform user about translation processing
|
|
62
|
+
console.log('🌐 Hint: The plugin is released but it might take some time until all translations are being processed.');
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.log('❌ Error:', error.message);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
79
68
|
}
|
|
80
69
|
releaseProcess();
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { EventBus } from '../fromRimori/EventBus';
|
|
2
2
|
export const skillCategories = ['reading', 'listening', 'speaking', 'writing', 'learning', 'community'];
|
|
3
3
|
export class AccomplishmentController {
|
|
4
|
+
pluginId;
|
|
5
|
+
eventBus;
|
|
4
6
|
constructor(pluginId, eventBus) {
|
|
5
7
|
this.pluginId = pluginId;
|
|
6
|
-
this.eventBus = eventBus
|
|
8
|
+
this.eventBus = eventBus ?? EventBus;
|
|
7
9
|
}
|
|
8
10
|
emitAccomplishment(payload) {
|
|
9
|
-
const accomplishmentPayload =
|
|
11
|
+
const accomplishmentPayload = {
|
|
12
|
+
...payload,
|
|
13
|
+
type: 'durationMinutes' in payload ? 'macro' : 'micro',
|
|
14
|
+
};
|
|
10
15
|
if (!this.validateAccomplishment(accomplishmentPayload)) {
|
|
11
16
|
return;
|
|
12
17
|
}
|
|
@@ -40,22 +45,17 @@ export class AccomplishmentController {
|
|
|
40
45
|
if (payload.type === 'macro' && (payload.errorRatio < 0 || payload.errorRatio > 1)) {
|
|
41
46
|
throw new Error('The error ratio must be between 0 and 1');
|
|
42
47
|
}
|
|
43
|
-
//regex check meta data key
|
|
44
|
-
if (payload.meta) {
|
|
45
|
-
payload.meta.forEach((meta) => {
|
|
46
|
-
if (!/^[a-z_]+$/.test(meta.key)) {
|
|
47
|
-
throw new Error('Invalid meta data key ' + meta.key + ', only lowercase letters and underscores are allowed');
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
48
|
return true;
|
|
52
49
|
}
|
|
53
50
|
sanitizeAccomplishment(payload) {
|
|
54
|
-
var _a;
|
|
55
51
|
payload.description = payload.description.replace(/[^\x20-\x7E]/g, '');
|
|
56
|
-
|
|
52
|
+
payload.meta?.forEach((meta) => {
|
|
57
53
|
meta.description = meta.description.replace(/[^\x20-\x7E]/g, '');
|
|
58
54
|
});
|
|
55
|
+
//convert meta keys to snakecase
|
|
56
|
+
payload.meta?.forEach((meta) => {
|
|
57
|
+
meta.key = meta.key.replace(/([A-Z])/g, '_$1').toLowerCase();
|
|
58
|
+
});
|
|
59
59
|
return payload;
|
|
60
60
|
}
|
|
61
61
|
getDecoupledTopic(topic) {
|
|
@@ -1,12 +1,3 @@
|
|
|
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
1
|
import { EventBus } from '../fromRimori/EventBus';
|
|
11
2
|
/**
|
|
12
3
|
* AudioController is a class that provides methods to record audio. It is a wrapper around the Capacitor Voice Recorder plugin. For more information, see https://github.com/tchvu3/capacitor-voice-recorder.
|
|
@@ -16,6 +7,7 @@ import { EventBus } from '../fromRimori/EventBus';
|
|
|
16
7
|
* await audioController.startRecording();
|
|
17
8
|
*/
|
|
18
9
|
export class AudioController {
|
|
10
|
+
pluginId;
|
|
19
11
|
constructor(pluginId) {
|
|
20
12
|
this.pluginId = pluginId;
|
|
21
13
|
}
|
|
@@ -27,10 +19,8 @@ export class AudioController {
|
|
|
27
19
|
* await audioController.startRecording();
|
|
28
20
|
* @returns void
|
|
29
21
|
*/
|
|
30
|
-
startRecording() {
|
|
31
|
-
|
|
32
|
-
EventBus.emit(this.pluginId, 'global.microphone.triggerStartRecording');
|
|
33
|
-
});
|
|
22
|
+
async startRecording() {
|
|
23
|
+
EventBus.emit(this.pluginId, 'global.microphone.triggerStartRecording');
|
|
34
24
|
}
|
|
35
25
|
/**
|
|
36
26
|
* Stop the recording and return the audio data.
|
|
@@ -41,28 +31,20 @@ export class AudioController {
|
|
|
41
31
|
* audioRef.oncanplaythrough = () => audioRef.play()
|
|
42
32
|
* audioRef.load()
|
|
43
33
|
*/
|
|
44
|
-
stopRecording() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return result.data;
|
|
48
|
-
});
|
|
34
|
+
async stopRecording() {
|
|
35
|
+
const result = await EventBus.request(this.pluginId, 'global.microphone.triggerStopRecording');
|
|
36
|
+
return result.data;
|
|
49
37
|
}
|
|
50
|
-
pauseRecording() {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return result.data;
|
|
54
|
-
});
|
|
38
|
+
async pauseRecording() {
|
|
39
|
+
const result = await EventBus.request(this.pluginId, 'global.microphone.triggerPauseRecording');
|
|
40
|
+
return result.data;
|
|
55
41
|
}
|
|
56
|
-
resumeRecording() {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return result.data;
|
|
60
|
-
});
|
|
42
|
+
async resumeRecording() {
|
|
43
|
+
const result = await EventBus.request(this.pluginId, 'global.microphone.triggerResumeRecording');
|
|
44
|
+
return result.data;
|
|
61
45
|
}
|
|
62
|
-
getCurrentStatus() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return result.data;
|
|
66
|
-
});
|
|
46
|
+
async getCurrentStatus() {
|
|
47
|
+
const result = await EventBus.request(this.pluginId, 'global.microphone.triggerGetCurrentStatus');
|
|
48
|
+
return result.data;
|
|
67
49
|
}
|
|
68
50
|
}
|
|
@@ -1,20 +1,17 @@
|
|
|
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
1
|
import { createInstance } from 'i18next';
|
|
11
2
|
/**
|
|
12
3
|
* Translator class for handling internationalization
|
|
13
4
|
*/
|
|
14
5
|
export class Translator {
|
|
6
|
+
currentLanguage;
|
|
7
|
+
initializationState;
|
|
8
|
+
initializationPromise;
|
|
9
|
+
i18n;
|
|
10
|
+
translationUrl;
|
|
11
|
+
ai;
|
|
12
|
+
aiTranslationCache = new Map();
|
|
13
|
+
aiTranslationPending = new Map();
|
|
15
14
|
constructor(initialLanguage, translationUrl, ai) {
|
|
16
|
-
this.aiTranslationCache = new Map();
|
|
17
|
-
this.aiTranslationPending = new Map();
|
|
18
15
|
this.currentLanguage = initialLanguage;
|
|
19
16
|
this.initializationState = 'not-inited';
|
|
20
17
|
this.initializationPromise = null;
|
|
@@ -25,59 +22,56 @@ export class Translator {
|
|
|
25
22
|
* Initialize translator with user's language
|
|
26
23
|
* @param userLanguage - Language code from user info
|
|
27
24
|
*/
|
|
28
|
-
initialize() {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
async initialize() {
|
|
26
|
+
// If already finished, return immediately
|
|
27
|
+
if (this.initializationState === 'finished') {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// If currently initializing, wait for the existing initialization to complete
|
|
31
|
+
if (this.initializationState === 'initing' && this.initializationPromise) {
|
|
32
|
+
return this.initializationPromise;
|
|
33
|
+
}
|
|
34
|
+
// Start initialization
|
|
35
|
+
this.initializationState = 'initing';
|
|
36
|
+
// Create a promise that will be resolved when initialization completes
|
|
37
|
+
this.initializationPromise = (async () => {
|
|
38
|
+
try {
|
|
39
|
+
const translations = await this.fetchTranslations(this.currentLanguage);
|
|
40
|
+
const instance = createInstance({
|
|
41
|
+
lng: this.currentLanguage,
|
|
42
|
+
resources: {
|
|
43
|
+
[this.currentLanguage]: {
|
|
44
|
+
translation: translations,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
debug: false,
|
|
48
|
+
showSupportNotice: false,
|
|
49
|
+
parseMissingKeyHandler: (key, defaultValue) => {
|
|
50
|
+
if (!key.trim())
|
|
51
|
+
return '';
|
|
52
|
+
if (this.isTranslationKey(key)) {
|
|
53
|
+
console.warn(`Translation key not found: ${key}`);
|
|
54
|
+
return defaultValue ?? '';
|
|
55
|
+
}
|
|
56
|
+
void this.fetchTranslation(key).then((translation) => {
|
|
57
|
+
this.i18n?.addResource(this.currentLanguage, 'translation', key, translation);
|
|
58
|
+
this.i18n?.emit('languageChanged'); // triggers re-render
|
|
59
|
+
});
|
|
60
|
+
return key;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
await instance.init();
|
|
64
|
+
this.i18n = instance;
|
|
65
|
+
this.initializationState = 'finished';
|
|
33
66
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
67
|
+
catch (error) {
|
|
68
|
+
// Reset state on error so it can be retried
|
|
69
|
+
this.initializationState = 'not-inited';
|
|
70
|
+
this.initializationPromise = null;
|
|
71
|
+
throw error;
|
|
37
72
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// Create a promise that will be resolved when initialization completes
|
|
41
|
-
this.initializationPromise = (() => __awaiter(this, void 0, void 0, function* () {
|
|
42
|
-
try {
|
|
43
|
-
const translations = yield this.fetchTranslations(this.currentLanguage);
|
|
44
|
-
const instance = createInstance({
|
|
45
|
-
lng: this.currentLanguage,
|
|
46
|
-
resources: {
|
|
47
|
-
[this.currentLanguage]: {
|
|
48
|
-
translation: translations,
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
debug: false,
|
|
52
|
-
showSupportNotice: false,
|
|
53
|
-
parseMissingKeyHandler: (key, defaultValue) => {
|
|
54
|
-
if (!key.trim())
|
|
55
|
-
return '';
|
|
56
|
-
if (this.isTranslationKey(key)) {
|
|
57
|
-
console.warn(`Translation key not found: ${key}`);
|
|
58
|
-
return defaultValue !== null && defaultValue !== void 0 ? defaultValue : '';
|
|
59
|
-
}
|
|
60
|
-
void this.fetchTranslation(key).then((translation) => {
|
|
61
|
-
var _a, _b;
|
|
62
|
-
(_a = this.i18n) === null || _a === void 0 ? void 0 : _a.addResource(this.currentLanguage, 'translation', key, translation);
|
|
63
|
-
(_b = this.i18n) === null || _b === void 0 ? void 0 : _b.emit('languageChanged'); // triggers re-render
|
|
64
|
-
});
|
|
65
|
-
return key;
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
yield instance.init();
|
|
69
|
-
this.i18n = instance;
|
|
70
|
-
this.initializationState = 'finished';
|
|
71
|
-
}
|
|
72
|
-
catch (error) {
|
|
73
|
-
// Reset state on error so it can be retried
|
|
74
|
-
this.initializationState = 'not-inited';
|
|
75
|
-
this.initializationPromise = null;
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
}))();
|
|
79
|
-
return this.initializationPromise;
|
|
80
|
-
});
|
|
73
|
+
})();
|
|
74
|
+
return this.initializationPromise;
|
|
81
75
|
}
|
|
82
76
|
getTranslationUrl(language) {
|
|
83
77
|
const baseUrl = this.translationUrl || window.location.origin;
|
|
@@ -105,26 +99,24 @@ export class Translator {
|
|
|
105
99
|
* @param language - Language code to fetch
|
|
106
100
|
* @returns Promise with translation data
|
|
107
101
|
*/
|
|
108
|
-
fetchTranslations(language) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
throw new Error(`Failed to fetch translations for ${language}`);
|
|
114
|
-
}
|
|
115
|
-
return (yield response.json());
|
|
102
|
+
async fetchTranslations(language) {
|
|
103
|
+
try {
|
|
104
|
+
const response = await fetch(this.getTranslationUrl(language));
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`Failed to fetch translations for ${language}`);
|
|
116
107
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
108
|
+
return (await response.json());
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.warn(`Failed to fetch translations for ${language}:`, error);
|
|
112
|
+
if (language === 'en')
|
|
113
|
+
return {};
|
|
114
|
+
// Fallback to English
|
|
115
|
+
return this.fetchTranslations('en').catch((error) => {
|
|
116
|
+
console.error('Failed to fetch fallback translations:', error);
|
|
117
|
+
return {};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
128
120
|
}
|
|
129
121
|
/**
|
|
130
122
|
* Get translation for a key or freeform text. If the key is not a valid translation key, the freeform text is translated using AI and cached.
|
|
@@ -154,43 +146,41 @@ export class Translator {
|
|
|
154
146
|
isTranslationKey(key) {
|
|
155
147
|
return /^[^\s.]+(\.[^\s.]+)+$/.test(key);
|
|
156
148
|
}
|
|
157
|
-
fetchTranslation(text, additionalInstructions) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return translation;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
console.warn('Failed to translate freeform text:', { text, error });
|
|
186
|
-
}
|
|
187
|
-
finally {
|
|
188
|
-
this.aiTranslationPending.delete(text);
|
|
149
|
+
async fetchTranslation(text, additionalInstructions) {
|
|
150
|
+
const cached = this.aiTranslationCache.get(text);
|
|
151
|
+
if (cached)
|
|
152
|
+
return cached;
|
|
153
|
+
const pending = this.aiTranslationPending.get(text);
|
|
154
|
+
if (pending)
|
|
155
|
+
return pending;
|
|
156
|
+
if (!this.ai || this.currentLanguage === 'en')
|
|
157
|
+
return text;
|
|
158
|
+
const promise = (async () => {
|
|
159
|
+
try {
|
|
160
|
+
const response = await this.ai.getObject({
|
|
161
|
+
prompt: 'global.translator.translate',
|
|
162
|
+
variables: {
|
|
163
|
+
additionalInstructions: additionalInstructions ?? '',
|
|
164
|
+
language: this.currentLanguage,
|
|
165
|
+
text,
|
|
166
|
+
},
|
|
167
|
+
cache: true,
|
|
168
|
+
});
|
|
169
|
+
const translation = response?.translation;
|
|
170
|
+
if (translation) {
|
|
171
|
+
this.aiTranslationCache.set(text, translation);
|
|
172
|
+
return translation;
|
|
189
173
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.warn('Failed to translate freeform text:', { text, error });
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
this.aiTranslationPending.delete(text);
|
|
180
|
+
}
|
|
181
|
+
return text;
|
|
182
|
+
})();
|
|
183
|
+
this.aiTranslationPending.set(text, promise);
|
|
184
|
+
return promise;
|
|
195
185
|
}
|
|
196
186
|
}
|