@agent-link/server 0.1.160 → 0.1.162
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/package.json +1 -1
- package/web/app.js +263 -238
- package/web/landing.html +0 -52
- package/web/landing.zh.html +0 -50
- package/web/locales/en.json +250 -0
- package/web/locales/zh.json +250 -0
- package/web/modules/appHelpers.js +4 -2
- package/web/modules/connection.js +17 -15
- package/web/modules/i18n.js +101 -0
- package/web/modules/messageHelpers.js +27 -20
- package/web/modules/sidebar.js +19 -10
|
@@ -25,6 +25,8 @@ export function createConnection(deps) {
|
|
|
25
25
|
// Multi-session parallel
|
|
26
26
|
currentConversationId, processingConversations, conversationCache,
|
|
27
27
|
switchConversation,
|
|
28
|
+
// i18n
|
|
29
|
+
t,
|
|
28
30
|
} = deps;
|
|
29
31
|
|
|
30
32
|
// Dequeue callback — set after creation to resolve circular dependency
|
|
@@ -163,7 +165,7 @@ export function createConnection(deps) {
|
|
|
163
165
|
const sid = getSessionId();
|
|
164
166
|
if (!sid) {
|
|
165
167
|
status.value = 'No Session';
|
|
166
|
-
error.value = '
|
|
168
|
+
error.value = t('error.noSessionId');
|
|
167
169
|
return;
|
|
168
170
|
}
|
|
169
171
|
sessionId.value = sid;
|
|
@@ -194,9 +196,9 @@ export function createConnection(deps) {
|
|
|
194
196
|
return;
|
|
195
197
|
}
|
|
196
198
|
if (parsed.type === 'auth_failed') {
|
|
197
|
-
authError.value = parsed.message || '
|
|
199
|
+
authError.value = parsed.message || t('error.incorrectPassword');
|
|
198
200
|
authAttempts.value = parsed.attemptsRemaining != null
|
|
199
|
-
?
|
|
201
|
+
? t('error.attemptsRemaining', { n: parsed.attemptsRemaining })
|
|
200
202
|
: null;
|
|
201
203
|
authPassword.value = '';
|
|
202
204
|
return;
|
|
@@ -204,7 +206,7 @@ export function createConnection(deps) {
|
|
|
204
206
|
if (parsed.type === 'auth_locked') {
|
|
205
207
|
authLocked.value = true;
|
|
206
208
|
authRequired.value = false;
|
|
207
|
-
authError.value = parsed.message || '
|
|
209
|
+
authError.value = parsed.message || t('error.tooManyAttempts');
|
|
208
210
|
status.value = 'Locked';
|
|
209
211
|
return;
|
|
210
212
|
}
|
|
@@ -283,7 +285,7 @@ export function createConnection(deps) {
|
|
|
283
285
|
wsSend({ type: 'query_active_conversations' });
|
|
284
286
|
} else {
|
|
285
287
|
status.value = 'Waiting';
|
|
286
|
-
error.value = '
|
|
288
|
+
error.value = t('error.agentNotConnected');
|
|
287
289
|
}
|
|
288
290
|
} else if (msg.type === 'pong') {
|
|
289
291
|
if (typeof msg.ts === 'number') {
|
|
@@ -294,7 +296,7 @@ export function createConnection(deps) {
|
|
|
294
296
|
status.value = 'Waiting';
|
|
295
297
|
agentName.value = '';
|
|
296
298
|
hostname.value = '';
|
|
297
|
-
error.value = '
|
|
299
|
+
error.value = t('error.agentDisconnected');
|
|
298
300
|
isProcessing.value = false;
|
|
299
301
|
isCompacting.value = false;
|
|
300
302
|
queuedMessages.value = [];
|
|
@@ -423,7 +425,7 @@ export function createConnection(deps) {
|
|
|
423
425
|
isCompacting.value = true;
|
|
424
426
|
messages.value.push({
|
|
425
427
|
id: streaming.nextId(), role: 'system',
|
|
426
|
-
content: '
|
|
428
|
+
content: t('system.contextCompacting'), isCompactStart: true,
|
|
427
429
|
timestamp: new Date(),
|
|
428
430
|
});
|
|
429
431
|
scrollToBottom();
|
|
@@ -432,7 +434,7 @@ export function createConnection(deps) {
|
|
|
432
434
|
// Update the start message to show completed
|
|
433
435
|
const startMsg = [...messages.value].reverse().find(m => m.isCompactStart && !m.compactDone);
|
|
434
436
|
if (startMsg) {
|
|
435
|
-
startMsg.content = '
|
|
437
|
+
startMsg.content = t('system.contextCompacted');
|
|
436
438
|
startMsg.compactDone = true;
|
|
437
439
|
}
|
|
438
440
|
scrollToBottom();
|
|
@@ -451,7 +453,7 @@ export function createConnection(deps) {
|
|
|
451
453
|
needsResume.value = true;
|
|
452
454
|
messages.value.push({
|
|
453
455
|
id: streaming.nextId(), role: 'system',
|
|
454
|
-
content: '
|
|
456
|
+
content: t('system.generationStopped'), timestamp: new Date(),
|
|
455
457
|
});
|
|
456
458
|
scrollToBottom();
|
|
457
459
|
}
|
|
@@ -507,20 +509,20 @@ export function createConnection(deps) {
|
|
|
507
509
|
isProcessing.value = true;
|
|
508
510
|
messages.value.push({
|
|
509
511
|
id: streaming.nextId(), role: 'system',
|
|
510
|
-
content: '
|
|
512
|
+
content: t('system.contextCompacting'), isCompactStart: true,
|
|
511
513
|
timestamp: new Date(),
|
|
512
514
|
});
|
|
513
515
|
} else if (msg.isProcessing) {
|
|
514
516
|
isProcessing.value = true;
|
|
515
517
|
messages.value.push({
|
|
516
518
|
id: streaming.nextId(), role: 'system',
|
|
517
|
-
content: '
|
|
519
|
+
content: t('system.agentProcessing'),
|
|
518
520
|
timestamp: new Date(),
|
|
519
521
|
});
|
|
520
522
|
} else {
|
|
521
523
|
messages.value.push({
|
|
522
524
|
id: streaming.nextId(), role: 'system',
|
|
523
|
-
content: '
|
|
525
|
+
content: t('system.sessionRestored'),
|
|
524
526
|
timestamp: new Date(),
|
|
525
527
|
});
|
|
526
528
|
}
|
|
@@ -564,7 +566,7 @@ export function createConnection(deps) {
|
|
|
564
566
|
}
|
|
565
567
|
messages.value.push({
|
|
566
568
|
id: streaming.nextId(), role: 'system',
|
|
567
|
-
content: '
|
|
569
|
+
content: t('system.workdirChanged', { dir: msg.workDir }),
|
|
568
570
|
timestamp: new Date(),
|
|
569
571
|
});
|
|
570
572
|
// Clear old history immediately so UI doesn't show stale data
|
|
@@ -601,13 +603,13 @@ export function createConnection(deps) {
|
|
|
601
603
|
function scheduleReconnect(scheduleHighlight) {
|
|
602
604
|
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
603
605
|
status.value = 'Disconnected';
|
|
604
|
-
error.value = '
|
|
606
|
+
error.value = t('error.unableToReconnect');
|
|
605
607
|
return;
|
|
606
608
|
}
|
|
607
609
|
const delay = Math.min(RECONNECT_BASE_DELAY * Math.pow(1.5, reconnectAttempts), RECONNECT_MAX_DELAY);
|
|
608
610
|
reconnectAttempts++;
|
|
609
611
|
status.value = 'Reconnecting...';
|
|
610
|
-
error.value = '
|
|
612
|
+
error.value = t('error.connectionLost', { n: reconnectAttempts });
|
|
611
613
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
612
614
|
reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(scheduleHighlight); }, delay);
|
|
613
615
|
}
|
|
@@ -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
|
|
69
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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">
|
|
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) {
|
package/web/modules/sidebar.js
CHANGED
|
@@ -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: '
|
|
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
|
|
371
|
-
if (s.lastModified >= todayStart)
|
|
372
|
-
else if (s.lastModified >= yesterdayStart)
|
|
373
|
-
else if (s.lastModified >= weekStart)
|
|
374
|
-
else
|
|
375
|
-
if (!groups[
|
|
376
|
-
groups[
|
|
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 = ['
|
|
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 {
|