@agent-link/server 0.1.160 → 0.1.161

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.
@@ -0,0 +1,101 @@
1
+ // ── Lightweight i18n module ─────────────────────────────────────────────────
2
+ const { ref, computed } = Vue;
3
+
4
+ /**
5
+ * Creates i18n functionality: t() translator, locale switching, persistence.
6
+ * Locale data is loaded dynamically from /locales/<lang>.json.
7
+ *
8
+ * @returns {{ t: Function, locale: import('vue').Ref<string>, setLocale: Function }}
9
+ */
10
+ export function createI18n() {
11
+ const STORAGE_KEY = 'agentlink-language';
12
+ const SUPPORTED = ['en', 'zh'];
13
+ const DEFAULT_LOCALE = 'en';
14
+
15
+ // Detect initial locale
16
+ function detectLocale() {
17
+ // 1. Explicit user choice
18
+ const stored = localStorage.getItem(STORAGE_KEY);
19
+ if (stored && SUPPORTED.includes(stored)) return stored;
20
+
21
+ // 2. Browser preference
22
+ const nav = (navigator.language || '').toLowerCase();
23
+ if (nav.startsWith('zh')) return 'zh';
24
+
25
+ // 3. Default
26
+ return DEFAULT_LOCALE;
27
+ }
28
+
29
+ const locale = ref(detectLocale());
30
+ const _messages = ref({});
31
+ let _loadedLocale = null;
32
+
33
+ // Load locale JSON
34
+ async function loadMessages(lang) {
35
+ if (_loadedLocale === lang && Object.keys(_messages.value).length > 0) return;
36
+ try {
37
+ const resp = await fetch(`/locales/${lang}.json`);
38
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
39
+ _messages.value = await resp.json();
40
+ _loadedLocale = lang;
41
+ } catch (e) {
42
+ console.warn(`[i18n] Failed to load locale "${lang}":`, e);
43
+ // Fallback: try loading English
44
+ if (lang !== DEFAULT_LOCALE) {
45
+ try {
46
+ const resp = await fetch(`/locales/${DEFAULT_LOCALE}.json`);
47
+ if (resp.ok) {
48
+ _messages.value = await resp.json();
49
+ _loadedLocale = DEFAULT_LOCALE;
50
+ }
51
+ } catch { /* give up */ }
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Translate a key, with optional parameter substitution.
58
+ * Returns the key itself if no translation is found (fallback).
59
+ *
60
+ * @param {string} key - Dot-notation key, e.g. "button.send"
61
+ * @param {object} [params] - Substitution params, e.g. { n: 5 }
62
+ * @returns {string}
63
+ */
64
+ function t(key, params) {
65
+ let str = _messages.value[key];
66
+ if (str === undefined) return key;
67
+ if (params) {
68
+ for (const [k, v] of Object.entries(params)) {
69
+ str = str.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
70
+ }
71
+ }
72
+ return str;
73
+ }
74
+
75
+ /**
76
+ * Switch locale, persist choice, and reload strings.
77
+ * @param {string} lang
78
+ */
79
+ async function setLocale(lang) {
80
+ if (!SUPPORTED.includes(lang)) return;
81
+ locale.value = lang;
82
+ localStorage.setItem(STORAGE_KEY, lang);
83
+ await loadMessages(lang);
84
+ }
85
+
86
+ /**
87
+ * Toggle between supported locales (EN ↔ 中).
88
+ */
89
+ async function toggleLocale() {
90
+ const next = locale.value === 'en' ? 'zh' : 'en';
91
+ await setLocale(next);
92
+ }
93
+
94
+ // The display label for the language switcher button
95
+ const localeLabel = computed(() => locale.value === 'en' ? 'EN' : '中');
96
+
97
+ // Load initial messages
98
+ loadMessages(locale.value);
99
+
100
+ return { t, locale, setLocale, toggleLocale, localeLabel };
101
+ }
@@ -8,15 +8,15 @@ export function isContextSummary(text) {
8
8
  return typeof text === 'string' && text.trimStart().startsWith(CONTEXT_SUMMARY_PREFIX);
9
9
  }
10
10
 
11
- export function formatRelativeTime(ts) {
11
+ export function formatRelativeTime(ts, t) {
12
12
  const diff = Date.now() - ts;
13
13
  const mins = Math.floor(diff / 60000);
14
- if (mins < 1) return 'just now';
15
- if (mins < 60) return `${mins}m ago`;
14
+ if (mins < 1) return t ? t('time.justNow') : 'just now';
15
+ if (mins < 60) return t ? t('time.minutesAgo', { n: mins }) : `${mins}m ago`;
16
16
  const hours = Math.floor(mins / 60);
17
- if (hours < 24) return `${hours}h ago`;
17
+ if (hours < 24) return t ? t('time.hoursAgo', { n: hours }) : `${hours}h ago`;
18
18
  const days = Math.floor(hours / 24);
19
- if (days < 30) return `${days}d ago`;
19
+ if (days < 30) return t ? t('time.daysAgo', { n: days }) : `${days}d ago`;
20
20
  return new Date(ts).toLocaleDateString();
21
21
  }
22
22
 
@@ -53,7 +53,7 @@ export function toggleTool(msg) {
53
53
  msg.expanded = !msg.expanded;
54
54
  }
55
55
 
56
- export function getToolSummary(msg) {
56
+ export function getToolSummary(msg, t) {
57
57
  const name = msg.toolName;
58
58
  const input = msg.toolInput;
59
59
  try {
@@ -65,8 +65,8 @@ export function getToolSummary(msg) {
65
65
  if (name === 'Glob' && obj.pattern) return obj.pattern;
66
66
  if (name === 'Grep' && obj.pattern) return obj.pattern;
67
67
  if (name === 'TodoWrite' && obj.todos) {
68
- const done = obj.todos.filter(t => t.status === 'completed').length;
69
- return `${done}/${obj.todos.length} done`;
68
+ const doneCount = obj.todos.filter(td => td.status === 'completed').length;
69
+ return t ? t('tool.done', { done: doneCount, total: obj.todos.length }) : `${doneCount}/${obj.todos.length} done`;
70
70
  }
71
71
  if (name === 'Task' && obj.description) return obj.description;
72
72
  if (name === 'Agent' && obj.description) return obj.description;
@@ -81,7 +81,7 @@ export function isEditTool(msg) {
81
81
  return msg.role === 'tool' && msg.toolName === 'Edit' && msg.toolInput;
82
82
  }
83
83
 
84
- export function getFormattedToolInput(msg) {
84
+ export function getFormattedToolInput(msg, t) {
85
85
  if (!msg.toolInput) return null;
86
86
  try {
87
87
  const obj = JSON.parse(msg.toolInput);
@@ -91,18 +91,22 @@ export function getFormattedToolInput(msg) {
91
91
  if (name === 'Read' && obj.file_path) {
92
92
  let detail = esc(obj.file_path);
93
93
  if (obj.offset && obj.limit) {
94
- detail += ` <span class="tool-input-meta">lines ${obj.offset}\u2013${obj.offset + obj.limit - 1}</span>`;
94
+ const meta = t ? t('tool.lines', { start: obj.offset, end: obj.offset + obj.limit - 1 }) : `lines ${obj.offset}\u2013${obj.offset + obj.limit - 1}`;
95
+ detail += ` <span class="tool-input-meta">${meta}</span>`;
95
96
  } else if (obj.offset) {
96
- detail += ` <span class="tool-input-meta">from line ${obj.offset}</span>`;
97
+ const meta = t ? t('tool.fromLine', { offset: obj.offset }) : `from line ${obj.offset}`;
98
+ detail += ` <span class="tool-input-meta">${meta}</span>`;
97
99
  } else if (obj.limit) {
98
- detail += ` <span class="tool-input-meta">first ${obj.limit} lines</span>`;
100
+ const meta = t ? t('tool.firstLines', { limit: obj.limit }) : `first ${obj.limit} lines`;
101
+ detail += ` <span class="tool-input-meta">${meta}</span>`;
99
102
  }
100
103
  return detail;
101
104
  }
102
105
 
103
106
  if (name === 'Write' && obj.file_path) {
104
107
  const lines = (obj.content || '').split('\n').length;
105
- return esc(obj.file_path) + ` <span class="tool-input-meta">${lines} lines</span>`;
108
+ const lineCount = t ? t('tool.lineCount', { n: lines }) : `${lines} lines`;
109
+ return esc(obj.file_path) + ` <span class="tool-input-meta">${lineCount}</span>`;
106
110
  }
107
111
 
108
112
  if (name === 'Bash' && obj.command) {
@@ -113,13 +117,13 @@ export function getFormattedToolInput(msg) {
113
117
 
114
118
  if (name === 'Glob' && obj.pattern) {
115
119
  let html = '<code class="tool-input-cmd">' + esc(obj.pattern) + '</code>';
116
- if (obj.path) html += ' <span class="tool-input-meta">in ' + esc(obj.path) + '</span>';
120
+ if (obj.path) html += ' <span class="tool-input-meta">' + (t ? t('tool.inPath', { path: esc(obj.path) }) : 'in ' + esc(obj.path)) + '</span>';
117
121
  return html;
118
122
  }
119
123
 
120
124
  if (name === 'Grep' && obj.pattern) {
121
125
  let html = '<code class="tool-input-cmd">' + esc(obj.pattern) + '</code>';
122
- if (obj.path) html += ' <span class="tool-input-meta">in ' + esc(obj.path) + '</span>';
126
+ if (obj.path) html += ' <span class="tool-input-meta">' + (t ? t('tool.inPath', { path: esc(obj.path) }) : 'in ' + esc(obj.path)) + '</span>';
123
127
  return html;
124
128
  }
125
129
 
@@ -139,10 +143,13 @@ export function getFormattedToolInput(msg) {
139
143
 
140
144
  if (name === 'Task' || name === 'Agent') {
141
145
  let html = '';
142
- if (obj.description) html += '<div class="task-field"><span class="tool-input-meta">Description</span> ' + esc(obj.description) + '</div>';
143
- if (obj.subagent_type) html += '<div class="task-field"><span class="tool-input-meta">Agent</span> <code class="tool-input-cmd">' + esc(obj.subagent_type) + '</code></div>';
146
+ const descLabel = t ? t('tool.description') : 'Description';
147
+ const agentLabel = t ? t('tool.agent') : 'Agent';
148
+ const promptLabel = t ? t('tool.prompt') : 'Prompt';
149
+ if (obj.description) html += '<div class="task-field"><span class="tool-input-meta">' + descLabel + '</span> ' + esc(obj.description) + '</div>';
150
+ if (obj.subagent_type) html += '<div class="task-field"><span class="tool-input-meta">' + agentLabel + '</span> <code class="tool-input-cmd">' + esc(obj.subagent_type) + '</code></div>';
144
151
  if (obj.prompt) {
145
- html += '<div class="task-field"><span class="tool-input-meta">Prompt</span></div><div class="task-prompt">' + esc(obj.prompt) + '</div>';
152
+ html += '<div class="task-field"><span class="tool-input-meta">' + promptLabel + '</span></div><div class="task-prompt">' + esc(obj.prompt) + '</div>';
146
153
  }
147
154
  if (html) return html;
148
155
  }
@@ -161,7 +168,7 @@ export function getFormattedToolInput(msg) {
161
168
  return null;
162
169
  }
163
170
 
164
- export function getEditDiffHtml(msg) {
171
+ export function getEditDiffHtml(msg, t) {
165
172
  try {
166
173
  const obj = JSON.parse(msg.toolInput);
167
174
  if (!obj.old_string && !obj.new_string) return null;
@@ -171,7 +178,7 @@ export function getEditDiffHtml(msg) {
171
178
  const newLines = (obj.new_string || '').split('\n');
172
179
  let html = '';
173
180
  if (filePath) {
174
- html += '<div class="diff-file">' + esc(filePath) + (obj.replace_all ? ' <span class="diff-replace-all">(replace all)</span>' : '') + '</div>';
181
+ html += '<div class="diff-file">' + esc(filePath) + (obj.replace_all ? ' <span class="diff-replace-all">' + (t ? t('tool.replaceAll') : '(replace all)') + '</span>' : '') + '</div>';
175
182
  }
176
183
  html += '<div class="diff-lines">';
177
184
  for (const line of oldLines) {
@@ -35,6 +35,8 @@ export function createSidebar(deps) {
35
35
  // Multi-session parallel
36
36
  currentConversationId, conversationCache, processingConversations,
37
37
  switchConversation,
38
+ // i18n
39
+ t,
38
40
  } = deps;
39
41
 
40
42
  // Late-binding callback: called when user switches to a normal chat session
@@ -130,7 +132,7 @@ export function createSidebar(deps) {
130
132
  switchConversation(newConvId);
131
133
  messages.value.push({
132
134
  id: streaming.nextId(), role: 'system',
133
- content: 'New conversation started.',
135
+ content: t('system.newConversation'),
134
136
  timestamp: new Date(),
135
137
  });
136
138
  return;
@@ -365,18 +367,25 @@ export function createSidebar(deps) {
365
367
  const yesterdayStart = todayStart - 86400000;
366
368
  const weekStart = todayStart - 6 * 86400000;
367
369
 
370
+ const GROUP_KEYS = {
371
+ today: 'session.today',
372
+ yesterday: 'session.yesterday',
373
+ thisWeek: 'session.thisWeek',
374
+ earlier: 'session.earlier',
375
+ };
376
+
368
377
  const groups = {};
369
378
  for (const s of historySessions.value) {
370
- let label;
371
- if (s.lastModified >= todayStart) label = 'Today';
372
- else if (s.lastModified >= yesterdayStart) label = 'Yesterday';
373
- else if (s.lastModified >= weekStart) label = 'This week';
374
- else label = 'Earlier';
375
- if (!groups[label]) groups[label] = [];
376
- groups[label].push(s);
379
+ let key;
380
+ if (s.lastModified >= todayStart) key = 'today';
381
+ else if (s.lastModified >= yesterdayStart) key = 'yesterday';
382
+ else if (s.lastModified >= weekStart) key = 'thisWeek';
383
+ else key = 'earlier';
384
+ if (!groups[key]) groups[key] = [];
385
+ groups[key].push(s);
377
386
  }
378
- const order = ['Today', 'Yesterday', 'This week', 'Earlier'];
379
- return order.filter(k => groups[k]).map(k => ({ label: k, sessions: groups[k] }));
387
+ const order = ['today', 'yesterday', 'thisWeek', 'earlier'];
388
+ return order.filter(k => groups[k]).map(k => ({ label: t(GROUP_KEYS[k]), sessions: groups[k] }));
380
389
  });
381
390
 
382
391
  return {