t9n-cli 0.1.0 → 0.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.
- package/dictionaries/ar.json +244 -0
- package/dictionaries/de.json +244 -0
- package/dictionaries/el.json +244 -0
- package/dictionaries/en.json +244 -0
- package/dictionaries/es.json +244 -0
- package/dictionaries/fr.json +244 -0
- package/dictionaries/hi.json +244 -0
- package/dictionaries/id.json +244 -0
- package/dictionaries/it.json +244 -0
- package/dictionaries/ja.json +244 -0
- package/dictionaries/ko.json +244 -0
- package/dictionaries/ms.json +244 -0
- package/dictionaries/nl.json +244 -0
- package/dictionaries/pl.json +244 -0
- package/dictionaries/pt.json +244 -0
- package/dictionaries/ru.json +244 -0
- package/dictionaries/sv.json +244 -0
- package/dictionaries/th.json +244 -0
- package/dictionaries/tr.json +244 -0
- package/dictionaries/vi.json +244 -0
- package/dictionaries/yue.json +244 -0
- package/dictionaries/zh-CN.json +244 -0
- package/dictionaries/zh-TW.json +244 -0
- package/dist/api.d.ts +1 -0
- package/dist/api.js +52 -0
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +41 -0
- package/dist/commands/diff.d.ts +1 -0
- package/dist/commands/diff.js +96 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +32 -0
- package/dist/commands/scan.d.ts +1 -0
- package/dist/commands/scan.js +151 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +42 -0
- package/dist/commands/translate.d.ts +1 -0
- package/dist/commands/translate.js +76 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +60 -0
- package/dist/i18n.d.ts +7 -0
- package/dist/i18n.js +52 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +58 -0
- package/dist/lib/jsonUtils.d.ts +12 -0
- package/dist/lib/jsonUtils.js +151 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
{
|
|
2
|
+
"home": {
|
|
3
|
+
"badge": "t9n - MVP",
|
|
4
|
+
"title": "尊重程式碼的 i18n 翻譯。",
|
|
5
|
+
"subtitle": "零語法錯誤。變數保護。上下文感知。",
|
|
6
|
+
"button": "開始翻譯"
|
|
7
|
+
},
|
|
8
|
+
"editor": {
|
|
9
|
+
"run": "執行翻譯",
|
|
10
|
+
"translating": "正在翻譯",
|
|
11
|
+
"source": {
|
|
12
|
+
"label": "來源語言",
|
|
13
|
+
"upload": "上傳 JSON",
|
|
14
|
+
"upload_multiple": "點擊上傳 JSON 檔案",
|
|
15
|
+
"context": "上下文",
|
|
16
|
+
"context_placeholder": "加入上下文以提升翻譯準確度...",
|
|
17
|
+
"pro_feature": "專業版功能",
|
|
18
|
+
"pro_context_msg": "升級以提供上下文,獲取更高準確度。",
|
|
19
|
+
"info": "來源",
|
|
20
|
+
"lines": "行數",
|
|
21
|
+
"clear_context": "清除",
|
|
22
|
+
"large_file_title": "已載入大檔案 ({size})",
|
|
23
|
+
"large_file_desc": "為提升效能已停用預覽。完整內容將送出翻譯。"
|
|
24
|
+
},
|
|
25
|
+
"diff_dialog": {
|
|
26
|
+
"title": "增量更新 (Diff)",
|
|
27
|
+
"files_preview": "檔案預覽",
|
|
28
|
+
"base_tag": "基準",
|
|
29
|
+
"start_diff": "開始比對"
|
|
30
|
+
},
|
|
31
|
+
"target": {
|
|
32
|
+
"label": "目標語言",
|
|
33
|
+
"copy": "複製",
|
|
34
|
+
"download": "下載",
|
|
35
|
+
"no_selection": "未選擇語言",
|
|
36
|
+
"placeholder": "選擇語言並執行翻譯後在此查看結果",
|
|
37
|
+
"stale_warning": "來源已更改 - 翻譯已過期",
|
|
38
|
+
"max_select": "最多 {n}",
|
|
39
|
+
"more_suffix": "(+{n})"
|
|
40
|
+
},
|
|
41
|
+
"mobile": {
|
|
42
|
+
"view_result": "查看結果",
|
|
43
|
+
"back_to_code": "返回代碼"
|
|
44
|
+
},
|
|
45
|
+
"features": {
|
|
46
|
+
"diff_title": "增量更新",
|
|
47
|
+
"diff_msg": "增量更新功能即將推出!",
|
|
48
|
+
"soon": "即將推出"
|
|
49
|
+
},
|
|
50
|
+
"messages": {
|
|
51
|
+
"pro_feature_title": "專業版功能 (即將推出)",
|
|
52
|
+
"upload_pro_msg": "上傳是專業版功能。\n\n我們正在開發進階功能,敬請期待!\n\n需要協助?請聯繫:{email}",
|
|
53
|
+
"download_pro_msg": "下載是專業版功能。\n\n我們正在開發進階功能,敬請期待!\n\n需要協助?請聯繫:{email}",
|
|
54
|
+
"diff_upload_hint": "上傳所有翻譯檔案。最完整的一個將被用作基準。",
|
|
55
|
+
"file_too_large_title": "檔案過大",
|
|
56
|
+
"file_too_large_msg": "檔案過大 ({size})。上限為 {limit}。\n\n檔案切割功能即將推出!",
|
|
57
|
+
"copied": "已複製到剪貼簿!",
|
|
58
|
+
"copy_failed": "複製失敗",
|
|
59
|
+
"invalid_json": "來源 JSON 無效",
|
|
60
|
+
"translation_complete": "翻譯完成!",
|
|
61
|
+
"translation_failed": "翻譯失敗",
|
|
62
|
+
"daily_limit_reached": "已達每日上限",
|
|
63
|
+
"daily_limit_desc": "您今日的免費翻譯額度已用完。",
|
|
64
|
+
"pro_plan_coming_soon": "專業版方案與無限存取即將推出!",
|
|
65
|
+
"building_premium": "我們正在開發進階功能,敬請期待!",
|
|
66
|
+
"contact_us": "需要更多?請聯繫我們:{email}",
|
|
67
|
+
"error_details": "錯誤詳情:",
|
|
68
|
+
"limit_exceeded": "超過限制",
|
|
69
|
+
"limit_exceeded_desc": "{tier} 方案每次請求最多支援 {max} 個鍵。您的檔案有 {keys} 個鍵。",
|
|
70
|
+
"insufficient_credits": "額度不足",
|
|
71
|
+
"insufficient_credits_desc": "此翻譯需要 {cost} 點數,但您剩餘 {available} 點。",
|
|
72
|
+
"free_plan_limit_exceeded": "超過免費版限制",
|
|
73
|
+
"upgrade_to_pro": "升級至專業版 (即將推出)",
|
|
74
|
+
"pro_benefits_1": "檔案鍵數無限制",
|
|
75
|
+
"pro_benefits_2": "上下文長度無限制",
|
|
76
|
+
"pro_benefits_3": "批次翻譯",
|
|
77
|
+
"reduce_size": "目前請縮減檔案大小或上下文內容。",
|
|
78
|
+
"select_target_lang": "請至少選擇一個目標語言。",
|
|
79
|
+
"up_to_date": "所有選擇的語言皆為最新。",
|
|
80
|
+
"translating_placeholder": "// 翻譯中...",
|
|
81
|
+
"error_no_data": "// 錯誤:未回傳資料。",
|
|
82
|
+
"error_failed_generic": "// 錯誤:翻譯失敗。請重試。",
|
|
83
|
+
"max_langs_reached": "免費版最多支援 {n} 種目標語言。",
|
|
84
|
+
"credits_needed": "所需點數",
|
|
85
|
+
"credits_remaining": "剩餘",
|
|
86
|
+
"invalid_base_json": "基準 JSON 無效",
|
|
87
|
+
"invalid_base_content": "基準檔案內容無效"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"common": {
|
|
91
|
+
"contact": "聯繫我們",
|
|
92
|
+
"email": "z17682341097@gmail.com",
|
|
93
|
+
"pro": "專業版",
|
|
94
|
+
"coming_soon": "即將推出",
|
|
95
|
+
"pricing": "價格方案",
|
|
96
|
+
"docs": "文件與 CLI",
|
|
97
|
+
"cli": "CLI",
|
|
98
|
+
"i18n_note": "本網站的所有翻譯皆由 t9n 生成。",
|
|
99
|
+
"upgrade": "升級",
|
|
100
|
+
"auto_detect": "自動偵測",
|
|
101
|
+
"soon": "即將推出",
|
|
102
|
+
"pro_only": "(僅限 {pro})",
|
|
103
|
+
"contact_support": "聯繫支援",
|
|
104
|
+
"pricing_limits": "價格與限制",
|
|
105
|
+
"back_to_home": "返回首頁",
|
|
106
|
+
"back_to_editor": "返回編輯器",
|
|
107
|
+
"mail_to": "發送郵件至",
|
|
108
|
+
"cancel": "取消",
|
|
109
|
+
"close": "關閉",
|
|
110
|
+
"confirm": "確認",
|
|
111
|
+
"notification": "通知"
|
|
112
|
+
},
|
|
113
|
+
"auth": {
|
|
114
|
+
"login": "登入",
|
|
115
|
+
"logout": "登出",
|
|
116
|
+
"welcome_back": "歡迎回來",
|
|
117
|
+
"login_desc": "登入以使用專業版功能與更高限制",
|
|
118
|
+
"cli_login_required": "請先使用此命令登入:t9n auth <your-api-key>",
|
|
119
|
+
"continue_google": "使用 Google 帳號繼續",
|
|
120
|
+
"continue_github": "使用 GitHub 帳號繼續",
|
|
121
|
+
"terms": "繼續操作即表示您同意我們的服務條款與隱私權政策。"
|
|
122
|
+
},
|
|
123
|
+
"settings": {
|
|
124
|
+
"title": "設定",
|
|
125
|
+
"apikeys": {
|
|
126
|
+
"title": "API 金鑰",
|
|
127
|
+
"desc": "管理您的 API 金鑰,用於 CLI 工具或 CI/CD 流程。",
|
|
128
|
+
"create": "建立新金鑰",
|
|
129
|
+
"name_label": "金鑰名稱",
|
|
130
|
+
"name_placeholder": "例如:我的筆電 CLI",
|
|
131
|
+
"key_secret_warning": "請立即複製您的 API 金鑰。為了安全,金鑰將不再顯示。",
|
|
132
|
+
"copy_key": "複製金鑰",
|
|
133
|
+
"list_empty": "尚未建立 API 金鑰。",
|
|
134
|
+
"revoke": "撤銷",
|
|
135
|
+
"revoke_confirm": "確定要撤銷此 API 金鑰嗎?使用此金鑰的工具將停止運作。",
|
|
136
|
+
"last_used": "上次使用",
|
|
137
|
+
"never_used": "未使用過",
|
|
138
|
+
"created": "建立日期"
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"pricing": {
|
|
142
|
+
"title": "簡單透明的價格",
|
|
143
|
+
"subtitle": "選擇最符合您翻譯需求的方案。",
|
|
144
|
+
"monthly": "每月",
|
|
145
|
+
"daily": "每日",
|
|
146
|
+
"per_request": "按次計費",
|
|
147
|
+
"get_started": "立即開始",
|
|
148
|
+
"upgrade": "立即升級",
|
|
149
|
+
"contact_sales": "聯繫業務",
|
|
150
|
+
"features": {
|
|
151
|
+
"credits": "{n} 點數",
|
|
152
|
+
"keys": "最多 {n} 個鍵",
|
|
153
|
+
"langs": "最多 {n} 種語言",
|
|
154
|
+
"upload_download": "上傳 / 下載",
|
|
155
|
+
"diff": "增量更新 (Diff)",
|
|
156
|
+
"context": "上下文感知"
|
|
157
|
+
},
|
|
158
|
+
"most_popular": "最熱門",
|
|
159
|
+
"custom": "客製化"
|
|
160
|
+
},
|
|
161
|
+
"metadata": {
|
|
162
|
+
"title": "t9n - 工程師專用的 JSON 翻譯器 | i18n AI 翻譯工具",
|
|
163
|
+
"description": "開發者優先的 i18n JSON 檔案 AI 翻譯器。零語法錯誤、變數保護 (React Intl, i18next) 與智慧上下文感知。翻譯的是軟體,不只是文字。",
|
|
164
|
+
"og_title": "t9n - 工程師專用的 JSON 翻譯器",
|
|
165
|
+
"og_description": "在不破壞代碼變數的情況下翻譯 i18n JSON 檔案。支援 React Intl、i18next 等。"
|
|
166
|
+
},
|
|
167
|
+
"docs": {
|
|
168
|
+
"title": "說明文件",
|
|
169
|
+
"subtitle": "深入了解 t9n 的功能,從基礎用法到進階增量更新與 AI 上下文優化。",
|
|
170
|
+
"sections": {
|
|
171
|
+
"basic": {
|
|
172
|
+
"title": "基礎功能:即時翻譯",
|
|
173
|
+
"content": "t9n 專為 i18n JSON 格式設計。我們優化了 Prompt 策略,確保所有程式碼變數 (如 {name} 或 {{count}}) 在翻譯過程中受到嚴格保護。",
|
|
174
|
+
"item1": "支援批次選擇目標語言,一次生成多份翻譯。",
|
|
175
|
+
"item2": "即時預覽:來源內容與目標內容並排對比。"
|
|
176
|
+
},
|
|
177
|
+
"context": {
|
|
178
|
+
"title": "上下文感知",
|
|
179
|
+
"content": "翻譯不僅僅是字對字。透過提供應用程式背景,AI 可以更準確地選擇詞彙。",
|
|
180
|
+
"example": "例如,同一個詞 \"Menu\",在餐廳 App 中是「菜單」,在設計軟體中則是「功能表」。提供上下文可消除歧義。"
|
|
181
|
+
},
|
|
182
|
+
"diff": {
|
|
183
|
+
"title": "智慧增量更新 (Diff 模式)",
|
|
184
|
+
"content": "當您的來源語言檔案 (例如 en.json) 有新內容時,您無需重新翻譯整個檔案。t9n 的增量更新模式現已支援全自動化。",
|
|
185
|
+
"new_feature": "新功能:全自動多檔案識別",
|
|
186
|
+
"step1_title": "1. 多選上傳",
|
|
187
|
+
"step1_desc": "在上傳對話框中同時選擇英文 (基準) 和現有的翻譯檔案 (例如中文、日文)。不再需要手動比對。",
|
|
188
|
+
"step2_title": "2. 智慧基準偵測",
|
|
189
|
+
"step2_desc": "系統會自動統計每個檔案中的鍵數,並將最完整的一個視為「翻譯基準」,自動填補其他檔案中缺失的部分。",
|
|
190
|
+
"step3_title": "3. 精準翻譯",
|
|
191
|
+
"step3_desc": "缺失的鍵會被標記為 🚧 [MISSING]。AI 僅翻譯這些標記,為您節省超過 90% 的點數。",
|
|
192
|
+
"quote": "「保留 100% 現有翻譯,僅處理新需求。」"
|
|
193
|
+
},
|
|
194
|
+
"cli": {
|
|
195
|
+
"tag": "現已推出",
|
|
196
|
+
"title": "t9n CLI",
|
|
197
|
+
"content": "從終端機進行專業的 i18n 管理。使用我們強大的命令列介面自動化您的翻譯工作流程。",
|
|
198
|
+
"install": "npm install -g @t9n/cli",
|
|
199
|
+
"command_auth": "t9n auth <key>",
|
|
200
|
+
"command_init": "t9n init",
|
|
201
|
+
"command_scan": "t9n scan ./src",
|
|
202
|
+
"command_translate": "t9n translate ./en.json -t zh-TW,ja",
|
|
203
|
+
"command_diff": "t9n diff ./dictionaries",
|
|
204
|
+
"desc_auth": "將您的本地環境與 t9n 帳戶連結。",
|
|
205
|
+
"desc_init": "建立 t9n.config.json 以儲存您的專案偏好設定。",
|
|
206
|
+
"desc_scan": "在原始碼中尋找尚未出現在 JSON 中的缺失鍵。",
|
|
207
|
+
"desc_translate": "直接翻譯本地檔案並自動儲存結果。",
|
|
208
|
+
"desc_diff": "針對資料夾中的所有語言進行智慧增量更新。",
|
|
209
|
+
"progress": "準備進入生產環境"
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
"example_correct": "Portfolio\" → \"投資組合\"",
|
|
213
|
+
"example_incorrect": "Portfolio\" → \"作品集\""
|
|
214
|
+
},
|
|
215
|
+
"cli": {
|
|
216
|
+
"diff": {
|
|
217
|
+
"title": "🛰️ T9N CLI - 增量更新",
|
|
218
|
+
"analyzing": "🔍 正在分析差異...",
|
|
219
|
+
"comparing": "正在與現有檔案比對...",
|
|
220
|
+
"new_lang": "偵測到新語言。",
|
|
221
|
+
"success": "翻譯接收成功!",
|
|
222
|
+
"updated": "已更新:",
|
|
223
|
+
"all_up_to_date": "✨ 所有翻譯皆為最新!"
|
|
224
|
+
},
|
|
225
|
+
"scan": {
|
|
226
|
+
"title": "🔍 T9N CLI - 代碼掃描器",
|
|
227
|
+
"scanning": "正在掃描檔案...",
|
|
228
|
+
"scanned": "已掃描 {n} 個檔案。",
|
|
229
|
+
"missing_found": "找到 {n} 個缺失鍵 (在代碼中但不在字典中):",
|
|
230
|
+
"all_present": "✨ 代碼中的所有鍵皆已存在於字典中。",
|
|
231
|
+
"orphaned": "找到 {n} 個可能未使用的鍵 (在字典中但不在代碼中):"
|
|
232
|
+
},
|
|
233
|
+
"auth": {
|
|
234
|
+
"verifying": "正在驗證 API 金鑰...",
|
|
235
|
+
"success": "認證成功!({target} 配置)",
|
|
236
|
+
"key_missing": "請提供 API 金鑰。用法:t9n auth <key>"
|
|
237
|
+
},
|
|
238
|
+
"init": {
|
|
239
|
+
"title": "🎬 T9N - 專案初始化",
|
|
240
|
+
"created": "已建立專案配置:{path}",
|
|
241
|
+
"already_exists": "配置檔案已存在於:{path}"
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function request(path: string, options?: RequestInit): Promise<any>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.request = request;
|
|
7
|
+
const config_1 = require("./config");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const i18n_1 = require("./i18n");
|
|
10
|
+
async function request(path, options = {}) {
|
|
11
|
+
const apiKey = (0, config_1.getApiKey)();
|
|
12
|
+
const baseUrl = (0, config_1.getApiUrl)();
|
|
13
|
+
if (!apiKey && !path.includes('/auth/')) {
|
|
14
|
+
throw new Error((0, i18n_1.t)('auth.cli_login_required'));
|
|
15
|
+
}
|
|
16
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
17
|
+
...options,
|
|
18
|
+
headers: {
|
|
19
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
'Accept-Language': (0, i18n_1.getCLILocale)(),
|
|
22
|
+
...options.headers,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
let text = '';
|
|
26
|
+
try {
|
|
27
|
+
text = await response.text();
|
|
28
|
+
let data;
|
|
29
|
+
try {
|
|
30
|
+
data = JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`Request failed (${response.status}): ${text || response.statusText}`);
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Failed to parse JSON response. Raw: ${text.substring(0, 100)}`);
|
|
37
|
+
}
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const errorTitle = chalk_1.default.bold.red(data.error || 'Error');
|
|
40
|
+
const details = data.details ? `\n${chalk_1.default.white(data.details)}` : '';
|
|
41
|
+
const pricingUrl = `${baseUrl}/pricing`.replace('api.', ''); // fallback logic if URL structure differs
|
|
42
|
+
const upgradeTip = data.upgrade
|
|
43
|
+
? `\n${chalk_1.default.yellow(`💡 Tip: Visit ${pricingUrl} to upgrade your plan.`)}`
|
|
44
|
+
: '';
|
|
45
|
+
throw new Error(`${errorTitle}${details}${upgradeTip}`);
|
|
46
|
+
}
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function auth(key: string, options: any): Promise<void>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.auth = auth;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const config_1 = require("../config");
|
|
10
|
+
const api_1 = require("../api");
|
|
11
|
+
const i18n_1 = require("../i18n");
|
|
12
|
+
async function auth(key, options) {
|
|
13
|
+
if (!key) {
|
|
14
|
+
console.error(chalk_1.default.red(`\n ${(0, i18n_1.t)('cli.auth.key_missing')}\n`));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const spinner = (0, ora_1.default)({
|
|
18
|
+
text: chalk_1.default.blue((0, i18n_1.t)('cli.auth.verifying')),
|
|
19
|
+
color: 'blue'
|
|
20
|
+
}).start();
|
|
21
|
+
try {
|
|
22
|
+
const target = options.local ? 'local' : 'global';
|
|
23
|
+
// Temporarily set the key for the verification request
|
|
24
|
+
(0, config_1.writeConfig)({ apiKey: key }, target);
|
|
25
|
+
if (options.url) {
|
|
26
|
+
(0, config_1.writeConfig)({ apiUrl: options.url }, target);
|
|
27
|
+
}
|
|
28
|
+
const data = await (0, api_1.request)('/api/usage');
|
|
29
|
+
spinner.succeed(chalk_1.default.green((0, i18n_1.t)('cli.auth.success', { target })));
|
|
30
|
+
console.log(chalk_1.default.gray('\n ----------------------------------------'));
|
|
31
|
+
console.log(`${chalk_1.default.cyan(' 👤 Plan: ')} ${chalk_1.default.bold(data.tierLabel)}`);
|
|
32
|
+
console.log(`${chalk_1.default.cyan(' 💎 Credits:')} ${chalk_1.default.bold(data.credits)} / ${data.creditsTotal}`);
|
|
33
|
+
console.log(chalk_1.default.gray(' ----------------------------------------\n'));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
spinner.fail(chalk_1.default.red('Authentication failed.'));
|
|
37
|
+
console.error(`\n ${chalk_1.default.red('Error:')} ${error.message}\n`);
|
|
38
|
+
// Cleanup if failed
|
|
39
|
+
(0, config_1.writeConfig)({ apiKey: '' });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function diff(folderPath: string, options: any): Promise<void>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.diff = diff;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const ora_1 = __importDefault(require("ora"));
|
|
11
|
+
const api_1 = require("../api");
|
|
12
|
+
const config_1 = require("../config");
|
|
13
|
+
const jsonUtils_1 = require("../lib/jsonUtils");
|
|
14
|
+
const i18n_1 = require("../i18n");
|
|
15
|
+
async function diff(folderPath, options) {
|
|
16
|
+
const config = (0, config_1.readConfig)();
|
|
17
|
+
if (options.url) {
|
|
18
|
+
(0, config_1.writeConfig)({ apiUrl: options.url });
|
|
19
|
+
}
|
|
20
|
+
const dir = path_1.default.resolve(folderPath);
|
|
21
|
+
if (!fs_1.default.existsSync(dir) || !fs_1.default.statSync(dir).isDirectory()) {
|
|
22
|
+
console.error(chalk_1.default.red(`\n ❌ Error: [${folderPath}] is not a valid directory.`));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const sourceLang = options.source || config.sourceLang || 'en';
|
|
26
|
+
const sourceFile = path_1.default.join(dir, `${sourceLang}.json`);
|
|
27
|
+
if (!fs_1.default.existsSync(sourceFile)) {
|
|
28
|
+
console.error(chalk_1.default.red(`\n ❌ Error: Source file [${sourceLang}.json] not found in directory.`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log(chalk_1.default.bold.cyan(`\n ${(0, i18n_1.t)('cli.diff.title')}`));
|
|
32
|
+
console.log(chalk_1.default.gray(' ----------------------------------------'));
|
|
33
|
+
console.log(`${chalk_1.default.yellow(' 📁 Folder:')} ${dir}`);
|
|
34
|
+
console.log(`${chalk_1.default.yellow(' 📖 Source:')} ${sourceLang}.json`);
|
|
35
|
+
const sourceJson = JSON.parse(fs_1.default.readFileSync(sourceFile, 'utf-8'));
|
|
36
|
+
const files = fs_1.default.readdirSync(dir);
|
|
37
|
+
const jsonFiles = files.filter(f => f.endsWith('.json') && f !== `${sourceLang}.json`);
|
|
38
|
+
const targetLangsFromOptions = options.target ? options.target.split(',').map((l) => l.trim()) : [];
|
|
39
|
+
const activeLangs = Array.from(new Set([...targetLangsFromOptions, ...jsonFiles.map(f => f.replace('.json', ''))]));
|
|
40
|
+
if (activeLangs.length === 0) {
|
|
41
|
+
console.log(chalk_1.default.yellow('\n ⚠️ No target languages detected. Use -t <langs> to add new ones.'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const targetJsonMap = {};
|
|
45
|
+
console.log(chalk_1.default.gray(`\n ${(0, i18n_1.t)('cli.diff.analyzing')}`));
|
|
46
|
+
for (const lang of activeLangs) {
|
|
47
|
+
const targetFile = path_1.default.join(dir, `${lang}.json`);
|
|
48
|
+
let existingJson = null;
|
|
49
|
+
if (fs_1.default.existsSync(targetFile)) {
|
|
50
|
+
try {
|
|
51
|
+
existingJson = JSON.parse(fs_1.default.readFileSync(targetFile, 'utf-8'));
|
|
52
|
+
console.log(` ${chalk_1.default.blue('•')} ${lang.padEnd(6)} ${chalk_1.default.gray((0, i18n_1.t)('cli.diff.comparing'))}`);
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
console.warn(` ${chalk_1.default.yellow('•')} ${lang.padEnd(6)} ${chalk_1.default.red('Parse error, translating from scratch.')}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(` ${chalk_1.default.green('•')} ${lang.padEnd(6)} ${chalk_1.default.gray((0, i18n_1.t)('cli.diff.new_lang'))}`);
|
|
60
|
+
}
|
|
61
|
+
targetJsonMap[lang] = (0, jsonUtils_1.fillMissingKeys)(sourceJson, existingJson);
|
|
62
|
+
}
|
|
63
|
+
const spinner = (0, ora_1.default)({
|
|
64
|
+
text: chalk_1.default.cyan('Requesting AI translation...'),
|
|
65
|
+
color: 'cyan'
|
|
66
|
+
}).start();
|
|
67
|
+
try {
|
|
68
|
+
const response = await (0, api_1.request)('/api/translate', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
sourceJson,
|
|
72
|
+
sourceLang,
|
|
73
|
+
targetLang: activeLangs,
|
|
74
|
+
targetJson: targetJsonMap,
|
|
75
|
+
userContext: options.context || '',
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
spinner.succeed(chalk_1.default.green((0, i18n_1.t)('cli.diff.success')));
|
|
79
|
+
console.log(chalk_1.default.gray(' ----------------------------------------'));
|
|
80
|
+
for (const lang of activeLangs) {
|
|
81
|
+
if (response[lang]) {
|
|
82
|
+
const outPath = path_1.default.join(dir, `${lang}.json`);
|
|
83
|
+
fs_1.default.writeFileSync(outPath, JSON.stringify(response[lang], null, 2));
|
|
84
|
+
console.log(` ${chalk_1.default.green('✓')} ${chalk_1.default.bold(lang.padEnd(6))} ${chalk_1.default.gray('Updated:')} ${chalk_1.default.white(outPath)}`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.error(` ${chalk_1.default.red('✗')} ${chalk_1.default.bold(lang.padEnd(6))} ${chalk_1.default.red('Failed to update.')}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
console.log(chalk_1.default.bold.green(`\n ${(0, i18n_1.t)('cli.diff.all_up_to_date')}\n`));
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
spinner.fail(chalk_1.default.red('Translation request failed.'));
|
|
94
|
+
console.error(`\n ${chalk_1.default.red('Details:')} ${error.message}\n`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function init(): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.init = init;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const i18n_1 = require("../i18n");
|
|
11
|
+
async function init() {
|
|
12
|
+
const localConfigFile = path_1.default.join(process.cwd(), 't9n.config.json');
|
|
13
|
+
console.log(chalk_1.default.bold.cyan(`\n ${(0, i18n_1.t)('cli.init.title')}`));
|
|
14
|
+
console.log(chalk_1.default.gray(' ----------------------------------------'));
|
|
15
|
+
if (fs_1.default.existsSync(localConfigFile)) {
|
|
16
|
+
console.log(`${chalk_1.default.yellow(' ⚠️ ')} ${(0, i18n_1.t)('cli.init.already_exists', { path: 't9n.config.json' })}`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const defaultConfig = {
|
|
20
|
+
sourceLang: 'en',
|
|
21
|
+
dictionary: './dictionaries/en.json',
|
|
22
|
+
};
|
|
23
|
+
try {
|
|
24
|
+
fs_1.default.writeFileSync(localConfigFile, JSON.stringify(defaultConfig, null, 2));
|
|
25
|
+
console.log(`${chalk_1.default.green(' ✓ ')} ${(0, i18n_1.t)('cli.init.created', { path: chalk_1.default.bold('t9n.config.json') })}`);
|
|
26
|
+
console.log(chalk_1.default.gray('\n Tip: Edit this file to match your project structure.'));
|
|
27
|
+
console.log(chalk_1.default.gray(' You can then run "t9n scan" or "t9n diff" without extra flags.'));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error(chalk_1.default.red(`\n ❌ Failed to create config: ${error.message}`));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function scan(folder: string, options: any): Promise<void>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.scan = scan;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const ora_1 = __importDefault(require("ora"));
|
|
11
|
+
const config_1 = require("../config");
|
|
12
|
+
const i18n_1 = require("../i18n");
|
|
13
|
+
const DEFAULT_EXCLUDES = ['node_modules', '.next', '.git', 'dist', 'build', 'public'];
|
|
14
|
+
const DEFAULT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.vue', '.svelte'];
|
|
15
|
+
// JS methods commonly chained after translation keys
|
|
16
|
+
const EXCLUDED_METHODS = ['replace', 'split', 'toUpperCase', 'toLowerCase', 'trim', 'map', 'filter', 'join', 'toString'];
|
|
17
|
+
async function scan(folder, options) {
|
|
18
|
+
const config = (0, config_1.readConfig)();
|
|
19
|
+
const rootDir = path_1.default.resolve(folder);
|
|
20
|
+
const dictionaryPath = path_1.default.resolve(options.dictionary || config.dictionary || './dictionaries/en.json');
|
|
21
|
+
console.log(chalk_1.default.bold.cyan(`\n ${(0, i18n_1.t)('cli.scan.title')}`));
|
|
22
|
+
console.log(chalk_1.default.gray(' ----------------------------------------'));
|
|
23
|
+
console.log(`${chalk_1.default.yellow(' 📁 Scanning:')} ${rootDir}`);
|
|
24
|
+
console.log(`${chalk_1.default.yellow(' 📖 Dictionary:')} ${dictionaryPath}`);
|
|
25
|
+
if (!fs_1.default.existsSync(dictionaryPath)) {
|
|
26
|
+
console.error(chalk_1.default.red(`\n ❌ Dictionary file not found at: ${dictionaryPath}`));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const spinner = (0, ora_1.default)({
|
|
30
|
+
text: chalk_1.default.cyan((0, i18n_1.t)('cli.scan.scanning')),
|
|
31
|
+
color: 'cyan'
|
|
32
|
+
}).start();
|
|
33
|
+
const foundKeys = new Set();
|
|
34
|
+
let fileCount = 0;
|
|
35
|
+
function walk(currentDir) {
|
|
36
|
+
const files = fs_1.default.readdirSync(currentDir);
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const fullPath = path_1.default.join(currentDir, file);
|
|
39
|
+
const stat = fs_1.default.statSync(fullPath);
|
|
40
|
+
if (stat.isDirectory()) {
|
|
41
|
+
if (!DEFAULT_EXCLUDES.includes(file)) {
|
|
42
|
+
walk(fullPath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const ext = path_1.default.extname(file);
|
|
47
|
+
if (DEFAULT_EXTENSIONS.includes(ext)) {
|
|
48
|
+
// Skip scanning the scanner itself to avoid false positives from regex definitions
|
|
49
|
+
if (file === 'scan.ts')
|
|
50
|
+
continue;
|
|
51
|
+
fileCount++;
|
|
52
|
+
const content = fs_1.default.readFileSync(fullPath, 'utf8');
|
|
53
|
+
// --- Namespace detection ---
|
|
54
|
+
// Use string splitting to avoid self-matching during scan
|
|
55
|
+
// Supports both useTranslations('...') and getTranslations({ namespace: '...' })
|
|
56
|
+
// and also getTranslations({ locale: '...', namespace: '...' })
|
|
57
|
+
const nsRegex = new RegExp('(?:use|get)' + 'Translations\\((?:[\'"`](.+?)[\'"`]|\\{[^}]*?namespace:\\s*[\'"`](.+?)[\'"`])', 'g');
|
|
58
|
+
let nsMatch;
|
|
59
|
+
let namespace = null;
|
|
60
|
+
if ((nsMatch = nsRegex.exec(content)) !== null) {
|
|
61
|
+
namespace = nsMatch[1] || nsMatch[2];
|
|
62
|
+
}
|
|
63
|
+
// Rule 1: t('...') or i18n.t('...')
|
|
64
|
+
// Use string splitting to avoid self-matching during scan
|
|
65
|
+
const tRegex = new RegExp('\\b(?:t|i18n\\.t)\\(\\s*[\'"`](.+?)[\'"`]\\s*[\\),]', 'g');
|
|
66
|
+
let match;
|
|
67
|
+
while ((match = tRegex.exec(content)) !== null) {
|
|
68
|
+
// Avoid capturing variables like ${id} and internal scanner strings
|
|
69
|
+
if (match[1] && !match[1].includes('${') && !match[1].includes('(.+?)')) {
|
|
70
|
+
const fullKey = namespace ? `${namespace}.${match[1]}` : match[1];
|
|
71
|
+
foundKeys.add(fullKey);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Rule 2: dict.a.b.c
|
|
75
|
+
const dRegex = new RegExp('\\b' + 'dict(?:\\.[a-zA-Z0-9_$]+){1,}', 'g');
|
|
76
|
+
while ((match = dRegex.exec(content)) !== null) {
|
|
77
|
+
let fullKey = match[0].replace('dict.', '');
|
|
78
|
+
const parts = fullKey.split('.');
|
|
79
|
+
// If the last part is a common JS method, remove it
|
|
80
|
+
if (parts.length > 1 && EXCLUDED_METHODS.includes(parts[parts.length - 1])) {
|
|
81
|
+
parts.pop();
|
|
82
|
+
fullKey = parts.join('.');
|
|
83
|
+
}
|
|
84
|
+
if (fullKey !== 'dict' && fullKey !== '') {
|
|
85
|
+
foundKeys.add(fullKey);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
walk(rootDir);
|
|
94
|
+
spinner.succeed((0, i18n_1.t)('cli.scan.scanned', { n: fileCount.toString() }));
|
|
95
|
+
// Load existing keys from dictionary
|
|
96
|
+
const dictContent = fs_1.default.readFileSync(dictionaryPath, 'utf8');
|
|
97
|
+
const dictJson = JSON.parse(dictContent);
|
|
98
|
+
const existingKeys = new Set();
|
|
99
|
+
function collectKeys(obj, prefix = '') {
|
|
100
|
+
if (typeof obj !== 'object' || obj === null)
|
|
101
|
+
return;
|
|
102
|
+
for (const key in obj) {
|
|
103
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
104
|
+
existingKeys.add(fullKey);
|
|
105
|
+
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
|
106
|
+
collectKeys(obj[key], fullKey);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
collectKeys(dictJson);
|
|
111
|
+
const missingKeys = Array.from(foundKeys).filter(k => !existingKeys.has(k));
|
|
112
|
+
const orphanedKeys = Array.from(existingKeys).filter(k => !foundKeys.has(k));
|
|
113
|
+
console.log(chalk_1.default.gray(' ----------------------------------------'));
|
|
114
|
+
console.log(`${chalk_1.default.blue(' 🔑 Total unique keys in code:')} ${foundKeys.size}`);
|
|
115
|
+
console.log(`${chalk_1.default.blue(' 📖 Total unique keys in dict:')} ${existingKeys.size}`);
|
|
116
|
+
if (missingKeys.length > 0) {
|
|
117
|
+
// Filter out obvious false positives
|
|
118
|
+
const filteredMissing = missingKeys.filter(k => {
|
|
119
|
+
if (k.length <= 1)
|
|
120
|
+
return false;
|
|
121
|
+
if (/^[0-9]+$/.test(k))
|
|
122
|
+
return false;
|
|
123
|
+
if (['props', 'state', 'params', 'query', 'data', 'a.b.c'].includes(k))
|
|
124
|
+
return false;
|
|
125
|
+
if (k.includes('(.+?)'))
|
|
126
|
+
return false; // Regex artifacts
|
|
127
|
+
return true;
|
|
128
|
+
});
|
|
129
|
+
if (filteredMissing.length > 0) {
|
|
130
|
+
console.log(chalk_1.default.bold.yellow(`\n ⚠️ ${(0, i18n_1.t)('cli.scan.missing_found', { n: filteredMissing.length.toString() })}`));
|
|
131
|
+
filteredMissing.sort().forEach(k => console.log(` ${chalk_1.default.red('+')} ${k}`));
|
|
132
|
+
console.log(chalk_1.default.gray(`\n Total missing: ${filteredMissing.length}`));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
console.log(chalk_1.default.bold.green(`\n ${(0, i18n_1.t)('cli.scan.all_present')}`));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.log(chalk_1.default.bold.green(`\n ${(0, i18n_1.t)('cli.scan.all_present')}`));
|
|
140
|
+
}
|
|
141
|
+
if (options.unused && orphanedKeys.length > 0) {
|
|
142
|
+
console.log(chalk_1.default.bold.magenta(`\n 🗑️ Found ${orphanedKeys.length} potentially unused keys (in dict but NOT in code):`));
|
|
143
|
+
orphanedKeys.sort().forEach(k => console.log(` ${chalk_1.default.gray('-')} ${k}`));
|
|
144
|
+
}
|
|
145
|
+
console.log('');
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
spinner.fail(chalk_1.default.red('Scan failed.'));
|
|
149
|
+
console.error(`\n ${chalk_1.default.red('Error:')} ${error.message}\n`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function status(options: any): Promise<void>;
|