@l10nmonster/server 3.0.0-alpha.9 → 3.1.0
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/.releaserc.json +1 -1
- package/CHANGELOG.md +38 -0
- package/CLAUDE.md +808 -0
- package/index.js +121 -17
- package/package.json +9 -8
- package/routes/dispatcher.js +116 -0
- package/routes/info.js +25 -0
- package/routes/providers.js +17 -0
- package/routes/sources.js +49 -7
- package/routes/status.js +27 -27
- package/routes/tm.js +156 -6
- package/ui/dist/assets/Cart-CiY5V__G.js +1 -0
- package/ui/dist/assets/Job-D-5ikxga.js +1 -0
- package/ui/dist/assets/Providers-BZVmclS1.js +1 -0
- package/ui/dist/assets/Sources-BwZ8Vub0.js +1 -0
- package/ui/dist/assets/SourcesDetail-CXgslRDb.js +1 -0
- package/ui/dist/assets/SourcesResource-Br3Bspz2.js +1 -0
- package/ui/dist/assets/Status-Bx0Ui7d2.js +1 -0
- package/ui/dist/assets/StatusDetail-BzJ2TIme.js +1 -0
- package/ui/dist/assets/TMByProvider-B2MrTxO0.js +1 -0
- package/ui/dist/assets/TMDetail-BxbKr57p.js +1 -0
- package/ui/dist/assets/TMToc-CQ1zhmPh.js +1 -0
- package/ui/dist/assets/Welcome-Tp-UfiIW.js +1 -0
- package/ui/dist/assets/index-543A5WcJ.js +1 -0
- package/ui/dist/assets/index-CPrLFF-N.js +2 -0
- package/ui/dist/assets/vendor-BVgSJH5C.js +19 -0
- package/ui/dist/index.html +3 -1
- package/ui/dist/assets/Sources-D0R-Sgwf.js +0 -1
- package/ui/dist/assets/Status-XBRD-MuK.js +0 -1
- package/ui/dist/assets/TM-DZ2x6--n.js +0 -1
- package/ui/dist/assets/Welcome-p4gi31Lo.js +0 -1
- package/ui/dist/assets/api-DXOYnFyU.js +0 -1
- package/ui/dist/assets/badge-CveKztw5.js +0 -1
- package/ui/dist/assets/grid-DetiGbYY.js +0 -1
- package/ui/dist/assets/index-Ce8PP-0Z.js +0 -76
- package/ui/dist/assets/v-stack-CQ6LIfdw.js +0 -1
package/index.js
CHANGED
|
@@ -2,60 +2,164 @@ import express from 'express';
|
|
|
2
2
|
import cors from 'cors';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import open from 'open';
|
|
5
|
+
import os from 'os';
|
|
5
6
|
import { readFileSync } from 'fs';
|
|
6
|
-
import {
|
|
7
|
+
import { setupInfoRoute } from './routes/info.js';
|
|
7
8
|
import { setupStatusRoute } from './routes/status.js';
|
|
8
|
-
import {
|
|
9
|
+
import { setupChannelRoutes } from './routes/sources.js';
|
|
9
10
|
import { setupTmRoutes } from './routes/tm.js';
|
|
11
|
+
import { setupProviderRoute } from './routes/providers.js';
|
|
12
|
+
import { setupDispatcherRoutes } from './routes/dispatcher.js';
|
|
13
|
+
import { logVerbose, consoleLog } from '@l10nmonster/core';
|
|
10
14
|
|
|
11
15
|
const serverPackage = JSON.parse(readFileSync(path.join(import.meta.dirname, 'package.json'), 'utf-8'));
|
|
12
16
|
|
|
13
|
-
export default class
|
|
17
|
+
export default class ServeAction {
|
|
18
|
+
static extensions = {};
|
|
19
|
+
static name = 'serve';
|
|
14
20
|
static help = {
|
|
15
21
|
description: 'starts the L10n Monster server.',
|
|
16
22
|
options: [
|
|
17
|
-
[ '--
|
|
23
|
+
[ '--host <address>', 'hostname/IP to bind to and open in browser (default: all interfaces)' ],
|
|
24
|
+
[ '--port <number>', 'listen to specified port (default: 9691)' ],
|
|
18
25
|
[ '--ui', 'also serve a web frontend' ],
|
|
26
|
+
[ '--open', 'open browser with web frontend' ],
|
|
19
27
|
]
|
|
20
28
|
};
|
|
21
29
|
|
|
30
|
+
static registerExtension(name, routeMaker) {
|
|
31
|
+
ServeAction.extensions[name] = routeMaker;
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
static async action(mm, options) {
|
|
23
35
|
const port = options.port ?? 9691;
|
|
36
|
+
const host = options.host; // undefined means listen on all interfaces
|
|
24
37
|
const app = express();
|
|
25
38
|
|
|
26
39
|
// === Middleware ===
|
|
27
40
|
app.use(cors());
|
|
28
41
|
app.use(express.json());
|
|
42
|
+
app.use(express.json({ limit: '10mb' }));
|
|
29
43
|
|
|
30
44
|
// === API Routes ===
|
|
31
45
|
const apiRouter = express.Router();
|
|
32
|
-
|
|
33
|
-
apiRouter.get('/info', async (req, res) => {
|
|
34
|
-
res.json({
|
|
35
|
-
version: serverPackage.version,
|
|
36
|
-
description: serverPackage.description,
|
|
37
|
-
baseDir: path.resolve(getBaseDir()),
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
46
|
|
|
41
47
|
// Setup routes from separate files
|
|
48
|
+
setupInfoRoute(apiRouter, mm, serverPackage);
|
|
42
49
|
setupStatusRoute(apiRouter, mm);
|
|
43
|
-
|
|
50
|
+
setupChannelRoutes(apiRouter, mm);
|
|
44
51
|
setupTmRoutes(apiRouter, mm);
|
|
52
|
+
setupProviderRoute(apiRouter, mm);
|
|
53
|
+
setupDispatcherRoutes(apiRouter, mm);
|
|
45
54
|
|
|
46
55
|
// Mount the API router under the /api prefix
|
|
47
56
|
app.use('/api', apiRouter);
|
|
48
57
|
|
|
49
|
-
|
|
58
|
+
for (const [name, routeMaker] of Object.entries(ServeAction.extensions)) {
|
|
59
|
+
const extensionRouter = express.Router();
|
|
60
|
+
const routes = routeMaker(mm);
|
|
61
|
+
for (const [method, path, handler] of routes) {
|
|
62
|
+
extensionRouter[method](path, handler);
|
|
63
|
+
}
|
|
64
|
+
app.use(`/api/ext/${name}`, extensionRouter);
|
|
65
|
+
consoleLog`Mounted extension ${name} at /api/ext/${name}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// API 404 handler - must come before the UI catch-all
|
|
69
|
+
app.use('/api/*splat', (req, res) => {
|
|
70
|
+
res.status(404).json({
|
|
71
|
+
error: 'API endpoint not found',
|
|
72
|
+
path: req.path,
|
|
73
|
+
method: req.method
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (options.ui || options.open) {
|
|
50
78
|
const uiDistPath = path.join(import.meta.dirname, 'ui', 'dist');
|
|
51
79
|
app.use(express.static(uiDistPath)); // rest of dist files
|
|
52
80
|
app.get('/*splat', (req, res) => res.sendFile(path.join(uiDistPath, 'index.html'))); // fallback for Client-Side Routing
|
|
53
81
|
}
|
|
54
82
|
|
|
55
83
|
// === Start Server ===
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
84
|
+
const listenArgs = [port];
|
|
85
|
+
if (host) {
|
|
86
|
+
listenArgs.push(host);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const server = app.listen(...listenArgs, async () => {
|
|
90
|
+
const address = server.address();
|
|
91
|
+
|
|
92
|
+
consoleLog`L10n Monster Server v${serverPackage.version} started 🚀\n`;
|
|
93
|
+
|
|
94
|
+
// Handle Unix domain sockets (string) vs TCP sockets (AddressInfo)
|
|
95
|
+
if (typeof address === 'string') {
|
|
96
|
+
consoleLog` Listening on Unix socket:`;
|
|
97
|
+
consoleLog` - ${address}`;
|
|
98
|
+
|
|
99
|
+
if (options.open && options.ui) {
|
|
100
|
+
consoleLog`\n ⚠️ Cannot open browser for Unix domain socket. Please access the server manually.`;
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
const boundPort = address.port;
|
|
104
|
+
const boundAddress = address.address;
|
|
105
|
+
|
|
106
|
+
consoleLog` Available at:`;
|
|
107
|
+
|
|
108
|
+
// Determine what URLs to show and what to open
|
|
109
|
+
let openHost = host || 'localhost'; // Default to localhost for opening
|
|
110
|
+
|
|
111
|
+
// If listening on all interfaces (0.0.0.0 or ::)
|
|
112
|
+
if (!host || boundAddress === '0.0.0.0' || boundAddress === '::') {
|
|
113
|
+
// Show localhost first
|
|
114
|
+
consoleLog` - http://localhost:${boundPort}`;
|
|
115
|
+
consoleLog` - http://127.0.0.1:${boundPort}`;
|
|
116
|
+
|
|
117
|
+
// Get all network interfaces
|
|
118
|
+
const interfaces = os.networkInterfaces();
|
|
119
|
+
for (const [name, addresses] of Object.entries(interfaces)) {
|
|
120
|
+
for (const addr of addresses) {
|
|
121
|
+
// Skip internal interfaces and IPv6 link-local addresses
|
|
122
|
+
if (!addr.internal && addr.family === 'IPv4') {
|
|
123
|
+
consoleLog` - http://${addr.address}:${boundPort} (${name})`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
// Specific address was bound
|
|
129
|
+
openHost = boundAddress; // Use the bound address for opening
|
|
130
|
+
consoleLog` - http://${boundAddress}:${boundPort}`;
|
|
131
|
+
|
|
132
|
+
// Also show localhost if we bound to 127.0.0.1
|
|
133
|
+
if (boundAddress === '127.0.0.1') {
|
|
134
|
+
consoleLog` - http://localhost:${boundPort}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (options.open) {
|
|
139
|
+
const openUrl = `http://${openHost}:${boundPort}`;
|
|
140
|
+
consoleLog``;
|
|
141
|
+
consoleLog` Opening browser at: ${openUrl}`;
|
|
142
|
+
await open(openUrl);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
consoleLog``;
|
|
59
146
|
});
|
|
147
|
+
|
|
148
|
+
// Handle server binding errors
|
|
149
|
+
server.on('error', (error) => {
|
|
150
|
+
if (error.code === 'EADDRINUSE') {
|
|
151
|
+
const addressStr = host ? `${host}:${port}` : `port ${port}`;
|
|
152
|
+
throw new Error(`Failed to bind server: Address ${addressStr} is already in use`);
|
|
153
|
+
} else if (error.code === 'EACCES') {
|
|
154
|
+
const addressStr = host ? `${host}:${port}` : `port ${port}`;
|
|
155
|
+
throw new Error(`Failed to bind server: Permission denied for ${addressStr}`);
|
|
156
|
+
} else if (error.code === 'EADDRNOTAVAIL') {
|
|
157
|
+
throw new Error(`Failed to bind server: Address ${host} is not available on this system`);
|
|
158
|
+
} else {
|
|
159
|
+
throw new Error(`Failed to bind server: ${error.message}`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return server;
|
|
60
164
|
}
|
|
61
165
|
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@l10nmonster/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "L10n Monster Manager",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@chakra-ui/react": "^3.2.0",
|
|
9
9
|
"@emotion/react": "^11.14.0",
|
|
10
|
+
"@tanstack/react-query": "^5.85.5",
|
|
10
11
|
"cors": "^2.8.5",
|
|
11
12
|
"dotenv": "^17",
|
|
12
13
|
"express": "^5.1.0",
|
|
13
|
-
"lucide-react": "^0.
|
|
14
|
+
"lucide-react": "^0.553.0",
|
|
14
15
|
"next-themes": "^0.4.6",
|
|
15
16
|
"open": "^10.1.1",
|
|
16
17
|
"react": "^19.1.0",
|
|
@@ -60,21 +61,21 @@
|
|
|
60
61
|
"@types/react-dom": "^19.0.2",
|
|
61
62
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
62
63
|
"@typescript-eslint/parser": "^8.0.0",
|
|
63
|
-
"@vitejs/plugin-react": "^
|
|
64
|
-
"@vitest/coverage-v8": "^
|
|
65
|
-
"@vitest/ui": "^
|
|
64
|
+
"@vitejs/plugin-react": "^5",
|
|
65
|
+
"@vitest/coverage-v8": "^4",
|
|
66
|
+
"@vitest/ui": "^4",
|
|
66
67
|
"eslint": "^9",
|
|
67
68
|
"eslint-config-prettier": "^10",
|
|
68
69
|
"eslint-plugin-prettier": "^5.0.0",
|
|
69
70
|
"eslint-plugin-react": "^7.35.0",
|
|
70
|
-
"eslint-plugin-react-hooks": "^
|
|
71
|
-
"jsdom": "^
|
|
71
|
+
"eslint-plugin-react-hooks": "^7",
|
|
72
|
+
"jsdom": "^27",
|
|
72
73
|
"prettier": "^3.5.0",
|
|
73
74
|
"supertest": "^7.0.0",
|
|
74
75
|
"typescript": "^5.7.3",
|
|
75
76
|
"vite": "^7",
|
|
76
77
|
"vite-tsconfig-paths": "^5.1.4",
|
|
77
|
-
"vitest": "^
|
|
78
|
+
"vitest": "^4"
|
|
78
79
|
},
|
|
79
80
|
"engines": {
|
|
80
81
|
"node": ">=22.11.0"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { logInfo, logVerbose, logWarn } from '@l10nmonster/core';
|
|
2
|
+
|
|
3
|
+
async function createJob(mm, sourceLang, targetLang, guids, provider) {
|
|
4
|
+
logVerbose`Creating job with ${guids.length} TUs for provider ${provider}`;
|
|
5
|
+
|
|
6
|
+
// Expand TUs from guids to full TU data
|
|
7
|
+
const tm = mm.tmm.getTM(sourceLang, targetLang);
|
|
8
|
+
const expandedTus = await tm.queryByGuids(guids);
|
|
9
|
+
|
|
10
|
+
// Call the MonsterManager dispatcher with single provider
|
|
11
|
+
const jobs = await mm.dispatcher.createJobs(
|
|
12
|
+
{ sourceLang, targetLang, tus: expandedTus },
|
|
13
|
+
{ providerList: [provider], skipQualityCheck: true, skipGroupCheck: true }
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
// Find the job for this provider (if accepted)
|
|
17
|
+
const job = jobs.find(j => j.translationProvider === provider);
|
|
18
|
+
|
|
19
|
+
if (!job) {
|
|
20
|
+
logVerbose`Provider ${provider} did not accept any TUs`;
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return job;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function setupDispatcherRoutes(router, mm) {
|
|
28
|
+
router.post('/dispatcher/estimateJob', async (req, res) => {
|
|
29
|
+
logInfo`/dispatcher/estimateJob`;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const { sourceLang, targetLang, guids, provider } = req.body;
|
|
33
|
+
|
|
34
|
+
// Validate required parameters
|
|
35
|
+
if (!sourceLang || !targetLang || !guids || !provider) {
|
|
36
|
+
return res.status(400).json({
|
|
37
|
+
error: 'Missing required parameters: sourceLang, targetLang, guids, provider'
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Validate that guids is an array
|
|
42
|
+
if (!Array.isArray(guids)) {
|
|
43
|
+
return res.status(400).json({
|
|
44
|
+
error: 'guids must be an array'
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Create job with provider
|
|
49
|
+
const job = await createJob(mm, sourceLang, targetLang, guids, provider);
|
|
50
|
+
|
|
51
|
+
if (!job) {
|
|
52
|
+
return res.json(null);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Return job with guids array instead of full tus to minimize payload
|
|
56
|
+
const { tus: jobTus, ...jobWithoutTus } = job;
|
|
57
|
+
const estimatedJob = {
|
|
58
|
+
...jobWithoutTus,
|
|
59
|
+
guids: jobTus.map(tu => tu.guid)
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
logVerbose`Estimated job with ${estimatedJob.guids.length} TUs`;
|
|
63
|
+
res.json(estimatedJob);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logWarn`Error estimating job: ${error.message}`;
|
|
66
|
+
res.status(500).json({
|
|
67
|
+
error: 'Failed to estimate job',
|
|
68
|
+
message: error.message
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
router.post('/dispatcher/startJob', async (req, res) => {
|
|
74
|
+
logInfo`/dispatcher/startJob`;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const { sourceLang, targetLang, guids, provider, jobName, instructions } = req.body;
|
|
78
|
+
|
|
79
|
+
// Validate required parameters
|
|
80
|
+
if (!sourceLang || !targetLang || !guids || !provider) {
|
|
81
|
+
return res.status(400).json({
|
|
82
|
+
error: 'Missing required parameters: sourceLang, targetLang, guids, provider'
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate that guids is an array
|
|
87
|
+
if (!Array.isArray(guids)) {
|
|
88
|
+
return res.status(400).json({
|
|
89
|
+
error: 'guids must be an array'
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Create job with provider
|
|
94
|
+
const job = await createJob(mm, sourceLang, targetLang, guids, provider);
|
|
95
|
+
|
|
96
|
+
if (!job) {
|
|
97
|
+
return res.status(400).json({
|
|
98
|
+
error: `Provider ${provider} did not accept any TUs`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Start the job
|
|
103
|
+
const result = await mm.dispatcher.startJobs([job], { jobName, instructions });
|
|
104
|
+
|
|
105
|
+
logVerbose`Started job with name: ${jobName || 'none'} and instructions: ${instructions || 'none'}`;
|
|
106
|
+
res.json(result);
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logWarn`Error starting job: ${error.message}`;
|
|
110
|
+
res.status(500).json({
|
|
111
|
+
error: 'Failed to start job',
|
|
112
|
+
message: error.message
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
package/routes/info.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { logInfo, getBaseDir, logWarn } from '@l10nmonster/core';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function setupInfoRoute(router, mm, serverPackage) {
|
|
5
|
+
router.get('/info', async (req, res) => {
|
|
6
|
+
logInfo`/info`;
|
|
7
|
+
try {
|
|
8
|
+
res.json({
|
|
9
|
+
version: serverPackage.version,
|
|
10
|
+
description: serverPackage.description,
|
|
11
|
+
baseDir: path.resolve(getBaseDir()),
|
|
12
|
+
providers: mm.dispatcher.providers.map(p => ({id: p.id, type: p.constructor.name})),
|
|
13
|
+
channels: mm.rm.channelIds.map(id => mm.rm.getChannel(id).getInfo()),
|
|
14
|
+
tmStores: mm.tmm.tmStoreIds.map(id => mm.tmm.getTmStoreInfo(id)),
|
|
15
|
+
snapStores: mm.rm.snapStoreIds.map(id => mm.rm.getSnapStoreInfo(id)),
|
|
16
|
+
});
|
|
17
|
+
} catch (error) {
|
|
18
|
+
logWarn`Error in /info: ${error.message}`;
|
|
19
|
+
res.status(500).json({
|
|
20
|
+
error: 'Failed to get system info',
|
|
21
|
+
message: error.message
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { logInfo, logVerbose } from '@l10nmonster/core';
|
|
2
|
+
|
|
3
|
+
export function setupProviderRoute(apiRouter, mm) {
|
|
4
|
+
apiRouter.get('/providers/:providerId', async (req, res) => {
|
|
5
|
+
const { providerId } = req.params;
|
|
6
|
+
logInfo`/providers/${providerId}`;
|
|
7
|
+
try {
|
|
8
|
+
const provider = mm.dispatcher.getProvider(providerId);
|
|
9
|
+
const { id, ...info } = await provider.info();
|
|
10
|
+
logVerbose`Returned provider info for ${id}`;
|
|
11
|
+
res.json({ properties: provider, info });
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error('Error fetching provider data:', error);
|
|
14
|
+
res.status(500).json({ error: 'Failed to fetch provider data' });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
package/routes/sources.js
CHANGED
|
@@ -1,10 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { logInfo, logVerbose, logWarn } from '@l10nmonster/core';
|
|
2
|
+
|
|
3
|
+
export function setupChannelRoutes(router, mm) {
|
|
4
|
+
router.get('/channel/:channelId', async (req, res) => {
|
|
5
|
+
const { channelId } = req.params;
|
|
6
|
+
logInfo`/channel/${channelId}`;
|
|
7
|
+
try {
|
|
8
|
+
const { ts, store } = await mm.rm.getChannelMeta(channelId);
|
|
9
|
+
const projects = await mm.rm.getActiveContentStats(channelId);
|
|
10
|
+
logVerbose`Returned active content stats for ${projects.length} projects`;
|
|
11
|
+
res.json({ ts, store, projects });
|
|
12
|
+
} catch (error) {
|
|
13
|
+
logWarn`Error in /channel/${channelId}: ${error.message}`;
|
|
14
|
+
res.status(500).json({
|
|
15
|
+
error: 'Failed to get channel data',
|
|
16
|
+
message: error.message
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
router.get('/channel/:channelId/:prj', async (req, res) => {
|
|
21
|
+
const { channelId, prj } = req.params;
|
|
22
|
+
const { offset, limit } = req.query;
|
|
23
|
+
logInfo`/channel/${channelId}/${prj} (offset=${offset}, limit=${limit})`;
|
|
24
|
+
try {
|
|
25
|
+
const projectTOC = await mm.rm.getProjectTOC(channelId, prj, offset, limit);
|
|
26
|
+
logVerbose`Returned project TOC for ${prj} with ${projectTOC.length} resources`;
|
|
27
|
+
res.json(projectTOC);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
logWarn`Error in /channel/${channelId}/${prj}: ${error.message}`;
|
|
30
|
+
res.status(500).json({
|
|
31
|
+
error: 'Failed to get project TOC',
|
|
32
|
+
message: error.message
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
router.get('/resource/:channelId', async (req, res) => {
|
|
37
|
+
const { channelId } = req.params;
|
|
38
|
+
const { rid } = req.query;
|
|
39
|
+
logInfo`/resource/${channelId}/${rid}`;
|
|
40
|
+
try {
|
|
41
|
+
const resource = await mm.rm.getResourceHandle(channelId, rid, { keepRaw: true });
|
|
42
|
+
logVerbose`Returned resource ${rid} with ${resource.segments.length} segments`;
|
|
43
|
+
res.json(resource);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logWarn`Error in /resource/${channelId}/${rid}: ${error.message}`;
|
|
46
|
+
res.status(500).json({
|
|
47
|
+
error: 'Failed to get resource',
|
|
48
|
+
message: error.message
|
|
49
|
+
});
|
|
7
50
|
}
|
|
8
|
-
res.json(sources);
|
|
9
51
|
});
|
|
10
52
|
}
|
package/routes/status.js
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
+
import { logInfo, logVerbose, logWarn } from '@l10nmonster/core';
|
|
2
|
+
|
|
1
3
|
export function setupStatusRoute(router, mm) {
|
|
2
|
-
router.get('/status', async (req, res) => {
|
|
4
|
+
router.get('/status/:channelId', async (req, res) => {
|
|
5
|
+
const { channelId } = req.params;
|
|
6
|
+
logInfo`/status/${channelId}`;
|
|
3
7
|
try {
|
|
4
|
-
const status = await mm.getTranslationStatus();
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// to:
|
|
9
|
-
// channel -> project -> source_lang -> target_lang -> data
|
|
10
|
-
const flippedStatus = {};
|
|
11
|
-
|
|
12
|
-
for (const [sourceLang, targetLangs] of Object.entries(status)) {
|
|
13
|
-
for (const [targetLang, channels] of Object.entries(targetLangs)) {
|
|
14
|
-
for (const [channelId, projects] of Object.entries(channels)) {
|
|
15
|
-
for (const [projectName, data] of Object.entries(projects)) {
|
|
16
|
-
// Initialize nested structure if it doesn't exist
|
|
17
|
-
flippedStatus[channelId] ??= {};
|
|
18
|
-
flippedStatus[channelId][projectName] ??= {};
|
|
19
|
-
flippedStatus[channelId][projectName][sourceLang] ??= {};
|
|
20
|
-
|
|
21
|
-
// Set the data at the new location
|
|
22
|
-
flippedStatus[channelId][projectName][sourceLang][targetLang] = data;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
res.json(flippedStatus);
|
|
8
|
+
const status = await mm.getTranslationStatus(channelId);
|
|
9
|
+
// channel -> source_lang -> target_lang -> project -> data
|
|
10
|
+
logVerbose`Returned translation status`;
|
|
11
|
+
res.json(status[channelId]);
|
|
29
12
|
} catch (error) {
|
|
30
13
|
console.error('Error fetching status: ', error);
|
|
31
14
|
res.status(500).json({ message: 'Problems fetching status data' });
|
|
32
15
|
}
|
|
33
16
|
});
|
|
34
|
-
|
|
17
|
+
|
|
18
|
+
router.get('/status/:channelId/:sourceLang/:targetLang', async (req, res) => {
|
|
19
|
+
const { channelId, sourceLang, targetLang } = req.params;
|
|
20
|
+
const { prj } = req.query;
|
|
21
|
+
logInfo`/status/${channelId}/${sourceLang}/${targetLang}${prj ? `?prj=${prj}` : ''}`;
|
|
22
|
+
try {
|
|
23
|
+
const tm = mm.tmm.getTM(sourceLang, targetLang);
|
|
24
|
+
const options = { limit: 500 };
|
|
25
|
+
if (prj) options.prj = [prj];
|
|
26
|
+
const tus = await tm.getUntranslatedContent(channelId, options);
|
|
27
|
+
logVerbose`Returned ${tus.length} untranslated TUs for ${sourceLang}->${targetLang} in channel ${channelId}${prj ? ` (project: ${prj})` : ''}`;
|
|
28
|
+
res.json(tus);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
logWarn`Error: ${error.message}`;
|
|
31
|
+
res.status(500).json({ error: error.message });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|