feat(collab): add shared links, chat images, and plugin fullscreen

Adds a Collab Links panel (add/pin/delete) with translations in every locale so tab and button labels are not raw i18n keys, chat image attachments (up to 4), and native fullscreen on plugin iframes. Includes Zod request DTOs and CollabPanel coverage for the Links tab.
This commit is contained in:
Godric Hood
2026-09-06 18:21:18 +01:00
committed by Maurice
parent 59a1756529
commit 9543fdc3f2
47 changed files with 743 additions and 76 deletions
+22 -2
View File
@@ -3,6 +3,8 @@ import {
collabNoteUpdateRequestSchema,
collabPollCreateRequestSchema,
collabPollVoteRequestSchema,
collabLinkCreateRequestSchema,
collabLinkUpdateRequestSchema,
collabMessageCreateRequestSchema,
collabReactionRequestSchema,
} from './collab.schema';
@@ -71,11 +73,29 @@ describe('collabPollVoteRequestSchema', () => {
});
});
describe('collabLinkCreateRequestSchema', () => {
it('requires a title and an http(s) URL', () => {
expect(collabLinkCreateRequestSchema.safeParse({ title: 'Docs', url: 'https://example.com' }).success).toBe(true);
expect(collabLinkCreateRequestSchema.safeParse({ title: 'Docs', url: 'ftp://example.com' }).success).toBe(false);
expect(collabLinkCreateRequestSchema.safeParse({ title: ' ', url: 'https://example.com' }).success).toBe(false);
expect(collabLinkCreateRequestSchema.safeParse({ title: 'Docs', url: 'not-a-url' }).success).toBe(false);
});
});
describe('collabLinkUpdateRequestSchema', () => {
it('allows partial updates and still rejects non-http URLs', () => {
expect(collabLinkUpdateRequestSchema.safeParse({ pinned: true }).success).toBe(true);
expect(collabLinkUpdateRequestSchema.safeParse({ url: 'https://example.com/x' }).success).toBe(true);
expect(collabLinkUpdateRequestSchema.safeParse({ url: 'javascript:alert(1)' }).success).toBe(false);
});
});
describe('collabMessageCreateRequestSchema', () => {
it('requires text, caps it at 5000, allows a nullable reply_to', () => {
it('caps text at 5000, allows empty text for image-only messages, and accepts multipart reply_to strings', () => {
expect(collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: null }).success).toBe(true);
expect(collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: 4 }).success).toBe(true);
expect(collabMessageCreateRequestSchema.safeParse({ text: '' }).success).toBe(false);
expect(collabMessageCreateRequestSchema.safeParse({ text: '', reply_to: '4' }).success).toBe(true);
expect(collabMessageCreateRequestSchema.safeParse({}).success).toBe(true);
expect(collabMessageCreateRequestSchema.safeParse({ text: 'x'.repeat(5001) }).success).toBe(false);
});
});
+28 -2
View File
@@ -48,9 +48,35 @@ export const collabPollVoteRequestSchema = z.object({
});
export type CollabPollVoteRequest = z.infer<typeof collabPollVoteRequestSchema>;
const httpUrl = z.string().trim().min(1).refine((value) => {
try {
const parsed = new URL(value);
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
} catch {
return false;
}
}, 'A valid http(s) URL is required');
export const collabLinkCreateRequestSchema = z.object({
title: z.string().trim().min(1),
url: httpUrl,
pinned: z.union([z.boolean(), z.number()]).optional(),
});
export type CollabLinkCreateRequest = z.infer<typeof collabLinkCreateRequestSchema>;
export const collabLinkUpdateRequestSchema = z.object({
title: z.string().trim().min(1).optional(),
url: httpUrl.optional(),
pinned: z.union([z.boolean(), z.number()]).optional(),
});
export type CollabLinkUpdateRequest = z.infer<typeof collabLinkUpdateRequestSchema>;
// text may be empty when the chat message is image-only (multipart). The
// controller still rejects a request with neither text nor files.
export const collabMessageCreateRequestSchema = z.object({
text: z.string().min(1).max(5000),
reply_to: z.number().nullable().optional(),
text: z.string().max(5000).optional(),
// Multipart fields arrive as strings; JSON chat still sends a number/null.
reply_to: z.union([z.number(), z.string(), z.null()]).optional(),
});
export type CollabMessageCreateRequest = z.infer<typeof collabMessageCreateRequestSchema>;
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'الدردشة',
'collab.tabs.notes': 'الملاحظات',
'collab.tabs.polls': 'الاستطلاعات',
'collab.tabs.links': 'الروابط',
'collab.links.add': 'إضافة رابط',
'collab.links.cancel': 'إلغاء',
'collab.links.save': 'حفظ الرابط',
'collab.links.titlePlaceholder': 'عنوان الرابط',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'لا توجد روابط مشتركة بعد',
'collab.links.pin': 'تثبيت الرابط',
'collab.links.unpin': 'إلغاء تثبيت الرابط',
'collab.links.delete': 'حذف الرابط',
'collab.whatsNext.title': 'ما التالي',
'collab.whatsNext.today': 'اليوم',
'collab.whatsNext.tomorrow': 'غدًا',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Chat',
'collab.tabs.notes': 'Notas',
'collab.tabs.polls': 'Enquetes',
'collab.tabs.links': 'Links',
'collab.links.add': 'Adicionar link',
'collab.links.cancel': 'Cancelar',
'collab.links.save': 'Salvar link',
'collab.links.titlePlaceholder': 'Título do link',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Nenhum link compartilhado ainda',
'collab.links.pin': 'Fixar link',
'collab.links.unpin': 'Desafixar link',
'collab.links.delete': 'Excluir link',
'collab.whatsNext.title': 'Próximos passos',
'collab.whatsNext.today': 'Hoje',
'collab.whatsNext.tomorrow': 'Amanhã',
+10
View File
@@ -4,6 +4,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Missatges',
'collab.tabs.notes': 'Notes',
'collab.tabs.polls': 'Enquestes',
'collab.tabs.links': 'Enllaços',
'collab.links.add': 'Afegeix un enllaç',
'collab.links.cancel': 'Cancel·la',
'collab.links.save': 'Desa l\'enllaç',
'collab.links.titlePlaceholder': 'Títol de l\'enllaç',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Encara no hi ha enllaços compartits',
'collab.links.pin': 'Fixa l\'enllaç',
'collab.links.unpin': 'Desfixa l\'enllaç',
'collab.links.delete': 'Suprimeix l\'enllaç',
'collab.whatsNext.title': 'Què ve ara',
'collab.whatsNext.today': 'Avui',
'collab.whatsNext.tomorrow': 'Demà',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Chat',
'collab.tabs.notes': 'Poznámky',
'collab.tabs.polls': 'Hlasování',
'collab.tabs.links': 'Odkazy',
'collab.links.add': 'Přidat odkaz',
'collab.links.cancel': 'Zrušit',
'collab.links.save': 'Uložit odkaz',
'collab.links.titlePlaceholder': 'Název odkazu',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Zatím žádné sdílené odkazy',
'collab.links.pin': 'Připnout odkaz',
'collab.links.unpin': 'Odepnout odkaz',
'collab.links.delete': 'Smazat odkaz',
'collab.whatsNext.title': 'Co následuje',
'collab.whatsNext.today': 'Dnes',
'collab.whatsNext.tomorrow': 'Zítra',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Chat',
'collab.tabs.notes': 'Notizen',
'collab.tabs.polls': 'Umfragen',
'collab.tabs.links': 'Links',
'collab.links.add': 'Link hinzufügen',
'collab.links.cancel': 'Abbrechen',
'collab.links.save': 'Link speichern',
'collab.links.titlePlaceholder': 'Linktitel',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Noch keine geteilten Links',
'collab.links.pin': 'Link anheften',
'collab.links.unpin': 'Link lösen',
'collab.links.delete': 'Link löschen',
'collab.whatsNext.title': 'Nächste',
'collab.whatsNext.today': 'Heute',
'collab.whatsNext.tomorrow': 'Morgen',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Chat',
'collab.tabs.notes': 'Notes',
'collab.tabs.polls': 'Polls',
'collab.tabs.links': 'Links',
'collab.links.add': 'Add link',
'collab.links.cancel': 'Cancel',
'collab.links.save': 'Save link',
'collab.links.titlePlaceholder': 'Link title',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'No shared links yet',
'collab.links.pin': 'Pin link',
'collab.links.unpin': 'Unpin link',
'collab.links.delete': 'Delete link',
'collab.whatsNext.title': "What's Next",
'collab.whatsNext.today': 'Today',
'collab.whatsNext.tomorrow': 'Tomorrow',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Mensajes',
'collab.tabs.notes': 'Notas',
'collab.tabs.polls': 'Encuestas',
'collab.tabs.links': 'Enlaces',
'collab.links.add': 'Añadir enlace',
'collab.links.cancel': 'Cancelar',
'collab.links.save': 'Guardar enlace',
'collab.links.titlePlaceholder': 'Título del enlace',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Aún no hay enlaces compartidos',
'collab.links.pin': 'Fijar enlace',
'collab.links.unpin': 'Desfijar enlace',
'collab.links.delete': 'Eliminar enlace',
'collab.whatsNext.title': 'Qué viene ahora',
'collab.whatsNext.today': 'Hoy',
'collab.whatsNext.tomorrow': 'Mañana',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Discussion',
'collab.tabs.notes': 'Notes',
'collab.tabs.polls': 'Sondages',
'collab.tabs.links': 'Liens',
'collab.links.add': 'Ajouter un lien',
'collab.links.cancel': 'Annuler',
'collab.links.save': 'Enregistrer le lien',
'collab.links.titlePlaceholder': 'Titre du lien',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Aucun lien partagé pour le moment',
'collab.links.pin': 'Épingler le lien',
'collab.links.unpin': 'Désépingler le lien',
'collab.links.delete': 'Supprimer le lien',
'collab.whatsNext.title': 'À venir',
'collab.whatsNext.today': "Aujourd'hui",
'collab.whatsNext.tomorrow': 'Demain',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Συνομιλία',
'collab.tabs.notes': 'Σημειώσεις',
'collab.tabs.polls': 'Δημοσκοπήσεις',
'collab.tabs.links': 'Σύνδεσμοι',
'collab.links.add': 'Προσθήκη συνδέσμου',
'collab.links.cancel': 'Ακύρωση',
'collab.links.save': 'Αποθήκευση συνδέσμου',
'collab.links.titlePlaceholder': 'Τίτλος συνδέσμου',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Δεν υπάρχουν κοινόχρηστοι σύνδεσμοι ακόμη',
'collab.links.pin': 'Καρφίτσωμα συνδέσμου',
'collab.links.unpin': 'Ξεκαρφίτσωμα συνδέσμου',
'collab.links.delete': 'Διαγραφή συνδέσμου',
'collab.whatsNext.title': 'Τι Επόμενο',
'collab.whatsNext.today': 'Σήμερα',
'collab.whatsNext.tomorrow': 'Αύριο',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Csevegés',
'collab.tabs.notes': 'Jegyzetek',
'collab.tabs.polls': 'Szavazások',
'collab.tabs.links': 'Linkek',
'collab.links.add': 'Link hozzáadása',
'collab.links.cancel': 'Mégse',
'collab.links.save': 'Link mentése',
'collab.links.titlePlaceholder': 'Link címe',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Még nincsenek megosztott linkek',
'collab.links.pin': 'Link rögzítése',
'collab.links.unpin': 'Link feloldása',
'collab.links.delete': 'Link törlése',
'collab.whatsNext.title': 'Mi következik',
'collab.whatsNext.today': 'Ma',
'collab.whatsNext.tomorrow': 'Holnap',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Chat',
'collab.tabs.notes': 'Catatan',
'collab.tabs.polls': 'Polling',
'collab.tabs.links': 'Tautan',
'collab.links.add': 'Tambah tautan',
'collab.links.cancel': 'Batal',
'collab.links.save': 'Simpan tautan',
'collab.links.titlePlaceholder': 'Judul tautan',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Belum ada tautan bersama',
'collab.links.pin': 'Sematkan tautan',
'collab.links.unpin': 'Lepas sematan tautan',
'collab.links.delete': 'Hapus tautan',
'collab.whatsNext.title': 'Berikutnya',
'collab.whatsNext.today': 'Hari ini',
'collab.whatsNext.tomorrow': 'Besok',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Chat',
'collab.tabs.notes': 'Note',
'collab.tabs.polls': 'Sondaggi',
'collab.tabs.links': 'Link',
'collab.links.add': 'Aggiungi link',
'collab.links.cancel': 'Annulla',
'collab.links.save': 'Salva link',
'collab.links.titlePlaceholder': 'Titolo del link',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Nessun link condiviso',
'collab.links.pin': 'Fissa link',
'collab.links.unpin': 'Sblocca link',
'collab.links.delete': 'Elimina link',
'collab.whatsNext.title': "Cosa c'è dopo",
'collab.whatsNext.today': 'Oggi',
'collab.whatsNext.tomorrow': 'Domani',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'チャット',
'collab.tabs.notes': 'ノート',
'collab.tabs.polls': '投票',
'collab.tabs.links': 'リンク',
'collab.links.add': 'リンクを追加',
'collab.links.cancel': 'キャンセル',
'collab.links.save': 'リンクを保存',
'collab.links.titlePlaceholder': 'リンクのタイトル',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': '共有リンクはまだありません',
'collab.links.pin': 'リンクを固定',
'collab.links.unpin': 'リンクの固定を解除',
'collab.links.delete': 'リンクを削除',
'collab.whatsNext.title': '次にすること',
'collab.whatsNext.today': '今日',
'collab.whatsNext.tomorrow': '明日',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': '채팅',
'collab.tabs.notes': '메모',
'collab.tabs.polls': '투표',
'collab.tabs.links': '링크',
'collab.links.add': '링크 추가',
'collab.links.cancel': '취소',
'collab.links.save': '링크 저장',
'collab.links.titlePlaceholder': '링크 제목',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': '공유된 링크가 아직 없습니다',
'collab.links.pin': '링크 고정',
'collab.links.unpin': '링크 고정 해제',
'collab.links.delete': '링크 삭제',
'collab.whatsNext.title': '다음 할 일',
'collab.whatsNext.today': '오늘',
'collab.whatsNext.tomorrow': '내일',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Chat',
'collab.tabs.notes': 'Notities',
'collab.tabs.polls': 'Polls',
'collab.tabs.links': 'Links',
'collab.links.add': 'Link toevoegen',
'collab.links.cancel': 'Annuleren',
'collab.links.save': 'Link opslaan',
'collab.links.titlePlaceholder': 'Linktitel',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Nog geen gedeelde links',
'collab.links.pin': 'Link vastmaken',
'collab.links.unpin': 'Link losmaken',
'collab.links.delete': 'Link verwijderen',
'collab.whatsNext.title': 'Wat komt er',
'collab.whatsNext.today': 'Vandaag',
'collab.whatsNext.tomorrow': 'Morgen',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Czat',
'collab.tabs.notes': 'Notatki',
'collab.tabs.polls': 'Ankiety',
'collab.tabs.links': 'Linki',
'collab.links.add': 'Dodaj link',
'collab.links.cancel': 'Anuluj',
'collab.links.save': 'Zapisz link',
'collab.links.titlePlaceholder': 'Tytuł linku',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Brak udostępnionych linków',
'collab.links.pin': 'Przypnij link',
'collab.links.unpin': 'Odepnij link',
'collab.links.delete': 'Usuń link',
'collab.whatsNext.title': 'Co dalej',
'collab.whatsNext.today': 'Dzisiaj',
'collab.whatsNext.tomorrow': 'Jutro',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Чат',
'collab.tabs.notes': 'Заметки',
'collab.tabs.polls': 'Опросы',
'collab.tabs.links': 'Ссылки',
'collab.links.add': 'Добавить ссылку',
'collab.links.cancel': 'Отмена',
'collab.links.save': 'Сохранить ссылку',
'collab.links.titlePlaceholder': 'Название ссылки',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Пока нет общих ссылок',
'collab.links.pin': 'Закрепить ссылку',
'collab.links.unpin': 'Открепить ссылку',
'collab.links.delete': 'Удалить ссылку',
'collab.whatsNext.title': 'Что дальше',
'collab.whatsNext.today': 'Сегодня',
'collab.whatsNext.tomorrow': 'Завтра',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Chatt',
'collab.tabs.notes': 'Noteringar',
'collab.tabs.polls': 'Omröstningar',
'collab.tabs.links': 'Länkar',
'collab.links.add': 'Lägg till länk',
'collab.links.cancel': 'Avbryt',
'collab.links.save': 'Spara länk',
'collab.links.titlePlaceholder': 'Länktitel',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Inga delade länkar än',
'collab.links.pin': 'Fäst länk',
'collab.links.unpin': 'Lossa länk',
'collab.links.delete': 'Ta bort länk',
'collab.whatsNext.title': 'Vad händer härnäst?',
'collab.whatsNext.today': 'Idag',
'collab.whatsNext.tomorrow': 'I morgon',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Sohbet',
'collab.tabs.notes': 'Notlar',
'collab.tabs.polls': 'Anketler',
'collab.tabs.links': 'Bağlantılar',
'collab.links.add': 'Bağlantı ekle',
'collab.links.cancel': 'İptal',
'collab.links.save': 'Bağlantıyı kaydet',
'collab.links.titlePlaceholder': 'Bağlantı başlığı',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Henüz paylaşılan bağlantı yok',
'collab.links.pin': 'Bağlantıyı sabitle',
'collab.links.unpin': 'Sabitlemeyi kaldır',
'collab.links.delete': 'Bağlantıyı sil',
'collab.whatsNext.title': "What's Next",
'collab.whatsNext.today': 'Bugün',
'collab.whatsNext.tomorrow': 'Yarın',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Чат',
'collab.tabs.notes': 'Нотатки',
'collab.tabs.polls': 'Опитування',
'collab.tabs.links': 'Посилання',
'collab.links.add': 'Додати посилання',
'collab.links.cancel': 'Скасувати',
'collab.links.save': 'Зберегти посилання',
'collab.links.titlePlaceholder': 'Назва посилання',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Ще немає спільних посилань',
'collab.links.pin': 'Закріпити посилання',
'collab.links.unpin': 'Відкріпити посилання',
'collab.links.delete': 'Видалити посилання',
'collab.whatsNext.title': 'Що далі',
'collab.whatsNext.today': 'Сьогодні',
'collab.whatsNext.tomorrow': 'Завтра',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': 'Trò chuyện',
'collab.tabs.notes': 'Ghi chú',
'collab.tabs.polls': 'Thăm dò ý kiến',
'collab.tabs.links': 'Liên kết',
'collab.links.add': 'Thêm liên kết',
'collab.links.cancel': 'Hủy',
'collab.links.save': 'Lưu liên kết',
'collab.links.titlePlaceholder': 'Tiêu đề liên kết',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': 'Chưa có liên kết được chia sẻ',
'collab.links.pin': 'Ghim liên kết',
'collab.links.unpin': 'Bỏ ghim liên kết',
'collab.links.delete': 'Xóa liên kết',
'collab.whatsNext.title': 'Tiếp theo là gì',
'collab.whatsNext.today': 'Hôm nay',
'collab.whatsNext.tomorrow': 'Ngày mai',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': '聊天',
'collab.tabs.notes': '筆記',
'collab.tabs.polls': '投票',
'collab.tabs.links': '連結',
'collab.links.add': '新增連結',
'collab.links.cancel': '取消',
'collab.links.save': '儲存連結',
'collab.links.titlePlaceholder': '連結標題',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': '尚無共享連結',
'collab.links.pin': '釘選連結',
'collab.links.unpin': '取消釘選連結',
'collab.links.delete': '刪除連結',
'collab.whatsNext.title': '接下來',
'collab.whatsNext.today': '今天',
'collab.whatsNext.tomorrow': '明天',
+10
View File
@@ -12,6 +12,16 @@ const collab: TranslationStrings = {
'collab.tabs.chat': '聊天',
'collab.tabs.notes': '笔记',
'collab.tabs.polls': '投票',
'collab.tabs.links': '链接',
'collab.links.add': '添加链接',
'collab.links.cancel': '取消',
'collab.links.save': '保存链接',
'collab.links.titlePlaceholder': '链接标题',
'collab.links.urlPlaceholder': 'https://...',
'collab.links.empty': '还没有共享链接',
'collab.links.pin': '固定链接',
'collab.links.unpin': '取消固定链接',
'collab.links.delete': '删除链接',
'collab.whatsNext.title': '接下来',
'collab.whatsNext.today': '今天',
'collab.whatsNext.tomorrow': '明天',