Files
ackify/webapp/scripts/check-i18n.js
Benjamin e95185f9c7 feat: migrate to Vue.js SPA with API-first architecture
Major refactoring to modernize the application architecture:

Backend changes:
- Restructure API with v1 versioning and modular handlers
- Add comprehensive OpenAPI specification
- Implement RESTful endpoints for documents, signatures, admin
- Add checksum verification system for document integrity
- Add server-side runtime injection of ACKIFY_BASE_URL and meta tags
- Generate dynamic Open Graph/Twitter Card meta tags for unfurling
- Remove legacy HTML template handlers
- Isolate backend source on dedicated folder
- Improve tests suite

Frontend changes:
- Migrate from Go templates to Vue.js 3 SPA with TypeScript
- Add Tailwind CSS with shadcn/vue components
- Implement i18n support (fr, en, es, de, it)
- Add admin dashboard for document and signer management
- Add signature tracking with file checksum verification
- Add embed page with sign button linking to main app
- Implement dark mode and accessibility features
- Auto load file to compute checksum

Infrastructure:
- Update Dockerfile for SPA build process
- Simplify deployment with embedded frontend assets
- Add migration for checksum_verifications table

This enables better UX, proper link previews on social platforms,
and provides a foundation for future enhancements.
2025-10-26 02:32:10 +02:00

129 lines
3.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Script to verify i18n translation coverage
* Checks that all keys in en.json exist in other locale files
*/
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const localesDir = join(__dirname, '../src/locales');
const referenceLocale = 'en';
const otherLocales = ['fr', 'es', 'de', 'it'];
/**
* Flatten nested object keys
* e.g. { a: { b: { c: 'value' } } } => ['a.b.c']
*/
function flattenKeys(obj, prefix = '') {
return Object.keys(obj).reduce((acc, key) => {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
return acc.concat(flattenKeys(obj[key], newKey));
}
return acc.concat(newKey);
}, []);
}
/**
* Check if a key exists in an object
*/
function hasKey(obj, keyPath) {
const keys = keyPath.split('.');
let current = obj;
for (const key of keys) {
if (typeof current !== 'object' || current === null || !(key in current)) {
return false;
}
current = current[key];
}
return true;
}
// Load reference locale
let referenceMessages;
try {
referenceMessages = JSON.parse(
readFileSync(join(localesDir, `${referenceLocale}.json`), 'utf-8')
);
} catch (error) {
console.error(`❌ Failed to load reference locale (${referenceLocale}.json):`, error.message);
process.exit(1);
}
const referenceKeys = flattenKeys(referenceMessages);
console.log(`📚 Reference locale (${referenceLocale}): ${referenceKeys.length} keys\n`);
let hasErrors = false;
const report = [];
// Check each locale
for (const locale of otherLocales) {
try {
const messages = JSON.parse(
readFileSync(join(localesDir, `${locale}.json`), 'utf-8')
);
const localeKeys = flattenKeys(messages);
const missingKeys = referenceKeys.filter(key => !hasKey(messages, key));
const extraKeys = localeKeys.filter(key => !hasKey(referenceMessages, key));
if (missingKeys.length === 0 && extraKeys.length === 0) {
console.log(`${locale}.json: ${localeKeys.length} keys (complete)`);
report.push({ locale, status: 'ok', total: localeKeys.length });
} else {
hasErrors = true;
console.log(`⚠️ ${locale}.json: ${localeKeys.length} keys`);
if (missingKeys.length > 0) {
console.log(` Missing ${missingKeys.length} keys:`);
missingKeys.slice(0, 10).forEach(key => console.log(` - ${key}`));
if (missingKeys.length > 10) {
console.log(` ... and ${missingKeys.length - 10} more`);
}
}
if (extraKeys.length > 0) {
console.log(` Extra ${extraKeys.length} keys (not in reference):`);
extraKeys.slice(0, 5).forEach(key => console.log(` - ${key}`));
if (extraKeys.length > 5) {
console.log(` ... and ${extraKeys.length - 5} more`);
}
}
report.push({
locale,
status: 'incomplete',
total: localeKeys.length,
missing: missingKeys.length,
extra: extraKeys.length
});
}
console.log('');
} catch (error) {
console.error(`❌ Failed to load ${locale}.json:`, error.message);
hasErrors = true;
report.push({ locale, status: 'error', error: error.message });
}
}
// Summary
console.log('='.repeat(60));
if (hasErrors) {
console.log('❌ Translation coverage check FAILED');
console.log('\nSome locales have missing or extra keys.');
console.log('Please update the translations to match the reference locale.');
process.exit(1);
} else {
console.log('✅ All translations are complete!');
console.log(`\nAll ${otherLocales.length} locales have ${referenceKeys.length} keys matching the reference.`);
process.exit(0);
}