@ollang-dev/sdk 0.3.1
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/bin/tms.js +47 -0
- package/dist/browser/index.d.ts +143 -0
- package/dist/browser/index.js +2336 -0
- package/dist/browser/ollang-browser.min.js +1 -0
- package/dist/browser/script-loader.d.ts +1 -0
- package/dist/browser/script-loader.js +53 -0
- package/dist/client.d.ts +13 -0
- package/dist/client.js +60 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +74 -0
- package/dist/resources/cms.d.ts +29 -0
- package/dist/resources/cms.js +34 -0
- package/dist/resources/customInstructions.d.ts +11 -0
- package/dist/resources/customInstructions.js +24 -0
- package/dist/resources/orders.d.ts +13 -0
- package/dist/resources/orders.js +65 -0
- package/dist/resources/projects.d.ts +8 -0
- package/dist/resources/projects.js +29 -0
- package/dist/resources/revisions.d.ts +9 -0
- package/dist/resources/revisions.js +18 -0
- package/dist/resources/scans.d.ts +38 -0
- package/dist/resources/scans.js +52 -0
- package/dist/resources/uploads.d.ts +8 -0
- package/dist/resources/uploads.js +25 -0
- package/dist/tms/config.d.ts +16 -0
- package/dist/tms/config.js +172 -0
- package/dist/tms/detector/audio-detector.d.ts +27 -0
- package/dist/tms/detector/audio-detector.js +168 -0
- package/dist/tms/detector/auto-detect.d.ts +1 -0
- package/dist/tms/detector/auto-detect.js +152 -0
- package/dist/tms/detector/cms-detector.d.ts +17 -0
- package/dist/tms/detector/cms-detector.js +94 -0
- package/dist/tms/detector/content-type-detector.d.ts +79 -0
- package/dist/tms/detector/content-type-detector.js +2 -0
- package/dist/tms/detector/hardcoded-detector.d.ts +11 -0
- package/dist/tms/detector/hardcoded-detector.js +154 -0
- package/dist/tms/detector/i18n-detector.d.ts +16 -0
- package/dist/tms/detector/i18n-detector.js +311 -0
- package/dist/tms/detector/image-detector.d.ts +11 -0
- package/dist/tms/detector/image-detector.js +170 -0
- package/dist/tms/detector/text-detector.d.ts +9 -0
- package/dist/tms/detector/text-detector.js +35 -0
- package/dist/tms/detector/video-detector.d.ts +12 -0
- package/dist/tms/detector/video-detector.js +188 -0
- package/dist/tms/index.d.ts +12 -0
- package/dist/tms/index.js +38 -0
- package/dist/tms/multi-content-tms.d.ts +42 -0
- package/dist/tms/multi-content-tms.js +230 -0
- package/dist/tms/server/index.d.ts +1 -0
- package/dist/tms/server/index.js +1473 -0
- package/dist/tms/server/strapi-pusher.d.ts +31 -0
- package/dist/tms/server/strapi-pusher.js +296 -0
- package/dist/tms/server/strapi-schema.d.ts +47 -0
- package/dist/tms/server/strapi-schema.js +93 -0
- package/dist/tms/tms.d.ts +48 -0
- package/dist/tms/tms.js +875 -0
- package/dist/tms/types.d.ts +189 -0
- package/dist/tms/types.js +2 -0
- package/dist/tms/ui-dist/assets/index-5U1Hw3uo.css +1 -0
- package/dist/tms/ui-dist/assets/index-HvrqZV5Z.js +149 -0
- package/dist/tms/ui-dist/index.html +14 -0
- package/dist/types/index.d.ts +174 -0
- package/dist/types/index.js +2 -0
- package/package.json +98 -0
|
@@ -0,0 +1,1473 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const tms_js_1 = require("../tms.js");
|
|
9
|
+
const auto_detect_js_1 = require("../detector/auto-detect.js");
|
|
10
|
+
const strapi_pusher_js_1 = require("./strapi-pusher.js");
|
|
11
|
+
const strapi_schema_js_1 = require("./strapi-schema.js");
|
|
12
|
+
const app = (0, express_1.default)();
|
|
13
|
+
const strapiSchemaCache = new Map();
|
|
14
|
+
const PORT = process.env.TMS_PORT || 5972;
|
|
15
|
+
const PROJECT_ROOT = process.env.TMS_PROJECT_ROOT || process.cwd();
|
|
16
|
+
const LANG_REGEX = /^[a-zA-Z]{2,5}(-[a-zA-Z0-9]{2,8})?$/;
|
|
17
|
+
const VALID_VIDEO_TYPES = ['aiDubbing', 'subtitle'];
|
|
18
|
+
const ALLOWED_ORIGINS = new Set([
|
|
19
|
+
`http://localhost:${PORT}`,
|
|
20
|
+
`http://127.0.0.1:${PORT}`,
|
|
21
|
+
'http://localhost:3000',
|
|
22
|
+
'http://localhost:5173',
|
|
23
|
+
'http://localhost:8080',
|
|
24
|
+
'http://localhost:4200',
|
|
25
|
+
'http://localhost:8000',
|
|
26
|
+
]);
|
|
27
|
+
// Allow additional origins via env var
|
|
28
|
+
const extraOrigins = (process.env.TMS_CORS_ORIGINS || '').split(',').filter(Boolean);
|
|
29
|
+
extraOrigins.forEach((o) => ALLOWED_ORIGINS.add(o.trim()));
|
|
30
|
+
app.use((req, res, next) => {
|
|
31
|
+
const origin = req.headers.origin;
|
|
32
|
+
if (origin && ALLOWED_ORIGINS.has(origin)) {
|
|
33
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
34
|
+
res.header('Access-Control-Allow-Credentials', 'true');
|
|
35
|
+
}
|
|
36
|
+
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
|
|
37
|
+
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, x-api-key, x-strapi-token');
|
|
38
|
+
if (req.method === 'OPTIONS') {
|
|
39
|
+
return res.sendStatus(204);
|
|
40
|
+
}
|
|
41
|
+
next();
|
|
42
|
+
});
|
|
43
|
+
app.use(express_1.default.json());
|
|
44
|
+
const UNPROTECTED_ROUTES = [
|
|
45
|
+
{ method: 'POST', path: '/api/config/apikey' },
|
|
46
|
+
{ method: 'GET', path: '/api/config' },
|
|
47
|
+
];
|
|
48
|
+
app.use('/api', (req, res, next) => {
|
|
49
|
+
const isUnprotected = UNPROTECTED_ROUTES.some((r) => r.method === req.method && req.path === r.path.replace('/api', ''));
|
|
50
|
+
if (isUnprotected)
|
|
51
|
+
return next();
|
|
52
|
+
const configuredKey = process.env.OLLANG_API_KEY;
|
|
53
|
+
if (!configuredKey)
|
|
54
|
+
return next(); // first-time setup, no key configured yet
|
|
55
|
+
const providedKey = req.headers['x-api-key'] ||
|
|
56
|
+
(req.headers.authorization?.startsWith('Bearer ')
|
|
57
|
+
? req.headers.authorization.slice(7)
|
|
58
|
+
: '');
|
|
59
|
+
if (providedKey !== configuredKey) {
|
|
60
|
+
return res.status(401).json({ success: false, error: 'Unauthorized: invalid or missing API key' });
|
|
61
|
+
}
|
|
62
|
+
next();
|
|
63
|
+
});
|
|
64
|
+
const uiDistPath = path_1.default.join(__dirname, '../ui-dist');
|
|
65
|
+
app.use(express_1.default.static(uiDistPath));
|
|
66
|
+
let tms = null;
|
|
67
|
+
const folderStates = new Map();
|
|
68
|
+
const DEFAULT_FOLDER_KEY = '__default__';
|
|
69
|
+
function getFolderKey(folderName) {
|
|
70
|
+
return folderName && folderName.trim().length > 0 ? folderName : DEFAULT_FOLDER_KEY;
|
|
71
|
+
}
|
|
72
|
+
function getOrCreateFolderState(folderName) {
|
|
73
|
+
const key = getFolderKey(folderName);
|
|
74
|
+
let state = folderStates.get(key);
|
|
75
|
+
if (!state) {
|
|
76
|
+
state = {
|
|
77
|
+
currentScanId: null,
|
|
78
|
+
texts: [],
|
|
79
|
+
videos: [],
|
|
80
|
+
images: [],
|
|
81
|
+
audios: [],
|
|
82
|
+
};
|
|
83
|
+
folderStates.set(key, state);
|
|
84
|
+
}
|
|
85
|
+
return state;
|
|
86
|
+
}
|
|
87
|
+
async function updateCurrentScan(folderName) {
|
|
88
|
+
const state = getOrCreateFolderState(folderName);
|
|
89
|
+
const { currentScanId, texts, videos, images, audios } = state;
|
|
90
|
+
if (!tms) {
|
|
91
|
+
console.warn('⚠️ Cannot update scan: TMS not initialized');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const sdk = tms.getSDK();
|
|
96
|
+
if (!sdk) {
|
|
97
|
+
console.warn('⚠️ SDK not available');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!currentScanId) {
|
|
101
|
+
const session = await sdk.initializeScanSession(tms.getConfig().ollang.projectId);
|
|
102
|
+
const previousScans = session.scannedDocs || [];
|
|
103
|
+
if (previousScans.length > 0) {
|
|
104
|
+
state.currentScanId = previousScans[0].id;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.warn('⚠️ No existing scan found, cannot update');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
let existingScanData = {};
|
|
112
|
+
try {
|
|
113
|
+
const existingScan = await sdk.scans.getScan(state.currentScanId);
|
|
114
|
+
existingScanData =
|
|
115
|
+
typeof existingScan.scanData === 'string'
|
|
116
|
+
? JSON.parse(existingScan.scanData)
|
|
117
|
+
: existingScan.scanData || {};
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.warn('⚠️ Could not load existing scan data while updating scan:', error?.message || error);
|
|
121
|
+
existingScanData = {};
|
|
122
|
+
}
|
|
123
|
+
const i18nSetup = tms.getI18nSetup();
|
|
124
|
+
await sdk.scans.updateScan(state.currentScanId, {
|
|
125
|
+
scanData: {
|
|
126
|
+
...existingScanData,
|
|
127
|
+
texts,
|
|
128
|
+
videos,
|
|
129
|
+
images,
|
|
130
|
+
i18nSetup,
|
|
131
|
+
timestamp: new Date().toISOString(),
|
|
132
|
+
projectRoot: tms.getConfig().projectRoot,
|
|
133
|
+
sourceLanguage: tms.getConfig().sourceLanguage,
|
|
134
|
+
targetLanguages: tms.getConfig().targetLanguages,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
console.error('❌ Failed to update scan:', error.message);
|
|
140
|
+
console.error('❌ Error stack:', error.stack);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function initTMS() {
|
|
144
|
+
const apiKey = process.env.OLLANG_API_KEY || '';
|
|
145
|
+
const projectId = process.env.OLLANG_PROJECT_ID;
|
|
146
|
+
let includePaths = process.env.TMS_INCLUDE_PATHS?.split(',') || [];
|
|
147
|
+
if (includePaths.length === 0) {
|
|
148
|
+
const i18nDirs = await (0, auto_detect_js_1.autoDetectI18nDirs)(PROJECT_ROOT);
|
|
149
|
+
includePaths = ['.'];
|
|
150
|
+
}
|
|
151
|
+
let fileConfig = {};
|
|
152
|
+
try {
|
|
153
|
+
const fs = require('fs');
|
|
154
|
+
const configPath = path_1.default.join(PROJECT_ROOT, 'ollang.config.ts');
|
|
155
|
+
if (fs.existsSync(configPath)) {
|
|
156
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
157
|
+
const srcMatch = raw.match(/sourceLanguage['": \s]+['"]([^'"]+)['"]/);
|
|
158
|
+
if (srcMatch) {
|
|
159
|
+
fileConfig.sourceLanguage = srcMatch[1];
|
|
160
|
+
}
|
|
161
|
+
const targetsMatch = raw.match(/targetLanguages['": \s]+(\[[^\]]*\])/);
|
|
162
|
+
if (targetsMatch) {
|
|
163
|
+
try {
|
|
164
|
+
const jsonReady = targetsMatch[1].replace(/'/g, '"');
|
|
165
|
+
const arr = JSON.parse(jsonReady);
|
|
166
|
+
if (Array.isArray(arr)) {
|
|
167
|
+
fileConfig.targetLanguages = arr.map((v) => String(v));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch { }
|
|
171
|
+
}
|
|
172
|
+
const videoMatch = raw.match(/translationType['": \s]+['"]([^'"]+)['"]/);
|
|
173
|
+
if (videoMatch) {
|
|
174
|
+
fileConfig.video = { translationType: videoMatch[1] };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
console.warn('⚠️ Failed to read ollang.config.ts, falling back to env/defaults');
|
|
180
|
+
}
|
|
181
|
+
const sourceLanguage = fileConfig.sourceLanguage || process.env.TMS_SOURCE_LANGUAGE || 'en';
|
|
182
|
+
const targetLanguages = fileConfig.targetLanguages ||
|
|
183
|
+
(process.env.TMS_TARGET_LANGUAGES || 'tr,fr,es,de')
|
|
184
|
+
.split(',')
|
|
185
|
+
.map((l) => l.trim())
|
|
186
|
+
.filter((l) => l.length > 0);
|
|
187
|
+
tms = new tms_js_1.TranslationManagementSystem({
|
|
188
|
+
projectRoot: PROJECT_ROOT,
|
|
189
|
+
sourceLanguage,
|
|
190
|
+
targetLanguages,
|
|
191
|
+
ollang: {
|
|
192
|
+
apiKey,
|
|
193
|
+
baseUrl: 'http://localhost:8080',
|
|
194
|
+
projectId,
|
|
195
|
+
defaultLevel: 0,
|
|
196
|
+
mockMode: process.env.TMS_MOCK_MODE === 'true',
|
|
197
|
+
},
|
|
198
|
+
detection: {
|
|
199
|
+
includePaths,
|
|
200
|
+
excludePaths: ['node_modules', '.git', 'dist', 'build', '.next', 'out'],
|
|
201
|
+
includePatterns: [
|
|
202
|
+
'**/*.{ts,tsx,js,jsx,vue}',
|
|
203
|
+
'**/i18n/**/*.json',
|
|
204
|
+
'**/locales/**/*.json',
|
|
205
|
+
'**/*.json',
|
|
206
|
+
'**/*.{mp4,mov,avi,mkv,webm}',
|
|
207
|
+
'**/*.{png,jpg,jpeg,gif,webp}',
|
|
208
|
+
'**/*.{mp3,wav,m4a,aac,ogg,flac}',
|
|
209
|
+
],
|
|
210
|
+
detectI18n: true,
|
|
211
|
+
detectHardcoded: true,
|
|
212
|
+
detectCMS: false,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
if (!apiKey) {
|
|
216
|
+
console.warn('⚠️ OLLANG_API_KEY not set. Translation features will not work.');
|
|
217
|
+
}
|
|
218
|
+
return tms;
|
|
219
|
+
}
|
|
220
|
+
app.get('/api/config', async (req, res) => {
|
|
221
|
+
if (!tms) {
|
|
222
|
+
tms = await initTMS();
|
|
223
|
+
}
|
|
224
|
+
const config = tms.getConfig();
|
|
225
|
+
res.json({
|
|
226
|
+
projectRoot: config.projectRoot,
|
|
227
|
+
sourceLanguage: config.sourceLanguage,
|
|
228
|
+
targetLanguages: config.targetLanguages,
|
|
229
|
+
hasApiKey: !!config.ollang.apiKey,
|
|
230
|
+
hasProjectId: !!config.ollang.projectId,
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
app.get('/api/projects', async (req, res) => {
|
|
234
|
+
try {
|
|
235
|
+
if (!tms) {
|
|
236
|
+
tms = await initTMS();
|
|
237
|
+
}
|
|
238
|
+
const sdk = tms.getSDK();
|
|
239
|
+
const config = tms.getConfig();
|
|
240
|
+
if (!config.ollang.apiKey) {
|
|
241
|
+
return res.status(401).json({
|
|
242
|
+
success: false,
|
|
243
|
+
error: 'API key not configured',
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const response = await fetch(`${config.ollang.baseUrl}/integration/project?page=1&limit=100`, {
|
|
247
|
+
method: 'GET',
|
|
248
|
+
headers: {
|
|
249
|
+
'x-api-key': config.ollang.apiKey,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
if (!response.ok) {
|
|
253
|
+
throw new Error(`Failed to fetch projects: ${response.statusText}`);
|
|
254
|
+
}
|
|
255
|
+
const data = await response.json();
|
|
256
|
+
res.json({
|
|
257
|
+
success: true,
|
|
258
|
+
projects: data.data || [],
|
|
259
|
+
total: data.meta?.itemCount || 0,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
console.error('❌ Failed to load projects:', error.message);
|
|
264
|
+
res.status(500).json({
|
|
265
|
+
success: false,
|
|
266
|
+
error: error.message,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
app.post('/api/config/apikey', async (req, res) => {
|
|
271
|
+
try {
|
|
272
|
+
const { apiKey } = req.body;
|
|
273
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
274
|
+
return res.status(400).json({
|
|
275
|
+
success: false,
|
|
276
|
+
error: 'API key is required',
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (apiKey.length > 500) {
|
|
280
|
+
return res.status(400).json({ success: false, error: 'API key too long' });
|
|
281
|
+
}
|
|
282
|
+
const previousApiKey = process.env.OLLANG_API_KEY || '';
|
|
283
|
+
process.env.OLLANG_API_KEY = apiKey;
|
|
284
|
+
tms = await initTMS();
|
|
285
|
+
try {
|
|
286
|
+
const config = tms.getConfig();
|
|
287
|
+
const baseUrl = (config.ollang.baseUrl || '').replace(/\/$/, '');
|
|
288
|
+
if (!baseUrl) {
|
|
289
|
+
throw new Error('Base URL is not configured');
|
|
290
|
+
}
|
|
291
|
+
const response = await fetch(`${baseUrl}/scans/folders`, {
|
|
292
|
+
method: 'GET',
|
|
293
|
+
headers: {
|
|
294
|
+
'Content-Type': 'application/json',
|
|
295
|
+
'x-api-key': apiKey,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
if (!response.ok) {
|
|
299
|
+
throw new Error(`Invalid API key (status ${response.status})`);
|
|
300
|
+
}
|
|
301
|
+
const data = await response.json();
|
|
302
|
+
const folders = Array.isArray(data) ? data : data.folders;
|
|
303
|
+
if (!folders || !Array.isArray(folders)) {
|
|
304
|
+
throw new Error('Invalid response from server while validating API key');
|
|
305
|
+
}
|
|
306
|
+
console.log(`✅ API key validated successfully. Accessible folders: ${folders.length}`);
|
|
307
|
+
res.json({
|
|
308
|
+
success: true,
|
|
309
|
+
message: 'API key validated and updated successfully',
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
catch (validationError) {
|
|
313
|
+
console.error('❌ API key validation failed:', validationError.message);
|
|
314
|
+
process.env.OLLANG_API_KEY = previousApiKey;
|
|
315
|
+
tms = await initTMS();
|
|
316
|
+
return res.status(401).json({
|
|
317
|
+
success: false,
|
|
318
|
+
error: 'Invalid TMS API key. Please check your token and try again.',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
console.error('❌ Failed to update API key:', error.message);
|
|
324
|
+
res.status(500).json({
|
|
325
|
+
success: false,
|
|
326
|
+
error: error.message,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
app.post('/api/config/update', async (req, res) => {
|
|
331
|
+
try {
|
|
332
|
+
const { sourceLanguage, targetLanguages, videoTranslationType } = req.body;
|
|
333
|
+
if (!sourceLanguage || !videoTranslationType) {
|
|
334
|
+
return res.status(400).json({
|
|
335
|
+
success: false,
|
|
336
|
+
error: 'Source language and video translation type are required',
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (!Array.isArray(targetLanguages) || targetLanguages.length === 0) {
|
|
340
|
+
return res.status(400).json({
|
|
341
|
+
success: false,
|
|
342
|
+
error: 'At least one target language is required',
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
if (!LANG_REGEX.test(sourceLanguage)) {
|
|
346
|
+
return res.status(400).json({ success: false, error: 'Invalid source language format' });
|
|
347
|
+
}
|
|
348
|
+
if (!targetLanguages.every((l) => LANG_REGEX.test(l))) {
|
|
349
|
+
return res.status(400).json({ success: false, error: 'Invalid target language format' });
|
|
350
|
+
}
|
|
351
|
+
if (!VALID_VIDEO_TYPES.includes(videoTranslationType)) {
|
|
352
|
+
return res.status(400).json({ success: false, error: 'Invalid video translation type. Must be aiDubbing or subtitle.' });
|
|
353
|
+
}
|
|
354
|
+
process.env.TMS_SOURCE_LANGUAGE = sourceLanguage;
|
|
355
|
+
process.env.TMS_TARGET_LANGUAGES = targetLanguages.join(',');
|
|
356
|
+
process.env.VIDEO_TRANSLATION_TYPE = videoTranslationType;
|
|
357
|
+
const fs = require('fs').promises;
|
|
358
|
+
const path = require('path');
|
|
359
|
+
const configPath = path.join(PROJECT_ROOT, 'ollang.config.ts');
|
|
360
|
+
const configContent = `export default ${JSON.stringify({
|
|
361
|
+
projectRoot: PROJECT_ROOT,
|
|
362
|
+
sourceLanguage,
|
|
363
|
+
targetLanguages,
|
|
364
|
+
video: { translationType: videoTranslationType },
|
|
365
|
+
}, null, 2)};\n`;
|
|
366
|
+
await fs.writeFile(configPath, configContent, 'utf-8');
|
|
367
|
+
tms = await initTMS();
|
|
368
|
+
res.json({
|
|
369
|
+
success: true,
|
|
370
|
+
message: 'Configuration updated successfully',
|
|
371
|
+
configPath,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
console.error('❌ Failed to update config:', error.message);
|
|
376
|
+
res.status(500).json({
|
|
377
|
+
success: false,
|
|
378
|
+
error: error.message,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
app.post('/api/scan', async (req, res) => {
|
|
383
|
+
try {
|
|
384
|
+
if (!tms) {
|
|
385
|
+
tms = await initTMS();
|
|
386
|
+
}
|
|
387
|
+
const { folderName } = req.body;
|
|
388
|
+
if (!folderName) {
|
|
389
|
+
return res.status(400).json({
|
|
390
|
+
success: false,
|
|
391
|
+
error: 'folderName is required',
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const scanResult = await tms.scanAll();
|
|
395
|
+
const folderState = getOrCreateFolderState(folderName);
|
|
396
|
+
folderState.videos = scanResult.videos;
|
|
397
|
+
folderState.images = scanResult.images;
|
|
398
|
+
folderState.audios = scanResult.audios;
|
|
399
|
+
const videoItems = scanResult.videos.map((video) => ({
|
|
400
|
+
id: `video-${video.id}`,
|
|
401
|
+
text: video.url || video.filename || 'Unknown video',
|
|
402
|
+
type: 'cms',
|
|
403
|
+
source: {
|
|
404
|
+
file: video.path,
|
|
405
|
+
line: video.line || 0,
|
|
406
|
+
column: video.column || 0,
|
|
407
|
+
context: video.url ? 'Hardcoded video URL' : 'Physical video file',
|
|
408
|
+
},
|
|
409
|
+
selected: false,
|
|
410
|
+
status: 'scanned',
|
|
411
|
+
category: 'video',
|
|
412
|
+
tags: ['video'],
|
|
413
|
+
_videoData: video,
|
|
414
|
+
}));
|
|
415
|
+
// Convert images to TextItem format
|
|
416
|
+
const imageItems = scanResult.images.map((image) => ({
|
|
417
|
+
id: `image-${image.id}`,
|
|
418
|
+
text: image.url || image.filename || 'Unknown image',
|
|
419
|
+
type: 'cms',
|
|
420
|
+
source: {
|
|
421
|
+
file: image.path,
|
|
422
|
+
line: image.line || 0,
|
|
423
|
+
column: image.column || 0,
|
|
424
|
+
context: image.url ? 'Hardcoded image URL' : 'Physical image file',
|
|
425
|
+
},
|
|
426
|
+
selected: false,
|
|
427
|
+
status: 'scanned',
|
|
428
|
+
category: 'image',
|
|
429
|
+
tags: ['image'],
|
|
430
|
+
_imageData: image,
|
|
431
|
+
}));
|
|
432
|
+
// Convert audios to TextItem format
|
|
433
|
+
const audioItems = scanResult.audios.map((audio) => ({
|
|
434
|
+
id: `audio-${audio.id}`,
|
|
435
|
+
text: audio.url || audio.filename || 'Unknown audio',
|
|
436
|
+
type: 'cms',
|
|
437
|
+
source: {
|
|
438
|
+
file: audio.path,
|
|
439
|
+
line: audio.line || 0,
|
|
440
|
+
column: audio.column || 0,
|
|
441
|
+
context: audio.url ? 'Hardcoded audio URL' : 'Physical audio file',
|
|
442
|
+
},
|
|
443
|
+
selected: false,
|
|
444
|
+
status: 'scanned',
|
|
445
|
+
category: 'audio',
|
|
446
|
+
tags: ['audio'],
|
|
447
|
+
_audioData: audio,
|
|
448
|
+
}));
|
|
449
|
+
folderState.texts = [...scanResult.texts, ...videoItems, ...imageItems, ...audioItems];
|
|
450
|
+
const i18nSetup = tms.getI18nSetup();
|
|
451
|
+
try {
|
|
452
|
+
const sdk = tms.getSDK();
|
|
453
|
+
if (sdk) {
|
|
454
|
+
const projectIdToUse = folderName ? undefined : tms.getConfig().ollang.projectId;
|
|
455
|
+
const session = await sdk.initializeScanSession(projectIdToUse, folderName);
|
|
456
|
+
if (session.projectId) {
|
|
457
|
+
tms['config'].ollang.projectId = session.projectId;
|
|
458
|
+
}
|
|
459
|
+
let latestScanData = null;
|
|
460
|
+
try {
|
|
461
|
+
const allScans = await sdk.scans.listScans();
|
|
462
|
+
const matchingScans = allScans.filter((s) => {
|
|
463
|
+
let data = s.scanData;
|
|
464
|
+
if (typeof data === 'string') {
|
|
465
|
+
try {
|
|
466
|
+
data = JSON.parse(data);
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (!data || typeof data !== 'object')
|
|
473
|
+
return false;
|
|
474
|
+
if (folderName && data.folderName !== folderName)
|
|
475
|
+
return false;
|
|
476
|
+
const currentRoot = tms ? tms.getConfig().projectRoot : undefined;
|
|
477
|
+
if (currentRoot && data.projectRoot && data.projectRoot !== currentRoot) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
return true;
|
|
481
|
+
});
|
|
482
|
+
if (matchingScans.length > 0) {
|
|
483
|
+
const latestScan = matchingScans[0];
|
|
484
|
+
folderState.currentScanId = latestScan.id;
|
|
485
|
+
let data = latestScan.scanData;
|
|
486
|
+
latestScanData = typeof data === 'string' ? JSON.parse(data) : data;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch (e) {
|
|
490
|
+
console.error('⚠️ Could not load previous scans via listScans:', e?.message || e);
|
|
491
|
+
}
|
|
492
|
+
if (latestScanData && latestScanData.texts && folderState.currentScanId) {
|
|
493
|
+
const previousTextsMap = new Map(latestScanData.texts.map((t) => [t.id, t]));
|
|
494
|
+
folderState.texts = folderState.texts.map((t) => {
|
|
495
|
+
const previousText = previousTextsMap.get(t.id);
|
|
496
|
+
if (previousText && previousText.status) {
|
|
497
|
+
return {
|
|
498
|
+
...t,
|
|
499
|
+
status: previousText.status,
|
|
500
|
+
translations: previousText.translations || {},
|
|
501
|
+
statusByLanguage: previousText.statusByLanguage || t.statusByLanguage || {},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
return { ...t, status: 'scanned' };
|
|
505
|
+
});
|
|
506
|
+
tms['state'].texts = folderState.texts;
|
|
507
|
+
for (const targetLang of tms.getConfig().targetLanguages) {
|
|
508
|
+
await tms.syncWithCodebase(targetLang);
|
|
509
|
+
}
|
|
510
|
+
const syncedTexts = tms['state'].texts;
|
|
511
|
+
const syncedTextsMap = new Map(syncedTexts.map((t) => [t.id, t]));
|
|
512
|
+
folderState.texts = folderState.texts.map((t) => {
|
|
513
|
+
const syncedText = syncedTextsMap.get(t.id);
|
|
514
|
+
if (syncedText) {
|
|
515
|
+
return syncedText;
|
|
516
|
+
}
|
|
517
|
+
return t;
|
|
518
|
+
});
|
|
519
|
+
await sdk.scans.updateScan(folderState.currentScanId, {
|
|
520
|
+
scanData: {
|
|
521
|
+
texts: folderState.texts,
|
|
522
|
+
videos: folderState.videos,
|
|
523
|
+
images: folderState.images,
|
|
524
|
+
audios: folderState.audios,
|
|
525
|
+
i18nSetup,
|
|
526
|
+
timestamp: new Date().toISOString(),
|
|
527
|
+
projectRoot: tms.getConfig().projectRoot,
|
|
528
|
+
sourceLanguage: tms.getConfig().sourceLanguage,
|
|
529
|
+
targetLanguages: tms.getConfig().targetLanguages,
|
|
530
|
+
projectId: tms.getConfig().ollang.projectId || 'unknown',
|
|
531
|
+
folderName: folderName,
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
folderState.texts = folderState.texts.map((t) => ({ ...t, status: 'scanned' }));
|
|
537
|
+
const createdScan = await sdk.scans.createScan({
|
|
538
|
+
scanData: {
|
|
539
|
+
texts: folderState.texts,
|
|
540
|
+
videos: folderState.videos,
|
|
541
|
+
images: folderState.images,
|
|
542
|
+
audios: folderState.audios,
|
|
543
|
+
i18nSetup,
|
|
544
|
+
timestamp: new Date().toISOString(),
|
|
545
|
+
projectRoot: tms.getConfig().projectRoot,
|
|
546
|
+
sourceLanguage: tms.getConfig().sourceLanguage,
|
|
547
|
+
targetLanguages: tms.getConfig().targetLanguages,
|
|
548
|
+
projectId: tms.getConfig().ollang.projectId || 'unknown',
|
|
549
|
+
folderName: folderName,
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
folderState.currentScanId = createdScan.id;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch (saveError) {
|
|
557
|
+
console.error('⚠️ Failed to save scan results:', saveError.message);
|
|
558
|
+
}
|
|
559
|
+
const scanTime = new Date().toISOString();
|
|
560
|
+
res.json({
|
|
561
|
+
success: true,
|
|
562
|
+
texts: folderState.texts,
|
|
563
|
+
i18nSetup,
|
|
564
|
+
lastScanTime: scanTime,
|
|
565
|
+
count: {
|
|
566
|
+
i18n: scanResult.texts.length,
|
|
567
|
+
videos: scanResult.videos.length,
|
|
568
|
+
images: scanResult.images.length,
|
|
569
|
+
audios: scanResult.audios.length,
|
|
570
|
+
total: folderState.texts.length,
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
console.error('❌ Scan error:', error.message);
|
|
576
|
+
res.status(500).json({
|
|
577
|
+
success: false,
|
|
578
|
+
error: error.message,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
app.post('/api/translate', async (req, res) => {
|
|
583
|
+
try {
|
|
584
|
+
if (!tms) {
|
|
585
|
+
return res.status(400).json({
|
|
586
|
+
success: false,
|
|
587
|
+
error: 'TMS not initialized. Please scan first.',
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
const { textIds, targetLanguage, targetLanguages, level, folderName } = req.body;
|
|
591
|
+
if (!textIds || !Array.isArray(textIds) || textIds.length === 0) {
|
|
592
|
+
return res.status(400).json({
|
|
593
|
+
success: false,
|
|
594
|
+
error: 'textIds array is required',
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
const languages = Array.isArray(targetLanguages) && targetLanguages.length > 0
|
|
598
|
+
? targetLanguages
|
|
599
|
+
: targetLanguage
|
|
600
|
+
? [targetLanguage]
|
|
601
|
+
: [];
|
|
602
|
+
if (languages.length === 0) {
|
|
603
|
+
return res.status(400).json({
|
|
604
|
+
success: false,
|
|
605
|
+
error: 'targetLanguage or targetLanguages is required',
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
const folderState = getOrCreateFolderState(folderName);
|
|
609
|
+
// If folderName provided, load the latest scan for that folder and get folderId
|
|
610
|
+
let folderId;
|
|
611
|
+
const sdk = tms.getSDK();
|
|
612
|
+
if (sdk && folderName) {
|
|
613
|
+
try {
|
|
614
|
+
console.log(`📂 Loading latest scan for folder: ${folderName}`);
|
|
615
|
+
// Get folderId from TMS server's /api/folders endpoint
|
|
616
|
+
try {
|
|
617
|
+
const axios = require('axios');
|
|
618
|
+
const foldersResponse = await axios.get('http://localhost:5972/api/folders');
|
|
619
|
+
const folders = foldersResponse.data.folders || [];
|
|
620
|
+
const targetFolder = folders.find((f) => f.name === folderName);
|
|
621
|
+
if (targetFolder && targetFolder.id) {
|
|
622
|
+
folderId = targetFolder.id;
|
|
623
|
+
console.log(`📁 Found folderId: ${folderId} for folder: ${folderName}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch (folderError) {
|
|
627
|
+
console.warn('⚠️ Could not get folders:', folderError.message);
|
|
628
|
+
}
|
|
629
|
+
const scans = await sdk.scans.listScans();
|
|
630
|
+
// Find the latest scan for this folder
|
|
631
|
+
const folderScans = scans.filter((scan) => {
|
|
632
|
+
const scanData = typeof scan.scanData === 'string' ? JSON.parse(scan.scanData) : scan.scanData;
|
|
633
|
+
return scanData.folderName === folderName;
|
|
634
|
+
});
|
|
635
|
+
if (folderScans.length > 0) {
|
|
636
|
+
const latestScan = folderScans[0];
|
|
637
|
+
folderState.currentScanId = latestScan.id;
|
|
638
|
+
const scanData = typeof latestScan.scanData === 'string'
|
|
639
|
+
? JSON.parse(latestScan.scanData)
|
|
640
|
+
: latestScan.scanData;
|
|
641
|
+
if (scanData.texts) {
|
|
642
|
+
folderState.texts = scanData.texts;
|
|
643
|
+
console.log(`✅ Loaded ${folderState.texts.length} items from folder ${folderName}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
console.warn('⚠️ Could not load folder scan:', error.message);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
else if (sdk && folderState.currentScanId) {
|
|
652
|
+
// Fallback: reload from currentScanId
|
|
653
|
+
try {
|
|
654
|
+
const scan = await sdk.scans.getScan(folderState.currentScanId);
|
|
655
|
+
const scanData = typeof scan.scanData === 'string' ? JSON.parse(scan.scanData) : scan.scanData;
|
|
656
|
+
if (scanData.texts) {
|
|
657
|
+
folderState.texts = scanData.texts;
|
|
658
|
+
console.log(`✅ Reloaded ${folderState.texts.length} items from scan ${folderState.currentScanId}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
console.warn('⚠️ Could not reload scan data, using cached currentTexts');
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// Items are already grouped by entry (each entry = one item with cmsFields).
|
|
666
|
+
// Direct lookup by textIds is sufficient.
|
|
667
|
+
const selectedItems = folderState.texts.filter((t) => textIds.includes(t.id));
|
|
668
|
+
if (selectedItems.length === 0) {
|
|
669
|
+
console.error(`❌ No items found. Requested IDs: ${textIds.slice(0, 3).join(', ')}...`);
|
|
670
|
+
console.error(`❌ Available IDs: ${folderState.texts
|
|
671
|
+
.slice(0, 3)
|
|
672
|
+
.map((t) => t.id)
|
|
673
|
+
.join(', ')}...`);
|
|
674
|
+
return res.status(400).json({
|
|
675
|
+
success: false,
|
|
676
|
+
error: 'No items found with provided IDs',
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
// Separate items by category
|
|
680
|
+
const i18nItems = selectedItems.filter((item) => item.category !== 'video' && item.category !== 'image' && item.category !== 'audio');
|
|
681
|
+
const videoItems = selectedItems.filter((item) => item.category === 'video');
|
|
682
|
+
const imageItems = selectedItems.filter((item) => item.category === 'image');
|
|
683
|
+
const audioItems = selectedItems.filter((item) => item.category === 'audio');
|
|
684
|
+
console.log(`📊 Translation breakdown:`);
|
|
685
|
+
console.log(` - i18n texts: ${i18nItems.length}`);
|
|
686
|
+
console.log(` - Videos: ${videoItems.length}`);
|
|
687
|
+
console.log(` - Images: ${imageItems.length}`);
|
|
688
|
+
console.log(` - Audios: ${audioItems.length}`);
|
|
689
|
+
// Log cmsFields info for entry-based items
|
|
690
|
+
for (const item of i18nItems) {
|
|
691
|
+
if (item.cmsFields) {
|
|
692
|
+
console.log(` 📦 Entry ${item.id}: ${Object.keys(item.cmsFields).length} fields [${Object.keys(item.cmsFields).join(', ')}]`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// Update all items to 'translating' status immediately (per language)
|
|
696
|
+
textIds.forEach((textId) => {
|
|
697
|
+
const textIndex = folderState.texts.findIndex((t) => t.id === textId);
|
|
698
|
+
if (textIndex !== -1) {
|
|
699
|
+
const existing = folderState.texts[textIndex];
|
|
700
|
+
const statusByLanguage = { ...(existing.statusByLanguage || {}) };
|
|
701
|
+
languages.forEach((lang) => {
|
|
702
|
+
statusByLanguage[lang] = 'translating';
|
|
703
|
+
});
|
|
704
|
+
folderState.texts[textIndex] = {
|
|
705
|
+
...existing,
|
|
706
|
+
status: 'translating',
|
|
707
|
+
statusByLanguage,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
// Save translating status
|
|
712
|
+
try {
|
|
713
|
+
await updateCurrentScan(folderName);
|
|
714
|
+
}
|
|
715
|
+
catch (saveError) {
|
|
716
|
+
console.error('⚠️ Failed to save translating status:', saveError.message);
|
|
717
|
+
}
|
|
718
|
+
// Return immediately with translating status
|
|
719
|
+
res.json({
|
|
720
|
+
success: true,
|
|
721
|
+
message: 'Translation started',
|
|
722
|
+
status: 'translating',
|
|
723
|
+
itemsCount: selectedItems.length,
|
|
724
|
+
breakdown: {
|
|
725
|
+
i18n: i18nItems.length,
|
|
726
|
+
videos: videoItems.length,
|
|
727
|
+
images: imageItems.length,
|
|
728
|
+
audios: audioItems.length,
|
|
729
|
+
},
|
|
730
|
+
});
|
|
731
|
+
(async () => {
|
|
732
|
+
try {
|
|
733
|
+
const primaryLang = languages[0];
|
|
734
|
+
if (i18nItems.length > 0) {
|
|
735
|
+
for (const lang of languages) {
|
|
736
|
+
console.log(`🌍 Translating ${i18nItems.length} i18n texts to ${lang}...`);
|
|
737
|
+
try {
|
|
738
|
+
const order = await tms.translate(i18nItems, lang, level || 0, folderName, folderId);
|
|
739
|
+
const translations = tms.getTranslations();
|
|
740
|
+
i18nItems.forEach((item) => {
|
|
741
|
+
const textIndex = folderState.texts.findIndex((t) => t.id === item.id);
|
|
742
|
+
if (textIndex === -1)
|
|
743
|
+
return;
|
|
744
|
+
const existing = folderState.texts[textIndex];
|
|
745
|
+
if (item.cmsFields && Object.keys(item.cmsFields).length > 0) {
|
|
746
|
+
const translatedCmsFields = {
|
|
747
|
+
...(existing.translatedCmsFields || {}),
|
|
748
|
+
};
|
|
749
|
+
let titleTranslation = '';
|
|
750
|
+
for (const field of Object.keys(item.cmsFields)) {
|
|
751
|
+
const subId = `${item.id}__${field}`;
|
|
752
|
+
const tr = Array.from(translations.values()).find((t) => t.textId === subId);
|
|
753
|
+
if (tr) {
|
|
754
|
+
translatedCmsFields[field] = tr.translatedText;
|
|
755
|
+
if (field.includes('title')) {
|
|
756
|
+
titleTranslation = tr.translatedText;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const mergedTranslations = {
|
|
761
|
+
...(existing.translations || {}),
|
|
762
|
+
[lang]: titleTranslation || Object.values(translatedCmsFields)[0] || '',
|
|
763
|
+
};
|
|
764
|
+
const statusByLanguage = { ...(existing.statusByLanguage || {}) };
|
|
765
|
+
statusByLanguage[lang] = 'translated';
|
|
766
|
+
folderState.texts[textIndex] = {
|
|
767
|
+
...existing,
|
|
768
|
+
status: 'translated',
|
|
769
|
+
translatedCmsFields,
|
|
770
|
+
translations: mergedTranslations,
|
|
771
|
+
statusByLanguage,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
const tr = Array.from(translations.values()).find((t) => t.textId === item.id);
|
|
776
|
+
const mergedTranslations = {
|
|
777
|
+
...(existing.translations || {}),
|
|
778
|
+
...(tr ? { [lang]: tr.translatedText } : {}),
|
|
779
|
+
};
|
|
780
|
+
const statusByLanguage = { ...(existing.statusByLanguage || {}) };
|
|
781
|
+
statusByLanguage[lang] = 'translated';
|
|
782
|
+
folderState.texts[textIndex] = {
|
|
783
|
+
...existing,
|
|
784
|
+
status: 'translated',
|
|
785
|
+
translations: mergedTranslations,
|
|
786
|
+
statusByLanguage,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
console.error(`❌ i18n translation error for lang ${lang}:`, error.message);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (videoItems.length > 0) {
|
|
797
|
+
console.log(`🎬 Translating ${videoItems.length} videos to ${primaryLang}...`);
|
|
798
|
+
for (const item of videoItems) {
|
|
799
|
+
try {
|
|
800
|
+
const videoData = item._videoData;
|
|
801
|
+
if (videoData) {
|
|
802
|
+
const orderId = await tms.translateVideo(videoData, primaryLang, level || 0);
|
|
803
|
+
console.log(`✅ Video translation order created: ${orderId}`);
|
|
804
|
+
const textIndex = folderState.texts.findIndex((t) => t.id === item.id);
|
|
805
|
+
if (textIndex !== -1) {
|
|
806
|
+
folderState.texts[textIndex] = {
|
|
807
|
+
...folderState.texts[textIndex],
|
|
808
|
+
status: 'translated',
|
|
809
|
+
translations: { [targetLanguage]: `Order: ${orderId}` },
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
console.error(`❌ Video translation error for ${item.id}:`, error.message);
|
|
816
|
+
const textIndex = folderState.texts.findIndex((t) => t.id === item.id);
|
|
817
|
+
if (textIndex !== -1) {
|
|
818
|
+
folderState.texts[textIndex] = {
|
|
819
|
+
...folderState.texts[textIndex],
|
|
820
|
+
status: 'scanned',
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (imageItems.length > 0) {
|
|
827
|
+
console.log(`🖼️ Translating ${imageItems.length} images to ${primaryLang}...`);
|
|
828
|
+
for (const item of imageItems) {
|
|
829
|
+
try {
|
|
830
|
+
const imageData = item._imageData;
|
|
831
|
+
if (imageData) {
|
|
832
|
+
imageData.textItemId = item.id;
|
|
833
|
+
const orderId = await tms.translateImage(imageData, primaryLang, level || 0);
|
|
834
|
+
console.log(`✅ Image translation order created: ${orderId}`);
|
|
835
|
+
const textIndex = folderState.texts.findIndex((t) => t.id === item.id);
|
|
836
|
+
if (textIndex !== -1) {
|
|
837
|
+
folderState.texts[textIndex] = {
|
|
838
|
+
...folderState.texts[textIndex],
|
|
839
|
+
status: 'translated',
|
|
840
|
+
translations: { [targetLanguage]: `Order: ${orderId}` },
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
catch (error) {
|
|
846
|
+
console.error(`❌ Image translation error for ${item.id}:`, error.message);
|
|
847
|
+
const textIndex = folderState.texts.findIndex((t) => t.id === item.id);
|
|
848
|
+
if (textIndex !== -1) {
|
|
849
|
+
folderState.texts[textIndex] = {
|
|
850
|
+
...folderState.texts[textIndex],
|
|
851
|
+
status: 'scanned',
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (audioItems.length > 0) {
|
|
858
|
+
console.log(`🎵 Translating ${audioItems.length} audios to ${primaryLang}...`);
|
|
859
|
+
for (const item of audioItems) {
|
|
860
|
+
try {
|
|
861
|
+
const audioData = item._audioData;
|
|
862
|
+
if (audioData) {
|
|
863
|
+
const orderId = await tms.translateAudio(audioData, primaryLang, level || 0);
|
|
864
|
+
const textIndex = folderState.texts.findIndex((t) => t.id === item.id);
|
|
865
|
+
if (textIndex !== -1) {
|
|
866
|
+
folderState.texts[textIndex] = {
|
|
867
|
+
...folderState.texts[textIndex],
|
|
868
|
+
status: 'translated',
|
|
869
|
+
translations: { [targetLanguage]: `Order: ${orderId}` },
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
console.error(`❌ Audio translation error for ${item.id}:`, error.message);
|
|
876
|
+
const textIndex = folderState.texts.findIndex((t) => t.id === item.id);
|
|
877
|
+
if (textIndex !== -1) {
|
|
878
|
+
folderState.texts[textIndex] = {
|
|
879
|
+
...folderState.texts[textIndex],
|
|
880
|
+
status: 'scanned',
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
try {
|
|
887
|
+
await updateCurrentScan(folderName);
|
|
888
|
+
console.log('✅ All translations completed and saved');
|
|
889
|
+
}
|
|
890
|
+
catch (saveError) {
|
|
891
|
+
console.warn('⚠️ Failed to save translation statuses:', saveError.message);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
catch (error) {
|
|
895
|
+
console.error('❌ Translation error:', error.message);
|
|
896
|
+
}
|
|
897
|
+
})();
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
console.error('❌ Translation error:', error.message);
|
|
901
|
+
res.status(500).json({
|
|
902
|
+
success: false,
|
|
903
|
+
error: error.message,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
app.post('/api/apply', async (req, res) => {
|
|
908
|
+
try {
|
|
909
|
+
if (!tms) {
|
|
910
|
+
return res.status(400).json({
|
|
911
|
+
success: false,
|
|
912
|
+
error: 'TMS not initialized.',
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
const { targetLanguage, textIds, folderName, strapiUrl: reqStrapiUrl, strapiToken: reqStrapiToken, } = req.body;
|
|
916
|
+
const effectiveStrapiToken = req.headers['x-strapi-token'] || reqStrapiToken;
|
|
917
|
+
if (!targetLanguage) {
|
|
918
|
+
return res.status(400).json({
|
|
919
|
+
success: false,
|
|
920
|
+
error: 'targetLanguage is required',
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
const folderState = getOrCreateFolderState(folderName);
|
|
924
|
+
const sdk = tms.getSDK();
|
|
925
|
+
if (sdk && folderName) {
|
|
926
|
+
try {
|
|
927
|
+
const scans = await sdk.scans.listScans();
|
|
928
|
+
const folderScans = scans.filter((scan) => {
|
|
929
|
+
const scanData = typeof scan.scanData === 'string' ? JSON.parse(scan.scanData) : scan.scanData;
|
|
930
|
+
return scanData.folderName === folderName;
|
|
931
|
+
});
|
|
932
|
+
if (folderScans.length > 0) {
|
|
933
|
+
const latestScan = folderScans[0];
|
|
934
|
+
folderState.currentScanId = latestScan.id;
|
|
935
|
+
const scanData = typeof latestScan.scanData === 'string'
|
|
936
|
+
? JSON.parse(latestScan.scanData)
|
|
937
|
+
: latestScan.scanData;
|
|
938
|
+
if (scanData.texts) {
|
|
939
|
+
folderState.texts = scanData.texts;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
console.error('⚠️ Could not load folder scan:', error.message);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
let translations = tms.getTranslations();
|
|
948
|
+
const translatedTexts = folderState.texts.filter((t) => t.status === 'translated' && t.translations && t.translations[targetLanguage]);
|
|
949
|
+
let loadedCount = 0;
|
|
950
|
+
translatedTexts.forEach((text) => {
|
|
951
|
+
if (!translations.has(text.id)) {
|
|
952
|
+
const translatedText = text.translations[targetLanguage];
|
|
953
|
+
// @ts-ignore - accessing private state to sync translation
|
|
954
|
+
tms['state'].translations.set(text.id, {
|
|
955
|
+
textId: text.id,
|
|
956
|
+
originalText: text.text,
|
|
957
|
+
translatedText: translatedText,
|
|
958
|
+
});
|
|
959
|
+
loadedCount++;
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
translations = tms.getTranslations();
|
|
963
|
+
const hasTargetLangTranslation = targetLanguage && Array.from(translations.values()).some((t) => !!t && !!t.translatedText);
|
|
964
|
+
if (!hasTargetLangTranslation) {
|
|
965
|
+
return res.status(400).json({
|
|
966
|
+
success: false,
|
|
967
|
+
error: 'No translations available. Please translate first.',
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
const appliedTextIds = textIds && textIds.length > 0
|
|
971
|
+
? textIds
|
|
972
|
+
: Array.from(translations.values()).map((t) => t.textId);
|
|
973
|
+
const cmsEntryItems = [];
|
|
974
|
+
const fileItems = [];
|
|
975
|
+
for (const textId of appliedTextIds) {
|
|
976
|
+
const item = folderState.texts.find((t) => t.id === textId);
|
|
977
|
+
if (!item) {
|
|
978
|
+
fileItems.push(textId);
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
if (item.cmsFields && Object.keys(item.cmsFields).length > 0) {
|
|
982
|
+
cmsEntryItems.push(item);
|
|
983
|
+
}
|
|
984
|
+
else if (item.strapiContentType && item.strapiEntryId && item.strapiField) {
|
|
985
|
+
cmsEntryItems.push(item);
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
fileItems.push(textId);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
let updatedFiles = 0;
|
|
992
|
+
let strapiResults = { pushed: 0, failed: 0 };
|
|
993
|
+
if (fileItems.length > 0) {
|
|
994
|
+
console.log(`📝 Applying ${fileItems.length} file-based translations...`);
|
|
995
|
+
tms['state'].texts = folderState.texts;
|
|
996
|
+
updatedFiles = await tms.applyTranslations(targetLanguage, fileItems);
|
|
997
|
+
console.log(`✅ Updated ${updatedFiles} files`);
|
|
998
|
+
}
|
|
999
|
+
if (cmsEntryItems.length > 0) {
|
|
1000
|
+
const strapiUrl = reqStrapiUrl || process.env.STRAPI_URL || process.env.STRAPI_BASE_URL || '';
|
|
1001
|
+
const strapiToken = effectiveStrapiToken || process.env.STRAPI_TOKEN || process.env.STRAPI_API_TOKEN || '';
|
|
1002
|
+
if (!strapiUrl || !strapiToken) {
|
|
1003
|
+
console.warn('⚠️ STRAPI_URL and STRAPI_TOKEN env vars required for CMS push. Skipping Strapi push.');
|
|
1004
|
+
console.warn(' Set: STRAPI_URL=https://cms.ollang.com STRAPI_TOKEN=your-token');
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
console.log(`🌐 Pushing ${cmsEntryItems.length} CMS entries to Strapi (${strapiUrl})...`);
|
|
1008
|
+
const pusher = new strapi_pusher_js_1.StrapiPusher({ strapiUrl, strapiToken });
|
|
1009
|
+
const strapiTranslations = [];
|
|
1010
|
+
for (const item of cmsEntryItems) {
|
|
1011
|
+
const route = item.strapiRoute || item.metadata?.strapiRoute;
|
|
1012
|
+
if (item.translatedCmsFields && Object.keys(item.translatedCmsFields).length > 0) {
|
|
1013
|
+
for (const [field, translatedText] of Object.entries(item.translatedCmsFields)) {
|
|
1014
|
+
const payload = {
|
|
1015
|
+
contentType: item.strapiContentType,
|
|
1016
|
+
entryId: item.strapiEntryId,
|
|
1017
|
+
field,
|
|
1018
|
+
translatedText,
|
|
1019
|
+
};
|
|
1020
|
+
if (route)
|
|
1021
|
+
payload.route = route;
|
|
1022
|
+
strapiTranslations.push(payload);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
else {
|
|
1026
|
+
const translatedText = item.translations?.[targetLanguage] ||
|
|
1027
|
+
Array.from(translations.values()).find((tr) => tr.textId === item.id)?.translatedText;
|
|
1028
|
+
if (translatedText && item.strapiField) {
|
|
1029
|
+
const payload = {
|
|
1030
|
+
contentType: item.strapiContentType,
|
|
1031
|
+
entryId: item.strapiEntryId,
|
|
1032
|
+
field: item.strapiField,
|
|
1033
|
+
translatedText,
|
|
1034
|
+
};
|
|
1035
|
+
if (route)
|
|
1036
|
+
payload.route = route;
|
|
1037
|
+
strapiTranslations.push(payload);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (strapiTranslations.length > 0) {
|
|
1042
|
+
const result = await pusher.pushBatch(strapiTranslations, targetLanguage);
|
|
1043
|
+
strapiResults = {
|
|
1044
|
+
pushed: result.results.length,
|
|
1045
|
+
failed: result.errors.length,
|
|
1046
|
+
results: result.results,
|
|
1047
|
+
errors: result.errors,
|
|
1048
|
+
};
|
|
1049
|
+
if (result.errors.length > 0) {
|
|
1050
|
+
console.error(' Strapi push errors (per field):', JSON.stringify(result.errors, null, 2));
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const successfulCmsKeys = new Set((strapiResults.results || []).map((r) => `${r.contentType}:${r.entryId}:${r.field}`));
|
|
1056
|
+
for (const textId of appliedTextIds) {
|
|
1057
|
+
const textIndex = folderState.texts.findIndex((t) => t.id === textId);
|
|
1058
|
+
if (textIndex === -1)
|
|
1059
|
+
continue;
|
|
1060
|
+
const item = folderState.texts[textIndex];
|
|
1061
|
+
if (item.cmsFields && item.strapiContentType && item.strapiEntryId) {
|
|
1062
|
+
const anySuccess = Object.keys(item.cmsFields).some((field) => successfulCmsKeys.has(`${item.strapiContentType}:${item.strapiEntryId}:${field}`));
|
|
1063
|
+
if (anySuccess) {
|
|
1064
|
+
const statusByLanguage = { ...(item.statusByLanguage || {}) };
|
|
1065
|
+
statusByLanguage[targetLanguage] = 'submitted';
|
|
1066
|
+
folderState.texts[textIndex] = { ...item, status: 'submitted', statusByLanguage };
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
const statusByLanguage = { ...(item.statusByLanguage || {}) };
|
|
1071
|
+
statusByLanguage[targetLanguage] = 'submitted';
|
|
1072
|
+
folderState.texts[textIndex] = { ...item, status: 'submitted', statusByLanguage };
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
try {
|
|
1076
|
+
await updateCurrentScan(folderName);
|
|
1077
|
+
}
|
|
1078
|
+
catch (saveError) {
|
|
1079
|
+
console.error('⚠️ Failed to save apply statuses:', saveError.message);
|
|
1080
|
+
}
|
|
1081
|
+
const appliedCount = appliedTextIds.length;
|
|
1082
|
+
res.json({
|
|
1083
|
+
success: strapiResults.failed === 0,
|
|
1084
|
+
updatedFiles,
|
|
1085
|
+
translationsCount: appliedCount,
|
|
1086
|
+
strapiPushed: strapiResults.pushed,
|
|
1087
|
+
strapiFailed: strapiResults.failed,
|
|
1088
|
+
strapiResults,
|
|
1089
|
+
message: `Applied ${appliedCount} translations: ${updatedFiles} files updated, ${strapiResults.pushed} CMS items pushed to Strapi`,
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
console.error('❌ Apply error:', error.message);
|
|
1094
|
+
res.status(500).json({
|
|
1095
|
+
success: false,
|
|
1096
|
+
error: error.message,
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
app.get('/api/translations', (_req, res) => {
|
|
1101
|
+
if (!tms) {
|
|
1102
|
+
return res.json({
|
|
1103
|
+
success: true,
|
|
1104
|
+
translations: [],
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
const translations = tms.getTranslations();
|
|
1108
|
+
res.json({
|
|
1109
|
+
success: true,
|
|
1110
|
+
translations: Array.from(translations.values()),
|
|
1111
|
+
});
|
|
1112
|
+
});
|
|
1113
|
+
function getOllangBackendBase() {
|
|
1114
|
+
let backendBase = process.env.OLLANG_BASE_URL || '';
|
|
1115
|
+
if (!backendBase && tms) {
|
|
1116
|
+
backendBase = tms.getConfig().ollang.baseUrl || '';
|
|
1117
|
+
}
|
|
1118
|
+
if (!backendBase) {
|
|
1119
|
+
backendBase = 'http://localhost:8080';
|
|
1120
|
+
}
|
|
1121
|
+
backendBase = backendBase.replace(/\/$/, '');
|
|
1122
|
+
try {
|
|
1123
|
+
const parsed = new URL(backendBase);
|
|
1124
|
+
const allowedHosts = new Set([
|
|
1125
|
+
'localhost',
|
|
1126
|
+
'127.0.0.1',
|
|
1127
|
+
'api-integration.ollang.com',
|
|
1128
|
+
]);
|
|
1129
|
+
const extraHosts = (process.env.OLLANG_ALLOWED_HOSTS || '').split(',').filter(Boolean);
|
|
1130
|
+
extraHosts.forEach((h) => allowedHosts.add(h.trim()));
|
|
1131
|
+
if (!allowedHosts.has(parsed.hostname)) {
|
|
1132
|
+
console.warn(`\u26a0\ufe0f Backend URL hostname "${parsed.hostname}" not in allowlist, using default`);
|
|
1133
|
+
return 'http://localhost:8080';
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
catch {
|
|
1137
|
+
console.warn('\u26a0\ufe0f Invalid backend URL format, using default');
|
|
1138
|
+
return 'http://localhost:8080';
|
|
1139
|
+
}
|
|
1140
|
+
return backendBase;
|
|
1141
|
+
}
|
|
1142
|
+
app.get('/scans/folders', async (req, res) => {
|
|
1143
|
+
const apiKey = req.headers['x-api-key'] || '';
|
|
1144
|
+
if (!apiKey) {
|
|
1145
|
+
return res.status(401).json({ success: false, error: 'x-api-key required' });
|
|
1146
|
+
}
|
|
1147
|
+
try {
|
|
1148
|
+
const base = getOllangBackendBase();
|
|
1149
|
+
const response = await fetch(`${base}/scans/folders`, {
|
|
1150
|
+
method: 'GET',
|
|
1151
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
|
1152
|
+
});
|
|
1153
|
+
const data = await response.json().catch(() => ({}));
|
|
1154
|
+
res.status(response.status).json(data);
|
|
1155
|
+
}
|
|
1156
|
+
catch (error) {
|
|
1157
|
+
console.error('❌ /scans/folders proxy error:', error?.message);
|
|
1158
|
+
res.status(500).json({
|
|
1159
|
+
success: false,
|
|
1160
|
+
error: error?.message || 'Failed to reach Ollang API',
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
app.get('/scans', async (req, res) => {
|
|
1165
|
+
const apiKey = req.headers['x-api-key'] || '';
|
|
1166
|
+
if (!apiKey) {
|
|
1167
|
+
return res.status(401).json({ success: false, error: 'x-api-key required' });
|
|
1168
|
+
}
|
|
1169
|
+
try {
|
|
1170
|
+
const base = getOllangBackendBase();
|
|
1171
|
+
const qs = new URLSearchParams(req.query).toString();
|
|
1172
|
+
const url = qs ? `${base}/scans?${qs}` : `${base}/scans`;
|
|
1173
|
+
const response = await fetch(url, {
|
|
1174
|
+
method: 'GET',
|
|
1175
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
|
1176
|
+
});
|
|
1177
|
+
const data = await response.json().catch(() => ({}));
|
|
1178
|
+
res.status(response.status).json(data);
|
|
1179
|
+
}
|
|
1180
|
+
catch (error) {
|
|
1181
|
+
console.error('❌ GET /scans proxy error:', error?.message);
|
|
1182
|
+
res.status(500).json({
|
|
1183
|
+
success: false,
|
|
1184
|
+
error: error?.message || 'Failed to reach Ollang API',
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
app.post('/scans', async (req, res) => {
|
|
1189
|
+
const apiKey = req.headers['x-api-key'] || '';
|
|
1190
|
+
if (!apiKey) {
|
|
1191
|
+
return res.status(401).json({ success: false, error: 'x-api-key required' });
|
|
1192
|
+
}
|
|
1193
|
+
try {
|
|
1194
|
+
const base = getOllangBackendBase();
|
|
1195
|
+
const response = await fetch(`${base}/scans`, {
|
|
1196
|
+
method: 'POST',
|
|
1197
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
|
1198
|
+
body: JSON.stringify(req.body || {}),
|
|
1199
|
+
});
|
|
1200
|
+
const data = await response.json().catch(() => ({}));
|
|
1201
|
+
res.status(response.status).json(data);
|
|
1202
|
+
}
|
|
1203
|
+
catch (error) {
|
|
1204
|
+
console.error('❌ POST /scans proxy error:', error?.message);
|
|
1205
|
+
res.status(500).json({
|
|
1206
|
+
success: false,
|
|
1207
|
+
error: error?.message || 'Failed to reach Ollang API',
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
app.patch('/scans/:id', async (req, res) => {
|
|
1212
|
+
const apiKey = req.headers['x-api-key'] || '';
|
|
1213
|
+
const scanId = req.params.id;
|
|
1214
|
+
if (!apiKey) {
|
|
1215
|
+
return res.status(401).json({ success: false, error: 'x-api-key required' });
|
|
1216
|
+
}
|
|
1217
|
+
if (!scanId) {
|
|
1218
|
+
return res.status(400).json({ success: false, error: 'scan id required' });
|
|
1219
|
+
}
|
|
1220
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(scanId)) {
|
|
1221
|
+
return res.status(400).json({ success: false, error: 'Invalid scan ID format' });
|
|
1222
|
+
}
|
|
1223
|
+
try {
|
|
1224
|
+
const base = getOllangBackendBase();
|
|
1225
|
+
const response = await fetch(`${base}/scans/${scanId}`, {
|
|
1226
|
+
method: 'PATCH',
|
|
1227
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
|
1228
|
+
body: JSON.stringify(req.body || {}),
|
|
1229
|
+
});
|
|
1230
|
+
const data = await response.json().catch(() => ({}));
|
|
1231
|
+
res.status(response.status).json(data);
|
|
1232
|
+
}
|
|
1233
|
+
catch (error) {
|
|
1234
|
+
console.error('❌ PATCH /scans/:id proxy error:', error?.message);
|
|
1235
|
+
res.status(500).json({
|
|
1236
|
+
success: false,
|
|
1237
|
+
error: error?.message || 'Failed to reach Ollang API',
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
app.get('/api/folders', async (req, res) => {
|
|
1242
|
+
try {
|
|
1243
|
+
if (!tms) {
|
|
1244
|
+
tms = await initTMS();
|
|
1245
|
+
}
|
|
1246
|
+
const sdk = tms.getSDK();
|
|
1247
|
+
const config = tms.getConfig();
|
|
1248
|
+
if (!config.ollang.apiKey) {
|
|
1249
|
+
return res.json({
|
|
1250
|
+
success: true,
|
|
1251
|
+
folders: [],
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
if (!sdk) {
|
|
1255
|
+
return res.status(500).json({
|
|
1256
|
+
success: false,
|
|
1257
|
+
error: 'SDK not available',
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
const folders = await sdk
|
|
1261
|
+
.getClient()
|
|
1262
|
+
.get('/scans/folders');
|
|
1263
|
+
const normalizedFolders = folders.map((f) => ({
|
|
1264
|
+
...f,
|
|
1265
|
+
isCms: !!f.isCms,
|
|
1266
|
+
}));
|
|
1267
|
+
res.json({
|
|
1268
|
+
success: true,
|
|
1269
|
+
folders: normalizedFolders,
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
catch (error) {
|
|
1273
|
+
console.error('❌ Failed to load folders:', error.message);
|
|
1274
|
+
console.error('❌ Error details:', error.response?.data);
|
|
1275
|
+
res.status(500).json({
|
|
1276
|
+
success: false,
|
|
1277
|
+
error: error.message,
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
app.get('/api/scans', async (req, res) => {
|
|
1282
|
+
try {
|
|
1283
|
+
if (!tms) {
|
|
1284
|
+
tms = await initTMS();
|
|
1285
|
+
}
|
|
1286
|
+
const sdk = tms.getSDK();
|
|
1287
|
+
const projectId = req.query.projectId;
|
|
1288
|
+
const folderName = req.query.folderName;
|
|
1289
|
+
const scans = await sdk.scans.listScans();
|
|
1290
|
+
let foldersMap = new Map();
|
|
1291
|
+
try {
|
|
1292
|
+
const folders = await sdk
|
|
1293
|
+
.getClient()
|
|
1294
|
+
.get('/scans/folders');
|
|
1295
|
+
folders.forEach((folder) => {
|
|
1296
|
+
if (folder.projectId) {
|
|
1297
|
+
foldersMap.set(folder.projectId, folder.name);
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
catch (error) {
|
|
1302
|
+
console.warn('⚠️ Could not load folders for mapping');
|
|
1303
|
+
}
|
|
1304
|
+
const scansWithFolderName = scans.map((scan) => {
|
|
1305
|
+
const scanData = typeof scan.scanData === 'string' ? JSON.parse(scan.scanData) : scan.scanData;
|
|
1306
|
+
let scanFolderName = scanData.folderName;
|
|
1307
|
+
if (!scanFolderName && scanData.projectId) {
|
|
1308
|
+
scanFolderName = foldersMap.get(scanData.projectId);
|
|
1309
|
+
}
|
|
1310
|
+
return {
|
|
1311
|
+
id: scan.id,
|
|
1312
|
+
createdAt: scan.createdAt,
|
|
1313
|
+
scanData: {
|
|
1314
|
+
...scanData,
|
|
1315
|
+
folderName: scanFolderName || 'Unknown',
|
|
1316
|
+
},
|
|
1317
|
+
};
|
|
1318
|
+
});
|
|
1319
|
+
let filteredScans = scansWithFolderName;
|
|
1320
|
+
if (folderName) {
|
|
1321
|
+
filteredScans = scansWithFolderName.filter((scan) => {
|
|
1322
|
+
return scan.scanData.folderName === folderName;
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
else if (projectId) {
|
|
1326
|
+
filteredScans = scansWithFolderName.filter((scan) => {
|
|
1327
|
+
return scan.scanData.projectId === projectId;
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
res.json({
|
|
1331
|
+
success: true,
|
|
1332
|
+
scans: filteredScans,
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
catch (error) {
|
|
1336
|
+
console.error('❌ Failed to load scans:', error.message);
|
|
1337
|
+
res.status(500).json({
|
|
1338
|
+
success: false,
|
|
1339
|
+
error: error.message,
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
app.get('/api/scans/:scanId', async (req, res) => {
|
|
1344
|
+
try {
|
|
1345
|
+
if (!tms) {
|
|
1346
|
+
tms = await initTMS();
|
|
1347
|
+
}
|
|
1348
|
+
const sdk = tms.getSDK();
|
|
1349
|
+
const { scanId } = req.params;
|
|
1350
|
+
const scan = await sdk.scans.getScan(scanId);
|
|
1351
|
+
const scanData = typeof scan.scanData === 'string' ? JSON.parse(scan.scanData) : scan.scanData;
|
|
1352
|
+
const folderState = getOrCreateFolderState(scanData.folderName);
|
|
1353
|
+
folderState.currentScanId = scan.id;
|
|
1354
|
+
if (scanData.texts) {
|
|
1355
|
+
folderState.texts = scanData.texts;
|
|
1356
|
+
}
|
|
1357
|
+
if (scanData.videos) {
|
|
1358
|
+
folderState.videos = scanData.videos;
|
|
1359
|
+
}
|
|
1360
|
+
if (scanData.images) {
|
|
1361
|
+
folderState.images = scanData.images;
|
|
1362
|
+
}
|
|
1363
|
+
if (scanData.audios) {
|
|
1364
|
+
folderState.audios = scanData.audios;
|
|
1365
|
+
}
|
|
1366
|
+
res.json({
|
|
1367
|
+
success: true,
|
|
1368
|
+
scan: {
|
|
1369
|
+
id: scan.id,
|
|
1370
|
+
createdAt: scan.createdAt,
|
|
1371
|
+
scanData,
|
|
1372
|
+
},
|
|
1373
|
+
texts: folderState.texts,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
catch (error) {
|
|
1377
|
+
console.error('❌ Failed to load scan:', error.message);
|
|
1378
|
+
res.status(500).json({
|
|
1379
|
+
success: false,
|
|
1380
|
+
error: error.message,
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
app.get('/api/state', (_req, res) => {
|
|
1385
|
+
if (!tms) {
|
|
1386
|
+
return res.json({
|
|
1387
|
+
success: true,
|
|
1388
|
+
state: null,
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
const state = tms.getState();
|
|
1392
|
+
res.json({
|
|
1393
|
+
success: true,
|
|
1394
|
+
state: {
|
|
1395
|
+
isScanning: state.isScanning,
|
|
1396
|
+
isTranslating: state.isTranslating,
|
|
1397
|
+
isApplying: state.isApplying,
|
|
1398
|
+
textsCount: state.texts.length,
|
|
1399
|
+
translationsCount: state.translations.size,
|
|
1400
|
+
currentOrder: state.currentOrder
|
|
1401
|
+
? {
|
|
1402
|
+
id: state.currentOrder.id,
|
|
1403
|
+
status: state.currentOrder.status,
|
|
1404
|
+
}
|
|
1405
|
+
: null,
|
|
1406
|
+
},
|
|
1407
|
+
});
|
|
1408
|
+
});
|
|
1409
|
+
app.post('/api/strapi-schema', async (req, res) => {
|
|
1410
|
+
try {
|
|
1411
|
+
const { strapiUrl, strapiToken: bodyToken } = req.body || {};
|
|
1412
|
+
const strapiToken = req.headers['x-strapi-token'] || bodyToken;
|
|
1413
|
+
if (!strapiUrl || !strapiToken) {
|
|
1414
|
+
return res.status(400).json({
|
|
1415
|
+
success: false,
|
|
1416
|
+
error: 'strapiUrl and strapiToken (Strapi API token) are required. This is not the Ollang TMS API token.',
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
const base = String(strapiUrl).replace(/\/$/, '');
|
|
1420
|
+
const config = await (0, strapi_schema_js_1.loadStrapiSchema)(base, strapiToken);
|
|
1421
|
+
strapiSchemaCache.set(base, config);
|
|
1422
|
+
const contentTypes = Object.keys(config.fieldsByContentType);
|
|
1423
|
+
return res.json({
|
|
1424
|
+
success: true,
|
|
1425
|
+
strapiUrl: base,
|
|
1426
|
+
contentTypes,
|
|
1427
|
+
fieldsByContentType: config.fieldsByContentType,
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
catch (error) {
|
|
1431
|
+
console.error('Strapi schema fetch failed:', error?.message || error);
|
|
1432
|
+
return res.status(500).json({
|
|
1433
|
+
success: false,
|
|
1434
|
+
error: error?.message || 'Failed to fetch Strapi schema',
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
app.get('/api/strapi-field-config', (req, res) => {
|
|
1439
|
+
const strapiUrl = req.query.strapiUrl;
|
|
1440
|
+
if (!strapiUrl) {
|
|
1441
|
+
return res.status(400).json({
|
|
1442
|
+
success: false,
|
|
1443
|
+
error: 'strapiUrl query parameter is required',
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
const base = String(strapiUrl).replace(/\/$/, '');
|
|
1447
|
+
const config = strapiSchemaCache.get(base);
|
|
1448
|
+
if (!config) {
|
|
1449
|
+
return res.json({
|
|
1450
|
+
success: true,
|
|
1451
|
+
fieldsByContentType: {},
|
|
1452
|
+
message: 'No schema cached for this URL. Use Strapi dialog to fetch schema.',
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
return res.json({
|
|
1456
|
+
success: true,
|
|
1457
|
+
fieldsByContentType: config.fieldsByContentType,
|
|
1458
|
+
fetchedAt: config.fetchedAt,
|
|
1459
|
+
});
|
|
1460
|
+
});
|
|
1461
|
+
app.get('*', (_req, res) => {
|
|
1462
|
+
res.sendFile(path_1.default.join(__dirname, '../ui-dist/index.html'));
|
|
1463
|
+
});
|
|
1464
|
+
app.listen(PORT, () => {
|
|
1465
|
+
console.log('🚀 Translation Management System starting...');
|
|
1466
|
+
console.log(`📦 Project: ${PROJECT_ROOT}`);
|
|
1467
|
+
console.log(`🌐 Control panel: http://localhost:${PORT}`);
|
|
1468
|
+
console.log(`💡 Opening in your browser...`);
|
|
1469
|
+
console.log(` If not opened: http://localhost:${PORT}`);
|
|
1470
|
+
console.log(`⌨️ To stop: Ctrl+C\n`);
|
|
1471
|
+
const open = require('open');
|
|
1472
|
+
open(`http://localhost:${PORT}`).catch(() => { });
|
|
1473
|
+
});
|