xw-devtool-cli 1.0.43 → 1.0.45

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_EN.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  A Node.js-based developer command-line toolbox designed to provide out-of-the-box common development tools to help developers handle daily tasks quickly.
6
6
 
7
- Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, QR code recognition, special symbols, Markdown snippets, VS Code snippet generation, current directory tree generation, etc. Most results can be copied to the clipboard in one step, greatly improving development efficiency.
7
+ Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, console clock, countdown timer, stopwatch, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, QR code recognition, special symbols, Markdown snippets, VS Code snippet generation, current directory tree generation, etc. Most results can be copied to the clipboard in one step, greatly improving development efficiency.
8
8
 
9
9
  ## ✨ Features
10
10
 
@@ -29,6 +29,9 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
29
29
  - **Formatting**: Timestamp/Date string -> `YYYY-MM-DD HH:mm:ss`.
30
30
  - **Timestamp**: Quickly get current millisecond timestamp.
31
31
  - **Calculation**: Calculate date differences or offsets (Add/Subtract).
32
+ - **Console Clock**: Show real-time current time in terminal, press `Enter` or `Q` to go back.
33
+ - **Countdown Timer**: Select seconds/minutes/hours first, then enter the value to start; once it reaches zero, it keeps counting with a negative sign (such as `-00:00:05`).
34
+ - **Stopwatch**: Start from `00:00:00` and count up every second in terminal.
32
35
  - **Dev Tools**:
33
36
  - **URL Encode/Decode**
34
37
  - **Unicode Encode/Decode**: Text <-> Unicode escape sequences (\uXXXX).
@@ -224,6 +227,22 @@ s. Settings (Language)
224
227
  - **Diff**: Calculate difference between two dates.
225
228
  - **Offset**: Calculate date after add/subtract time units.
226
229
 
230
+ ### Console Clock
231
+ - Select `Console Clock` in the menu.
232
+ - Current time refreshes every second in terminal.
233
+ - Press `Enter` or `Q` to go back.
234
+
235
+ ### Countdown Timer
236
+ - Select `Countdown Timer` in the menu.
237
+ - Select unit first (Seconds / Minutes / Hours), then enter the value.
238
+ - After reaching zero, the timer continues with a negative sign (for example `-00:00:01`).
239
+ - Press `Enter` or `Q` to go back.
240
+
241
+ ### Stopwatch
242
+ - Select `Stopwatch` in the menu.
243
+ - It starts from `00:00:00` and updates every second.
244
+ - Press `Enter` or `Q` to go back.
245
+
227
246
  ### 14. Color Converter
228
247
  - Select `e`.
229
248
  - Hex <-> RGB.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xw-devtool-cli",
3
- "version": "1.0.43",
3
+ "version": "1.0.45",
4
4
  "type": "module",
5
5
  "description": "基于node的开发者助手cli",
6
6
  "main": "index.js",
@@ -34,6 +34,12 @@
34
34
  "mock-data",
35
35
  "timestamp",
36
36
  "time",
37
+ "clock",
38
+ "console-clock",
39
+ "countdown",
40
+ "countdown-timer",
41
+ "stopwatch",
42
+ "stopwatch-timer",
37
43
  "date-calculation",
38
44
  "image",
39
45
  "image-conversion",
@@ -0,0 +1,142 @@
1
+ import readline from 'readline';
2
+ import i18next from '../i18n.js';
3
+
4
+ function getLocale() {
5
+ return i18next.language && i18next.language.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en-US';
6
+ }
7
+
8
+ function getNowDisplay(locale) {
9
+ const now = new Date();
10
+ const timeText = new Intl.DateTimeFormat(locale, {
11
+ hour12: false,
12
+ hour: '2-digit',
13
+ minute: '2-digit',
14
+ second: '2-digit'
15
+ }).format(now);
16
+ const dateText = new Intl.DateTimeFormat(locale, {
17
+ year: 'numeric',
18
+ month: '2-digit',
19
+ day: '2-digit',
20
+ weekday: 'long'
21
+ }).format(now);
22
+ return { timeText, dateText };
23
+ }
24
+
25
+ function getCharDisplayWidth(char) {
26
+ if (/[\u0000-\u001f\u007f]/.test(char)) {
27
+ return 0;
28
+ }
29
+ if (
30
+ /[\u1100-\u115f\u2329\u232a\u2e80-\u303e\u3040-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]/.test(char)
31
+ ) {
32
+ return 2;
33
+ }
34
+ return 1;
35
+ }
36
+
37
+ function getDisplayWidth(text) {
38
+ let width = 0;
39
+ for (const char of text) {
40
+ width += getCharDisplayWidth(char);
41
+ }
42
+ return width;
43
+ }
44
+
45
+ function fitText(text, width) {
46
+ let result = '';
47
+ let currentWidth = 0;
48
+ for (const char of text) {
49
+ const charWidth = getCharDisplayWidth(char);
50
+ if (currentWidth + charWidth > width) {
51
+ break;
52
+ }
53
+ result += char;
54
+ currentWidth += charWidth;
55
+ }
56
+ const remaining = width - getDisplayWidth(result);
57
+ if (remaining > 0) {
58
+ return `${result}${' '.repeat(remaining)}`;
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function renderClockBlock() {
64
+ const { timeText, dateText } = getNowDisplay(getLocale());
65
+ const contentLines = [
66
+ i18next.t('clock.title'),
67
+ '',
68
+ `${i18next.t('clock.now')}${timeText}`,
69
+ dateText,
70
+ i18next.t('clock.exitTip')
71
+ ];
72
+ const innerWidth = Math.max(34, ...contentLines.map((text) => getDisplayWidth(text)));
73
+ const top = `┌${'─'.repeat(innerWidth + 2)}┐`;
74
+ const bottom = `└${'─'.repeat(innerWidth + 2)}┘`;
75
+ const line = (text) => `│ ${fitText(text, innerWidth)} │`;
76
+ return [
77
+ top,
78
+ line(contentLines[0]),
79
+ line(''),
80
+ line(contentLines[2]),
81
+ line(contentLines[3]),
82
+ line(contentLines[4]),
83
+ bottom
84
+ ];
85
+ }
86
+
87
+ export async function clockHandler() {
88
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
89
+ const { timeText, dateText } = getNowDisplay(getLocale());
90
+ console.log(`${i18next.t('clock.now')}${timeText} (${dateText})`);
91
+ return;
92
+ }
93
+
94
+ await new Promise((resolve) => {
95
+ let renderedLineCount = 0;
96
+ let shouldExitApp = false;
97
+
98
+ const render = () => {
99
+ const lines = renderClockBlock();
100
+ if (renderedLineCount > 0) {
101
+ readline.moveCursor(process.stdout, 0, -renderedLineCount);
102
+ readline.cursorTo(process.stdout, 0);
103
+ readline.clearScreenDown(process.stdout);
104
+ }
105
+ process.stdout.write(`${lines.join('\n')}\n`);
106
+ renderedLineCount = lines.length;
107
+ };
108
+
109
+ const cleanup = () => {
110
+ clearInterval(timer);
111
+ process.stdin.off('data', onData);
112
+ process.stdin.setRawMode(false);
113
+ process.stdin.pause();
114
+ readline.cursorTo(process.stdout, 0);
115
+ readline.clearScreenDown(process.stdout);
116
+ process.stdout.write('\n');
117
+ if (shouldExitApp) {
118
+ console.log(i18next.t('menu.bye'));
119
+ process.exit(0);
120
+ }
121
+ resolve();
122
+ };
123
+
124
+ const onData = (chunk) => {
125
+ const key = chunk.toString().toLowerCase();
126
+ if (key === '0') {
127
+ shouldExitApp = true;
128
+ cleanup();
129
+ return;
130
+ }
131
+ if (key === '\r' || key === 'q' || key === '\u0003') {
132
+ cleanup();
133
+ }
134
+ };
135
+
136
+ const timer = setInterval(render, 1000);
137
+ process.stdin.setRawMode(true);
138
+ process.stdin.resume();
139
+ process.stdin.on('data', onData);
140
+ render();
141
+ });
142
+ }
@@ -0,0 +1,195 @@
1
+ import inquirer from 'inquirer';
2
+ import readline from 'readline';
3
+ import i18next from '../i18n.js';
4
+ import { selectFromMenu } from '../utils/menu.js';
5
+
6
+ function getCharDisplayWidth(char) {
7
+ if (/[\u0000-\u001f\u007f]/.test(char)) {
8
+ return 0;
9
+ }
10
+ if (
11
+ /[\u1100-\u115f\u2329\u232a\u2e80-\u303e\u3040-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]/.test(char)
12
+ ) {
13
+ return 2;
14
+ }
15
+ return 1;
16
+ }
17
+
18
+ function getDisplayWidth(text) {
19
+ let width = 0;
20
+ for (const char of text) {
21
+ width += getCharDisplayWidth(char);
22
+ }
23
+ return width;
24
+ }
25
+
26
+ function fitText(text, width) {
27
+ let result = '';
28
+ let currentWidth = 0;
29
+ for (const char of text) {
30
+ const charWidth = getCharDisplayWidth(char);
31
+ if (currentWidth + charWidth > width) {
32
+ break;
33
+ }
34
+ result += char;
35
+ currentWidth += charWidth;
36
+ }
37
+ const remaining = width - getDisplayWidth(result);
38
+ if (remaining > 0) {
39
+ return `${result}${' '.repeat(remaining)}`;
40
+ }
41
+ return result;
42
+ }
43
+
44
+ function formatSeconds(seconds) {
45
+ const isNegative = seconds < 0;
46
+ const absSeconds = Math.abs(seconds);
47
+ const hours = Math.floor(absSeconds / 3600);
48
+ const minutes = Math.floor((absSeconds % 3600) / 60);
49
+ const secs = absSeconds % 60;
50
+ const sign = isNegative ? '-' : '';
51
+ return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
52
+ }
53
+
54
+ function parseAmount(input) {
55
+ const value = Number(input);
56
+ if (!Number.isFinite(value) || value < 0) {
57
+ return null;
58
+ }
59
+ return value;
60
+ }
61
+
62
+ function renderCountdownBlock(totalSeconds, elapsedSeconds) {
63
+ const remainSeconds = totalSeconds - elapsedSeconds;
64
+ const contentLines = [
65
+ i18next.t('countdown.title'),
66
+ '',
67
+ `${i18next.t('countdown.remaining')}${formatSeconds(remainSeconds)}`,
68
+ `${i18next.t('countdown.total')}${formatSeconds(totalSeconds)}`,
69
+ `${i18next.t('countdown.status')}${remainSeconds < 0 ? i18next.t('countdown.statusOvertime') : i18next.t('countdown.statusCounting')}`,
70
+ i18next.t('countdown.exitTip')
71
+ ];
72
+ const innerWidth = Math.max(34, ...contentLines.map((text) => getDisplayWidth(text)));
73
+ const top = `┌${'─'.repeat(innerWidth + 2)}┐`;
74
+ const bottom = `└${'─'.repeat(innerWidth + 2)}┘`;
75
+ const line = (text) => `│ ${fitText(text, innerWidth)} │`;
76
+
77
+ return [
78
+ top,
79
+ line(contentLines[0]),
80
+ line(''),
81
+ line(contentLines[2]),
82
+ line(contentLines[3]),
83
+ line(contentLines[4]),
84
+ line(contentLines[5]),
85
+ bottom
86
+ ];
87
+ }
88
+
89
+ export async function countdownHandler() {
90
+ const unitOptions = [
91
+ { name: i18next.t('countdown.unitSecond'), value: 'second' },
92
+ { name: i18next.t('countdown.unitMinute'), value: 'minute' },
93
+ { name: i18next.t('countdown.unitHour'), value: 'hour' }
94
+ ];
95
+
96
+ const unit = await selectFromMenu(
97
+ i18next.t('countdown.selectUnit'),
98
+ unitOptions,
99
+ true,
100
+ i18next.t('common.back')
101
+ );
102
+
103
+ if (unit === '__BACK__') {
104
+ return;
105
+ }
106
+
107
+ const unitNameMap = {
108
+ second: i18next.t('countdown.unitSecond'),
109
+ minute: i18next.t('countdown.unitMinute'),
110
+ hour: i18next.t('countdown.unitHour')
111
+ };
112
+
113
+ const { amountInput } = await inquirer.prompt([
114
+ {
115
+ type: 'input',
116
+ name: 'amountInput',
117
+ message: i18next.t('countdown.inputPrompt', { unit: unitNameMap[unit] }),
118
+ default: '1',
119
+ validate: (input) => {
120
+ const value = parseAmount(input);
121
+ if (value === null) {
122
+ return i18next.t('countdown.invalidAmount');
123
+ }
124
+ return true;
125
+ }
126
+ }
127
+ ]);
128
+
129
+ const amount = parseAmount(amountInput);
130
+ const multiplierMap = {
131
+ second: 1,
132
+ minute: 60,
133
+ hour: 3600
134
+ };
135
+ const totalSeconds = Math.round(amount * multiplierMap[unit]);
136
+
137
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
138
+ console.log(i18next.t('countdown.nonTtyTip', { value: formatSeconds(totalSeconds) }));
139
+ return;
140
+ }
141
+
142
+ await new Promise((resolve) => {
143
+ let renderedLineCount = 0;
144
+ let elapsedSeconds = 0;
145
+ let shouldExitApp = false;
146
+
147
+ const render = () => {
148
+ const lines = renderCountdownBlock(totalSeconds, elapsedSeconds);
149
+ if (renderedLineCount > 0) {
150
+ readline.moveCursor(process.stdout, 0, -renderedLineCount);
151
+ readline.cursorTo(process.stdout, 0);
152
+ readline.clearScreenDown(process.stdout);
153
+ }
154
+ process.stdout.write(`${lines.join('\n')}\n`);
155
+ renderedLineCount = lines.length;
156
+ };
157
+
158
+ const cleanup = () => {
159
+ clearInterval(timer);
160
+ process.stdin.off('data', onData);
161
+ process.stdin.setRawMode(false);
162
+ process.stdin.pause();
163
+ readline.cursorTo(process.stdout, 0);
164
+ readline.clearScreenDown(process.stdout);
165
+ process.stdout.write('\n');
166
+ if (shouldExitApp) {
167
+ console.log(i18next.t('menu.bye'));
168
+ process.exit(0);
169
+ }
170
+ resolve();
171
+ };
172
+
173
+ const onData = (chunk) => {
174
+ const key = chunk.toString().toLowerCase();
175
+ if (key === '0') {
176
+ shouldExitApp = true;
177
+ cleanup();
178
+ return;
179
+ }
180
+ if (key === '\r' || key === 'q' || key === '\u0003') {
181
+ cleanup();
182
+ }
183
+ };
184
+
185
+ const timer = setInterval(() => {
186
+ elapsedSeconds += 1;
187
+ render();
188
+ }, 1000);
189
+
190
+ process.stdin.setRawMode(true);
191
+ process.stdin.resume();
192
+ process.stdin.on('data', onData);
193
+ render();
194
+ });
195
+ }
@@ -0,0 +1,132 @@
1
+ import readline from 'readline';
2
+ import i18next from '../i18n.js';
3
+
4
+ function getCharDisplayWidth(char) {
5
+ if (/[\u0000-\u001f\u007f]/.test(char)) {
6
+ return 0;
7
+ }
8
+ if (
9
+ /[\u1100-\u115f\u2329\u232a\u2e80-\u303e\u3040-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]/.test(char)
10
+ ) {
11
+ return 2;
12
+ }
13
+ return 1;
14
+ }
15
+
16
+ function getDisplayWidth(text) {
17
+ let width = 0;
18
+ for (const char of text) {
19
+ width += getCharDisplayWidth(char);
20
+ }
21
+ return width;
22
+ }
23
+
24
+ function fitText(text, width) {
25
+ let result = '';
26
+ let currentWidth = 0;
27
+ for (const char of text) {
28
+ const charWidth = getCharDisplayWidth(char);
29
+ if (currentWidth + charWidth > width) {
30
+ break;
31
+ }
32
+ result += char;
33
+ currentWidth += charWidth;
34
+ }
35
+ const remaining = width - getDisplayWidth(result);
36
+ if (remaining > 0) {
37
+ return `${result}${' '.repeat(remaining)}`;
38
+ }
39
+ return result;
40
+ }
41
+
42
+ function formatSeconds(seconds) {
43
+ const hours = Math.floor(seconds / 3600);
44
+ const minutes = Math.floor((seconds % 3600) / 60);
45
+ const secs = seconds % 60;
46
+ return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
47
+ }
48
+
49
+ function renderStopwatchBlock(elapsedSeconds) {
50
+ const contentLines = [
51
+ i18next.t('stopwatch.title'),
52
+ '',
53
+ `${i18next.t('stopwatch.elapsed')}${formatSeconds(elapsedSeconds)}`,
54
+ `${i18next.t('stopwatch.status')}${i18next.t('stopwatch.statusRunning')}`,
55
+ i18next.t('stopwatch.exitTip')
56
+ ];
57
+ const innerWidth = Math.max(34, ...contentLines.map((text) => getDisplayWidth(text)));
58
+ const top = `┌${'─'.repeat(innerWidth + 2)}┐`;
59
+ const bottom = `└${'─'.repeat(innerWidth + 2)}┘`;
60
+ const line = (text) => `│ ${fitText(text, innerWidth)} │`;
61
+
62
+ return [
63
+ top,
64
+ line(contentLines[0]),
65
+ line(''),
66
+ line(contentLines[2]),
67
+ line(contentLines[3]),
68
+ line(contentLines[4]),
69
+ bottom
70
+ ];
71
+ }
72
+
73
+ export async function stopwatchHandler() {
74
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
75
+ console.log(i18next.t('stopwatch.nonTtyTip'));
76
+ return;
77
+ }
78
+
79
+ await new Promise((resolve) => {
80
+ let renderedLineCount = 0;
81
+ let elapsedSeconds = 0;
82
+ let shouldExitApp = false;
83
+
84
+ const render = () => {
85
+ const lines = renderStopwatchBlock(elapsedSeconds);
86
+ if (renderedLineCount > 0) {
87
+ readline.moveCursor(process.stdout, 0, -renderedLineCount);
88
+ readline.cursorTo(process.stdout, 0);
89
+ readline.clearScreenDown(process.stdout);
90
+ }
91
+ process.stdout.write(`${lines.join('\n')}\n`);
92
+ renderedLineCount = lines.length;
93
+ };
94
+
95
+ const cleanup = () => {
96
+ clearInterval(timer);
97
+ process.stdin.off('data', onData);
98
+ process.stdin.setRawMode(false);
99
+ process.stdin.pause();
100
+ readline.cursorTo(process.stdout, 0);
101
+ readline.clearScreenDown(process.stdout);
102
+ process.stdout.write('\n');
103
+ if (shouldExitApp) {
104
+ console.log(i18next.t('menu.bye'));
105
+ process.exit(0);
106
+ }
107
+ resolve();
108
+ };
109
+
110
+ const onData = (chunk) => {
111
+ const key = chunk.toString().toLowerCase();
112
+ if (key === '0') {
113
+ shouldExitApp = true;
114
+ cleanup();
115
+ return;
116
+ }
117
+ if (key === '\r' || key === 'q' || key === '\u0003') {
118
+ cleanup();
119
+ }
120
+ };
121
+
122
+ const timer = setInterval(() => {
123
+ elapsedSeconds += 1;
124
+ render();
125
+ }, 1000);
126
+
127
+ process.stdin.setRawMode(true);
128
+ process.stdin.resume();
129
+ process.stdin.on('data', onData);
130
+ render();
131
+ });
132
+ }
@@ -7,7 +7,10 @@ export async function urlHandler() {
7
7
  const mode = await selectFromMenu(i18next.t('url.title'), [
8
8
  { name: i18next.t('url.encode'), value: 'encode' },
9
9
  { name: i18next.t('url.decode'), value: 'decode' }
10
- ]);
10
+ ], true, i18next.t('common.back'));
11
+ if (mode === '__BACK__') {
12
+ return;
13
+ }
11
14
 
12
15
  const { input } = await inquirer.prompt([
13
16
  {
package/src/index.js CHANGED
@@ -14,6 +14,9 @@ import { imgSplitHandler } from './commands/imgSplit.js';
14
14
  import { imgMergeHandler } from './commands/imgMerge.js';
15
15
  import { timeFormatHandler } from './commands/timeFormat.js';
16
16
  import { timeCalcHandler } from './commands/timeCalc.js';
17
+ import { clockHandler } from './commands/clock.js';
18
+ import { countdownHandler } from './commands/countdown.js';
19
+ import { stopwatchHandler } from './commands/stopwatch.js';
17
20
  import { mockHandler } from './commands/mock.js';
18
21
  import { uuidHandler } from './commands/uuid.js';
19
22
  import { pinyinHandler } from './commands/pinyin.js';
@@ -84,6 +87,9 @@ function getFeatures() {
84
87
  // Utils (Time, Color, UUID, Hash)
85
88
  { name: i18next.t('menu.features.timeFormat'), value: 'timeFormat' },
86
89
  { name: i18next.t('menu.features.timeCalc'), value: 'timeCalc' },
90
+ { name: i18next.t('menu.features.clock'), value: 'clock' },
91
+ { name: i18next.t('menu.features.countdown'), value: 'countdown' },
92
+ { name: i18next.t('menu.features.stopwatch'), value: 'stopwatch' },
87
93
  { name: i18next.t('menu.features.color'), value: 'color' },
88
94
  { name: i18next.t('menu.features.colorPreview'), value: 'colorPreview' },
89
95
  { name: i18next.t('menu.features.uuid'), value: 'uuid' },
@@ -310,6 +316,15 @@ async function handleAction(action) {
310
316
  case 'timeCalc':
311
317
  await timeCalcHandler();
312
318
  break;
319
+ case 'clock':
320
+ await clockHandler();
321
+ break;
322
+ case 'countdown':
323
+ await countdownHandler();
324
+ break;
325
+ case 'stopwatch':
326
+ await stopwatchHandler();
327
+ break;
313
328
  case 'mock':
314
329
  await mockHandler();
315
330
  break;
package/src/locales/en.js CHANGED
@@ -61,6 +61,9 @@ export default {
61
61
  pinyin: 'Chinese to Pinyin',
62
62
  timeFormat: 'Time Format / Timestamp',
63
63
  timeCalc: 'Time Calculation (Diff/Offset)',
64
+ clock: 'Console Clock',
65
+ countdown: 'Countdown Timer',
66
+ stopwatch: 'Stopwatch',
64
67
  color: 'Color Converter (Hex <-> RGB)',
65
68
  colorPreview: 'Color Preview',
66
69
  uuid: 'Get UUID',
@@ -131,6 +134,36 @@ export default {
131
134
  result: 'Result:',
132
135
  error: 'Error processing URL:'
133
136
  },
137
+ clock: {
138
+ title: 'Console Clock',
139
+ start: 'Console clock started. Press Enter / Q to go back.',
140
+ now: 'Current time: ',
141
+ exitTip: 'Press Enter / Q to go back, 0 to exit app'
142
+ },
143
+ countdown: {
144
+ title: 'Countdown Timer',
145
+ selectUnit: 'Select countdown unit',
146
+ unitSecond: 'Seconds',
147
+ unitMinute: 'Minutes',
148
+ unitHour: 'Hours',
149
+ inputPrompt: 'Enter countdown {{unit}} value:',
150
+ invalidAmount: 'Please enter a number greater than or equal to 0',
151
+ remaining: 'Remaining: ',
152
+ total: 'Total: ',
153
+ status: 'Status: ',
154
+ statusCounting: 'Counting down',
155
+ statusOvertime: 'Overtime',
156
+ exitTip: 'Press Enter / Q to go back, 0 to exit app',
157
+ nonTtyTip: 'Current terminal does not support dynamic countdown. Target duration: {{value}}'
158
+ },
159
+ stopwatch: {
160
+ title: 'Stopwatch',
161
+ elapsed: 'Elapsed: ',
162
+ status: 'Status: ',
163
+ statusRunning: 'Running',
164
+ exitTip: 'Press Enter / Q to go back, 0 to exit app',
165
+ nonTtyTip: 'Current terminal does not support dynamic stopwatch.'
166
+ },
134
167
  baseConvert: {
135
168
  inputPrompt: 'Enter number:',
136
169
  invalidInput: 'Please enter a valid number',
package/src/locales/zh.js CHANGED
@@ -61,6 +61,9 @@ export default {
61
61
  pinyin: '汉字转拼音',
62
62
  timeFormat: '时间格式化 / 时间戳',
63
63
  timeCalc: '时间计算 (差值/偏移)',
64
+ clock: '控制台时钟',
65
+ countdown: '倒计时工具',
66
+ stopwatch: '计时工具',
64
67
  color: '颜色转换 (Hex <-> RGB)',
65
68
  colorPreview: '颜色预览',
66
69
  uuid: '生成 UUID',
@@ -131,6 +134,36 @@ export default {
131
134
  result: '结果:',
132
135
  error: '处理 URL 时出错:'
133
136
  },
137
+ clock: {
138
+ title: '控制台时钟',
139
+ start: '控制台时钟已启动,按 Enter / Q 返回上一步。',
140
+ now: '当前时间: ',
141
+ exitTip: '按 Enter / Q 返回上一步,按 0 直接退出'
142
+ },
143
+ countdown: {
144
+ title: '倒计时工具',
145
+ selectUnit: '请选择倒计时单位',
146
+ unitSecond: '秒',
147
+ unitMinute: '分钟',
148
+ unitHour: '小时',
149
+ inputPrompt: '请输入倒计时{{unit}}数值:',
150
+ invalidAmount: '请输入大于等于 0 的数字',
151
+ remaining: '剩余时间: ',
152
+ total: '倒计总时: ',
153
+ status: '当前状态: ',
154
+ statusCounting: '倒计时中',
155
+ statusOvertime: '已超时',
156
+ exitTip: '按 Enter / Q 返回上一步,按 0 直接退出',
157
+ nonTtyTip: '当前终端不支持动态倒计时。目标时长: {{value}}'
158
+ },
159
+ stopwatch: {
160
+ title: '计时工具',
161
+ elapsed: '已计时: ',
162
+ status: '当前状态: ',
163
+ statusRunning: '计时中',
164
+ exitTip: '按 Enter / Q 返回上一步,按 0 直接退出',
165
+ nonTtyTip: '当前终端不支持动态计时。'
166
+ },
134
167
  baseConvert: {
135
168
  inputPrompt: '请输入数字:',
136
169
  invalidInput: '请输入有效的数字',