addonova 1.0.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/LICENSE +674 -0
- package/README.md +2 -0
- package/bin/index.js +22 -0
- package/package.json +42 -0
- package/src/commands/init.js +108 -0
- package/src/index.js +1 -0
- package/src/utils/prompts.js +32 -0
- package/template/config/chrome.js +29 -0
- package/template/config/edge.js +29 -0
- package/template/config/firefox.js +29 -0
- package/template/config/naver.js +29 -0
- package/template/config/opera.js +29 -0
- package/template/config/thunderbird.js +29 -0
- package/template/package.json.tpl +39 -0
- package/template/src/_locales/en.i18n +4 -0
- package/template/src/html/popup.html +15 -0
- package/template/src/js/background.js +1 -0
- package/template/src/js/popup.js +6 -0
- package/template/src/manifest/manifest-firefox.json +29 -0
- package/template/src/manifest/manifest.json +24 -0
- package/template/src/scss/popup.scss +25 -0
- package/template/tasks/build.js +82 -0
- package/template/tasks/bundle-css.js +108 -0
- package/template/tasks/bundle-html.js +91 -0
- package/template/tasks/bundle-js.js +95 -0
- package/template/tasks/bundle-locales.js +139 -0
- package/template/tasks/bundle-manifest.js +54 -0
- package/template/tasks/cli.js +85 -0
- package/template/tasks/copy.js +49 -0
- package/template/tasks/folder.js +25 -0
- package/template/tasks/paths.js +21 -0
- package/template/tasks/task.js +60 -0
- package/template/tasks/translate.js +255 -0
- package/template/tasks/utils.js +134 -0
- package/template/tasks/watch.js +46 -0
- package/template/tasks/zip.js +66 -0
- package/template/tools/json2i18n.js +33 -0
- package/template/tools/translate.js +299 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {log} from './utils.js';
|
|
2
|
+
import watch from './watch.js';
|
|
3
|
+
|
|
4
|
+
class Task {
|
|
5
|
+
constructor(name, run) {
|
|
6
|
+
this.name = name;
|
|
7
|
+
this._run = run;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
addWatcher(files, onChange) {
|
|
11
|
+
this._watchFiles = files;
|
|
12
|
+
this._onChange = onChange;
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async _measureTime(fn) {
|
|
17
|
+
const start = Date.now();
|
|
18
|
+
await fn();
|
|
19
|
+
const end = Date.now();
|
|
20
|
+
log(`${this.name} (${(end - start).toFixed(0)}ms)`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async run(options) {
|
|
24
|
+
await this._measureTime(
|
|
25
|
+
() => this._run(options)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async watch(platforms, isDebug) {
|
|
30
|
+
if (!this._watchFiles || !this._onChange) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const watcher = watch({
|
|
35
|
+
files: typeof this._watchFiles === 'function' ?
|
|
36
|
+
await this._watchFiles() :
|
|
37
|
+
this._watchFiles,
|
|
38
|
+
onChange: async (files) => {
|
|
39
|
+
await this._measureTime(
|
|
40
|
+
() => this._onChange(files, watcher, platforms, isDebug)
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createTask(name, run) {
|
|
48
|
+
return new Task(name, run);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function runTasks(tasks, options) {
|
|
52
|
+
for (const task of tasks) {
|
|
53
|
+
try {
|
|
54
|
+
await task.run(options);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
log.error(`${task.name} error\n${err.stack || err}`);
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import * as readline from 'node:readline/promises';
|
|
3
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
4
|
+
import {readFile, writeFile, httpsRequest, timeout} from './utils.js';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const LOCALES_ROOT = 'src/_locales';
|
|
8
|
+
|
|
9
|
+
function toMessageId(message) {
|
|
10
|
+
if (typeof message !== 'string') return '';
|
|
11
|
+
|
|
12
|
+
return message
|
|
13
|
+
.trim()
|
|
14
|
+
.split(/\s+/) // split by any whitespace
|
|
15
|
+
.slice(0, 3) // only first 3 words
|
|
16
|
+
.map(word =>
|
|
17
|
+
word
|
|
18
|
+
.replace(/[^\w]/g, '') // remove special characters
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
)
|
|
21
|
+
.filter(Boolean) // remove empty results
|
|
22
|
+
.join('_');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function getSupportedLocales() {
|
|
26
|
+
const fileList = await fs.readdir(LOCALES_ROOT);
|
|
27
|
+
|
|
28
|
+
const locales = [];
|
|
29
|
+
|
|
30
|
+
for (const file of fileList) {
|
|
31
|
+
if (file.endsWith('.i18n')) {
|
|
32
|
+
const locale = file.substring(0, file.lastIndexOf('.i18n'));
|
|
33
|
+
locales.push(locale);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return locales;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function stringifyLocale(messages) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
messages.forEach((message, id) => {
|
|
43
|
+
lines.push(`@${id}`);
|
|
44
|
+
const hasDoubleNewLines = /\n\n/.test(message);
|
|
45
|
+
message.split('\n')
|
|
46
|
+
.filter((line) => line.trim())
|
|
47
|
+
.forEach((line, index, filtered) => {
|
|
48
|
+
lines.push(line);
|
|
49
|
+
if (hasDoubleNewLines && index < filtered.length - 1) {
|
|
50
|
+
lines.push('');
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
lines.push('');
|
|
54
|
+
});
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseLocale(content) {
|
|
59
|
+
const messages = new Map();
|
|
60
|
+
const lines = content.split('\n');
|
|
61
|
+
let id = '';
|
|
62
|
+
for (let i = 0; i < lines.length; i++) {
|
|
63
|
+
const line = lines[i];
|
|
64
|
+
if (line.startsWith('@')) {
|
|
65
|
+
id = line.substring(1);
|
|
66
|
+
} else if (line.startsWith('#')) {
|
|
67
|
+
// Ignore
|
|
68
|
+
} else if (messages.has(id)) {
|
|
69
|
+
const message = messages.get(id);
|
|
70
|
+
messages.set(id, `${message}\n${line}`);
|
|
71
|
+
} else {
|
|
72
|
+
messages.set(id, line);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
messages.forEach((value, id) => {
|
|
76
|
+
messages.set(id, value.trim());
|
|
77
|
+
});
|
|
78
|
+
return messages;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function translate(text, lang) {
|
|
82
|
+
const url = new URL('https://translate.googleapis.com/translate_a/single');
|
|
83
|
+
url.search = (new URLSearchParams({
|
|
84
|
+
client: 'gtx',
|
|
85
|
+
sl: 'en-US',
|
|
86
|
+
tl: lang,
|
|
87
|
+
dt: 't',
|
|
88
|
+
dj: '1',
|
|
89
|
+
q: text,
|
|
90
|
+
})).toString();
|
|
91
|
+
const response = await httpsRequest(url.toString());
|
|
92
|
+
const translation = JSON.parse(response.text());
|
|
93
|
+
return translation.sentences.map((s) => s.trans).join('\n').replaceAll(/\n+/g, '\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function deleteMessage(messageId) {
|
|
97
|
+
const supportedLocales = await getSupportedLocales();
|
|
98
|
+
|
|
99
|
+
if (!messageId || !messageId.trim()) {
|
|
100
|
+
console.log('⚠ Message key cannot be empty.');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const id = messageId.trim();
|
|
105
|
+
let foundAnywhere = false;
|
|
106
|
+
|
|
107
|
+
for (const locale of supportedLocales) {
|
|
108
|
+
const locFile = `${LOCALES_ROOT}/${locale}.i18n`;
|
|
109
|
+
const locContent = await readFile(locFile);
|
|
110
|
+
const locMessages = parseLocale(locContent);
|
|
111
|
+
|
|
112
|
+
if (!locMessages.has(id)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
locMessages.delete(id);
|
|
117
|
+
const output = stringifyLocale(locMessages);
|
|
118
|
+
await writeFile(locFile, output);
|
|
119
|
+
|
|
120
|
+
console.log(`✔ Removed from ${locale}.i18n`);
|
|
121
|
+
foundAnywhere = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!foundAnywhere) {
|
|
125
|
+
console.log('⚠ Message key not found in any locale.');
|
|
126
|
+
} else {
|
|
127
|
+
console.log('🗑 Deletion completed.');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async function translateEnMessage(message, customId) {
|
|
133
|
+
console.log(`Translating message: ${message}`);
|
|
134
|
+
|
|
135
|
+
const supportedLocales = await getSupportedLocales();
|
|
136
|
+
const messageId = customId && customId.trim()
|
|
137
|
+
? customId.trim()
|
|
138
|
+
: toMessageId(message);
|
|
139
|
+
|
|
140
|
+
// 1️⃣ Update English first
|
|
141
|
+
const enFile = `${LOCALES_ROOT}/en.i18n`;
|
|
142
|
+
const enContent = await readFile(enFile);
|
|
143
|
+
const enMessages = parseLocale(enContent);
|
|
144
|
+
|
|
145
|
+
if (!enMessages.has(messageId)) {
|
|
146
|
+
enMessages.set(messageId, message);
|
|
147
|
+
const output = stringifyLocale(enMessages);
|
|
148
|
+
await writeFile(enFile, output);
|
|
149
|
+
console.log(`en: ${message}`);
|
|
150
|
+
} else {
|
|
151
|
+
console.log('Message already exists in en.i18n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2️⃣ Translate other locales
|
|
155
|
+
for (const locale of supportedLocales) {
|
|
156
|
+
if (locale === 'en') continue;
|
|
157
|
+
|
|
158
|
+
await timeout(1000);
|
|
159
|
+
|
|
160
|
+
const locFile = `${LOCALES_ROOT}/${locale}.i18n`;
|
|
161
|
+
const locContent = await readFile(locFile);
|
|
162
|
+
const locMessages = parseLocale(locContent);
|
|
163
|
+
|
|
164
|
+
if (locMessages.has(messageId)) {
|
|
165
|
+
console.log(`Already exists in: ${locFile}`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const translated = await translate(message, locale);
|
|
170
|
+
locMessages.set(messageId, translated);
|
|
171
|
+
|
|
172
|
+
const output = stringifyLocale(locMessages);
|
|
173
|
+
await writeFile(locFile, output);
|
|
174
|
+
|
|
175
|
+
console.log(`${locale}: ${translated}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function main() {
|
|
180
|
+
const rl = readline.createInterface({ input, output });
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
while (true) {
|
|
184
|
+
console.log('\n=== Message Manager ===');
|
|
185
|
+
console.log('[1] Add new message');
|
|
186
|
+
console.log('[2] Delete message');
|
|
187
|
+
console.log('[0] Exit');
|
|
188
|
+
|
|
189
|
+
const choice = (await rl.question('Select option: '))
|
|
190
|
+
.trim()
|
|
191
|
+
.toLowerCase();
|
|
192
|
+
|
|
193
|
+
switch (choice) {
|
|
194
|
+
case '1': {
|
|
195
|
+
const newMessage = (await rl.question('Enter new message: '))
|
|
196
|
+
.trim();
|
|
197
|
+
|
|
198
|
+
if (!newMessage) {
|
|
199
|
+
console.log('⚠ Message cannot be empty.');
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const newMessageId = (await rl.question(
|
|
204
|
+
'Enter new message key (leave empty for auto-generation): '
|
|
205
|
+
)).trim();
|
|
206
|
+
|
|
207
|
+
await translateEnMessage(
|
|
208
|
+
newMessage,
|
|
209
|
+
newMessageId || undefined
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
console.log('✔ Message processed successfully.');
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case '2': {
|
|
217
|
+
const messageId = (await rl.question(
|
|
218
|
+
'Enter message key to delete: '
|
|
219
|
+
)).trim();
|
|
220
|
+
|
|
221
|
+
if (!messageId) {
|
|
222
|
+
console.log('⚠ Message key cannot be empty.');
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const confirm = (await rl.question(
|
|
227
|
+
`Are you sure you want to delete "${messageId}" from all locales? (y/n): `
|
|
228
|
+
))
|
|
229
|
+
.trim()
|
|
230
|
+
.toLowerCase();
|
|
231
|
+
|
|
232
|
+
if (confirm !== 'y') {
|
|
233
|
+
console.log('Deletion cancelled.');
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await deleteMessage(messageId);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case '0':
|
|
242
|
+
case 'exit': {
|
|
243
|
+
console.log('Exiting...');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
default: {
|
|
248
|
+
console.log('Invalid selection. Please try again.');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} finally {
|
|
253
|
+
rl.close();
|
|
254
|
+
}
|
|
255
|
+
}main();
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {accessSync, existsSync} from 'node:fs';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import https from 'node:https';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import {absolutePath} from './paths.js';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const colors = Object.entries({
|
|
10
|
+
gray: '\x1b[90m',
|
|
11
|
+
green: '\x1b[32m',
|
|
12
|
+
red: '\x1b[31m',
|
|
13
|
+
yellow: '\x1b[33m',
|
|
14
|
+
}).reduce((map, [key, value]) => Object.assign(map, {[key]: (text) => `${value}${text}\x1b[0m`}), {});
|
|
15
|
+
|
|
16
|
+
export function logWithTime(text) {
|
|
17
|
+
const now = new Date();
|
|
18
|
+
const hours = now.getHours();
|
|
19
|
+
const minutes = now.getMinutes();
|
|
20
|
+
const seconds = now.getSeconds();
|
|
21
|
+
const leftpad = (/** @type {number} */n) => String(n).padStart(2, '0');
|
|
22
|
+
return console.log(`${colors.gray([hours, minutes, seconds].map(leftpad).join(':'))} ${text}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const log = Object.assign((text) => logWithTime(text), {
|
|
26
|
+
ok: (text) => logWithTime(colors.green(text)),
|
|
27
|
+
warn: (text) => logWithTime(colors.yellow(text)),
|
|
28
|
+
error: (text) => logWithTime(colors.red(text)),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export async function pathExists(dest) {
|
|
32
|
+
try {
|
|
33
|
+
accessSync(dest);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function mkDirIfMissing(dest) {
|
|
41
|
+
const dir = path.dirname(dest);
|
|
42
|
+
if (!(await pathExists(dir))) {
|
|
43
|
+
await fs.mkdir(dir, {recursive: true});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function readFile(src, encoding = 'utf8') {
|
|
48
|
+
return await fs.readFile(src, encoding);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function writeFile(dest, content, encoding = 'utf8') {
|
|
52
|
+
await mkDirIfMissing(dest);
|
|
53
|
+
await fs.writeFile(dest, content, encoding);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function readJSON(path) {
|
|
57
|
+
const file = await readFile(path);
|
|
58
|
+
return JSON.parse(file);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function writeJSON(dest, content, space = 4) {
|
|
62
|
+
const string = JSON.stringify(content, null, space);
|
|
63
|
+
return await writeFile(dest, string);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function getConfig(platform) {
|
|
67
|
+
const config = {};
|
|
68
|
+
const targetConfigPath = path.resolve(`config/${platform}.js`);
|
|
69
|
+
if (existsSync(targetConfigPath)) {
|
|
70
|
+
const targetConfig = await import(
|
|
71
|
+
pathToFileURL(targetConfigPath).href
|
|
72
|
+
);
|
|
73
|
+
Object.assign(config, targetConfig.default ?? targetConfig);
|
|
74
|
+
}
|
|
75
|
+
return config;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function getAllFiles(folderPath){
|
|
79
|
+
const {globby} = await import('globby');
|
|
80
|
+
return await globby(folderPath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function removeFolder(dir) {
|
|
84
|
+
if (await pathExists(dir)) {
|
|
85
|
+
await fs.rm(dir, {recursive: true});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function createFolder(dir) {
|
|
90
|
+
if (!(await pathExists(dir))) {
|
|
91
|
+
await fs.mkdir(dir, {recursive: true});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function copyFile(src, dest) {
|
|
96
|
+
await mkDirIfMissing(dest);
|
|
97
|
+
await fs.copyFile(src, dest);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function fileExistsInConfig(config, absoluteFilePath) {
|
|
101
|
+
const normalizedTarget = absolutePath(absoluteFilePath);
|
|
102
|
+
for (const [dest, src] of Object.entries(config)) {
|
|
103
|
+
for (const file of src) {
|
|
104
|
+
const relativePath = absolutePath(file);
|
|
105
|
+
// console.log(`Checking if ${relativePath} == ${normalizedTarget} or ${absoluteFilePath}`);
|
|
106
|
+
if (relativePath == normalizedTarget || relativePath == absoluteFilePath) {
|
|
107
|
+
return JSON.parse(`{"${dest}":["${file}"]}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function timeout(delay) {
|
|
115
|
+
return new Promise((resolve) => setTimeout(resolve, delay));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function httpsRequest(url) {
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
const data = [];
|
|
121
|
+
https.get(url, (response) => {
|
|
122
|
+
response
|
|
123
|
+
.on('data', (chunk) => data.push(chunk))
|
|
124
|
+
.on('end', () => {
|
|
125
|
+
const buffer = Buffer.concat(data);
|
|
126
|
+
resolve({
|
|
127
|
+
buffer: () => buffer,
|
|
128
|
+
text: (encoding = 'utf8') => buffer.toString(encoding),
|
|
129
|
+
type: () => response.headers['content-type'] || '',
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {watch as chokidarWatch} from 'chokidar';
|
|
2
|
+
|
|
3
|
+
import {log} from './utils.js';
|
|
4
|
+
|
|
5
|
+
const DEBOUNCE = 200;
|
|
6
|
+
|
|
7
|
+
function watch(options) {
|
|
8
|
+
const queue = new Set();
|
|
9
|
+
let timeoutId = null;
|
|
10
|
+
|
|
11
|
+
function onChange(path) {
|
|
12
|
+
queue.add(path);
|
|
13
|
+
|
|
14
|
+
if (timeoutId !== null) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
timeoutId = setTimeout(async () => {
|
|
19
|
+
timeoutId = null;
|
|
20
|
+
try {
|
|
21
|
+
const changedFiles = Array.from(queue).sort();
|
|
22
|
+
log.ok(`🔄 Files changed:${changedFiles.map((path) => `\n${path}`)}`);
|
|
23
|
+
queue.clear();
|
|
24
|
+
await options.onChange(changedFiles);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
log.error(err);
|
|
27
|
+
}
|
|
28
|
+
}, DEBOUNCE);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const watcher = chokidarWatch(options.files, {ignoreInitial: true})
|
|
32
|
+
.on('add', onChange)
|
|
33
|
+
.on('change', onChange)
|
|
34
|
+
.on('unlink', onChange);
|
|
35
|
+
|
|
36
|
+
function stop() {
|
|
37
|
+
watcher.close();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.on('exit', stop);
|
|
41
|
+
process.on('SIGINT', stop);
|
|
42
|
+
|
|
43
|
+
return watcher;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default watch;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {exec} from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import yazl from 'yazl';
|
|
5
|
+
|
|
6
|
+
import {getDestDir, absolutePath} from './paths.js';
|
|
7
|
+
import {createTask} from './task.js';
|
|
8
|
+
|
|
9
|
+
import {getAllFiles, getConfig, readJSON} from './utils.js';
|
|
10
|
+
|
|
11
|
+
function archiveFiles({files, dest, cwd, date, mode}) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const archive = new yazl.ZipFile();
|
|
14
|
+
files.sort();
|
|
15
|
+
files.forEach((file) => archive.addFile(
|
|
16
|
+
file,
|
|
17
|
+
file.startsWith(`${cwd}/`) ? file.substring(cwd.length + 1) : file,
|
|
18
|
+
{mtime: date, mode}
|
|
19
|
+
));
|
|
20
|
+
const writeStream = fs.createWriteStream(dest);
|
|
21
|
+
archive.outputStream.pipe(writeStream).on('close', resolve);
|
|
22
|
+
archive.end();
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function getLastCommitTime() {
|
|
27
|
+
return new Promise((resolve) =>
|
|
28
|
+
exec('git log -1 --format=%ct', (_, stdout) => resolve(new Date(
|
|
29
|
+
Math.max(0, Number(stdout) + (new Date()).getTimezoneOffset() * 60) * 1000
|
|
30
|
+
))));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function archiveDirectory({dir, dest, date, mode}) {
|
|
34
|
+
const files = await getAllFiles(`${dir}/**/*.*`);
|
|
35
|
+
await archiveFiles({files, dest, cwd: dir, date, mode});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async function zip({platforms, isDebug, version}) {
|
|
40
|
+
if (isDebug) {
|
|
41
|
+
throw new Error('zip task does not support debug builds');
|
|
42
|
+
}
|
|
43
|
+
version = version ? `-${version}` : '';
|
|
44
|
+
const releaseDir = 'build/release';
|
|
45
|
+
const promises = [];
|
|
46
|
+
const date = await getLastCommitTime();
|
|
47
|
+
const xpiPlatforms = ["firefox", "thunderbird"];
|
|
48
|
+
for (const platform of platforms) {
|
|
49
|
+
const config = await getConfig(platform);
|
|
50
|
+
if (config && Object.keys(config).length != 0){
|
|
51
|
+
const packageJSON = await readJSON(absolutePath("package.json"));
|
|
52
|
+
const format = xpiPlatforms.includes(platform) ? 'xpi' : 'zip';
|
|
53
|
+
const dest = `${releaseDir}/${packageJSON.name}-${platform}${version}.${format}`;
|
|
54
|
+
promises.push(archiveDirectory({
|
|
55
|
+
dir: getDestDir({isDebug, platform}),
|
|
56
|
+
dest,
|
|
57
|
+
date,
|
|
58
|
+
mode: 0o644
|
|
59
|
+
}));
|
|
60
|
+
await Promise.all(promises);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const zipTask = createTask('zip', zip,);
|
|
66
|
+
export default zipTask;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
const localesDir = join(__dirname, "_locales");
|
|
5
|
+
const outputDir = join(__dirname, "output", "_locales");
|
|
6
|
+
|
|
7
|
+
mkdirSync(outputDir, { recursive: true });
|
|
8
|
+
|
|
9
|
+
console.log("Output directory ready.");
|
|
10
|
+
|
|
11
|
+
const languages = readdirSync(localesDir);
|
|
12
|
+
|
|
13
|
+
languages.forEach((lang) => {
|
|
14
|
+
const messagesPath = join(localesDir, lang, "messages.json");
|
|
15
|
+
|
|
16
|
+
if (!existsSync(messagesPath)) return;
|
|
17
|
+
|
|
18
|
+
const input = JSON.parse(readFileSync(messagesPath, "utf8"));
|
|
19
|
+
|
|
20
|
+
let output = "";
|
|
21
|
+
|
|
22
|
+
for (const key in input) {
|
|
23
|
+
output += `@${key}\n`;
|
|
24
|
+
output += `${input[key].message}\n\n`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const outputPath = join(outputDir, `${lang}.i18n`);
|
|
28
|
+
writeFileSync(outputPath, output);
|
|
29
|
+
|
|
30
|
+
console.log(`${lang}.i18n created`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
console.log("All conversions completed.");
|