UNPKG

i18ntk

Version:

i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.

63 lines (55 loc) 2.85 kB
'use strict'; const { isRtlLanguage } = require('./language-registry'); const PLACEHOLDER = /(?:\{\{\s*([\w.-]+)\s*\}\}|\{\s*([\w.-]+)(?:\s*,[^}]*)?\}|%\(([\w.-]+)\)s|%([a-z])|:\b([A-Za-z_]\w*)\b)/g; const TAG = /<\/?([A-Za-z][\w-]*)\b[^>]*>/g; const BIDI_CONTROL = /[\u202A-\u202E\u2066-\u2069]/g; const SOURCE_COPY_MARKER = /^\s*\[[A-Z]{2,3}(?:[-_][A-Z0-9]{2,4})?\]\s+/i; function isSourceCopyMarker(value) { return typeof value === 'string' && SOURCE_COPY_MARKER.test(value); } function collect(regex, value) { const found = []; let match; regex.lastIndex = 0; while ((match = regex.exec(String(value || ''))) !== null) found.push(match.slice(1).find(Boolean) || match[0]); return found.sort(); } function validateTranslation(source, target, locale) { const issues = []; const sourceText = String(source ?? ''); const targetText = String(target ?? ''); const sourcePlaceholders = collect(PLACEHOLDER, sourceText); const targetPlaceholders = collect(PLACEHOLDER, targetText); if (JSON.stringify(sourcePlaceholders) !== JSON.stringify(targetPlaceholders)) { issues.push({ code: 'placeholderMismatch', severity: 'error', source: sourcePlaceholders, target: targetPlaceholders }); } if (JSON.stringify(collect(TAG, sourceText)) !== JSON.stringify(collect(TAG, targetText))) { issues.push({ code: 'tagMismatch', severity: 'error' }); } if (targetText.match(BIDI_CONTROL)) issues.push({ code: 'unsafeBidiControl', severity: 'error' }); if (isSourceCopyMarker(targetText)) issues.push({ code: 'sourceCopyMarker', severity: 'warning' }); if (sourceText.length >= 4 && sourceText === targetText && !String(locale).toLowerCase().startsWith('en')) { issues.push({ code: 'possiblyUntranslated', severity: 'warning' }); } return { valid: !issues.some(issue => issue.severity === 'error'), locale, direction: isRtlLanguage(locale) ? 'rtl' : 'ltr', issues }; } function isolateForTerminal(value, locale) { const text = String(value ?? '').replace(BIDI_CONTROL, ''); return isRtlLanguage(locale) ? `\u2067${text}\u2069` : text; } function validateTranslationEntries(entries, locale) { const results = []; for (const entry of entries || []) { const result = validateTranslation(entry?.source, entry?.target, entry?.locale || locale); results.push({ key: entry?.key, ...result }); } const issues = results.flatMap(result => result.issues.map(issue => ({ key: result.key, locale: result.locale, ...issue }))); return { valid: results.every(result => result.valid), checked: results.length, errors: issues.filter(issue => issue.severity === 'error').length, warnings: issues.filter(issue => issue.severity === 'warning').length, issues, results }; } module.exports = { validateTranslation, validateTranslationEntries, isolateForTerminal, isSourceCopyMarker };