addonova 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/package.json +3 -2
- package/src/build/bundle-html.js +105 -76
- package/src/cli/help.js +1 -0
- package/src/cli/index.js +4 -0
- package/src/commands/tool.js +32 -0
- package/src/tools/tools-server.js +287 -0
- package/src/tools/tools.html +388 -0
- package/templates/extension/config/firefox.js +5 -4
- package/templates/extension/config/thunderbird.js +5 -4
- package/templates/extension/package.json.tpl +3 -2
- package/templates/extension/platform/naver/platform.js +0 -15
- package/src/tools/json2i18n.js +0 -37
- package/src/tools/translate.js +0 -299
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Addonova
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/code-hemu/addonova)
|
|
4
|
+
|
|
5
|
+
Addonova is a framework that allows developers to build extensions for multiple browsers. Developers can easily build, test, and manage extensions.
|
|
4
6
|
|
|
5
7
|
## Quick Start
|
|
6
8
|
|
|
@@ -26,6 +28,7 @@ npm run release
|
|
|
26
28
|
| `addonova init <name>` | Scaffold a new extension project |
|
|
27
29
|
| `addonova build [options]` | Build the current extension project |
|
|
28
30
|
| `addonova zip` | Create release zip bundles |
|
|
31
|
+
| `addonova tool` | Open the i18n tools UI in a browser |
|
|
29
32
|
| `addonova --help` | Show help |
|
|
30
33
|
|
|
31
34
|
## Build Scripts
|
|
@@ -50,7 +53,6 @@ npm run debug:thunderbird
|
|
|
50
53
|
npm run debug:naver
|
|
51
54
|
|
|
52
55
|
npm run dev
|
|
53
|
-
npm run watch
|
|
54
56
|
npm run zip
|
|
55
57
|
npm test
|
|
56
58
|
```
|
|
@@ -62,6 +64,7 @@ npx addonova build --all --release
|
|
|
62
64
|
npx addonova build --chrome --debug
|
|
63
65
|
npx addonova build --all --debug --watch
|
|
64
66
|
npx addonova zip
|
|
67
|
+
npx addonova tool
|
|
65
68
|
```
|
|
66
69
|
|
|
67
70
|
## Build Options
|
|
@@ -128,9 +131,19 @@ This is my extension description.
|
|
|
128
131
|
Run the interactive message manager from a generated project:
|
|
129
132
|
|
|
130
133
|
```bash
|
|
131
|
-
|
|
134
|
+
npm run tool
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Or use the i18n tools UI:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npx addonova tool
|
|
132
141
|
```
|
|
133
142
|
|
|
143
|
+
This opens a full UI at `http://localhost:9876` with:
|
|
144
|
+
- **Translate tab** — view all locale messages, add new messages with auto-translation, delete messages
|
|
145
|
+
- **JSON → i18n tab** — drag-and-drop a `messages.json` file to convert to `.i18n` format
|
|
146
|
+
|
|
134
147
|
## Development
|
|
135
148
|
|
|
136
149
|
Run the test suite:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "addonova",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Web Extension Framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
"templates/"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@craftamap/esbuild-plugin-html": "^0.9.0",
|
|
23
22
|
"chokidar": "^5.0.0",
|
|
24
23
|
"esbuild": "^0.27.3",
|
|
25
24
|
"globby": "^16.1.0",
|
|
25
|
+
"html-minifier-terser": "^7.2.0",
|
|
26
|
+
"htmlhint": "^1.9.2",
|
|
26
27
|
"web-ext": "^10.3.0",
|
|
27
28
|
"yazl": "^3.3.1"
|
|
28
29
|
},
|
package/src/build/bundle-html.js
CHANGED
|
@@ -1,91 +1,120 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { minify } from 'html-minifier-terser';
|
|
3
|
+
import htmlhint from 'htmlhint';
|
|
4
|
+
|
|
5
|
+
import { getDestDir } from './paths.js';
|
|
6
|
+
import { readFile, writeFile, getConfig, getAllFiles, log, fileExistsInConfig } from './utils.js';
|
|
7
|
+
import { createTask } from './task.js';
|
|
7
8
|
|
|
8
9
|
const srcHTMLDir = 'src/html';
|
|
10
|
+
const { HTMLHint } = htmlhint;
|
|
11
|
+
|
|
12
|
+
const htmlHintRules = {
|
|
13
|
+
'attr-lowercase': true,
|
|
14
|
+
'attr-no-duplication': true,
|
|
15
|
+
'doctype-first': false,
|
|
16
|
+
'id-unique': true,
|
|
17
|
+
'tag-pair': true,
|
|
18
|
+
'tagname-lowercase': true,
|
|
19
|
+
};
|
|
9
20
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
const htmlMinifyOptions = {
|
|
22
|
+
collapseBooleanAttributes: true,
|
|
23
|
+
collapseWhitespace: true,
|
|
24
|
+
conservativeCollapse: true,
|
|
25
|
+
keepClosingSlash: true,
|
|
26
|
+
minifyCSS: true,
|
|
27
|
+
minifyJS: true,
|
|
28
|
+
removeComments: true,
|
|
29
|
+
removeEmptyAttributes: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function getDestinationFile(destDir, dest, src, filename) {
|
|
33
|
+
const sourceName = path.basename(src, path.extname(src));
|
|
34
|
+
const outputName = `${filename.replace('[name]', sourceName)}.html`;
|
|
35
|
+
return path.join(destDir, dest, outputName);
|
|
14
36
|
}
|
|
15
37
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
function formatHtmlIssue(file, issue) {
|
|
39
|
+
return `${file}:${issue.line}:${issue.col} ${issue.type.toUpperCase()} ${issue.message}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validateHTML(file, html) {
|
|
43
|
+
const issues = HTMLHint.verify(html, htmlHintRules);
|
|
44
|
+
const errors = issues.filter((issue) => issue.type === 'error');
|
|
45
|
+
|
|
46
|
+
if (errors.length > 0) {
|
|
47
|
+
throw new Error(errors.map((issue) => formatHtmlIssue(file, issue)).join('\n'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const issue of issues) {
|
|
51
|
+
log.warn(`[HTML] ${formatHtmlIssue(file, issue)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function bundleHTML(config, isDebug, platform) {
|
|
56
|
+
const dir = getDestDir({ isDebug, platform });
|
|
57
|
+
|
|
58
|
+
for (const [dest, sources] of Object.entries(config.entry)) {
|
|
59
|
+
for (const src of sources) {
|
|
60
|
+
const html = await readFile(src, 'utf8');
|
|
61
|
+
validateHTML(src, html);
|
|
62
|
+
|
|
63
|
+
const output = isDebug
|
|
64
|
+
? html
|
|
65
|
+
: await minify(html, htmlMinifyOptions);
|
|
43
66
|
|
|
67
|
+
await writeFile(
|
|
68
|
+
getDestinationFile(dir, dest, src, config.filename),
|
|
69
|
+
output,
|
|
70
|
+
'utf8'
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
44
74
|
}
|
|
45
75
|
|
|
46
|
-
export function createBundleHTMLTask(srcHTMLDir){
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
}
|
|
76
|
+
export function createBundleHTMLTask(srcHTMLDir) {
|
|
77
|
+
const runBundleHTML = async ({ platforms, isDebug, logInfo, logWarn }) => {
|
|
78
|
+
for (const platform of platforms) {
|
|
79
|
+
const config = await getConfig(platform);
|
|
80
|
+
if (config.html) {
|
|
81
|
+
await bundleHTML(config.html, isDebug, platform);
|
|
82
|
+
if (logInfo) log.ok(`Bundling HTML for ${platform}...`);
|
|
83
|
+
} else if (logWarn) {
|
|
84
|
+
log.warn(`No HTML config found for ${platform}, skipping HTML bundling.`);
|
|
85
|
+
}
|
|
58
86
|
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const onChange = async (changedFiles, watcher, platforms, isDebug) => {
|
|
90
|
+
for (const platform of platforms) {
|
|
91
|
+
const config = await getConfig(platform);
|
|
59
92
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
await esbuildHTML(newConfig.html, isDebug, platform);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
93
|
+
if (!config.html) continue;
|
|
94
|
+
const exists = await fileExistsInConfig(
|
|
95
|
+
config.html.entry,
|
|
96
|
+
changedFiles[0]
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (exists) {
|
|
100
|
+
const newConfig = {
|
|
101
|
+
html: {
|
|
102
|
+
entry: exists,
|
|
103
|
+
filename: config.html.filename,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
await bundleHTML(newConfig.html, isDebug, platform);
|
|
107
|
+
}
|
|
80
108
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return createTask(
|
|
112
|
+
'bundle HTML',
|
|
113
|
+
runBundleHTML
|
|
114
|
+
).addWatcher(
|
|
115
|
+
() => getAllFiles(srcHTMLDir),
|
|
116
|
+
onChange
|
|
117
|
+
);
|
|
89
118
|
}
|
|
90
119
|
|
|
91
120
|
export default createBundleHTMLTask(srcHTMLDir);
|
package/src/cli/help.js
CHANGED
|
@@ -6,6 +6,7 @@ export function printHelp() {
|
|
|
6
6
|
npx addonova init <my-extension> Scaffold a new extension project
|
|
7
7
|
npx addonova build [options] Build the extension
|
|
8
8
|
npx addonova zip Create release zip bundles
|
|
9
|
+
npx addonova tool Open the helping tools UI in a browser
|
|
9
10
|
npx addonova --help Show this help
|
|
10
11
|
|
|
11
12
|
Build options:
|
package/src/cli/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
|
|
7
7
|
import { init } from '../commands/init.js';
|
|
8
|
+
import { runTool } from '../commands/tool.js';
|
|
8
9
|
import { printHelp } from './help.js';
|
|
9
10
|
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -41,6 +42,9 @@ export async function runCli(argv) {
|
|
|
41
42
|
case 'init':
|
|
42
43
|
await init(args);
|
|
43
44
|
break;
|
|
45
|
+
case 'tool':
|
|
46
|
+
await runTool();
|
|
47
|
+
break;
|
|
44
48
|
case 'build':
|
|
45
49
|
case 'zip':
|
|
46
50
|
await runBuildCommand(command, args);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { fork } from 'node:child_process';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { resolve, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
export async function runTool() {
|
|
10
|
+
const serverPath = resolve(__dirname, '../tools/tools-server.js');
|
|
11
|
+
|
|
12
|
+
if (!existsSync(serverPath)) {
|
|
13
|
+
console.error('[*] Addonova tools server not found. Reinstall the package.');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const child = fork(serverPath, [], {
|
|
18
|
+
stdio: 'inherit',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
process.on('SIGINT', () => {
|
|
22
|
+
child.kill('SIGKILL');
|
|
23
|
+
process.exit(130);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await new Promise((resolve, reject) =>
|
|
27
|
+
child.on('error', reject).on('close', (code) => {
|
|
28
|
+
if (code !== 0) process.exit(code);
|
|
29
|
+
resolve();
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { exec } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
import {readFile, writeFile, httpsRequest, timeout} from '../build/utils.js';
|
|
9
|
+
|
|
10
|
+
const openBrowser = (url) => {
|
|
11
|
+
const cmd = process.platform === 'win32' ? 'start' :
|
|
12
|
+
process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
13
|
+
exec(`${cmd} ${url}`);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const LOCALES_ROOT = path.resolve(process.cwd(), 'src/_locales');
|
|
17
|
+
const PORT = process.env.PORT || 9876;
|
|
18
|
+
const MIME = {
|
|
19
|
+
'.html': 'text/html; charset=utf-8',
|
|
20
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
21
|
+
'.css': 'text/css; charset=utf-8',
|
|
22
|
+
'.json': 'application/json',
|
|
23
|
+
'.png': 'image/png',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function toMessageId(message) {
|
|
27
|
+
if (typeof message !== 'string') return '';
|
|
28
|
+
return message.trim().split(/\s+/).slice(0, 3)
|
|
29
|
+
.map(w => w.replace(/[^\w]/g, '').toLowerCase())
|
|
30
|
+
.filter(Boolean).join('_');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseLocale(content) {
|
|
34
|
+
const messages = new Map();
|
|
35
|
+
const lines = content.split('\n');
|
|
36
|
+
let id = '';
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
const line = lines[i];
|
|
39
|
+
if (line.startsWith('@')) {
|
|
40
|
+
id = line.substring(1);
|
|
41
|
+
} else if (line.startsWith('#')) {
|
|
42
|
+
continue;
|
|
43
|
+
} else if (messages.has(id)) {
|
|
44
|
+
messages.set(id, `${messages.get(id)}\n${line}`);
|
|
45
|
+
} else {
|
|
46
|
+
messages.set(id, line);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
messages.forEach((value, id) => messages.set(id, value.trim()));
|
|
50
|
+
return messages;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function stringifyLocale(messages) {
|
|
54
|
+
const lines = [];
|
|
55
|
+
messages.forEach((message, id) => {
|
|
56
|
+
lines.push(`@${id}`);
|
|
57
|
+
const hasDoubleNewLines = /\n\n/.test(message);
|
|
58
|
+
message.split('\n').filter(l => l.trim()).forEach((line, index, filtered) => {
|
|
59
|
+
lines.push(line);
|
|
60
|
+
if (hasDoubleNewLines && index < filtered.length - 1) lines.push('');
|
|
61
|
+
});
|
|
62
|
+
lines.push('');
|
|
63
|
+
});
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function json2i18n(input) {
|
|
68
|
+
let output = '';
|
|
69
|
+
for (const key in input) {
|
|
70
|
+
output += `@${key}\n${input[key].message}\n\n`;
|
|
71
|
+
}
|
|
72
|
+
return output;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function getSupportedLocales() {
|
|
76
|
+
const entries = await fs.readdir(LOCALES_ROOT).catch(() => []);
|
|
77
|
+
return entries.filter(f => f.endsWith('.i18n')).map(f => f.replace('.i18n', ''));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function translate(text, lang) {
|
|
81
|
+
const url = new URL('https://translate.googleapis.com/translate_a/single');
|
|
82
|
+
url.search = new URLSearchParams({
|
|
83
|
+
client: 'gtx', sl: 'en-US', tl: lang, dt: 't', dj: '1', q: text,
|
|
84
|
+
}).toString();
|
|
85
|
+
const response = await httpsRequest(url.toString());
|
|
86
|
+
const data = JSON.parse(response.text());
|
|
87
|
+
return data.sentences.map(s => s.trans).join('\n').replaceAll(/\n+/g, '\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function jsonResponse(res, data, status = 200) {
|
|
91
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
92
|
+
res.end(JSON.stringify(data));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function serveStatic(req, res) {
|
|
96
|
+
let filePath = req.url === '/' ? '/tools.html' : req.url;
|
|
97
|
+
filePath = path.join(__dirname, filePath);
|
|
98
|
+
|
|
99
|
+
const ext = path.extname(filePath);
|
|
100
|
+
if (!MIME[ext]) {
|
|
101
|
+
res.writeHead(404);
|
|
102
|
+
res.end('Not found');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fs.readFile(filePath).then(content => {
|
|
107
|
+
res.writeHead(200, { 'Content-Type': MIME[ext] });
|
|
108
|
+
res.end(content);
|
|
109
|
+
}).catch(() => {
|
|
110
|
+
res.writeHead(404);
|
|
111
|
+
res.end('Not found');
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseBody(req) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
let body = '';
|
|
118
|
+
req.on('data', chunk => body += chunk);
|
|
119
|
+
req.on('end', () => {
|
|
120
|
+
try { resolve(JSON.parse(body)); }
|
|
121
|
+
catch { reject(new Error('Invalid JSON')); }
|
|
122
|
+
});
|
|
123
|
+
req.on('error', reject);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function handleAPI(req, res) {
|
|
128
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
129
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
130
|
+
|
|
131
|
+
if (parts[0] !== 'api') return false;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
switch (parts[1]) {
|
|
135
|
+
case 'locales': {
|
|
136
|
+
if (parts[2]) {
|
|
137
|
+
const filePath = path.join(LOCALES_ROOT, `${parts[2]}.i18n`);
|
|
138
|
+
const content = await readFile(filePath).catch(() => null);
|
|
139
|
+
if (!content) {
|
|
140
|
+
jsonResponse(res, { error: 'Locale not found' }, 404);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
const messages = parseLocale(content);
|
|
144
|
+
const obj = {};
|
|
145
|
+
messages.forEach((v, k) => obj[k] = v);
|
|
146
|
+
jsonResponse(res, { locale: parts[2], messages: obj });
|
|
147
|
+
} else {
|
|
148
|
+
const list = await getSupportedLocales();
|
|
149
|
+
const data = {};
|
|
150
|
+
for (const loc of list) {
|
|
151
|
+
const content = await readFile(path.join(LOCALES_ROOT, `${loc}.i18n`));
|
|
152
|
+
const messages = parseLocale(content);
|
|
153
|
+
const obj = {};
|
|
154
|
+
messages.forEach((v, k) => obj[k] = v);
|
|
155
|
+
data[loc] = obj;
|
|
156
|
+
}
|
|
157
|
+
jsonResponse(res, { locales: list, data });
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case 'translate': {
|
|
163
|
+
const { text, lang } = await parseBody(req);
|
|
164
|
+
if (!text || !lang) {
|
|
165
|
+
jsonResponse(res, { error: 'text and lang required' }, 400);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
const result = await translate(text, lang);
|
|
169
|
+
jsonResponse(res, { translated: result });
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case 'messages': {
|
|
174
|
+
if (req.method !== 'POST') {
|
|
175
|
+
jsonResponse(res, { error: 'Method not allowed' }, 405);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const body = await parseBody(req);
|
|
180
|
+
|
|
181
|
+
if (parts[2] === 'add') {
|
|
182
|
+
const { message, customId } = body;
|
|
183
|
+
if (!message) {
|
|
184
|
+
jsonResponse(res, { error: 'message required' }, 400);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const locales = await getSupportedLocales();
|
|
189
|
+
const messageId = (customId && customId.trim()) ? customId.trim() : toMessageId(message);
|
|
190
|
+
const results = { messageId, locales: {} };
|
|
191
|
+
|
|
192
|
+
for (const locale of locales) {
|
|
193
|
+
const filePath = path.join(LOCALES_ROOT, `${locale}.i18n`);
|
|
194
|
+
const content = await readFile(filePath);
|
|
195
|
+
const messages = parseLocale(content);
|
|
196
|
+
|
|
197
|
+
if (locale === 'en') {
|
|
198
|
+
if (!messages.has(messageId)) {
|
|
199
|
+
messages.set(messageId, message);
|
|
200
|
+
await writeFile(filePath, stringifyLocale(messages));
|
|
201
|
+
results.locales[locale] = message;
|
|
202
|
+
} else {
|
|
203
|
+
results.locales[locale] = { exists: true };
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
if (messages.has(messageId)) {
|
|
207
|
+
results.locales[locale] = { exists: true };
|
|
208
|
+
} else {
|
|
209
|
+
await timeout(1000);
|
|
210
|
+
const translated = await translate(message, locale);
|
|
211
|
+
messages.set(messageId, translated);
|
|
212
|
+
await writeFile(filePath, stringifyLocale(messages));
|
|
213
|
+
results.locales[locale] = translated;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
jsonResponse(res, results);
|
|
219
|
+
} else if (parts[2] === 'delete') {
|
|
220
|
+
const { messageId, targetLocale } = body;
|
|
221
|
+
if (!messageId) {
|
|
222
|
+
jsonResponse(res, { error: 'messageId required' }, 400);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const locales = targetLocale ? [targetLocale] : await getSupportedLocales();
|
|
227
|
+
const deleted = [];
|
|
228
|
+
|
|
229
|
+
for (const locale of locales) {
|
|
230
|
+
const filePath = path.join(LOCALES_ROOT, `${locale}.i18n`);
|
|
231
|
+
try {
|
|
232
|
+
const content = await readFile(filePath);
|
|
233
|
+
const messages = parseLocale(content);
|
|
234
|
+
if (messages.has(messageId)) {
|
|
235
|
+
messages.delete(messageId);
|
|
236
|
+
await writeFile(filePath, stringifyLocale(messages));
|
|
237
|
+
deleted.push(locale);
|
|
238
|
+
}
|
|
239
|
+
} catch { /* skip missing */ }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
jsonResponse(res, { deleted });
|
|
243
|
+
} else {
|
|
244
|
+
jsonResponse(res, { error: 'Unknown action' }, 400);
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case 'json2i18n': {
|
|
250
|
+
if (req.method !== 'POST') {
|
|
251
|
+
jsonResponse(res, { error: 'Method not allowed' }, 405);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
const body = await parseBody(req);
|
|
255
|
+
if (!body.json) {
|
|
256
|
+
jsonResponse(res, { error: 'json field required' }, 400);
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
const result = json2i18n(body.json);
|
|
260
|
+
jsonResponse(res, { i18n: result });
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
default:
|
|
265
|
+
jsonResponse(res, { error: 'Unknown endpoint' }, 404);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
} catch (err) {
|
|
269
|
+
jsonResponse(res, { error: err.message }, 500);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const server = http.createServer(async (req, res) => {
|
|
275
|
+
if (req.url.startsWith('/api/')) {
|
|
276
|
+
await handleAPI(req, res);
|
|
277
|
+
} else {
|
|
278
|
+
serveStatic(req, res);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
server.listen(PORT, () => {
|
|
283
|
+
const url = `http://localhost:${PORT}`;
|
|
284
|
+
console.log(`Addonova Tools UI → ${url}`);
|
|
285
|
+
console.log(`Working directory locales: ${LOCALES_ROOT}`);
|
|
286
|
+
openBrowser(url);
|
|
287
|
+
});
|