@link-assistant/hive-mind 1.65.2 → 1.66.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.66.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f744d5a: Add internationalisation (i18n) for user-facing terminal output and the Telegram bot. Translations are stored in `links-notation` files under `src/locales/` (`en`, `ru`, `zh`, `hi`) and loaded via `lino-objects-codec`. Adds a `--language <en|ru|zh|hi>` option to `solve`, `hive`, `task`, and `review` (defaults to detected system locale). The Telegram bot picks each user's language from `ctx.from.language_code` with a per-user override settable through a new `/language <code|default>` command (in-memory, resets on bot restart). Built-in commands `/limits`, `/version`, `/solve`, `/hive`, and `/language` now reply in the user's selected language. AI prompts are intentionally untouched - only human-facing strings are translated.
8
+
3
9
  ## 1.65.2
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.65.2",
3
+ "version": "1.66.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -76,6 +76,7 @@
76
76
  "dayjs": "^1.11.19",
77
77
  "decimal.js-light": "^2.5.1",
78
78
  "lino-arguments": "^0.3.0",
79
+ "lino-objects-codec": "^0.3.6",
79
80
  "secretlint": "^11.2.5",
80
81
  "semver": "^7.7.3"
81
82
  },
package/src/hive.mjs CHANGED
@@ -292,6 +292,10 @@ if (isRunningDirectly) {
292
292
  // Set global verbose mode
293
293
  global.verboseMode = argv.verbose;
294
294
 
295
+ // Initialize i18n based on --language (or detected system locale)
296
+ const { initI18n } = await import('./i18n.lib.mjs');
297
+ await initI18n(argv.language);
298
+
295
299
  setupVerboseLogInterceptor(); // Issue #1466: capture [VERBOSE] output in log files
296
300
  setupStdioLogInterceptor(); // Issue #1549: capture ALL terminal output in log file
297
301
 
@@ -0,0 +1,174 @@
1
+ // i18n module for hive-mind.
2
+ // - Translation files live in src/locales/<locale>.lino and are stored
3
+ // in Links Notation, parsed via lino-objects-codec.
4
+ // - Supported locales: en (default fallback), ru, zh, hi.
5
+ // - Public API: initI18n, t, getCurrentLocale, setLocale, getSupportedLocales,
6
+ // normalizeLocale, getUserLocale, setUserLocale, clearUserLocale,
7
+ // resolveLocaleFromTelegramCtx.
8
+
9
+ import { promises as fs } from 'fs';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { parseIndented } from 'lino-objects-codec';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
17
+ const DEFAULT_LOCALE = 'en';
18
+ const SUPPORTED_LOCALES = ['en', 'ru', 'zh', 'hi'];
19
+
20
+ const localeCache = new Map(); // locale -> { key: string }
21
+ const userLocales = new Map(); // userId/chatId -> locale (in-memory)
22
+
23
+ let currentLocale = DEFAULT_LOCALE;
24
+ let fallbackLoaded = false;
25
+
26
+ export function getSupportedLocales() {
27
+ return [...SUPPORTED_LOCALES];
28
+ }
29
+
30
+ export function normalizeLocale(input) {
31
+ if (!input || typeof input !== 'string') return null;
32
+ const lower = input.toLowerCase();
33
+ // Take only the language part (before "_" or "-")
34
+ const lang = lower.split(/[_\-.]/)[0];
35
+ if (SUPPORTED_LOCALES.includes(lang)) return lang;
36
+ return null;
37
+ }
38
+
39
+ export function detectLocale() {
40
+ const envLocale = process.env.LANG || process.env.LANGUAGE || process.env.LC_ALL || process.env.LC_MESSAGES || '';
41
+ return normalizeLocale(envLocale) || DEFAULT_LOCALE;
42
+ }
43
+
44
+ async function readLocaleFile(locale) {
45
+ const localesDir = path.join(__dirname, 'locales');
46
+ const linoFile = path.join(localesDir, `${locale}.lino`);
47
+ const data = await fs.readFile(linoFile, 'utf-8');
48
+ return parseIndentedToFlatMap(data);
49
+ }
50
+
51
+ // parseIndented returns { id, obj } where obj is the key->value map.
52
+ // Some keys contain dots (e.g., error.invalid_github_url). The parser
53
+ // supports them when the key is a plain reference (no spaces/quotes).
54
+ function unescapeString(s) {
55
+ // Convert literal escape sequences (e.g., "\n" inside a quoted string in
56
+ // Links Notation) into the corresponding JS characters. This keeps the
57
+ // .lino files single-line and human-friendly.
58
+ return s.replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\r/g, '\r').replace(/\\\\/g, '\\');
59
+ }
60
+
61
+ function parseIndentedToFlatMap(text) {
62
+ const parsed = parseIndented({ text });
63
+ // parsed: { id: <localeName>, obj: { key: value, ... } }
64
+ if (!parsed || !parsed.obj) return {};
65
+ const out = {};
66
+ for (const [k, v] of Object.entries(parsed.obj)) {
67
+ out[k] = typeof v === 'string' ? unescapeString(v) : String(v);
68
+ }
69
+ return out;
70
+ }
71
+
72
+ export async function loadTranslations(locale) {
73
+ if (localeCache.has(locale)) return localeCache.get(locale);
74
+
75
+ let translations = {};
76
+ try {
77
+ translations = await readLocaleFile(locale);
78
+ } catch {
79
+ translations = {};
80
+ }
81
+ localeCache.set(locale, translations);
82
+
83
+ // Always have the fallback (English) ready
84
+ if (!fallbackLoaded && locale !== DEFAULT_LOCALE) {
85
+ try {
86
+ const fb = await readLocaleFile(DEFAULT_LOCALE);
87
+ localeCache.set(DEFAULT_LOCALE, fb);
88
+ } catch {
89
+ localeCache.set(DEFAULT_LOCALE, {});
90
+ }
91
+ fallbackLoaded = true;
92
+ } else if (locale === DEFAULT_LOCALE) {
93
+ fallbackLoaded = true;
94
+ }
95
+
96
+ return translations;
97
+ }
98
+
99
+ export async function initI18n(localeInput = null) {
100
+ const requested = localeInput ? normalizeLocale(localeInput) : null;
101
+ const detectedLocale = requested || detectLocale();
102
+ currentLocale = detectedLocale;
103
+ await loadTranslations(detectedLocale);
104
+ if (detectedLocale !== DEFAULT_LOCALE) {
105
+ await loadTranslations(DEFAULT_LOCALE);
106
+ }
107
+ return detectedLocale;
108
+ }
109
+
110
+ function applyParams(text, params) {
111
+ if (!params) return text;
112
+ let out = text;
113
+ for (const [k, v] of Object.entries(params)) {
114
+ out = out.replace(new RegExp(`{{${k}}}`, 'g'), String(v));
115
+ }
116
+ return out;
117
+ }
118
+
119
+ export function t(key, params = {}, options = {}) {
120
+ const locale = options.locale ? normalizeLocale(options.locale) || currentLocale : currentLocale;
121
+ const main = localeCache.get(locale) || {};
122
+ const fallback = localeCache.get(DEFAULT_LOCALE) || {};
123
+ const value = main[key] ?? fallback[key] ?? key;
124
+ return applyParams(value, params);
125
+ }
126
+
127
+ export function getCurrentLocale() {
128
+ return currentLocale;
129
+ }
130
+
131
+ export function setLocale(locale) {
132
+ const normalized = normalizeLocale(locale);
133
+ if (normalized) currentLocale = normalized;
134
+ }
135
+
136
+ // In-memory per-user locale store (used by the Telegram bot).
137
+ export function getUserLocale(userId) {
138
+ if (userId === undefined || userId === null) return null;
139
+ return userLocales.get(String(userId)) || null;
140
+ }
141
+
142
+ export function setUserLocale(userId, locale) {
143
+ const normalized = normalizeLocale(locale);
144
+ if (!normalized || userId === undefined || userId === null) return false;
145
+ userLocales.set(String(userId), normalized);
146
+ return true;
147
+ }
148
+
149
+ export function clearUserLocale(userId) {
150
+ if (userId === undefined || userId === null) return false;
151
+ return userLocales.delete(String(userId));
152
+ }
153
+
154
+ // Resolve the best locale for a Telegram update context.
155
+ // Priority: per-user override -> ctx.from.language_code -> current default.
156
+ export function resolveLocaleFromTelegramCtx(ctx) {
157
+ const userId = ctx?.from?.id;
158
+ const userOverride = getUserLocale(userId);
159
+ if (userOverride) return userOverride;
160
+ const fromTelegram = normalizeLocale(ctx?.from?.language_code);
161
+ if (fromTelegram) return fromTelegram;
162
+ return currentLocale;
163
+ }
164
+
165
+ // Pre-load every supported locale (handy for the Telegram bot at startup).
166
+ export async function preloadAllLocales() {
167
+ for (const loc of SUPPORTED_LOCALES) {
168
+ try {
169
+ await loadTranslations(loc);
170
+ } catch {
171
+ // ignore - missing files fall back to English
172
+ }
173
+ }
174
+ }
@@ -0,0 +1,93 @@
1
+ en
2
+ error "Error"
3
+ success "Success"
4
+ warning "Warning"
5
+ failed "Failed"
6
+ info "Info"
7
+ error.invalid_github_url "Error: Invalid GitHub URL format"
8
+ error.invalid_github_url_monitoring "Error: Invalid GitHub URL for monitoring"
9
+ error.missing_required_url "Error: Missing required github issue or pull request URL"
10
+ error.unable_determine_version "Error: Unable to determine version"
11
+ error.invalid_url_type "Error: Invalid GitHub URL format"
12
+ error.url_type_not_supported "URL type '{{type}}' is not supported"
13
+ error.failed_to_get_current_user "Error: Failed to get current user"
14
+ error.failed_to_initialize_repository "Error: Failed to initialize repository"
15
+ error.failed_to_create_fork "Failed to create fork after all retries"
16
+ error.fork_not_accessible "Fork exists but not accessible after multiple retries"
17
+ error.failed_to_add_upstream_remote "Failed to add upstream remote"
18
+ error.failed_to_checkout "Failed to checkout {{branch}}"
19
+ error.failed_to_sync "Failed to sync {{branch}} with upstream"
20
+ error.failed_to_get_default_branch "Failed to get default branch name"
21
+ error.failed_to_get_current_branch "Failed to get current branch"
22
+ error.failed_to_fetch_upstream "Failed to fetch upstream"
23
+ error.failed_to_add_pr_fork_remote "Failed to add pr-fork remote"
24
+ error.failed_to_fetch_from_pr_fork "Failed to fetch from pr-fork"
25
+ error.pr_does_not_exist "Error: PR #{{prNumber}} does not exist in {{owner}}/{{repo}}"
26
+ error.youtrack_url_detected_no_config "Error: YouTrack URL detected but YouTrack configuration not found"
27
+ error.telegram_bot_token_not_set "Error: TELEGRAM_BOT_TOKEN environment variable or --token option is not set"
28
+ error.invalid_github_url_solve "Error: Invalid GitHub URL for solve command"
29
+ error.invalid_language "Invalid language '{{value}}'. Supported: {{supported}}"
30
+ success.readme_created "README.md created successfully"
31
+ success.process_completed "Process completed"
32
+ success.error_reported_to_sentry "Error reported to Sentry successfully"
33
+ success.language_set "Language set to {{language}}"
34
+ warning.session_log_not_found "Warning: Session log for {{session}} not found, but continuing with resume attempt"
35
+ warning.failed_to_create_readme "Failed: Could not create README.md"
36
+ warning.could_not_determine_root_repository "Could not determine root repository"
37
+ warning.could_not_check_fork_status "Warning: Could not check fork status: {{message}}"
38
+ warning.no_linked_issue_found "Warning: No linked issue found in PR body"
39
+ warning.could_not_search_for_existing_prs "Warning: Could not search for existing PRs: {{message}}"
40
+ warning.could_not_get_current_user "Warning: Could not get current GitHub user"
41
+ warning.could_not_check_github_permissions "Warning: Could not check GitHub permissions: {{message}}"
42
+ warning.could_not_determine_token_scopes "Warning: Could not determine token scopes from auth status"
43
+ warning.failed_to_fetch_branches "Warning: Failed to fetch branches from remote"
44
+ warning.error_during_auto_pr_creation "Warning: Error during auto PR creation: {{message}}"
45
+ warning.could_not_convert_pr_to_draft "Warning: Could not convert PR to draft"
46
+ warning.could_not_check_convert_pr_draft_status "Warning: Could not check/convert PR draft status"
47
+ warning.could_not_post_work_start_comment "Warning: Could not post work start comment"
48
+ warning.could_not_post_work_end_comment "Warning: Could not post work end comment"
49
+ warning.could_not_convert_pr_to_ready "Warning: Could not convert PR to ready"
50
+ warning.could_not_convert_pr_to_ready_status "Warning: Could not convert PR to ready status"
51
+ warning.pr_created_but_no_url "Warning: PR created but no URL returned"
52
+ warning.could_not_assign_user "Could not assign user"
53
+ info.expected_github_url_format "Expected: https://github.com/owner or https://github.com/owner/repo"
54
+ info.you_can_use_formats "You can use any of these formats:"
55
+ info.format_https_owner "- https://github.com/owner"
56
+ info.format_https_owner_repo "- https://github.com/owner/repo"
57
+ info.format_http_owner "- http://github.com/owner (will be converted to https)"
58
+ info.format_github_owner "- github.com/owner (will add https://)"
59
+ info.format_owner "- owner (will be converted to https://github.com/owner)"
60
+ info.format_owner_repo "- owner/repo (will be converted to https://github.com/owner/repo)"
61
+ info.full_log_file "Full log file: {{path}}"
62
+ info.process_exited_with_code "Process exited with code {{code}}"
63
+ info.current_configuration "Current Configuration:"
64
+ process.using_repository_fallback "Using repository-by-repository fallback for {{scope}}: {{owner}}"
65
+ process.fetching_repository_list "Fetching repository list..."
66
+ process.command "Command: {{command}}"
67
+ check.number "Check #{{iteration}}:"
68
+ time.current "Current time: {{time}}"
69
+ telegram.fetching_limits "🔄 Fetching usage limits..."
70
+ telegram.gathering_version "🔄 Gathering version information..."
71
+ telegram.usage_limits_title "📊 *Usage Limits*"
72
+ telegram.version_information_title "🤖 *Version Information*"
73
+ telegram.limits_only_in_groups "❌ The /limits command only works in group chats. Please add this bot to a group and make it an admin."
74
+ telegram.version_only_in_groups "❌ The /version command only works in group chats. Please add this bot to a group and make it an admin."
75
+ telegram.solve_only_in_groups "❌ The {{commandDisplay}} command only works in group chats. Please add this bot to a group and make it an admin."
76
+ telegram.hive_only_in_groups "❌ The /hive command only works in group chats. Please add this bot to a group and make it an admin."
77
+ telegram.solve_disabled "❌ The solve command is disabled on this bot instance."
78
+ telegram.hive_disabled "❌ The /hive command is disabled on this bot instance."
79
+ telegram.no_github_link_in_reply "❌ No GitHub issue/PR link found in the replied message.\n\nExample: Reply to a message containing a GitHub issue link with `/solve`\n\nOr with options: `/solve --model opus`"
80
+ telegram.invalid_options "❌ Invalid options: {{message}}\n\nUse /help to see available options"
81
+ telegram.invalid_isolation "❌ Invalid --isolation value '{{value}}'. Must be: screen, tmux, or docker"
82
+ telegram.invalid_locked_isolation "❌ Invalid locked --isolation value '{{value}}'. Must be: screen, tmux, or docker"
83
+ telegram.option_syntax_check "Please check your option syntax."
84
+ telegram.url_status_active "❌ This URL is {{statusText}}.\n\nURL: {{url}}\nStatus: {{status}}\n\n💡 Use /solve_queue to check the queue status."
85
+ telegram.url_session_running "❌ A working session is already running for this URL.\n\nURL: {{url}}\nSession: `{{session}}`\n\n💡 Wait for the current session to complete, or use /solve\\_stop to cancel it."
86
+ telegram.solve_rejected "❌ Solve command rejected.\n\n{{infoBlock}}\n\n🚫 Reason: {{reason}}"
87
+ telegram.language_invalid "❌ Invalid language. Supported: {{supported}}.\nUsage: /language <code>"
88
+ telegram.language_set "✅ Language set to *{{language}}*."
89
+ telegram.language_current "🌐 Current language: *{{language}}*.\nSupported: {{supported}}.\nUsage: /language <code>"
90
+ language.en "English"
91
+ language.ru "Russian"
92
+ language.zh "Chinese"
93
+ language.hi "Hindi"
@@ -0,0 +1,93 @@
1
+ hi
2
+ error "त्रुटि"
3
+ success "सफल"
4
+ warning "चेतावनी"
5
+ failed "विफल"
6
+ info "सूचना"
7
+ error.invalid_github_url "त्रुटि: अमान्य GitHub URL प्रारूप"
8
+ error.invalid_github_url_monitoring "त्रुटि: निगरानी के लिए अमान्य GitHub URL"
9
+ error.missing_required_url "त्रुटि: आवश्यक GitHub issue या pull request URL गायब है"
10
+ error.unable_determine_version "त्रुटि: संस्करण निर्धारित करने में असमर्थ"
11
+ error.invalid_url_type "त्रुटि: अमान्य GitHub URL प्रारूप"
12
+ error.url_type_not_supported "URL प्रकार '{{type}}' समर्थित नहीं है"
13
+ error.failed_to_get_current_user "त्रुटि: वर्तमान उपयोगकर्ता प्राप्त करने में विफल"
14
+ error.failed_to_initialize_repository "त्रुटि: रिपॉजिटरी आरंभ करने में विफल"
15
+ error.failed_to_create_fork "सभी प्रयासों के बाद fork बनाने में विफल"
16
+ error.fork_not_accessible "Fork मौजूद है लेकिन कई प्रयासों के बाद भी पहुँच योग्य नहीं"
17
+ error.failed_to_add_upstream_remote "upstream रिमोट जोड़ने में विफल"
18
+ error.failed_to_checkout "{{branch}} पर स्विच करने में विफल"
19
+ error.failed_to_sync "{{branch}} को upstream के साथ सिंक करने में विफल"
20
+ error.failed_to_get_default_branch "डिफ़ॉल्ट शाखा का नाम प्राप्त करने में विफल"
21
+ error.failed_to_get_current_branch "वर्तमान शाखा प्राप्त करने में विफल"
22
+ error.failed_to_fetch_upstream "upstream से डेटा प्राप्त करने में विफल"
23
+ error.failed_to_add_pr_fork_remote "pr-fork रिमोट जोड़ने में विफल"
24
+ error.failed_to_fetch_from_pr_fork "pr-fork से डेटा प्राप्त करने में विफल"
25
+ error.pr_does_not_exist "त्रुटि: PR #{{prNumber}} {{owner}}/{{repo}} में मौजूद नहीं है"
26
+ error.youtrack_url_detected_no_config "त्रुटि: YouTrack URL का पता चला लेकिन YouTrack कॉन्फ़िगरेशन नहीं मिला"
27
+ error.telegram_bot_token_not_set "त्रुटि: TELEGRAM_BOT_TOKEN पर्यावरण चर या --token विकल्प सेट नहीं है"
28
+ error.invalid_github_url_solve "त्रुटि: solve कमांड के लिए अमान्य GitHub URL"
29
+ error.invalid_language "अमान्य भाषा '{{value}}'. समर्थित: {{supported}}"
30
+ success.readme_created "README.md सफलतापूर्वक बनाया गया"
31
+ success.process_completed "प्रक्रिया पूरी हुई"
32
+ success.error_reported_to_sentry "त्रुटि सफलतापूर्वक Sentry को रिपोर्ट की गई"
33
+ success.language_set "भाषा सेट की गई: {{language}}"
34
+ warning.session_log_not_found "चेतावनी: सत्र {{session}} के लिए लॉग नहीं मिला, लेकिन फिर से शुरू करने का प्रयास जारी है"
35
+ warning.failed_to_create_readme "विफल: README.md नहीं बना सका"
36
+ warning.could_not_determine_root_repository "रूट रिपॉजिटरी निर्धारित नहीं की जा सकी"
37
+ warning.could_not_check_fork_status "चेतावनी: fork स्थिति की जाँच नहीं हो सकी: {{message}}"
38
+ warning.no_linked_issue_found "चेतावनी: PR विवरण में कोई जुड़ा हुआ issue नहीं मिला"
39
+ warning.could_not_search_for_existing_prs "चेतावनी: मौजूदा PR नहीं ढूँढ सके: {{message}}"
40
+ warning.could_not_get_current_user "चेतावनी: वर्तमान GitHub उपयोगकर्ता प्राप्त नहीं हो सका"
41
+ warning.could_not_check_github_permissions "चेतावनी: GitHub अनुमतियाँ जाँच नहीं सकी: {{message}}"
42
+ warning.could_not_determine_token_scopes "चेतावनी: ऑथ स्थिति से टोकन स्कोप निर्धारित नहीं हो सके"
43
+ warning.failed_to_fetch_branches "चेतावनी: रिमोट से शाखाएँ प्राप्त करने में विफल"
44
+ warning.error_during_auto_pr_creation "चेतावनी: स्वचालित PR निर्माण के दौरान त्रुटि: {{message}}"
45
+ warning.could_not_convert_pr_to_draft "चेतावनी: PR को draft में परिवर्तित नहीं कर सका"
46
+ warning.could_not_check_convert_pr_draft_status "चेतावनी: PR draft स्थिति की जाँच/परिवर्तन नहीं हो सका"
47
+ warning.could_not_post_work_start_comment "चेतावनी: कार्य प्रारंभ टिप्पणी पोस्ट नहीं कर सका"
48
+ warning.could_not_post_work_end_comment "चेतावनी: कार्य समाप्ति टिप्पणी पोस्ट नहीं कर सका"
49
+ warning.could_not_convert_pr_to_ready "चेतावनी: PR को ready में परिवर्तित नहीं कर सका"
50
+ warning.could_not_convert_pr_to_ready_status "चेतावनी: PR स्थिति को ready में परिवर्तित नहीं कर सका"
51
+ warning.pr_created_but_no_url "चेतावनी: PR बनाया गया लेकिन URL वापस नहीं आया"
52
+ warning.could_not_assign_user "उपयोगकर्ता नियुक्त नहीं कर सका"
53
+ info.expected_github_url_format "अपेक्षित: https://github.com/owner या https://github.com/owner/repo"
54
+ info.you_can_use_formats "आप इनमें से किसी भी प्रारूप का उपयोग कर सकते हैं:"
55
+ info.format_https_owner "- https://github.com/owner"
56
+ info.format_https_owner_repo "- https://github.com/owner/repo"
57
+ info.format_http_owner "- http://github.com/owner (https में परिवर्तित होगा)"
58
+ info.format_github_owner "- github.com/owner (https:// जोड़ा जाएगा)"
59
+ info.format_owner "- owner (https://github.com/owner में परिवर्तित होगा)"
60
+ info.format_owner_repo "- owner/repo (https://github.com/owner/repo में परिवर्तित होगा)"
61
+ info.full_log_file "पूरा लॉग फ़ाइल: {{path}}"
62
+ info.process_exited_with_code "प्रक्रिया कोड {{code}} के साथ समाप्त हुई"
63
+ info.current_configuration "वर्तमान कॉन्फ़िगरेशन:"
64
+ process.using_repository_fallback "{{scope}} के लिए रिपॉजिटरी-दर-रिपॉजिटरी फ़ॉलबैक का उपयोग: {{owner}}"
65
+ process.fetching_repository_list "रिपॉजिटरी सूची प्राप्त की जा रही है..."
66
+ process.command "कमांड: {{command}}"
67
+ check.number "जाँच #{{iteration}}:"
68
+ time.current "वर्तमान समय: {{time}}"
69
+ telegram.fetching_limits "🔄 उपयोग सीमाएँ प्राप्त की जा रही हैं..."
70
+ telegram.gathering_version "🔄 संस्करण जानकारी एकत्र की जा रही है..."
71
+ telegram.usage_limits_title "📊 *उपयोग सीमाएँ*"
72
+ telegram.version_information_title "🤖 *संस्करण जानकारी*"
73
+ telegram.limits_only_in_groups "❌ /limits कमांड केवल समूह चैट में काम करती है। कृपया बॉट को समूह में जोड़ें और व्यवस्थापक बनाएँ।"
74
+ telegram.version_only_in_groups "❌ /version कमांड केवल समूह चैट में काम करती है। कृपया बॉट को समूह में जोड़ें और व्यवस्थापक बनाएँ।"
75
+ telegram.solve_only_in_groups "❌ {{commandDisplay}} कमांड केवल समूह चैट में काम करती है। कृपया बॉट को समूह में जोड़ें और व्यवस्थापक बनाएँ।"
76
+ telegram.hive_only_in_groups "❌ /hive कमांड केवल समूह चैट में काम करती है। कृपया बॉट को समूह में जोड़ें और व्यवस्थापक बनाएँ।"
77
+ telegram.solve_disabled "❌ इस बॉट इंस्टेंस पर solve कमांड अक्षम है।"
78
+ telegram.hive_disabled "❌ इस बॉट इंस्टेंस पर /hive कमांड अक्षम है।"
79
+ telegram.no_github_link_in_reply "❌ उत्तर दिए गए संदेश में कोई GitHub issue/PR लिंक नहीं मिला।\n\nउदाहरण: GitHub issue लिंक वाले संदेश का उत्तर `/solve` से दें\n\nया विकल्पों के साथ: `/solve --model opus`"
80
+ telegram.invalid_options "❌ अमान्य विकल्प: {{message}}\n\nउपलब्ध विकल्प देखने के लिए /help का उपयोग करें"
81
+ telegram.invalid_isolation "❌ अमान्य --isolation मान '{{value}}'। होना चाहिए: screen, tmux, या docker"
82
+ telegram.invalid_locked_isolation "❌ अमान्य लॉक्ड --isolation मान '{{value}}'। होना चाहिए: screen, tmux, या docker"
83
+ telegram.option_syntax_check "कृपया अपने विकल्प सिंटैक्स की जाँच करें।"
84
+ telegram.url_status_active "❌ यह URL {{statusText}} है।\n\nURL: {{url}}\nस्थिति: {{status}}\n\n💡 कतार स्थिति देखने के लिए /solve_queue का उपयोग करें।"
85
+ telegram.url_session_running "❌ इस URL के लिए पहले से एक कार्य सत्र चल रहा है।\n\nURL: {{url}}\nसत्र: `{{session}}`\n\n💡 वर्तमान सत्र के पूरा होने की प्रतीक्षा करें, या रद्द करने के लिए /solve\\_stop का उपयोग करें।"
86
+ telegram.solve_rejected "❌ Solve कमांड अस्वीकृत।\n\n{{infoBlock}}\n\n🚫 कारण: {{reason}}"
87
+ telegram.language_invalid "❌ अमान्य भाषा। समर्थित: {{supported}}।\nउपयोग: /language <कोड>"
88
+ telegram.language_set "✅ भाषा सेट: *{{language}}*।"
89
+ telegram.language_current "🌐 वर्तमान भाषा: *{{language}}*।\nसमर्थित: {{supported}}।\nउपयोग: /language <कोड>"
90
+ language.en "अंग्रेज़ी"
91
+ language.ru "रूसी"
92
+ language.zh "चीनी"
93
+ language.hi "हिन्दी"
@@ -0,0 +1,93 @@
1
+ ru
2
+ error "Ошибка"
3
+ success "Успешно"
4
+ warning "Предупреждение"
5
+ failed "Не удалось"
6
+ info "Информация"
7
+ error.invalid_github_url "Ошибка: Неверный формат URL GitHub"
8
+ error.invalid_github_url_monitoring "Ошибка: Неверный URL GitHub для мониторинга"
9
+ error.missing_required_url "Ошибка: Отсутствует обязательный URL проблемы или запроса на слияние GitHub"
10
+ error.unable_determine_version "Ошибка: Не удается определить версию"
11
+ error.invalid_url_type "Ошибка: Неверный формат URL GitHub"
12
+ error.url_type_not_supported "Тип URL '{{type}}' не поддерживается"
13
+ error.failed_to_get_current_user "Ошибка: Не удалось получить текущего пользователя"
14
+ error.failed_to_initialize_repository "Ошибка: Не удалось инициализировать репозиторий"
15
+ error.failed_to_create_fork "Не удалось создать форк после всех попыток"
16
+ error.fork_not_accessible "Форк существует, но недоступен после нескольких попыток"
17
+ error.failed_to_add_upstream_remote "Не удалось добавить удалённый репозиторий upstream"
18
+ error.failed_to_checkout "Не удалось переключиться на {{branch}}"
19
+ error.failed_to_sync "Не удалось синхронизировать {{branch}} с upstream"
20
+ error.failed_to_get_default_branch "Не удалось получить имя ветки по умолчанию"
21
+ error.failed_to_get_current_branch "Не удалось получить текущую ветку"
22
+ error.failed_to_fetch_upstream "Не удалось получить данные из upstream"
23
+ error.failed_to_add_pr_fork_remote "Не удалось добавить удалённый репозиторий pr-fork"
24
+ error.failed_to_fetch_from_pr_fork "Не удалось получить данные из pr-fork"
25
+ error.pr_does_not_exist "Ошибка: PR #{{prNumber}} не существует в {{owner}}/{{repo}}"
26
+ error.youtrack_url_detected_no_config "Ошибка: Обнаружен URL YouTrack, но конфигурация YouTrack не найдена"
27
+ error.telegram_bot_token_not_set "Ошибка: Переменная окружения TELEGRAM_BOT_TOKEN или опция --token не установлена"
28
+ error.invalid_github_url_solve "Ошибка: Неверный URL GitHub для команды solve"
29
+ error.invalid_language "Неверный язык '{{value}}'. Поддерживаются: {{supported}}"
30
+ success.readme_created "README.md успешно создан"
31
+ success.process_completed "Процесс завершён"
32
+ success.error_reported_to_sentry "Ошибка успешно отправлена в Sentry"
33
+ success.language_set "Язык установлен: {{language}}"
34
+ warning.session_log_not_found "Предупреждение: Журнал сеанса для {{session}} не найден, но продолжаем попытку возобновления"
35
+ warning.failed_to_create_readme "Не удалось: Не удалось создать README.md"
36
+ warning.could_not_determine_root_repository "Не удалось определить корневой репозиторий"
37
+ warning.could_not_check_fork_status "Предупреждение: Не удалось проверить статус форка: {{message}}"
38
+ warning.no_linked_issue_found "Предупреждение: Связанная проблема не найдена в теле PR"
39
+ warning.could_not_search_for_existing_prs "Предупреждение: Не удалось найти существующие PR: {{message}}"
40
+ warning.could_not_get_current_user "Предупреждение: Не удалось получить текущего пользователя GitHub"
41
+ warning.could_not_check_github_permissions "Предупреждение: Не удалось проверить разрешения GitHub: {{message}}"
42
+ warning.could_not_determine_token_scopes "Предупреждение: Не удалось определить области токена из статуса аутентификации"
43
+ warning.failed_to_fetch_branches "Предупреждение: Не удалось получить ветки из удалённого репозитория"
44
+ warning.error_during_auto_pr_creation "Предупреждение: Ошибка при автоматическом создании PR: {{message}}"
45
+ warning.could_not_convert_pr_to_draft "Предупреждение: Не удалось преобразовать PR в черновик"
46
+ warning.could_not_check_convert_pr_draft_status "Предупреждение: Не удалось проверить/преобразовать статус черновика PR"
47
+ warning.could_not_post_work_start_comment "Предупреждение: Не удалось опубликовать комментарий о начале работы"
48
+ warning.could_not_post_work_end_comment "Предупреждение: Не удалось опубликовать комментарий о завершении работы"
49
+ warning.could_not_convert_pr_to_ready "Предупреждение: Не удалось преобразовать PR в готовый"
50
+ warning.could_not_convert_pr_to_ready_status "Предупреждение: Не удалось преобразовать статус PR в готовый"
51
+ warning.pr_created_but_no_url "Предупреждение: PR создан, но URL не возвращён"
52
+ warning.could_not_assign_user "Не удалось назначить пользователя"
53
+ info.expected_github_url_format "Ожидается: https://github.com/owner или https://github.com/owner/repo"
54
+ info.you_can_use_formats "Вы можете использовать любой из этих форматов:"
55
+ info.format_https_owner "- https://github.com/owner"
56
+ info.format_https_owner_repo "- https://github.com/owner/repo"
57
+ info.format_http_owner "- http://github.com/owner (будет преобразовано в https)"
58
+ info.format_github_owner "- github.com/owner (будет добавлено https://)"
59
+ info.format_owner "- owner (будет преобразовано в https://github.com/owner)"
60
+ info.format_owner_repo "- owner/repo (будет преобразовано в https://github.com/owner/repo)"
61
+ info.full_log_file "Полный файл журнала: {{path}}"
62
+ info.process_exited_with_code "Процесс завершился с кодом {{code}}"
63
+ info.current_configuration "Текущая конфигурация:"
64
+ process.using_repository_fallback "Использование резервного метода репозиторий-за-репозиторием для {{scope}}: {{owner}}"
65
+ process.fetching_repository_list "Получение списка репозиториев..."
66
+ process.command "Команда: {{command}}"
67
+ check.number "Проверка #{{iteration}}:"
68
+ time.current "Текущее время: {{time}}"
69
+ telegram.fetching_limits "🔄 Получение лимитов использования..."
70
+ telegram.gathering_version "🔄 Сбор информации о версии..."
71
+ telegram.usage_limits_title "📊 *Лимиты использования*"
72
+ telegram.version_information_title "🤖 *Информация о версии*"
73
+ telegram.limits_only_in_groups "❌ Команда /limits работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
74
+ telegram.version_only_in_groups "❌ Команда /version работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
75
+ telegram.solve_only_in_groups "❌ Команда {{commandDisplay}} работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
76
+ telegram.hive_only_in_groups "❌ Команда /hive работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
77
+ telegram.solve_disabled "❌ Команда solve отключена в этом экземпляре бота."
78
+ telegram.hive_disabled "❌ Команда /hive отключена в этом экземпляре бота."
79
+ telegram.no_github_link_in_reply "❌ В сообщении, на которое вы ответили, не найдена ссылка на проблему/PR GitHub.\n\nПример: Ответьте на сообщение со ссылкой на проблему GitHub командой `/solve`\n\nИли с опциями: `/solve --model opus`"
80
+ telegram.invalid_options "❌ Неверные опции: {{message}}\n\nИспользуйте /help, чтобы увидеть доступные опции"
81
+ telegram.invalid_isolation "❌ Неверное значение --isolation '{{value}}'. Допустимо: screen, tmux или docker"
82
+ telegram.invalid_locked_isolation "❌ Неверное заблокированное значение --isolation '{{value}}'. Допустимо: screen, tmux или docker"
83
+ telegram.option_syntax_check "Проверьте синтаксис опций."
84
+ telegram.url_status_active "❌ Этот URL имеет статус {{statusText}}.\n\nURL: {{url}}\nСтатус: {{status}}\n\n💡 Используйте /solve_queue для проверки очереди."
85
+ telegram.url_session_running "❌ Для этого URL уже выполняется рабочий сеанс.\n\nURL: {{url}}\nСеанс: `{{session}}`\n\n💡 Дождитесь завершения текущего сеанса или используйте /solve\\_stop для отмены."
86
+ telegram.solve_rejected "❌ Команда solve отклонена.\n\n{{infoBlock}}\n\n🚫 Причина: {{reason}}"
87
+ telegram.language_invalid "❌ Неверный язык. Поддерживаются: {{supported}}.\nИспользование: /language <код>"
88
+ telegram.language_set "✅ Язык установлен: *{{language}}*."
89
+ telegram.language_current "🌐 Текущий язык: *{{language}}*.\nПоддерживаются: {{supported}}.\nИспользование: /language <код>"
90
+ language.en "Английский"
91
+ language.ru "Русский"
92
+ language.zh "Китайский"
93
+ language.hi "Хинди"
@@ -0,0 +1,93 @@
1
+ zh
2
+ error "错误"
3
+ success "成功"
4
+ warning "警告"
5
+ failed "失败"
6
+ info "信息"
7
+ error.invalid_github_url "错误:GitHub URL 格式无效"
8
+ error.invalid_github_url_monitoring "错误:用于监控的 GitHub URL 无效"
9
+ error.missing_required_url "错误:缺少必需的 GitHub issue 或 pull request URL"
10
+ error.unable_determine_version "错误:无法确定版本"
11
+ error.invalid_url_type "错误:GitHub URL 格式无效"
12
+ error.url_type_not_supported "URL 类型 '{{type}}' 不受支持"
13
+ error.failed_to_get_current_user "错误:无法获取当前用户"
14
+ error.failed_to_initialize_repository "错误:无法初始化仓库"
15
+ error.failed_to_create_fork "多次重试后仍无法创建 fork"
16
+ error.fork_not_accessible "Fork 已存在但多次重试后仍无法访问"
17
+ error.failed_to_add_upstream_remote "无法添加 upstream 远程仓库"
18
+ error.failed_to_checkout "无法切换到 {{branch}}"
19
+ error.failed_to_sync "无法将 {{branch}} 与 upstream 同步"
20
+ error.failed_to_get_default_branch "无法获取默认分支名称"
21
+ error.failed_to_get_current_branch "无法获取当前分支"
22
+ error.failed_to_fetch_upstream "无法从 upstream 获取数据"
23
+ error.failed_to_add_pr_fork_remote "无法添加 pr-fork 远程仓库"
24
+ error.failed_to_fetch_from_pr_fork "无法从 pr-fork 获取数据"
25
+ error.pr_does_not_exist "错误:PR #{{prNumber}} 在 {{owner}}/{{repo}} 中不存在"
26
+ error.youtrack_url_detected_no_config "错误:检测到 YouTrack URL 但未找到 YouTrack 配置"
27
+ error.telegram_bot_token_not_set "错误:未设置 TELEGRAM_BOT_TOKEN 环境变量或 --token 选项"
28
+ error.invalid_github_url_solve "错误:solve 命令的 GitHub URL 无效"
29
+ error.invalid_language "无效的语言 '{{value}}'。支持的语言:{{supported}}"
30
+ success.readme_created "README.md 创建成功"
31
+ success.process_completed "进程已完成"
32
+ success.error_reported_to_sentry "错误已成功上报到 Sentry"
33
+ success.language_set "语言已设置为 {{language}}"
34
+ warning.session_log_not_found "警告:未找到会话 {{session}} 的日志,但将继续尝试恢复"
35
+ warning.failed_to_create_readme "失败:无法创建 README.md"
36
+ warning.could_not_determine_root_repository "无法确定根仓库"
37
+ warning.could_not_check_fork_status "警告:无法检查 fork 状态:{{message}}"
38
+ warning.no_linked_issue_found "警告:在 PR 内容中未找到关联的 issue"
39
+ warning.could_not_search_for_existing_prs "警告:无法搜索现有的 PR:{{message}}"
40
+ warning.could_not_get_current_user "警告:无法获取当前 GitHub 用户"
41
+ warning.could_not_check_github_permissions "警告:无法检查 GitHub 权限:{{message}}"
42
+ warning.could_not_determine_token_scopes "警告:无法从认证状态确定令牌范围"
43
+ warning.failed_to_fetch_branches "警告:无法从远程获取分支"
44
+ warning.error_during_auto_pr_creation "警告:自动创建 PR 时出错:{{message}}"
45
+ warning.could_not_convert_pr_to_draft "警告:无法将 PR 转换为草稿"
46
+ warning.could_not_check_convert_pr_draft_status "警告:无法检查/转换 PR 草稿状态"
47
+ warning.could_not_post_work_start_comment "警告:无法发布工作开始评论"
48
+ warning.could_not_post_work_end_comment "警告:无法发布工作结束评论"
49
+ warning.could_not_convert_pr_to_ready "警告:无法将 PR 转换为就绪状态"
50
+ warning.could_not_convert_pr_to_ready_status "警告:无法将 PR 状态转换为就绪"
51
+ warning.pr_created_but_no_url "警告:已创建 PR 但未返回 URL"
52
+ warning.could_not_assign_user "无法分配用户"
53
+ info.expected_github_url_format "预期格式:https://github.com/owner 或 https://github.com/owner/repo"
54
+ info.you_can_use_formats "您可以使用以下任意格式:"
55
+ info.format_https_owner "- https://github.com/owner"
56
+ info.format_https_owner_repo "- https://github.com/owner/repo"
57
+ info.format_http_owner "- http://github.com/owner(将转换为 https)"
58
+ info.format_github_owner "- github.com/owner(将添加 https://)"
59
+ info.format_owner "- owner(将转换为 https://github.com/owner)"
60
+ info.format_owner_repo "- owner/repo(将转换为 https://github.com/owner/repo)"
61
+ info.full_log_file "完整日志文件:{{path}}"
62
+ info.process_exited_with_code "进程退出代码:{{code}}"
63
+ info.current_configuration "当前配置:"
64
+ process.using_repository_fallback "使用按仓库回退方式处理 {{scope}}:{{owner}}"
65
+ process.fetching_repository_list "正在获取仓库列表……"
66
+ process.command "命令:{{command}}"
67
+ check.number "检查 #{{iteration}}:"
68
+ time.current "当前时间:{{time}}"
69
+ telegram.fetching_limits "🔄 正在获取使用限额……"
70
+ telegram.gathering_version "🔄 正在收集版本信息……"
71
+ telegram.usage_limits_title "📊 *使用限额*"
72
+ telegram.version_information_title "🤖 *版本信息*"
73
+ telegram.limits_only_in_groups "❌ /limits 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
74
+ telegram.version_only_in_groups "❌ /version 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
75
+ telegram.solve_only_in_groups "❌ {{commandDisplay}} 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
76
+ telegram.hive_only_in_groups "❌ /hive 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
77
+ telegram.solve_disabled "❌ 此机器人实例已禁用 solve 命令。"
78
+ telegram.hive_disabled "❌ 此机器人实例已禁用 /hive 命令。"
79
+ telegram.no_github_link_in_reply "❌ 被回复的消息中未找到 GitHub issue/PR 链接。\n\n示例:使用 `/solve` 回复包含 GitHub issue 链接的消息\n\n或带选项:`/solve --model opus`"
80
+ telegram.invalid_options "❌ 无效选项:{{message}}\n\n使用 /help 查看可用选项"
81
+ telegram.invalid_isolation "❌ 无效的 --isolation 值 '{{value}}'。必须是:screen、tmux 或 docker"
82
+ telegram.invalid_locked_isolation "❌ 无效的锁定 --isolation 值 '{{value}}'。必须是:screen、tmux 或 docker"
83
+ telegram.option_syntax_check "请检查您的选项语法。"
84
+ telegram.url_status_active "❌ 此 URL 状态为 {{statusText}}。\n\nURL:{{url}}\n状态:{{status}}\n\n💡 使用 /solve_queue 查看队列状态。"
85
+ telegram.url_session_running "❌ 该 URL 已有正在运行的工作会话。\n\nURL:{{url}}\n会话:`{{session}}`\n\n💡 等待当前会话完成,或使用 /solve\\_stop 取消。"
86
+ telegram.solve_rejected "❌ Solve 命令被拒绝。\n\n{{infoBlock}}\n\n🚫 原因:{{reason}}"
87
+ telegram.language_invalid "❌ 语言无效。支持的语言:{{supported}}。\n用法:/language <代码>"
88
+ telegram.language_set "✅ 语言已设置为 *{{language}}*。"
89
+ telegram.language_current "🌐 当前语言:*{{language}}*。\n支持的语言:{{supported}}。\n用法:/language <代码>"
90
+ language.en "英语"
91
+ language.ru "俄语"
92
+ language.zh "中文"
93
+ language.hi "印地语"
package/src/review.mjs CHANGED
@@ -101,6 +101,11 @@ const createReviewYargsConfig = yargsInstance =>
101
101
  description: 'Execute the AI tool using bunx (experimental, may improve speed and memory usage)',
102
102
  default: false,
103
103
  })
104
+ .option('language', {
105
+ type: 'string',
106
+ description: 'Language for user-facing output (en, ru, zh, hi). Defaults to detected system locale.',
107
+ choices: ['en', 'ru', 'zh', 'hi'],
108
+ })
104
109
  .check(parsed => {
105
110
  if (!parsed['pr-url'] && !parsed.prUrl && !parsed._?.[0]) {
106
111
  throw new Error('The GitHub pull request URL is required');
@@ -128,6 +133,10 @@ const prUrl = argv['pr-url'] || argv.prUrl || argv._[0];
128
133
  // Set global verbose mode for log function
129
134
  global.verboseMode = argv.verbose;
130
135
 
136
+ // Initialize i18n based on --language (or detected system locale)
137
+ const { initI18n } = await import('./i18n.lib.mjs');
138
+ await initI18n(argv.language);
139
+
131
140
  // Create permanent log file immediately with timestamp
132
141
  const scriptDir = path.dirname(process.argv[1]);
133
142
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
@@ -552,6 +552,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
552
552
  description: '[EXPERIMENTAL] Enable live progress monitoring. Accepts "comment" (default, updates a per-session PR comment) or "pr" (updates PR description). Plain --working-session-live-progress means "comment". Works with or without --interactive-mode.',
553
553
  default: false,
554
554
  },
555
+ language: {
556
+ type: 'string',
557
+ description: 'Language for user-facing output (en, ru, zh, hi). Defaults to detected system locale. Affects terminal status/error messages and bot-generated PR/issue comments. Does not affect AI prompts.',
558
+ choices: ['en', 'ru', 'zh', 'hi'],
559
+ },
555
560
  };
556
561
 
557
562
  // Function to create yargs configuration - avoids duplication
package/src/solve.mjs CHANGED
@@ -84,6 +84,10 @@ try {
84
84
  }
85
85
  global.verboseMode = argv.verbose;
86
86
 
87
+ // Initialize i18n based on --language (or detected system locale)
88
+ const { initI18n } = await import('./i18n.lib.mjs');
89
+ await initI18n(argv.language);
90
+
87
91
  setupVerboseLogInterceptor(); // Issue #1466: capture [VERBOSE] output in log files
88
92
  setupStdioLogInterceptor(); // Issue #1549: capture ALL terminal output in log file
89
93
 
@@ -85,6 +85,11 @@ export const createYargsConfig = yargsInstance =>
85
85
  choices: ['text', 'json'],
86
86
  default: 'text',
87
87
  })
88
+ .option('language', {
89
+ type: 'string',
90
+ description: 'Language for user-facing output (en, ru, zh, hi). Defaults to detected system locale.',
91
+ choices: ['en', 'ru', 'zh', 'hi'],
92
+ })
88
93
  .check(argv => {
89
94
  if (!argv['task-input'] && !argv._[0]) {
90
95
  throw new Error('Please provide a GitHub issue URL or task description');
package/src/task.mjs CHANGED
@@ -50,6 +50,10 @@ try {
50
50
  process.exit(1);
51
51
  }
52
52
 
53
+ // Initialize i18n based on --language (or detected system locale)
54
+ const { initI18n } = await import('./i18n.lib.mjs');
55
+ await initI18n(argv.language);
56
+
53
57
  const taskInput = argv['task-input'] || argv.taskInput || argv._[0];
54
58
  const selectedModel = argv.model || getDefaultTaskModel(argv.tool);
55
59
  const modelValidation = validateModelName(selectedModel, argv.tool);
@@ -297,6 +297,12 @@ await initializeSentry({
297
297
  environment: process.env.NODE_ENV || 'production',
298
298
  });
299
299
 
300
+ // Initialize i18n: pre-load every supported locale so per-user translations
301
+ // can resolve synchronously from the cache when handling Telegram updates.
302
+ const { initI18n, t, preloadAllLocales, resolveLocaleFromTelegramCtx } = await import('./i18n.lib.mjs');
303
+ await initI18n();
304
+ await preloadAllLocales();
305
+
300
306
  const telegrafModule = await use('telegraf');
301
307
  const { Telegraf } = telegrafModule;
302
308
 
@@ -564,6 +570,7 @@ bot.command('help', async ctx => {
564
570
  message += '`/solve_queue` - Show solve queue status\n';
565
571
  message += '*/limits* - Show usage limits\n';
566
572
  message += '*/version* - Show bot and runtime versions\n';
573
+ message += '*/language* `[en|ru|zh|hi]` - Set or show your preferred reply language (in-memory only, per-user)\n';
567
574
  message += '`/accept_invites` - Accept all pending GitHub invitations\n';
568
575
  message += '*/merge* - Merge queue (experimental)\n';
569
576
  message += 'Usage: `/merge <github-repo-url>`\n';
@@ -621,11 +628,12 @@ bot.command('limits', async ctx => {
621
628
  return;
622
629
  }
623
630
 
631
+ const userLocale = resolveLocaleFromTelegramCtx(ctx);
624
632
  if (!_isGroupChat(ctx)) {
625
633
  if (VERBOSE) {
626
634
  console.log('[VERBOSE] /limits ignored: not a group chat');
627
635
  }
628
- await ctx.reply('❌ The /limits command only works in group chats. Please add this bot to a group and make it an admin.', { reply_to_message_id: ctx.message.message_id });
636
+ await ctx.reply(t('telegram.limits_only_in_groups', {}, { locale: userLocale }), { reply_to_message_id: ctx.message.message_id });
629
637
  return;
630
638
  }
631
639
 
@@ -638,7 +646,7 @@ bot.command('limits', async ctx => {
638
646
  }
639
647
 
640
648
  // Send "fetching" message to indicate work is in progress
641
- const fetchingMessage = await ctx.reply('🔄 Fetching usage limits...', {
649
+ const fetchingMessage = await ctx.reply(t('telegram.fetching_limits', {}, { locale: userLocale }), {
642
650
  reply_to_message_id: ctx.message.message_id,
643
651
  });
644
652
 
@@ -651,7 +659,7 @@ bot.command('limits', async ctx => {
651
659
  const solveQueue = getSolveQueue({ verbose: VERBOSE });
652
660
  const queueStatus = await solveQueue.formatStatus();
653
661
  const codexSection = formatCodexLimitsSection(limits.codex.success ? limits.codex : null, codexError);
654
- const message = '📊 *Usage Limits*\n\n' + formatUsageMessage(limits.claude.success ? limits.claude.usage : null, limits.disk.success ? limits.disk.diskSpace : null, limits.github.success ? limits.github.githubRateLimit : null, limits.cpu.success ? limits.cpu.cpuLoad : null, limits.memory.success ? limits.memory.memory : null, claudeError, [codexSection, queueStatus]);
662
+ const message = t('telegram.usage_limits_title', {}, { locale: userLocale }) + '\n\n' + formatUsageMessage(limits.claude.success ? limits.claude.usage : null, limits.disk.success ? limits.disk.diskSpace : null, limits.github.success ? limits.github.githubRateLimit : null, limits.cpu.success ? limits.cpu.cpuLoad : null, limits.memory.success ? limits.memory.memory : null, claudeError, [codexSection, queueStatus]);
655
663
  await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown' });
656
664
  });
657
665
  bot.command('version', async ctx => {
@@ -663,16 +671,20 @@ bot.command('version', async ctx => {
663
671
  data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username },
664
672
  });
665
673
  if (isOldMessage(ctx) || isForwardedOrReply(ctx)) return;
666
- if (!_isGroupChat(ctx)) return await ctx.reply('❌ The /version command only works in group chats. Please add this bot to a group and make it an admin.', { reply_to_message_id: ctx.message.message_id });
674
+ const versionLocale = resolveLocaleFromTelegramCtx(ctx);
675
+ if (!_isGroupChat(ctx)) return await ctx.reply(t('telegram.version_only_in_groups', {}, { locale: versionLocale }), { reply_to_message_id: ctx.message.message_id });
667
676
  if (!isTopicAuthorized(ctx)) return await ctx.reply(buildAuthErrorMessage(ctx), { reply_to_message_id: ctx.message.message_id });
668
- const fetchingMessage = await ctx.reply('🔄 Gathering version information...', {
677
+ const fetchingMessage = await ctx.reply(t('telegram.gathering_version', {}, { locale: versionLocale }), {
669
678
  reply_to_message_id: ctx.message.message_id,
670
679
  });
671
680
  const result = await getVersionInfo(VERBOSE);
672
681
  if (!result.success) return await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, `❌ ${escapeMarkdownV2(result.error, { preserveCodeBlocks: true })}`, { parse_mode: 'MarkdownV2' });
673
- await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, '🤖 *Version Information*\n\n' + formatVersionMessage(result.versions), { parse_mode: 'Markdown' });
682
+ await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, t('telegram.version_information_title', {}, { locale: versionLocale }) + '\n\n' + formatVersionMessage(result.versions), { parse_mode: 'Markdown' });
674
683
  });
675
684
 
685
+ const { registerLanguageCommand } = await import('./telegram-language-command.lib.mjs');
686
+ registerLanguageCommand(bot, { VERBOSE, isOldMessage, isForwardedOrReply });
687
+
676
688
  const { registerAcceptInvitesCommand } = await import('./telegram-accept-invitations.lib.mjs');
677
689
  const sharedCommandOpts = { VERBOSE, isOldMessage, isForwardedOrReply, isGroupChat: _isGroupChat, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage, addBreadcrumb, isChatStopped, getStoppedChatRejectMessage };
678
690
  registerAcceptInvitesCommand(bot, sharedCommandOpts);
@@ -704,11 +716,12 @@ async function handleSolveCommand(ctx) {
704
716
  },
705
717
  });
706
718
 
719
+ const solveLocale = resolveLocaleFromTelegramCtx(ctx);
707
720
  if (!solveEnabled) {
708
721
  if (VERBOSE) {
709
722
  console.log(`[VERBOSE] ${solveCommandDisplay} ignored: command disabled`);
710
723
  }
711
- await ctx.reply(' The solve command is disabled on this bot instance.');
724
+ await ctx.reply(t('telegram.solve_disabled', {}, { locale: solveLocale }));
712
725
  return;
713
726
  }
714
727
 
@@ -737,7 +750,7 @@ async function handleSolveCommand(ctx) {
737
750
  if (VERBOSE) {
738
751
  console.log(`[VERBOSE] ${solveCommandDisplay} ignored: not a group chat`);
739
752
  }
740
- await ctx.reply(`❌ The ${solveCommandDisplay} command only works in group chats. Please add this bot to a group and make it an admin.`, { reply_to_message_id: ctx.message.message_id });
753
+ await ctx.reply(t('telegram.solve_only_in_groups', { commandDisplay: solveCommandDisplay }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
741
754
  return;
742
755
  }
743
756
 
@@ -802,7 +815,7 @@ async function handleSolveCommand(ctx) {
802
815
  if (VERBOSE) {
803
816
  console.log('[VERBOSE] No GitHub URL found in replied message');
804
817
  }
805
- await safeReply(ctx, ' No GitHub issue/PR link found in the replied message.\n\nExample: Reply to a message containing a GitHub issue link with `/solve`\n\nOr with options: `/solve --model opus`', { reply_to_message_id: ctx.message.message_id });
818
+ await safeReply(ctx, t('telegram.no_github_link_in_reply', {}, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
806
819
  return;
807
820
  }
808
821
  }
@@ -811,7 +824,7 @@ async function handleSolveCommand(ctx) {
811
824
 
812
825
  const { malformed, errors: malformedErrors } = detectMalformedFlags(userArgs);
813
826
  if (malformed.length > 0) {
814
- await safeReply(ctx, `❌ ${escapeMarkdown(malformedErrors.join('\n'))}\n\nPlease check your option syntax.`, { reply_to_message_id: ctx.message.message_id });
827
+ await safeReply(ctx, `❌ ${escapeMarkdown(malformedErrors.join('\n'))}\n\n${t('telegram.option_syntax_check', {}, { locale: solveLocale })}`, { reply_to_message_id: ctx.message.message_id });
815
828
  return;
816
829
  }
817
830
 
@@ -828,13 +841,13 @@ async function handleSolveCommand(ctx) {
828
841
  userArgs = moveArgumentToFront(userArgs, validation.normalizedUrl, cleanNonPrintableChars);
829
842
  const { backend: solvePerCommandIsolation, filteredArgs: userArgsWithoutIsolation } = extractIsolationFromArgs(userArgs); // issue #1534
830
843
  if (solvePerCommandIsolation && !isValidPerCommandIsolation(solvePerCommandIsolation)) {
831
- await safeReply(ctx, `❌ Invalid --isolation value '${escapeMarkdown(solvePerCommandIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
844
+ await safeReply(ctx, t('telegram.invalid_isolation', { value: escapeMarkdown(solvePerCommandIsolation) }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
832
845
  return;
833
846
  }
834
847
  const mergedSolveArgs = mergeArgsWithOverrides(userArgsWithoutIsolation, solveOverrides);
835
848
  const { backend: solveOverrideIsolation, filteredArgs: args } = extractIsolationFromArgs(mergedSolveArgs);
836
849
  if (solveOverrideIsolation && !isValidPerCommandIsolation(solveOverrideIsolation)) {
837
- await safeReply(ctx, `❌ Invalid locked --isolation value '${escapeMarkdown(solveOverrideIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
850
+ await safeReply(ctx, t('telegram.invalid_locked_isolation', { value: escapeMarkdown(solveOverrideIsolation) }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
838
851
  return;
839
852
  }
840
853
  const effectiveSolveIsolation = solveOverrideIsolation || solvePerCommandIsolation;
@@ -863,7 +876,7 @@ async function handleSolveCommand(ctx) {
863
876
  }
864
877
  const { malformed: mergedMalformed, errors: mergedMalformedErrors } = detectMalformedFlags(args);
865
878
  if (mergedMalformed.length > 0) {
866
- await safeReply(ctx, `❌ ${escapeMarkdown(mergedMalformedErrors.join('\n'))}\n\nPlease check your option syntax.`, { reply_to_message_id: ctx.message.message_id });
879
+ await safeReply(ctx, `❌ ${escapeMarkdown(mergedMalformedErrors.join('\n'))}\n\n${t('telegram.option_syntax_check', {}, { locale: solveLocale })}`, { reply_to_message_id: ctx.message.message_id });
867
880
  return;
868
881
  }
869
882
  // Validate merged arguments using solve's yargs config
@@ -871,7 +884,7 @@ async function handleSolveCommand(ctx) {
871
884
  try {
872
885
  parsedSolveArgs = await parseArgsWithYargs(args, yargs, createSolveYargsConfig);
873
886
  } catch (error) {
874
- await safeReply(ctx, `❌ Invalid options: ${escapeMarkdown(error.message || String(error))}\n\nUse /help to see available options`, {
887
+ await safeReply(ctx, t('telegram.invalid_options', { message: escapeMarkdown(error.message || String(error)) }, { locale: solveLocale }), {
875
888
  reply_to_message_id: ctx.message.message_id,
876
889
  });
877
890
  return;
@@ -909,20 +922,20 @@ async function handleSolveCommand(ctx) {
909
922
  const existingItem = solveQueue.findByUrl(normalizedUrl);
910
923
  if (existingItem) {
911
924
  const statusText = existingItem.status === 'starting' || existingItem.status === 'started' ? 'being processed' : 'already in the queue';
912
- await safeReply(ctx, `❌ This URL is ${statusText}.\n\nURL: ${escapeMarkdown(normalizedUrl)}\nStatus: ${existingItem.status}\n\n💡 Use /solve_queue to check the queue status.`, { reply_to_message_id: ctx.message.message_id });
925
+ await safeReply(ctx, t('telegram.url_status_active', { statusText, url: escapeMarkdown(normalizedUrl), status: existingItem.status }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
913
926
  return;
914
927
  }
915
928
  // Issue #1567: Prevent concurrent sessions on the same PR/issue
916
929
  const activeSession = await hasActiveSessionForUrlAsync(normalizedUrl, VERBOSE);
917
930
  if (activeSession.isActive) {
918
- await safeReply(ctx, `❌ A working session is already running for this URL.\n\nURL: ${escapeMarkdown(normalizedUrl)}\nSession: \`${activeSession.sessionName}\`\n\n💡 Wait for the current session to complete, or use /solve\\_stop to cancel it.`, { reply_to_message_id: ctx.message.message_id });
931
+ await safeReply(ctx, t('telegram.url_session_running', { url: escapeMarkdown(normalizedUrl), session: activeSession.sessionName }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
919
932
  return;
920
933
  }
921
934
  const check = await solveQueue.canStartCommand({ tool: solveTool }); // Skip Claude limits for agent (#1159)
922
935
  const queueStats = solveQueue.getStats();
923
936
  // Handle rejection: threshold strategy is 'reject' — fail immediately (issue #1267)
924
937
  if (check.rejected) {
925
- await safeReply(ctx, `❌ Solve command rejected.\n\n${infoBlock}\n\n🚫 Reason: ${escapeMarkdown(check.rejectReason || 'Unknown')}`, { reply_to_message_id: ctx.message.message_id });
938
+ await safeReply(ctx, t('telegram.solve_rejected', { infoBlock, reason: escapeMarkdown(check.rejectReason || 'Unknown') }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
926
939
  return;
927
940
  }
928
941
 
@@ -970,11 +983,12 @@ async function handleHiveCommand(ctx) {
970
983
  },
971
984
  });
972
985
 
986
+ const hiveLocale = resolveLocaleFromTelegramCtx(ctx);
973
987
  if (!hiveEnabled) {
974
988
  if (VERBOSE) {
975
989
  console.log('[VERBOSE] /hive ignored: command disabled');
976
990
  }
977
- await ctx.reply(' The /hive command is disabled on this bot instance.');
991
+ await ctx.reply(t('telegram.hive_disabled', {}, { locale: hiveLocale }));
978
992
  return;
979
993
  }
980
994
 
@@ -998,7 +1012,7 @@ async function handleHiveCommand(ctx) {
998
1012
  if (VERBOSE) {
999
1013
  console.log('[VERBOSE] /hive ignored: not a group chat');
1000
1014
  }
1001
- await ctx.reply('❌ The /hive command only works in group chats. Please add this bot to a group and make it an admin.', { reply_to_message_id: ctx.message.message_id });
1015
+ await ctx.reply(t('telegram.hive_only_in_groups', {}, { locale: hiveLocale }), { reply_to_message_id: ctx.message.message_id });
1002
1016
  return;
1003
1017
  }
1004
1018
 
@@ -1041,13 +1055,13 @@ async function handleHiveCommand(ctx) {
1041
1055
 
1042
1056
  const { backend: hivePerCommandIsolation, filteredArgs: normalizedArgsWithoutIsolation } = extractIsolationFromArgs(normalizedArgs); // issue #1534
1043
1057
  if (hivePerCommandIsolation && !isValidPerCommandIsolation(hivePerCommandIsolation)) {
1044
- await safeReply(ctx, `❌ Invalid --isolation value '${escapeMarkdown(hivePerCommandIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
1058
+ await safeReply(ctx, t('telegram.invalid_isolation', { value: escapeMarkdown(hivePerCommandIsolation) }, { locale: hiveLocale }), { reply_to_message_id: ctx.message.message_id });
1045
1059
  return;
1046
1060
  }
1047
1061
  const mergedHiveArgs = mergeArgsWithOverrides(normalizedArgsWithoutIsolation, hiveOverrides);
1048
1062
  const { backend: hiveOverrideIsolation, filteredArgs: args } = extractIsolationFromArgs(mergedHiveArgs);
1049
1063
  if (hiveOverrideIsolation && !isValidPerCommandIsolation(hiveOverrideIsolation)) {
1050
- await safeReply(ctx, `❌ Invalid locked --isolation value '${escapeMarkdown(hiveOverrideIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
1064
+ await safeReply(ctx, t('telegram.invalid_locked_isolation', { value: escapeMarkdown(hiveOverrideIsolation) }, { locale: hiveLocale }), { reply_to_message_id: ctx.message.message_id });
1051
1065
  return;
1052
1066
  }
1053
1067
  const effectiveHiveIsolation = hiveOverrideIsolation || hivePerCommandIsolation;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Telegram /language command implementation.
3
+ *
4
+ * Allows each user to override the bot's reply language for the current
5
+ * process. The override is in-memory only (resets when the bot restarts).
6
+ *
7
+ * Usage in chat:
8
+ * /language -> show current language
9
+ * /language <en|ru|zh|hi> -> set language for this user
10
+ * /language default -> clear the override (reset|clear also work)
11
+ */
12
+
13
+ import { t, getSupportedLocales, normalizeLocale, setUserLocale, clearUserLocale, resolveLocaleFromTelegramCtx } from './i18n.lib.mjs';
14
+
15
+ export function registerLanguageCommand(bot, options = {}) {
16
+ const { VERBOSE = false, isOldMessage, isForwardedOrReply } = options;
17
+
18
+ bot.command('language', async ctx => {
19
+ VERBOSE && console.log('[VERBOSE] /language command received');
20
+ if (isOldMessage?.(ctx) || isForwardedOrReply?.(ctx)) return;
21
+ const userId = ctx.from?.id;
22
+ const locale = resolveLocaleFromTelegramCtx(ctx);
23
+ const supported = getSupportedLocales();
24
+ const supportedList = supported.join(', ');
25
+ const text = ctx.message?.text || '';
26
+ const parts = text.trim().split(/\s+/);
27
+ const arg = parts.length > 1 ? parts[1] : null;
28
+ if (!arg) {
29
+ const langName = t(`language.${locale}`, {}, { locale });
30
+ await ctx.reply(t('telegram.language_current', { language: langName, supported: supportedList }, { locale }), { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
31
+ return;
32
+ }
33
+ if (['default', 'reset', 'clear'].includes(arg.toLowerCase())) {
34
+ clearUserLocale(userId);
35
+ const newLocale = resolveLocaleFromTelegramCtx(ctx);
36
+ const langName = t(`language.${newLocale}`, {}, { locale: newLocale });
37
+ await ctx.reply(t('telegram.language_set', { language: langName }, { locale: newLocale }), { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
38
+ return;
39
+ }
40
+ const target = normalizeLocale(arg);
41
+ if (!target) {
42
+ await ctx.reply(t('telegram.language_invalid', { supported: supportedList }, { locale }), { reply_to_message_id: ctx.message.message_id });
43
+ return;
44
+ }
45
+ setUserLocale(userId, target);
46
+ const langName = t(`language.${target}`, {}, { locale: target });
47
+ await ctx.reply(t('telegram.language_set', { language: langName }, { locale: target }), { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
48
+ });
49
+ }