feat: Complete internationalization and code cleanup
## Translation Files - Add 11 new language files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl) - Add 100+ missing translation keys across all 15 languages - New sections: notebook, pagination, ai.batchOrganization, ai.autoLabels - Update nav section with workspace, quickAccess, myLibrary keys ## Component Updates - Update 15+ components to use translation keys instead of hardcoded text - Components: notebook dialogs, sidebar, header, note-input, ghost-tags, etc. - Replace 80+ hardcoded English/French strings with t() calls - Ensure consistent UI across all supported languages ## Code Quality - Remove 77+ console.log statements from codebase - Clean up API routes, components, hooks, and services - Keep only essential error handling (no debugging logs) ## UI/UX Improvements - Update Keep logo to yellow post-it style (from-yellow-400 to-amber-500) - Change selection colors to #FEF3C6 (notebooks) and #EFB162 (nav items) - Make "+" button permanently visible in notebooks section - Fix grammar and syntax errors in multiple components ## Bug Fixes - Fix JSON syntax errors in it.json, nl.json, pl.json, zh.json - Fix syntax errors in notebook-suggestion-toast.tsx - Fix syntax errors in use-auto-tagging.ts - Fix syntax errors in paragraph-refactor.service.ts - Fix duplicate "fusion" section in nl.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Ou une version plus courte si vous préférez : feat(i18n): Add 15 languages, remove logs, update UI components - Create 11 new translation files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl) - Add 100+ translation keys: notebook, pagination, AI features - Update 15+ components to use translations (80+ strings) - Remove 77+ console.log statements from codebase - Fix JSON syntax errors in 4 translation files - Fix component syntax errors (toast, hooks, services) - Update logo to yellow post-it style - Change selection colors (#FEF3C6, #EFB162) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
245
scripts/migrate-to-notebooks.ts
Normal file
245
scripts/migrate-to-notebooks.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Migration Script: Tags → Notebooks with Contextual Labels
|
||||
*
|
||||
* This script migrates from the flat tags system to the new notebooks system.
|
||||
* It creates a default "Labels Migrés" notebook for each user and moves all
|
||||
* existing labels to that notebook. Notes remain in "Notes générales" (Inbox).
|
||||
*
|
||||
* IMPORTANT: This is a NON-BREAKING migration. The existing system continues
|
||||
* to work during and after migration.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/migrate-to-notebooks.ts
|
||||
*
|
||||
* Rollback:
|
||||
* npx tsx scripts/rollback-notebooks.ts
|
||||
*/
|
||||
|
||||
import { prisma } from '../keep-notes/lib/prisma'
|
||||
|
||||
interface MigrationStats {
|
||||
usersProcessed: number
|
||||
notebooksCreated: number
|
||||
labelsMigrated: number
|
||||
notesAffected: number
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Main migration function
|
||||
*/
|
||||
async function migrateToNotebooks(): Promise<MigrationStats> {
|
||||
console.log('🚀 Starting migration to notebooks...\n')
|
||||
|
||||
const stats: MigrationStats = {
|
||||
usersProcessed: 0,
|
||||
notebooksCreated: 0,
|
||||
labelsMigrated: 0,
|
||||
notesAffected: 0,
|
||||
errors: [],
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Get all users
|
||||
console.log('📊 Fetching users...')
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (users.length === 0) {
|
||||
console.log('⚠️ No users found. Nothing to migrate.')
|
||||
return stats
|
||||
}
|
||||
|
||||
console.log(`✅ Found ${users.length} user(s)\n`)
|
||||
|
||||
// Step 2: Process each user
|
||||
for (const user of users) {
|
||||
console.log(`\n👤 Processing user: ${user.name || user.email} (${user.id})`)
|
||||
|
||||
try {
|
||||
// Step 2.1: Check if migration notebook already exists
|
||||
const existingNotebook = await prisma.notebook.findFirst({
|
||||
where: {
|
||||
name: 'Labels Migrés',
|
||||
userId: user.id,
|
||||
},
|
||||
})
|
||||
|
||||
if (existingNotebook) {
|
||||
console.log(' ⏭️ Migration notebook already exists, skipping...')
|
||||
continue
|
||||
}
|
||||
|
||||
// Step 2.2: Create "Labels Migrés" notebook
|
||||
console.log(' 📁 Creating "Labels Migrés" notebook...')
|
||||
const migrationNotebook = await prisma.notebook.create({
|
||||
data: {
|
||||
id: `migrate-${user.id}`, // Deterministic ID
|
||||
name: 'Labels Migrés',
|
||||
icon: '📦',
|
||||
color: '#9CA3AF', // gray-400
|
||||
order: 999, // At the bottom
|
||||
userId: user.id,
|
||||
},
|
||||
})
|
||||
|
||||
stats.notebooksCreated++
|
||||
console.log(` ✅ Created notebook: ${migrationNotebook.id}`)
|
||||
|
||||
// Step 2.3: Migrate all labels for this user
|
||||
console.log(' 🏷️ Migrating labels...')
|
||||
const labels = await prisma.label.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
notebookId: {
|
||||
equals: '', // Empty string = not migrated yet
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Also try with null (in case database has null values)
|
||||
const labelsWithNull = await prisma.label.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
notebookId: null,
|
||||
},
|
||||
})
|
||||
|
||||
// Combine both results (avoid duplicates)
|
||||
const allLabels = [...labels]
|
||||
for (const label of labelsWithNull) {
|
||||
if (!allLabels.find(l => l.id === label.id)) {
|
||||
allLabels.push(label)
|
||||
}
|
||||
}
|
||||
|
||||
if (allLabels.length === 0) {
|
||||
console.log(' ℹ️ No labels to migrate')
|
||||
continue
|
||||
}
|
||||
|
||||
// Update all labels to belong to the migration notebook
|
||||
const labelUpdates = allLabels.map((label) =>
|
||||
prisma.label.update({
|
||||
where: { id: label.id },
|
||||
data: { notebookId: migrationNotebook.id },
|
||||
})
|
||||
)
|
||||
|
||||
await prisma.$transaction(labelUpdates)
|
||||
stats.labelsMigrated += allLabels.length
|
||||
console.log(` ✅ Migrated ${allLabels.length} label(s)`)
|
||||
|
||||
// Step 2.4: Count notes that will be affected (information only)
|
||||
const noteCount = await prisma.note.count({
|
||||
where: { userId: user.id },
|
||||
})
|
||||
stats.notesAffected += noteCount
|
||||
console.log(` ℹ️ User has ${noteCount} note(s) (will remain in "Notes générales")`)
|
||||
|
||||
stats.usersProcessed++
|
||||
} catch (userError) {
|
||||
const errorMsg = `Failed to migrate user ${user.id}: ${userError}`
|
||||
console.error(` ❌ ${errorMsg}`)
|
||||
stats.errors.push(errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '='.repeat(60))
|
||||
console.log('✅ Migration complete!\n')
|
||||
console.log('📊 Summary:')
|
||||
console.log(` Users processed: ${stats.usersProcessed}`)
|
||||
console.log(` Notebooks created: ${stats.notebooksCreated}`)
|
||||
console.log(` Labels migrated: ${stats.labelsMigrated}`)
|
||||
console.log(` Notes affected: ${stats.notesAffected} (all remain in Inbox)`)
|
||||
|
||||
if (stats.errors.length > 0) {
|
||||
console.log(`\n⚠️ Errors: ${stats.errors.length}`)
|
||||
stats.errors.forEach((error) => console.log(` ❌ ${error}`))
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60))
|
||||
console.log('\n✨ Migration successful!')
|
||||
console.log('\n📌 Next steps:')
|
||||
console.log(' 1. Test the application to ensure everything works')
|
||||
console.log(' 2. Users can now organize their notes into notebooks')
|
||||
console.log(' 3. Users can move labels from "Labels Migrés" to new notebooks')
|
||||
console.log(' 4. Consider deleting old labels field from Note model after verification\n')
|
||||
|
||||
return stats
|
||||
} catch (error) {
|
||||
console.error('\n❌ Migration failed:', error)
|
||||
throw error
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dry-run mode: Show what would happen without making changes
|
||||
*/
|
||||
async function dryRunMigration() {
|
||||
console.log('🔍 DRY RUN MODE - No changes will be made\n')
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`Found ${users.length} user(s)\n`)
|
||||
|
||||
for (const user of users) {
|
||||
console.log(`\n👤 User: ${user.name || user.email}`)
|
||||
|
||||
const labels = await prisma.label.findMany({
|
||||
where: { userId: user.id },
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
|
||||
const notes = await prisma.note.count({
|
||||
where: { userId: user.id },
|
||||
})
|
||||
|
||||
console.log(` Labels: ${labels.length}`)
|
||||
console.log(` Notes: ${notes}`)
|
||||
console.log(` Would create: "Labels Migrés" notebook`)
|
||||
console.log(` Would migrate: ${labels.length} labels`)
|
||||
}
|
||||
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
|
||||
// Run migration
|
||||
const args = process.argv.slice(2)
|
||||
const isDryRun = args.includes('--dry-run')
|
||||
|
||||
if (isDryRun) {
|
||||
dryRunMigration()
|
||||
.then(() => {
|
||||
console.log('\n✅ Dry run complete')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n❌ Dry run failed:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
} else {
|
||||
migrateToNotebooks()
|
||||
.then(() => {
|
||||
console.log('\n✨ All done!')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 Fatal error:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
167
scripts/rollback-notebooks.ts
Normal file
167
scripts/rollback-notebooks.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Rollback Script: Notebooks → Tags
|
||||
*
|
||||
* This script rolls back the notebooks migration by:
|
||||
* 1. Deleting all notebooks
|
||||
* 2. Removing notebookId from labels (set to empty string)
|
||||
* 3. Removing notebookId from notes (set to null)
|
||||
*
|
||||
* WARNING: This will revert ALL notebook-related changes!
|
||||
* Make sure you have a database backup before running this.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/rollback-notebooks.ts
|
||||
*
|
||||
* Confirm by adding --confirm flag:
|
||||
* npx tsx scripts/rollback-notebooks.ts --confirm
|
||||
*/
|
||||
|
||||
import { prisma } from '../keep-notes/lib/prisma'
|
||||
|
||||
interface RollbackStats {
|
||||
notebooksDeleted: number
|
||||
labelsUpdated: number
|
||||
notesUpdated: number
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Main rollback function
|
||||
*/
|
||||
async function rollbackNotebooks(confirm = false): Promise<RollbackStats> {
|
||||
console.log('⚠️ NOTEBOOKS ROLLBACK\n')
|
||||
|
||||
if (!confirm) {
|
||||
console.log('❌ ERROR: Rollback requires confirmation.')
|
||||
console.log('\nTo rollback, run with --confirm flag:')
|
||||
console.log(' npx tsx scripts/rollback-notebooks.ts --confirm')
|
||||
console.log('\n⚠️ This will:')
|
||||
console.log(' - Delete ALL notebooks')
|
||||
console.log(' - Remove notebookId from all labels')
|
||||
console.log(' - Remove notebookId from all notes')
|
||||
console.log(' - IRREVERSIBLY delete notebook associations\n')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('🔄 Starting rollback...\n')
|
||||
|
||||
const stats: RollbackStats = {
|
||||
notebooksDeleted: 0,
|
||||
labelsUpdated: 0,
|
||||
notesUpdated: 0,
|
||||
errors: [],
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Get count before rollback
|
||||
console.log('📊 Counting items to rollback...')
|
||||
|
||||
const notebookCount = await prisma.notebook.count()
|
||||
const labelCount = await prisma.label.count({
|
||||
where: {
|
||||
notebookId: {
|
||||
not: '',
|
||||
},
|
||||
},
|
||||
})
|
||||
const noteCount = await prisma.note.count({
|
||||
where: {
|
||||
notebookId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
console.log(` Notebooks to delete: ${notebookCount}`)
|
||||
console.log(` Labels to update: ${labelCount}`)
|
||||
console.log(` Notes to update: ${noteCount}\n`)
|
||||
|
||||
if (notebookCount === 0 && labelCount === 0 && noteCount === 0) {
|
||||
console.log('ℹ️ Nothing to rollback. System is already in pre-migration state.\n')
|
||||
return stats
|
||||
}
|
||||
|
||||
// Step 2: Update labels (remove notebookId)
|
||||
if (labelCount > 0) {
|
||||
console.log('🏷️ Updating labels...')
|
||||
const labelResult = await prisma.label.updateMany({
|
||||
where: {
|
||||
notebookId: {
|
||||
not: '',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
notebookId: '',
|
||||
},
|
||||
})
|
||||
stats.labelsUpdated = labelResult.count
|
||||
console.log(` ✅ Updated ${stats.labelsUpdated} label(s)`)
|
||||
}
|
||||
|
||||
// Step 3: Update notes (remove notebookId)
|
||||
if (noteCount > 0) {
|
||||
console.log('📝 Updating notes...')
|
||||
const noteResult = await prisma.note.updateMany({
|
||||
where: {
|
||||
notebookId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
notebookId: null,
|
||||
},
|
||||
})
|
||||
stats.notesUpdated = noteResult.count
|
||||
console.log(` ✅ Updated ${stats.notesUpdated} note(s)`)
|
||||
}
|
||||
|
||||
// Step 4: Delete notebooks (cascade deletes labels, but notes are already updated)
|
||||
if (notebookCount > 0) {
|
||||
console.log('📁 Deleting notebooks...')
|
||||
const notebookResult = await prisma.notebook.deleteMany({})
|
||||
stats.notebooksDeleted = notebookResult.count
|
||||
console.log(` ✅ Deleted ${stats.notebooksDeleted} notebook(s)`)
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '='.repeat(60))
|
||||
console.log('✅ Rollback complete!\n')
|
||||
console.log('📊 Summary:')
|
||||
console.log(` Notebooks deleted: ${stats.notebooksDeleted}`)
|
||||
console.log(` Labels updated: ${stats.labelsUpdated}`)
|
||||
console.log(` Notes updated: ${stats.notesUpdated}`)
|
||||
|
||||
if (stats.errors.length > 0) {
|
||||
console.log(`\n⚠️ Errors: ${stats.errors.length}`)
|
||||
stats.errors.forEach((error) => console.log(` ❌ ${error}`))
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60))
|
||||
console.log('\n✨ Rollback successful!')
|
||||
console.log('\n📌 System is now back to the flat tags model:')
|
||||
console.log(' - All notebooks removed')
|
||||
console.log(' - All labels detached from notebooks')
|
||||
console.log(' - All notes back in "Notes générales" (Inbox)\n')
|
||||
|
||||
return stats
|
||||
} catch (error) {
|
||||
console.error('\n❌ Rollback failed:', error)
|
||||
throw error
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Run rollback
|
||||
const args = process.argv.slice(2)
|
||||
const isConfirmed = args.includes('--confirm')
|
||||
|
||||
rollbackNotebooks(isConfirmed)
|
||||
.then(() => {
|
||||
console.log('✨ All done!')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 Fatal error:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
39
scripts/verify-migration.js
Normal file
39
scripts/verify-migration.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const { PrismaClient } = require('../keep-notes/prisma/client-generated');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
(async () => {
|
||||
console.log('=== VERIFICATION DE LA MIGRATION ===\n');
|
||||
|
||||
// 1. Verifier les notebooks
|
||||
const notebooks = await prisma.notebook.findMany({ include: { labels: true } });
|
||||
console.log('Notebooks crees:', notebooks.length);
|
||||
notebooks.forEach(nb => {
|
||||
console.log(` - ${nb.name}: ${nb.labels?.length || 0} labels`);
|
||||
});
|
||||
|
||||
// 2. Verifier les labels
|
||||
const labels = await prisma.label.findMany({ where: { notebookId: { not: null } } });
|
||||
console.log(`\nLabels avec notebookId: ${labels.length}`);
|
||||
|
||||
// 3. Verifier les notes
|
||||
const notesInbox = await prisma.note.count({ where: { notebookId: null } });
|
||||
const notesInNotebooks = await prisma.note.count({ where: { notebookId: { not: null } } });
|
||||
console.log(`Notes dans Inbox: ${notesInbox}`);
|
||||
console.log(`Notes dans des notebooks: ${notesInNotebooks}`);
|
||||
|
||||
// 4. Total
|
||||
const totalNotes = await prisma.note.count();
|
||||
const totalLabels = await prisma.label.count();
|
||||
console.log(`\nTotaux:`);
|
||||
console.log(` - Notes totales: ${totalNotes}`);
|
||||
console.log(` - Labels totaux: ${totalLabels}`);
|
||||
|
||||
// 5. Sample labels with their notebook
|
||||
console.log('\nSample labels:');
|
||||
const sampleLabels = await prisma.label.findMany({ take: 3, include: { notebook: true } });
|
||||
sampleLabels.forEach(l => {
|
||||
console.log(` - ${l.name} -> ${l.notebook?.name || 'no notebook'}`);
|
||||
});
|
||||
|
||||
await prisma.$disconnect();
|
||||
})();
|
||||
Reference in New Issue
Block a user