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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user