feat(core): integrate google calendar sync (#14248)

fix #14170 
fix #13893 
fix #13673 
fix #13543 
fix #13308 
fix #7607




#### PR Dependency Tree


* **PR #14247**
  * **PR #14248** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Integrations panel in Account Settings to link/unlink calendar
providers.
  * Collapsible settings wrapper for improved layout.

* **Improvements**
* Calendar system reworked: per-account calendar groups, simplified
toggles with explicit Save, richer event display (multi-dot date
indicators), improved event time/title handling across journal views.

* **Localization**
* Added calendar keys: save-error, no-journal, no-calendar; removed
legacy duplicate-error keys.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-01-13 02:38:16 +08:00
committed by GitHub
parent 89f0430242
commit 279b7bb64f
50 changed files with 1349 additions and 1002 deletions
+113
View File
@@ -2,10 +2,13 @@ import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { parse } from 'node:path';
import { fileURLToPath } from 'node:url';
import { ProjectRoot } from '@affine-tools/utils/path';
import { Package } from '@affine-tools/utils/workspace';
import { runCli } from '@magic-works/i18n-codegen';
import { glob } from 'glob';
const isDev = process.argv.includes('--dev');
const shouldCleanup = process.argv.includes('--cleanup');
const i18nPkg = new Package('@affine/i18n');
const resourcesDir = i18nPkg.join('src', 'resources').toString();
@@ -21,6 +24,113 @@ function writeResource(lang: string, resource: Record<string, string>) {
writeFileSync(filePath, JSON.stringify(resource, null, 2) + '\n');
}
async function cleanupResources() {
const escapeRegExp = (value: string) =>
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const files = await glob('packages/frontend/**/src/**/*.{js,tsx,ts}', {
ignore: [
'**/node_modules/**',
'**/packages/frontend/i18n/src/resources/*',
'**/packages/frontend/i18n/src/i18n.gen.ts',
'**/dist/**',
'**/lib/**',
],
cwd: ProjectRoot.toString(),
absolute: true,
});
const filesWithContent = files.map(file => {
return {
path: file,
content: readFileSync(file, 'utf8'),
};
});
const dynamicPrefixes = new Set<string>();
const templatePrefixRegex = /`[^`]*?(com\.affine\.[^`]*?)\$\{/g;
const concatPrefixRegex = /['"](com\.affine\.[^'"]*?\.)['"]\s*\+/g;
const addDynamicPrefix = (rawPrefix: string) => {
let prefix = rawPrefix;
if (!prefix.endsWith('.')) {
const lastDot = prefix.lastIndexOf('.');
if (lastDot === -1) {
return;
}
prefix = prefix.slice(0, lastDot + 1);
}
dynamicPrefixes.add(prefix);
};
for (const file of filesWithContent) {
templatePrefixRegex.lastIndex = 0;
concatPrefixRegex.lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = templatePrefixRegex.exec(file.content)) !== null) {
addDynamicPrefix(match[1]);
}
while ((match = concatPrefixRegex.exec(file.content)) !== null) {
addDynamicPrefix(match[1]);
}
}
const resources = readdirSync(resourcesDir)
.filter(file => file.endsWith('.json'))
.reduce(
(langs, file) => {
const lang = parse(file).name;
langs[lang] = readResource(lang);
return langs;
},
{} as Record<string, Record<string, string>>
);
const candidateKeys = new Set<string>();
for (const resource of Object.values(resources)) {
Object.keys(resource).forEach(key => {
if (!key.startsWith('com.affine.payment.modal.')) {
candidateKeys.add(key);
}
});
}
const unusedKeys = Array.from(candidateKeys).filter(key => {
const regex1 = new RegExp(`[\`'"]${escapeRegExp(key)}[\`'"]`, 'g');
const lastDot = key.lastIndexOf('.');
const keyPrefix = lastDot === -1 ? '' : key.slice(0, lastDot + 1);
if (keyPrefix && dynamicPrefixes.has(keyPrefix)) {
return false;
}
for (const file of filesWithContent) {
const match = file.content.match(regex1);
if (match) {
return false;
}
}
return true;
});
if (unusedKeys.length === 0) {
return;
}
const unusedKeySet = new Set(unusedKeys);
for (const [lang, resource] of Object.entries(resources)) {
let changed = false;
for (const key of Object.keys(resource)) {
if (unusedKeySet.has(key)) {
delete resource[key];
changed = true;
}
}
if (changed) {
writeResource(lang, resource);
}
}
}
function calcCompletenesses() {
const langs = readdirSync(resourcesDir)
.filter(file => file.endsWith('.json'))
@@ -124,5 +234,8 @@ async function appendErrorI18n() {
}
await appendErrorI18n();
if (shouldCleanup) {
await cleanupResources();
}
i18nnext();
calcCompletenesses();
+12 -10
View File
@@ -8227,17 +8227,9 @@ export function useAFFiNEI18N(): {
*/
["com.affine.integration.calendar.new-url-label"](): string;
/**
* `This is a duplicate calendar`
* `An error occurred while saving the calendar settings`
*/
["com.affine.integration.calendar.new-duplicate-error-title"](): string;
/**
* `This subscription calendar already exists in the account of subscribed calendars.`
*/
["com.affine.integration.calendar.new-duplicate-error-content"](): string;
/**
* `An error occurred while adding the calendar`
*/
["com.affine.integration.calendar.new-error"](): string;
["com.affine.integration.calendar.save-error"](): string;
/**
* `All day`
*/
@@ -8264,6 +8256,16 @@ export function useAFFiNEI18N(): {
["com.affine.integration.calendar.unsubscribe-content"](options: {
readonly name: string;
}): string;
/**
* `No journal page found for {{date}}. Please create a journal page first.`
*/
["com.affine.integration.calendar.no-journal"](options: {
readonly date: string;
}): string;
/**
* `No subscribed calendars yet.`
*/
["com.affine.integration.calendar.no-calendar"](): string;
/**
* `MCP Server`
*/
@@ -2037,9 +2037,6 @@
"com.affine.integration.calendar.unsubscribe": "إلغاء الاشتراك",
"com.affine.integration.calendar.new-title": "إضافة تقويم بواسطة الرابط",
"com.affine.integration.calendar.new-url-label": "رابط التقويم",
"com.affine.integration.calendar.new-duplicate-error-title": "هذا تقويم مكرر",
"com.affine.integration.calendar.new-duplicate-error-content": "تقويم الاشتراك هذا موجود بالفعل في حساب التقويمات المشتركة.",
"com.affine.integration.calendar.new-error": "حدث خطأ أثناء إضافة التقويم",
"com.affine.integration.calendar.all-day": "طوال اليوم",
"com.affine.integration.calendar.new-doc": "مستند جديد",
"com.affine.integration.calendar.show-events": "عرض أحداث التقويم",
@@ -580,9 +580,6 @@
"com.affine.integration.calendar.desc": "Els nous esdeveniments es programaran al diari d'AFFiNE",
"com.affine.integration.calendar.name": "Calendari",
"com.affine.integration.calendar.new-doc": "Nou document",
"com.affine.integration.calendar.new-duplicate-error-content": "Aquest calendari de subscripció ja existeix al compte de calendaris subscrits.",
"com.affine.integration.calendar.new-duplicate-error-title": "Aquest és un calendari duplicat",
"com.affine.integration.calendar.new-error": "S'ha produït un error en afegir el calendari",
"com.affine.integration.calendar.new-subscription": "Subscriure's",
"com.affine.integration.calendar.new-title": "Afegir un calendari per URL",
"com.affine.integration.calendar.new-url-label": "URL del calendari",
@@ -2053,9 +2053,6 @@
"com.affine.integration.calendar.unsubscribe": "Abonnement beenden",
"com.affine.integration.calendar.new-title": "Kalender per URL hinzufügen",
"com.affine.integration.calendar.new-url-label": "Kalender-URL",
"com.affine.integration.calendar.new-duplicate-error-title": "Dies ist ein doppelter Kalender",
"com.affine.integration.calendar.new-duplicate-error-content": "Dieses Kalender-Abonnement ist bereits im Konto der abonnierten Kalender vorhanden.",
"com.affine.integration.calendar.new-error": "Beim Hinzufügen des Kalenders ist ein Fehler aufgetreten",
"com.affine.integration.calendar.all-day": "Ganztägig",
"com.affine.integration.calendar.new-doc": "Neue Seite",
"com.affine.integration.calendar.show-events": "Kalenderereignisse anzeigen",
@@ -2037,9 +2037,6 @@
"com.affine.integration.calendar.unsubscribe": "Απεγγραφή",
"com.affine.integration.calendar.new-title": "Προσθέστε ημερολόγιο με URL",
"com.affine.integration.calendar.new-url-label": "URL Ημερολογίου",
"com.affine.integration.calendar.new-duplicate-error-title": "Αυτό είναι ένα διπλό ημερολόγιο",
"com.affine.integration.calendar.new-duplicate-error-content": "Αυτό το ημερολόγιο συνδρομής υπάρχει ήδη στο λογαριασμό των συνδρομητικών ημερολογίων.",
"com.affine.integration.calendar.new-error": "Προέκυψε σφάλμα κατά την προσθήκη του ημερολογίου",
"com.affine.integration.calendar.all-day": "Ολοήμερο",
"com.affine.integration.calendar.new-doc": "Νέο έγγραφο",
"com.affine.integration.calendar.show-events": "Εμφάνιση γεγονότων ημερολογίου",
+3 -3
View File
@@ -2065,15 +2065,15 @@
"com.affine.integration.calendar.unsubscribe": "Unsubscribe",
"com.affine.integration.calendar.new-title": "Add a calendar by URL",
"com.affine.integration.calendar.new-url-label": "Calendar URL",
"com.affine.integration.calendar.new-duplicate-error-title": "This is a duplicate calendar",
"com.affine.integration.calendar.new-duplicate-error-content": "This subscription calendar already exists in the account of subscribed calendars.",
"com.affine.integration.calendar.new-error": "An error occurred while adding the calendar",
"com.affine.integration.calendar.save-error": "An error occurred while saving the calendar settings",
"com.affine.integration.calendar.all-day": "All day",
"com.affine.integration.calendar.new-doc": "New doc",
"com.affine.integration.calendar.show-events": "Show calendar events",
"com.affine.integration.calendar.show-events-desc": "Enabling this setting allows you to connect your calendar events to your Journal in AFFiNE",
"com.affine.integration.calendar.show-all-day-events": "Show all day event",
"com.affine.integration.calendar.unsubscribe-content": "Are you sure you want to unsubscribe \"{{name}}\"? Unsubscribing this account will remove its data from Journal.",
"com.affine.integration.calendar.no-journal": "No journal page found for {{date}}. Please create a journal page first.",
"com.affine.integration.calendar.no-calendar": "No subscribed calendars yet.",
"com.affine.integration.mcp-server.name": "MCP Server",
"com.affine.integration.mcp-server.desc": "Enable other MCP Client to search and read the doc of AFFiNE.",
"com.affine.audio.notes": "Notes",
@@ -2037,9 +2037,6 @@
"com.affine.integration.calendar.unsubscribe": "Darse de baja",
"com.affine.integration.calendar.new-title": "Agregar un calendario por URL",
"com.affine.integration.calendar.new-url-label": "URL del calendario",
"com.affine.integration.calendar.new-duplicate-error-title": "Este es un calendario duplicado",
"com.affine.integration.calendar.new-duplicate-error-content": "Este calendario de suscripción ya existe en la cuenta de calendarios suscritos.",
"com.affine.integration.calendar.new-error": "Ocurrió un error al agregar el calendario",
"com.affine.integration.calendar.all-day": "Todo el día",
"com.affine.integration.calendar.new-doc": "Nuevo documento",
"com.affine.integration.calendar.show-events": "Mostrar eventos del calendario",
@@ -2037,9 +2037,6 @@
"com.affine.integration.calendar.unsubscribe": "لغو اشتراک",
"com.affine.integration.calendar.new-title": "افزودن یک تقویم از طریق URL",
"com.affine.integration.calendar.new-url-label": "آدرس اینترنتی تقویم",
"com.affine.integration.calendar.new-duplicate-error-title": "این یک تقویم تکراری است",
"com.affine.integration.calendar.new-duplicate-error-content": "این تقویم اشتراکی در حساب تقویم‌های اشتراک‌شده موجود است.",
"com.affine.integration.calendar.new-error": "هنگام افزودن تقویم خطایی رخ داده است",
"com.affine.integration.calendar.all-day": "تمام روز",
"com.affine.integration.calendar.new-doc": "سند جدید",
"com.affine.integration.calendar.show-events": "نمایش رویدادهای تقویم",
@@ -2061,9 +2061,6 @@
"com.affine.integration.calendar.unsubscribe": "Se désabonner",
"com.affine.integration.calendar.new-title": "Ajouter un calendrier par URL",
"com.affine.integration.calendar.new-url-label": "URL du calendrier",
"com.affine.integration.calendar.new-duplicate-error-title": "Ceci est un calendrier dupliqué",
"com.affine.integration.calendar.new-duplicate-error-content": "Ce calendrier d'abonnement existe déjà dans le compte des calendriers abonnés.",
"com.affine.integration.calendar.new-error": "Une erreur s'est produite lors de l'ajout du calendrier",
"com.affine.integration.calendar.all-day": "Toute la journée",
"com.affine.integration.calendar.new-doc": "Nouveau document",
"com.affine.integration.calendar.show-events": "Afficher les événements du calendrier",
@@ -2060,9 +2060,6 @@
"com.affine.integration.calendar.unsubscribe": "Annulla iscrizione",
"com.affine.integration.calendar.new-title": "Aggiungi un calendario tramite URL",
"com.affine.integration.calendar.new-url-label": "URL del calendario",
"com.affine.integration.calendar.new-duplicate-error-title": "Questo è un calendario duplicato",
"com.affine.integration.calendar.new-duplicate-error-content": "Questo calendario di abbonamento esiste già nell'account dei calendari abbonati.",
"com.affine.integration.calendar.new-error": "Si è verificato un errore durante l'aggiunta del calendario",
"com.affine.integration.calendar.all-day": "Tutto il giorno",
"com.affine.integration.calendar.new-doc": "Nuovo documento",
"com.affine.integration.calendar.show-events": "Mostra gli eventi del calendario",
@@ -2037,9 +2037,6 @@
"com.affine.integration.calendar.unsubscribe": "購読解除",
"com.affine.integration.calendar.new-title": "URLでカレンダーを追加",
"com.affine.integration.calendar.new-url-label": "カレンダーURL",
"com.affine.integration.calendar.new-duplicate-error-title": "これは重複したカレンダーです",
"com.affine.integration.calendar.new-duplicate-error-content": "このサブスクリプションカレンダーは既にサブスクライブされたカレンダーアカウントに存在します。",
"com.affine.integration.calendar.new-error": "カレンダーの追加中にエラーが発生しました",
"com.affine.integration.calendar.all-day": "終日",
"com.affine.integration.calendar.new-doc": "新規ドキュмент",
"com.affine.integration.calendar.show-events": "カレンダーイベントを表示",
@@ -2048,9 +2048,6 @@
"com.affine.integration.calendar.unsubscribe": "구독 취소",
"com.affine.integration.calendar.new-title": "URL로 캘린더 추가",
"com.affine.integration.calendar.new-url-label": "캘린더 URL",
"com.affine.integration.calendar.new-duplicate-error-title": "중복된 캘린더입니다",
"com.affine.integration.calendar.new-duplicate-error-content": "이 구독 캘린더는 구독된 캘린더 계정에 이미 존재합니다.",
"com.affine.integration.calendar.new-error": "캘린더 추가 중 오류가 발생했습니다",
"com.affine.integration.calendar.all-day": "종일",
"com.affine.integration.calendar.new-doc": "새 문서",
"com.affine.integration.calendar.show-events": "캘린더 이벤트 표시",
@@ -684,7 +684,6 @@
"com.affine.integration.calendar.show-all-day-events": "Vis heldagshendelse",
"com.affine.integration.calendar.new-url-label": "Kalender-URL",
"com.affine.integration.calendar.new-subscription": "Abonnér",
"com.affine.integration.calendar.new-duplicate-error-title": "Dette er en duplikat",
"com.affine.integration.calendar.new-doc": "Nytt dokument",
"com.affine.inactive": "Inaktiv",
"com.affine.inactive-member": "Inaktivt medlem",
@@ -2061,9 +2061,6 @@
"com.affine.integration.calendar.unsubscribe": "Anuluj subskrypcję",
"com.affine.integration.calendar.new-title": "Dodaj kalendarz przez URL",
"com.affine.integration.calendar.new-url-label": "URL kalendarza",
"com.affine.integration.calendar.new-duplicate-error-title": "Jest to duplikat kalendarza",
"com.affine.integration.calendar.new-duplicate-error-content": "Ten subskrybowany kalendarz już istnieje w koncie subskrybowanych kalendarzy.",
"com.affine.integration.calendar.new-error": "Wystąpił błąd podczas dodawania kalendarza",
"com.affine.integration.calendar.all-day": "Cały dzień",
"com.affine.integration.calendar.new-doc": "Nowy dokument",
"com.affine.integration.calendar.show-events": "Pokaż wydarzenia kalendarza",
@@ -2037,9 +2037,6 @@
"com.affine.integration.calendar.unsubscribe": "Desinscrever",
"com.affine.integration.calendar.new-title": "Adicionar um calendário por URL",
"com.affine.integration.calendar.new-url-label": "URL do Calendário",
"com.affine.integration.calendar.new-duplicate-error-title": "Este é um calendário duplicado",
"com.affine.integration.calendar.new-duplicate-error-content": "Este calendário de inscrição já existe na conta de calendários inscritos.",
"com.affine.integration.calendar.new-error": "Aconteceu um erro ao adicionar o calendário",
"com.affine.integration.calendar.all-day": "Dia inteiro",
"com.affine.integration.calendar.new-doc": "Novo documento",
"com.affine.integration.calendar.show-events": "Mostrar eventos de calendário",
@@ -2059,9 +2059,6 @@
"com.affine.integration.calendar.unsubscribe": "Отписаться",
"com.affine.integration.calendar.new-title": "Добавить календарь по URL",
"com.affine.integration.calendar.new-url-label": "URL-адрес календаря",
"com.affine.integration.calendar.new-duplicate-error-title": "Это дублирующийся календарь",
"com.affine.integration.calendar.new-duplicate-error-content": "Календарь подписки уже существует в учетной записи подписанных календарей.",
"com.affine.integration.calendar.new-error": "Произошла ошибка при добавлении календаря",
"com.affine.integration.calendar.all-day": "Весь день",
"com.affine.integration.calendar.new-doc": "Новый документ",
"com.affine.integration.calendar.show-events": "Показать события календаря",
@@ -2041,9 +2041,6 @@
"com.affine.integration.calendar.unsubscribe": "Avsluta prenumeration",
"com.affine.integration.calendar.new-title": "Lägg till en kalender via URL",
"com.affine.integration.calendar.new-url-label": "Kalender URL",
"com.affine.integration.calendar.new-duplicate-error-title": "Detta är en duplicerad kalender",
"com.affine.integration.calendar.new-duplicate-error-content": "Denna abonnemangskalender finns redan i kontot för prenumererade kalendrar.",
"com.affine.integration.calendar.new-error": "Ett fel inträffade när kalendern lades till.",
"com.affine.integration.calendar.all-day": "Hela dagen",
"com.affine.integration.calendar.new-doc": "Ny sida",
"com.affine.integration.calendar.show-events": "Visa kalenderhändelser",
@@ -2037,9 +2037,6 @@
"com.affine.integration.calendar.unsubscribe": "Відписатися",
"com.affine.integration.calendar.new-title": "Додати календар за URL",
"com.affine.integration.calendar.new-url-label": "URL календаря",
"com.affine.integration.calendar.new-duplicate-error-title": "Це дубльований календар",
"com.affine.integration.calendar.new-duplicate-error-content": "Цей календар підписки вже існує в акаунті підписаних календарів.",
"com.affine.integration.calendar.new-error": "Під час додавання календаря сталася помилка",
"com.affine.integration.calendar.all-day": "Цілий день",
"com.affine.integration.calendar.new-doc": "Новий документ",
"com.affine.integration.calendar.show-events": "Показати події календаря",
@@ -2063,9 +2063,6 @@
"com.affine.integration.calendar.unsubscribe": "取消订阅",
"com.affine.integration.calendar.new-title": "添加日历订阅的链接",
"com.affine.integration.calendar.new-url-label": "日历订阅链接",
"com.affine.integration.calendar.new-duplicate-error-title": "这是一个重复的日历",
"com.affine.integration.calendar.new-duplicate-error-content": "此订阅日历已存在于帐户中。",
"com.affine.integration.calendar.new-error": "添加日历时出错了",
"com.affine.integration.calendar.all-day": "全天",
"com.affine.integration.calendar.new-doc": "新建文档",
"com.affine.integration.calendar.show-events": "显示日历事件",
@@ -2040,9 +2040,6 @@
"com.affine.integration.calendar.unsubscribe": "取消訂閱",
"com.affine.integration.calendar.new-title": "添加日曆訂閱的鏈接",
"com.affine.integration.calendar.new-url-label": "日曆訂閱鏈接",
"com.affine.integration.calendar.new-duplicate-error-title": "這是一個重複的日曆",
"com.affine.integration.calendar.new-duplicate-error-content": "此訂閱日曆已存在於賬戶中。",
"com.affine.integration.calendar.new-error": "添加日曆時出錯了",
"com.affine.integration.calendar.all-day": "全天",
"com.affine.integration.calendar.new-doc": "新建文檔",
"com.affine.integration.calendar.show-events": "顯示日曆事件",