i18n: перевод страницы чата (ru база, uk/en)

- Шаблон chat.html → русская база с {% trans %}/{% blocktrans %}
- JS-строки chat.js → gettext() + JavaScriptCatalog (/jsi18n/), подключён
  в index.html; защитный fallback если каталог не загрузился
- Переводы uk/en для обоих доменов (django + djangojs), скомпилированы
- makemessages заодно извлёк непереведённые строки прошлых фич (2FA,
  nextcloud, документы) — оставлены пустыми (рендерятся на базовом ru)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 22:00:35 +03:00
parent 6a833413d2
commit 8de1ffaa34
12 changed files with 1477 additions and 573 deletions
+25 -21
View File
@@ -1,3 +1,7 @@
// Переклади для JS приходять із JavaScriptCatalog (/jsi18n/). Якщо каталог не
// підвантажився — не падаємо, показуємо базові (російські) рядки.
if (typeof window.gettext !== 'function') { window.gettext = function (s) { return s; }; }
var CHAT_QUICK_EMOJIS = [
['👍', 'thumbsup'], ['❤️', 'heart'], ['😂', 'joy'],
['😮', 'open_mouth'], ['😢', 'cry'], ['🙏', 'pray'],
@@ -303,7 +307,7 @@ function chatLoadChannels() {
}).catch(function (e) {
console.error(e);
var el = document.getElementById('chat-channel-list');
if (el) el.innerHTML = '<div style="padding:1rem; text-align:center; font-size:0.8125rem; color:var(--color-destructive);">Не вдалося завантажити канали</div>';
if (el) el.innerHTML = '<div style="padding:1rem; text-align:center; font-size:0.8125rem; color:var(--color-destructive);">' + gettext('Не удалось загрузить каналы') + '</div>';
});
}
@@ -395,7 +399,7 @@ function chatRenderChannelList() {
});
if (!list.length) {
el.innerHTML = '<div style="padding:1rem; text-align:center; font-size:0.8125rem; color:var(--color-muted-foreground);">Немає каналів</div>';
el.innerHTML = '<div style="padding:1rem; text-align:center; font-size:0.8125rem; color:var(--color-muted-foreground);">' + gettext('Нет каналов') + '</div>';
return;
}
@@ -416,7 +420,7 @@ function chatRenderDmList() {
});
if (!list.length) {
el.innerHTML = '<div style="padding:0.75rem; text-align:center; font-size:0.75rem; color:var(--color-muted-foreground);">Немає повідомлень</div>';
el.innerHTML = '<div style="padding:0.75rem; text-align:center; font-size:0.75rem; color:var(--color-muted-foreground);">' + gettext('Нет сообщений') + '</div>';
return;
}
@@ -425,7 +429,7 @@ function chatRenderDmList() {
var otherId = chatDmOtherUserId[ch.id];
var isBot = ch.type === 'D' && otherId && chatUserIsBot[otherId];
var row = chatBuildChannelRow(ch.id, chatDmLabels[ch.id] || '…',
ch.type === 'G' ? 'Групова розмова' : '', {avatar: true, bot: isBot});
ch.type === 'G' ? gettext('Групповой разговор') : '', {avatar: true, bot: isBot});
el.appendChild(row);
// Якщо співрозмовник ЛС уже відомий — одразу показати його аватар.
if (ch.type === 'D' && otherId) {
@@ -542,7 +546,7 @@ function chatSelectChannel(channelId) {
chatRenderComposerFileChips('chat-composer-files', chatComposerFileIds);
var messagesEl = document.getElementById('chat-messages');
if (messagesEl) messagesEl.innerHTML = '<div style="text-align:center; font-size:0.8125rem; color:var(--color-muted-foreground);">Завантаження…</div>';
if (messagesEl) messagesEl.innerHTML = '<div style="text-align:center; font-size:0.8125rem; color:var(--color-muted-foreground);">' + gettext('Загрузка…') + '</div>';
chatRenderedPostIds = {};
chatApiFetch('/channels/' + channelId + '/posts?per_page=30').then(function (data) {
@@ -554,7 +558,7 @@ function chatSelectChannel(channelId) {
});
}).catch(function (e) {
console.error(e);
if (messagesEl) messagesEl.innerHTML = '<div style="text-align:center; font-size:0.8125rem; color:var(--color-destructive);">Не вдалося завантажити повідомлення</div>';
if (messagesEl) messagesEl.innerHTML = '<div style="text-align:center; font-size:0.8125rem; color:var(--color-destructive);">' + gettext('Не удалось загрузить сообщения') + '</div>';
});
}
@@ -604,7 +608,7 @@ function chatSearchChannelLabel(channelId) {
var ch = chatFindChannel(channelId);
if (!ch) return '';
if (ch.type === 'O' || ch.type === 'P') return '#' + ch.display_name;
return chatDmLabels[ch.id] || 'Особисте';
return chatDmLabels[ch.id] || gettext('Личное');
}
function chatSearchMessages(terms) {
@@ -622,7 +626,7 @@ function chatSearchMessages(terms) {
resultsEl.style.display = 'flex';
if (messagesEl) messagesEl.style.display = 'none';
resultsEl.innerHTML = '<div style="text-align:center; font-size:0.8125rem; color:var(--color-muted-foreground); padding:1rem;">Пошук…</div>';
resultsEl.innerHTML = '<div style="text-align:center; font-size:0.8125rem; color:var(--color-muted-foreground); padding:1rem;">' + gettext('Поиск…') + '</div>';
var body = JSON.stringify({terms: terms, is_or_search: false, per_page: 30});
var merged = {}; // id -> post (дедуплікація між командами)
@@ -643,7 +647,7 @@ function chatSearchMessages(terms) {
});
}).catch(function (e) {
console.error(e);
resultsEl.innerHTML = '<div style="text-align:center; font-size:0.8125rem; color:var(--color-destructive); padding:1rem;">Не вдалося виконати пошук</div>';
resultsEl.innerHTML = '<div style="text-align:center; font-size:0.8125rem; color:var(--color-destructive); padding:1rem;">' + gettext('Не удалось выполнить поиск') + '</div>';
});
}
@@ -656,10 +660,10 @@ function chatRenderSearchResults(posts) {
head.style.cssText = 'display:flex; align-items:center; justify-content:space-between; margin-bottom:0.25rem;';
var title = document.createElement('span');
title.style.cssText = 'font-size:0.75rem; color:var(--color-muted-foreground);';
title.textContent = 'Знайдено: ' + posts.length;
title.textContent = gettext('Найдено:') + ' ' + posts.length;
var closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.textContent = 'Закрити пошук';
closeBtn.textContent = gettext('Закрыть поиск');
closeBtn.style.cssText = 'background:none; border:none; cursor:pointer; font-size:0.75rem; color:var(--color-primary);';
closeBtn.onclick = chatCloseSearch;
head.appendChild(title);
@@ -669,7 +673,7 @@ function chatRenderSearchResults(posts) {
if (!posts.length) {
var none = document.createElement('div');
none.style.cssText = 'text-align:center; font-size:0.8125rem; color:var(--color-muted-foreground); padding:1rem;';
none.textContent = 'Нічого не знайдено';
none.textContent = gettext('Ничего не найдено');
el.appendChild(none);
return;
}
@@ -711,7 +715,7 @@ function chatPostIsBot(post) {
function chatBotBadge() {
var b = document.createElement('span');
b.textContent = 'бот';
b.textContent = gettext('бот');
b.style.cssText = 'font-size:0.5625rem; font-weight:700; letter-spacing:0.03em; text-transform:uppercase; color:var(--color-muted-foreground); background:var(--color-muted); border:1px solid var(--color-border); border-radius:0.25rem; padding:0.0625rem 0.25rem; line-height:1.2;';
return b;
}
@@ -736,7 +740,7 @@ function chatBuildMessageEl(post, context) {
meta.style.cssText = 'display:flex; align-items:baseline; gap:0.5rem; margin-bottom:0.25rem;' + (isMine ? ' flex-direction:row-reverse;' : '');
var nameEl = document.createElement('span');
nameEl.style.cssText = 'font-size:0.8125rem; font-weight:600; color:var(--color-foreground);';
nameEl.textContent = isMine ? 'Ви' : senderName;
nameEl.textContent = isMine ? gettext('Вы') : senderName;
var timeEl = document.createElement('span');
timeEl.style.cssText = 'font-size:0.6875rem; color:var(--color-muted-foreground);';
timeEl.textContent = post.create_at ? new Date(post.create_at).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}) : '';
@@ -774,7 +778,7 @@ function chatBuildMessageEl(post, context) {
var reactBtn = document.createElement('button');
reactBtn.type = 'button';
reactBtn.style.cssText = 'background:none; border:none; cursor:pointer; font-size:0.6875rem; color:var(--color-muted-foreground); padding:0;';
reactBtn.textContent = '🙂 Реакція';
reactBtn.textContent = '🙂 ' + gettext('Реакция');
reactBtn.onclick = function (e) { chatToggleReactionPicker(e, post, context); };
actions.appendChild(reactBtn);
@@ -783,7 +787,7 @@ function chatBuildMessageEl(post, context) {
replyBtn.type = 'button';
replyBtn.id = 'chat-reply-count-' + post.id;
replyBtn.style.cssText = 'background:none; border:none; cursor:pointer; font-size:0.6875rem; color:var(--color-primary); padding:0;';
replyBtn.textContent = post.reply_count ? ('💬 ' + post.reply_count + ' відповідей') : '↩ Відповісти';
replyBtn.textContent = post.reply_count ? ('💬 ' + gettext('Ответов:') + ' ' + post.reply_count) : ('↩ ' + gettext('Ответить'));
replyBtn.onclick = function () { chatOpenThread(post.id); };
actions.appendChild(replyBtn);
}
@@ -945,7 +949,7 @@ function chatOpenThread(postId) {
document.getElementById('chat-thread-composer-input').value = '';
var el = document.getElementById('chat-thread-messages');
if (el) el.innerHTML = '<div style="text-align:center; font-size:0.8125rem; color:var(--color-muted-foreground);">Завантаження…</div>';
if (el) el.innerHTML = '<div style="text-align:center; font-size:0.8125rem; color:var(--color-muted-foreground);">' + gettext('Загрузка…') + '</div>';
chatApiFetch('/posts/' + postId + '/thread?perPage=60').then(function (data) {
var ids = (data.order || []).slice().reverse();
@@ -957,7 +961,7 @@ function chatOpenThread(postId) {
});
}).catch(function (e) {
console.error(e);
if (el) el.innerHTML = '<div style="text-align:center; font-size:0.8125rem; color:var(--color-destructive);">Не вдалося завантажити тред</div>';
if (el) el.innerHTML = '<div style="text-align:center; font-size:0.8125rem; color:var(--color-destructive);">' + gettext('Не удалось загрузить обсуждение') + '</div>';
});
}
@@ -995,7 +999,7 @@ function chatBumpReplyCount(rootId) {
if (!root) return;
root.reply_count = (root.reply_count || 0) + 1;
var btn = document.getElementById('chat-reply-count-' + rootId);
if (btn) btn.textContent = '💬 ' + root.reply_count + ' відповідей';
if (btn) btn.textContent = '💬 ' + gettext('Ответов:') + ' ' + root.reply_count;
}
function chatSendMessage() {
@@ -1101,7 +1105,7 @@ function chatRenderDmPickerList() {
container.innerHTML = '';
if (!filtered.length) {
container.innerHTML = '<div style="padding:0.5rem; text-align:center; font-size:0.8125rem; color:var(--color-muted-foreground);">Нікого не знайдено</div>';
container.innerHTML = '<div style="padding:0.5rem; text-align:center; font-size:0.8125rem; color:var(--color-muted-foreground);">' + gettext('Никого не найдено') + '</div>';
return;
}
@@ -1197,7 +1201,7 @@ function chatUpdateTitle() {
function chatNotifyNewMessage(post) {
try {
if (window.Notification && Notification.permission === 'granted') {
var who = chatUserNames[post.user_id] || 'Нове повідомлення';
var who = chatUserNames[post.user_id] || gettext('Новое сообщение');
new Notification(who, {body: (post.message || '').slice(0, 140)});
}
} catch (e) { /* ignore */ }