@l10nmonster/server 3.0.0-alpha.9 → 3.1.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/index.js +131 -24
  3. package/interfaces.d.ts +21 -0
  4. package/package.json +11 -9
  5. package/routes/dispatcher.js +116 -0
  6. package/routes/info.js +25 -0
  7. package/routes/providers.js +17 -0
  8. package/routes/sources.js +49 -7
  9. package/routes/status.js +27 -27
  10. package/routes/tm.js +156 -6
  11. package/tsconfig.json +18 -0
  12. package/ui/dist/assets/Cart-CiY5V__G.js +1 -0
  13. package/ui/dist/assets/Job-D-5ikxga.js +1 -0
  14. package/ui/dist/assets/Providers-BZVmclS1.js +1 -0
  15. package/ui/dist/assets/Sources-BwZ8Vub0.js +1 -0
  16. package/ui/dist/assets/SourcesDetail-CXgslRDb.js +1 -0
  17. package/ui/dist/assets/SourcesResource-Br3Bspz2.js +1 -0
  18. package/ui/dist/assets/Status-Bx0Ui7d2.js +1 -0
  19. package/ui/dist/assets/StatusDetail-BzJ2TIme.js +1 -0
  20. package/ui/dist/assets/TMByProvider-B2MrTxO0.js +1 -0
  21. package/ui/dist/assets/TMDetail-BxbKr57p.js +1 -0
  22. package/ui/dist/assets/TMToc-CQ1zhmPh.js +1 -0
  23. package/ui/dist/assets/Welcome-Tp-UfiIW.js +1 -0
  24. package/ui/dist/assets/index-543A5WcJ.js +1 -0
  25. package/ui/dist/assets/index-CPrLFF-N.js +2 -0
  26. package/ui/dist/assets/vendor-BVgSJH5C.js +19 -0
  27. package/ui/dist/index.html +3 -1
  28. package/.releaserc.json +0 -31
  29. package/ui/dist/assets/Sources-D0R-Sgwf.js +0 -1
  30. package/ui/dist/assets/Status-XBRD-MuK.js +0 -1
  31. package/ui/dist/assets/TM-DZ2x6--n.js +0 -1
  32. package/ui/dist/assets/Welcome-p4gi31Lo.js +0 -1
  33. package/ui/dist/assets/api-DXOYnFyU.js +0 -1
  34. package/ui/dist/assets/badge-CveKztw5.js +0 -1
  35. package/ui/dist/assets/grid-DetiGbYY.js +0 -1
  36. package/ui/dist/assets/index-Ce8PP-0Z.js +0 -76
  37. package/ui/dist/assets/v-stack-CQ6LIfdw.js +0 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,48 @@
1
+ ## @l10nmonster/server [3.1.1](https://public-github/l10nmonster/l10nmonster/compare/@l10nmonster/server@3.1.0...@l10nmonster/server@3.1.1) (2025-12-23)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Improve type definitions and checks ([826b412](https://public-github/l10nmonster/l10nmonster/commit/826b412f0f7e761d404165a243b0c2b26c416ac1))
7
+
8
+ # @l10nmonster/server [3.1.0](https://public-github/l10nmonster/l10nmonster/compare/@l10nmonster/server@3.0.0...@l10nmonster/server@3.1.0) (2025-12-20)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Active filter not working ([f1c5e85](https://public-github/l10nmonster/l10nmonster/commit/f1c5e85dedcd197b4ec94d48b90c43c67ceb824a))
14
+ * Calibrate log severities ([2b3350a](https://public-github/l10nmonster/l10nmonster/commit/2b3350a3123abb91e7f91a9c1864daeb6275c3ad))
15
+ * **core:** Performance improvements ([09dbd9d](https://public-github/l10nmonster/l10nmonster/commit/09dbd9db8bd494c2a5698af39547462fd0a853bc))
16
+ * **core:** Tm cleanup doesn't bump job date ([cfe5860](https://public-github/l10nmonster/l10nmonster/commit/cfe5860930ff6d98945930e136d262a94914df2b))
17
+ * **server:** Add project filtering to status pages ([89177fe](https://public-github/l10nmonster/l10nmonster/commit/89177feceae34c5fecb942c8e31406356f573479))
18
+ * **server:** Fix cart cleanup ([9bbcab9](https://public-github/l10nmonster/l10nmonster/commit/9bbcab93e1fd20aeb09f59c828665159f091f37c))
19
+ * **server:** Fix drop downs and alerts ([af9450c](https://public-github/l10nmonster/l10nmonster/commit/af9450c7faa6debf23edc2e215b92c7000fc574b))
20
+ * **server:** Fix unit test ([bace95e](https://public-github/l10nmonster/l10nmonster/commit/bace95e20a621425febb03e1c729f4507c453168))
21
+ * **server:** home page improvements ([afe9264](https://public-github/l10nmonster/l10nmonster/commit/afe9264fa54cb4440559b6eb3dfcaa8b66f9238d))
22
+ * **server:** Improve error handling ([e635adf](https://public-github/l10nmonster/l10nmonster/commit/e635adf499410da8b85cad91e61202abad642549))
23
+ * **server:** Improve TM Detail performance ([836bebf](https://public-github/l10nmonster/l10nmonster/commit/836bebf45aceafd0ee0887aa4689bf41affbbcab))
24
+ * **server:** Improve UI ([9b18a83](https://public-github/l10nmonster/l10nmonster/commit/9b18a83d7f1d831eff4b91a5b4e323175b233855))
25
+ * **server:** Migrate from Material UI to Chakra UI 3.0 ([f02f0b3](https://public-github/l10nmonster/l10nmonster/commit/f02f0b39487a993fe0ba8cea7ccdd25981ced149))
26
+ * **server:** More Status page optimizations ([1b37177](https://public-github/l10nmonster/l10nmonster/commit/1b3717708780767296e0e81e3b3cb43404dbf874))
27
+ * **server:** Move only leveraged filter server side plus optimizations ([c3a7525](https://public-github/l10nmonster/l10nmonster/commit/c3a7525f819a866f60e9ef52e2b0e38f8263e069))
28
+ * **server:** UI tweaks ([0ad528e](https://public-github/l10nmonster/l10nmonster/commit/0ad528eaaa01dd5deffd2b6443a752dda310ccc8))
29
+ * Version bumps ([d3030bd](https://public-github/l10nmonster/l10nmonster/commit/d3030bdd0af6ddbc79b3076af7427111ca9b04d0))
30
+
31
+
32
+ ### Features
33
+
34
+ * **core:** Major refactor ([6992ee4](https://public-github/l10nmonster/l10nmonster/commit/6992ee4d74ad2e25afef6220f92f2e72dfd02457))
35
+ * Improve LQA Boss ([fcb0818](https://public-github/l10nmonster/l10nmonster/commit/fcb0818181f1a7bd46764596c9d2b8d8f362375c))
36
+ * Refactor TM TOC page for performance ([0088b3c](https://public-github/l10nmonster/l10nmonster/commit/0088b3c2b69cdd00bc27697f50c892429f113f80))
37
+ * **server:** Add group to TMDetail page ([9d7ff6d](https://public-github/l10nmonster/l10nmonster/commit/9d7ff6d9ebf15747e7e345edd0e38f92ec7afe36))
38
+ * **server:** Add job page ([5014b8e](https://public-github/l10nmonster/l10nmonster/commit/5014b8e40accfe007281e5a744c8b714f4c34676))
39
+ * **server:** Add translation status to Sources page ([b73d4ec](https://public-github/l10nmonster/l10nmonster/commit/b73d4ec043ef37fc78ae2b548e7623aeb07c1ec1))
40
+ * **server:** Break down untranslated content by group ([7544c98](https://public-github/l10nmonster/l10nmonster/commit/7544c98e1c1beadf2f0b939eb29f527a7f11101d))
41
+ * **server:** Major improvements ([c8f9c2e](https://public-github/l10nmonster/l10nmonster/commit/c8f9c2e2fdf236a58df31ef1b9f22b626ccd35d8))
42
+ * **server:** New features and improvements ([1d18c14](https://public-github/l10nmonster/l10nmonster/commit/1d18c14037ba4838fda6090f120529b09ff9bfa9))
43
+ * **server:** Support for route extensions ([6e42ca0](https://public-github/l10nmonster/l10nmonster/commit/6e42ca0fa18af279fa152dfe447155b7951f8ff3))
44
+ * **server:** Support multi-select drop downs in TMDetail ([872c529](https://public-github/l10nmonster/l10nmonster/commit/872c529c621a9f5561b842658248d0aa41a13996))
45
+
1
46
  # Changelog
2
47
 
3
48
  All notable changes to this project will be documented in this file.
package/index.js CHANGED
@@ -2,60 +2,167 @@ import express from 'express';
2
2
  import cors from 'cors';
3
3
  import path from 'path';
4
4
  import open from 'open';
5
- import { readFileSync } from 'fs';
6
- import { getBaseDir } from '@l10nmonster/core';
5
+ import os from 'os';
6
+ import { setupInfoRoute } from './routes/info.js';
7
7
  import { setupStatusRoute } from './routes/status.js';
8
- import { setupActiveContentStatsRoute } from './routes/sources.js';
8
+ import { setupChannelRoutes } from './routes/sources.js';
9
9
  import { setupTmRoutes } from './routes/tm.js';
10
+ import { setupProviderRoute } from './routes/providers.js';
11
+ import { setupDispatcherRoutes } from './routes/dispatcher.js';
12
+ import { consoleLog, l10nMonsterVersion, l10nMonsterDescription } from '@l10nmonster/core';
10
13
 
11
- const serverPackage = JSON.parse(readFileSync(path.join(import.meta.dirname, 'package.json'), 'utf-8'));
12
-
13
- export default class serve {
14
- static help = {
14
+ /**
15
+ * CLI action for starting the L10n Monster server.
16
+ * @type {import('@l10nmonster/core').L10nAction & { extensions: Record<string, Function>, registerExtension: (name: string, routeMaker: Function) => void }}
17
+ */
18
+ const ServeAction = {
19
+ extensions: {},
20
+ name: 'serve',
21
+ help: {
15
22
  description: 'starts the L10n Monster server.',
16
23
  options: [
17
- [ '--port <number>', 'listen to specified port' ],
24
+ [ '--host <address>', 'hostname/IP to bind to and open in browser (default: all interfaces)' ],
25
+ [ '--port <number>', 'listen to specified port (default: 9691)' ],
18
26
  [ '--ui', 'also serve a web frontend' ],
27
+ [ '--open', 'open browser with web frontend' ],
19
28
  ]
20
- };
29
+ },
30
+
31
+ registerExtension(name, routeMaker) {
32
+ this.extensions[name] = routeMaker;
33
+ },
21
34
 
22
- static async action(mm, options) {
35
+ async action(mm, options) {
23
36
  const port = options.port ?? 9691;
37
+ const host = options.host; // undefined means listen on all interfaces
24
38
  const app = express();
25
39
 
26
40
  // === Middleware ===
27
41
  app.use(cors());
28
42
  app.use(express.json());
43
+ app.use(express.json({ limit: '10mb' }));
29
44
 
30
45
  // === API Routes ===
31
46
  const apiRouter = express.Router();
32
47
 
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
-
41
48
  // Setup routes from separate files
49
+ setupInfoRoute(apiRouter, mm, l10nMonsterVersion, l10nMonsterDescription);
42
50
  setupStatusRoute(apiRouter, mm);
43
- setupActiveContentStatsRoute(apiRouter, mm);
51
+ setupChannelRoutes(apiRouter, mm);
44
52
  setupTmRoutes(apiRouter, mm);
53
+ setupProviderRoute(apiRouter, mm);
54
+ setupDispatcherRoutes(apiRouter, mm);
45
55
 
46
56
  // Mount the API router under the /api prefix
47
57
  app.use('/api', apiRouter);
48
58
 
49
- if (options.ui) {
59
+ for (const [name, routeMaker] of Object.entries(ServeAction.extensions)) {
60
+ const extensionRouter = express.Router();
61
+ const routes = routeMaker(mm);
62
+ for (const [method, path, handler] of routes) {
63
+ extensionRouter[method](path, handler);
64
+ }
65
+ app.use(`/api/ext/${name}`, extensionRouter);
66
+ consoleLog`Mounted extension ${name} at /api/ext/${name}`;
67
+ }
68
+
69
+ // API 404 handler - must come before the UI catch-all
70
+ app.use('/api/*splat', (req, res) => {
71
+ res.status(404).json({
72
+ error: 'API endpoint not found',
73
+ path: req.path,
74
+ method: req.method
75
+ });
76
+ });
77
+
78
+ if (options.ui || options.open) {
50
79
  const uiDistPath = path.join(import.meta.dirname, 'ui', 'dist');
51
80
  app.use(express.static(uiDistPath)); // rest of dist files
52
81
  app.get('/*splat', (req, res) => res.sendFile(path.join(uiDistPath, 'index.html'))); // fallback for Client-Side Routing
53
82
  }
54
83
 
55
84
  // === Start Server ===
56
- app.listen(port, async () => {
57
- console.log(`🚀 L10n Monster Server listening on port ${port}`);
58
- options.ui && await open(`http://localhost:${port}`);
85
+ const listenArgs = [port];
86
+ if (host) {
87
+ listenArgs.push(host);
88
+ }
89
+
90
+ const server = app.listen(...listenArgs, async () => {
91
+ const address = server.address();
92
+
93
+ consoleLog`L10n Monster Server v${l10nMonsterVersion} started 🚀\n`;
94
+
95
+ // Handle Unix domain sockets (string) vs TCP sockets (AddressInfo)
96
+ if (typeof address === 'string') {
97
+ consoleLog` Listening on Unix socket:`;
98
+ consoleLog` - ${address}`;
99
+
100
+ if (options.open && options.ui) {
101
+ consoleLog`\n ⚠️ Cannot open browser for Unix domain socket. Please access the server manually.`;
102
+ }
103
+ } else {
104
+ const boundPort = address.port;
105
+ const boundAddress = address.address;
106
+
107
+ consoleLog` Available at:`;
108
+
109
+ // Determine what URLs to show and what to open
110
+ let openHost = host || 'localhost'; // Default to localhost for opening
111
+
112
+ // If listening on all interfaces (0.0.0.0 or ::)
113
+ if (!host || boundAddress === '0.0.0.0' || boundAddress === '::') {
114
+ // Show localhost first
115
+ consoleLog` - http://localhost:${boundPort}`;
116
+ consoleLog` - http://127.0.0.1:${boundPort}`;
117
+
118
+ // Get all network interfaces
119
+ const interfaces = os.networkInterfaces();
120
+ for (const [name, addresses] of Object.entries(interfaces)) {
121
+ for (const addr of addresses) {
122
+ // Skip internal interfaces and IPv6 link-local addresses
123
+ if (!addr.internal && addr.family === 'IPv4') {
124
+ consoleLog` - http://${addr.address}:${boundPort} (${name})`;
125
+ }
126
+ }
127
+ }
128
+ } else {
129
+ // Specific address was bound
130
+ openHost = boundAddress; // Use the bound address for opening
131
+ consoleLog` - http://${boundAddress}:${boundPort}`;
132
+
133
+ // Also show localhost if we bound to 127.0.0.1
134
+ if (boundAddress === '127.0.0.1') {
135
+ consoleLog` - http://localhost:${boundPort}`;
136
+ }
137
+ }
138
+
139
+ if (options.open) {
140
+ const openUrl = `http://${openHost}:${boundPort}`;
141
+ consoleLog``;
142
+ consoleLog` Opening browser at: ${openUrl}`;
143
+ await open(openUrl);
144
+ }
145
+ }
146
+ consoleLog``;
147
+ });
148
+
149
+ // Handle server binding errors
150
+ server.on('error', (error) => {
151
+ if (error.code === 'EADDRINUSE') {
152
+ const addressStr = host ? `${host}:${port}` : `port ${port}`;
153
+ throw new Error(`Failed to bind server: Address ${addressStr} is already in use`);
154
+ } else if (error.code === 'EACCES') {
155
+ const addressStr = host ? `${host}:${port}` : `port ${port}`;
156
+ throw new Error(`Failed to bind server: Permission denied for ${addressStr}`);
157
+ } else if (error.code === 'EADDRNOTAVAIL') {
158
+ throw new Error(`Failed to bind server: Address ${host} is not available on this system`);
159
+ } else {
160
+ throw new Error(`Failed to bind server: ${error.message}`);
161
+ }
59
162
  });
163
+
164
+ return server;
60
165
  }
61
- }
166
+ };
167
+
168
+ export default ServeAction;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Central type definitions for @l10nmonster/server
3
+ * This is the single source of truth for server-related interfaces.
4
+ */
5
+
6
+ /**
7
+ * Express-compatible route handler function.
8
+ */
9
+ export type RouteHandler = (req: unknown, res: unknown) => Promise<void> | void;
10
+
11
+ /**
12
+ * Route definition tuple: [method, path, handler].
13
+ * Method is HTTP method (get, post, put, delete, etc.).
14
+ */
15
+ export type RouteDefinition = [string, string, RouteHandler];
16
+
17
+ /**
18
+ * Server extension route factory function.
19
+ * Creates routes for a server extension given a MonsterManager instance.
20
+ */
21
+ export type ServerExtensionRouteMaker = (mm: unknown) => RouteDefinition[];
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@l10nmonster/server",
3
- "version": "3.0.0-alpha.9",
3
+ "version": "3.1.1",
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.525.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",
@@ -31,7 +32,8 @@
31
32
  "lint": "eslint .",
32
33
  "lint:fix": "eslint . --fix",
33
34
  "format": "prettier --write \"ui/src/**/*.{js,jsx,ts,tsx,json,css,md}\"",
34
- "type-check": "tsc --noEmit"
35
+ "type-check": "tsc --noEmit",
36
+ "typecheck": "tsc --noEmit"
35
37
  },
36
38
  "eslintConfig": {
37
39
  "extends": [
@@ -60,21 +62,21 @@
60
62
  "@types/react-dom": "^19.0.2",
61
63
  "@typescript-eslint/eslint-plugin": "^8.0.0",
62
64
  "@typescript-eslint/parser": "^8.0.0",
63
- "@vitejs/plugin-react": "^4.4.1",
64
- "@vitest/coverage-v8": "^3.1.2",
65
- "@vitest/ui": "^3.1.2",
65
+ "@vitejs/plugin-react": "^5",
66
+ "@vitest/coverage-v8": "^4",
67
+ "@vitest/ui": "^4",
66
68
  "eslint": "^9",
67
69
  "eslint-config-prettier": "^10",
68
70
  "eslint-plugin-prettier": "^5.0.0",
69
71
  "eslint-plugin-react": "^7.35.0",
70
- "eslint-plugin-react-hooks": "^5",
71
- "jsdom": "^26.1.0",
72
+ "eslint-plugin-react-hooks": "^7",
73
+ "jsdom": "^27",
72
74
  "prettier": "^3.5.0",
73
75
  "supertest": "^7.0.0",
74
76
  "typescript": "^5.7.3",
75
77
  "vite": "^7",
76
78
  "vite-tsconfig-paths": "^5.1.4",
77
- "vitest": "^3.1.2"
79
+ "vitest": "^4"
78
80
  },
79
81
  "engines": {
80
82
  "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, version, description) {
5
+ router.get('/info', async (req, res) => {
6
+ logInfo`/info`;
7
+ try {
8
+ res.json({
9
+ version,
10
+ 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
- export function setupActiveContentStatsRoute(router, mm) {
2
- router.get('/activeContentStats', async (req, res) => {
3
- const sources = {};
4
- for (const channelId of Object.keys(mm.rm.channels)) {
5
- const channelStats = await mm.rm.getActiveContentStats(channelId);
6
- sources[channelId] = channelStats;
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
- // Transform the structure from:
7
- // source_lang -> target_lang -> channel -> project -> data
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
+ }