Translation Infrastructure Complete:
- DeepL Pro API integration (2M+ chars translated)
- All 22 documents translated to German (de) and French (fr)
- 100% translation coverage across documentation
- Query parameter URL strategy (?lang=de, ?lang=fr)
Scripts & Tools:
- Updated translate-all-documents.js with 5-second rate limiting
- Added verify-translations.js for coverage verification
- Batch translation workflow with dry-run and progress tracking
Database:
- 43 translations stored in MongoDB (22 docs × 2 langs - 1 existing)
- Embedded translation schema with metadata tracking
- Zero translation failures
API Endpoints:
- GET /api/documents/:identifier?lang={de|fr}
- GET /api/documents/:identifier/translations
- POST /api/documents/:id/translate (admin)
Testing:
- All API endpoints verified and functional
- Language fallback to English working correctly
- Translation metadata tracking operational
🌐 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
132 lines
5.5 KiB
JavaScript
132 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Translation Verification Script
|
|
*
|
|
* Verifies that all documents have been translated correctly:
|
|
* - Checks which documents have German (de) translations
|
|
* - Checks which documents have French (fr) translations
|
|
* - Reports translation completeness and metadata
|
|
*/
|
|
|
|
require('dotenv').config();
|
|
const mongoose = require('mongoose');
|
|
const Document = require('../src/models/Document.model');
|
|
|
|
async function main() {
|
|
console.log('═══════════════════════════════════════════════════════════');
|
|
console.log(' TRANSLATION VERIFICATION');
|
|
console.log('═══════════════════════════════════════════════════════════\n');
|
|
|
|
// Connect to MongoDB
|
|
console.log('📡 Connecting to MongoDB...');
|
|
await mongoose.connect('mongodb://localhost:27017/tractatus_dev', {
|
|
serverSelectionTimeoutMS: 5000
|
|
});
|
|
console.log('✓ Connected to tractatus_dev\n');
|
|
|
|
// Fetch all public documents
|
|
const documents = await Document.list({
|
|
filter: { visibility: 'public' },
|
|
limit: 1000,
|
|
sort: { order: 1 }
|
|
});
|
|
|
|
console.log(`📚 Analyzing ${documents.length} public documents...\n`);
|
|
|
|
const stats = {
|
|
total: documents.length,
|
|
withDE: 0,
|
|
withFR: 0,
|
|
withBoth: 0,
|
|
withNone: 0,
|
|
details: []
|
|
};
|
|
|
|
// Check each document
|
|
for (const doc of documents) {
|
|
const hasDE = doc.translations && doc.translations.de && doc.translations.de.title;
|
|
const hasFR = doc.translations && doc.translations.fr && doc.translations.fr.title;
|
|
|
|
if (hasDE) stats.withDE++;
|
|
if (hasFR) stats.withFR++;
|
|
if (hasDE && hasFR) stats.withBoth++;
|
|
if (!hasDE && !hasFR) stats.withNone++;
|
|
|
|
stats.details.push({
|
|
slug: doc.slug,
|
|
title: doc.title,
|
|
de: hasDE,
|
|
fr: hasFR,
|
|
de_title: hasDE ? doc.translations.de.title : null,
|
|
fr_title: hasFR ? doc.translations.fr.title : null,
|
|
de_chars: hasDE ? doc.translations.de.content_markdown?.length || 0 : 0,
|
|
fr_chars: hasFR ? doc.translations.fr.content_markdown?.length || 0 : 0,
|
|
de_metadata: hasDE ? doc.translations.de.metadata : null,
|
|
fr_metadata: hasFR ? doc.translations.fr.metadata : null
|
|
});
|
|
}
|
|
|
|
// Display summary
|
|
console.log('═══════════════════════════════════════════════════════════');
|
|
console.log(' SUMMARY');
|
|
console.log('═══════════════════════════════════════════════════════════\n');
|
|
|
|
console.log(` Total documents: ${stats.total}`);
|
|
console.log(` With German (DE): ${stats.withDE} (${(stats.withDE / stats.total * 100).toFixed(1)}%)`);
|
|
console.log(` With French (FR): ${stats.withFR} (${(stats.withFR / stats.total * 100).toFixed(1)}%)`);
|
|
console.log(` With both languages: ${stats.withBoth} (${(stats.withBoth / stats.total * 100).toFixed(1)}%)`);
|
|
console.log(` With no translations: ${stats.withNone}\n`);
|
|
|
|
// Display details
|
|
console.log('═══════════════════════════════════════════════════════════');
|
|
console.log(' DOCUMENT DETAILS');
|
|
console.log('═══════════════════════════════════════════════════════════\n');
|
|
|
|
stats.details.forEach((detail, index) => {
|
|
const deStatus = detail.de ? '✓' : '✗';
|
|
const frStatus = detail.fr ? '✓' : '✗';
|
|
|
|
console.log(`${index + 1}. ${detail.title}`);
|
|
console.log(` Slug: ${detail.slug}`);
|
|
console.log(` DE: ${deStatus} ${detail.de ? `(${detail.de_chars.toLocaleString()} chars)` : ''}`);
|
|
if (detail.de) {
|
|
console.log(` Title: "${detail.de_title}"`);
|
|
console.log(` Translated: ${detail.de_metadata?.translated_at || 'unknown'}`);
|
|
}
|
|
console.log(` FR: ${frStatus} ${detail.fr ? `(${detail.fr_chars.toLocaleString()} chars)` : ''}`);
|
|
if (detail.fr) {
|
|
console.log(` Title: "${detail.fr_title}"`);
|
|
console.log(` Translated: ${detail.fr_metadata?.translated_at || 'unknown'}`);
|
|
}
|
|
console.log('');
|
|
});
|
|
|
|
// Missing translations
|
|
const missing = stats.details.filter(d => !d.de || !d.fr);
|
|
if (missing.length > 0) {
|
|
console.log('═══════════════════════════════════════════════════════════');
|
|
console.log(' MISSING TRANSLATIONS');
|
|
console.log('═══════════════════════════════════════════════════════════\n');
|
|
|
|
missing.forEach(doc => {
|
|
const missingLangs = [];
|
|
if (!doc.de) missingLangs.push('DE');
|
|
if (!doc.fr) missingLangs.push('FR');
|
|
console.log(` ${doc.slug}: Missing ${missingLangs.join(', ')}`);
|
|
});
|
|
console.log('');
|
|
}
|
|
|
|
await mongoose.disconnect();
|
|
console.log('✓ Database disconnected\n');
|
|
|
|
process.exit(0);
|
|
}
|
|
|
|
// Run
|
|
main().catch(err => {
|
|
console.error('\n❌ Fatal error:', err.message);
|
|
console.error(err.stack);
|
|
process.exit(1);
|
|
});
|