UNPKG

i18ntk

Version:

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

185 lines (161 loc) 6.78 kB
const DEFAULT_ENGLISH_THRESHOLD_PERCENT = 10; const URL_PATTERN = /https?:\/\/[^\s"'<>]+/i; const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/; const PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/; const BEARER_TOKEN_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/i; const CREDENTIAL_ASSIGNMENT_PATTERN = /\b(api[_-]?key|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|password|private[_-]?key|client[_-]?secret)\b\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{16,}/i; const CREDENTIAL_KEY_PATTERN = /\b(api[_-]?key|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|password|private[_-]?key|client[_-]?secret)\b/i; const OPAQUE_SECRET_PATTERN = /\b(AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|sk_live_[0-9A-Za-z]{16,}|xox[baprs]-[0-9A-Za-z-]{16,}|gh[pousr]_[0-9A-Za-z_]{20,}|[A-Za-z0-9._~+/=-]{32,})\b/; const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/; const ENGLISH_WORDS = new Set([ 'a', 'about', 'above', 'after', 'again', 'all', 'also', 'an', 'and', 'any', 'are', 'as', 'at', 'available', 'back', 'based', 'be', 'because', 'before', 'below', 'best', 'book', 'build', 'by', 'can', 'cash', 'challenge', 'change', 'check', 'choose', 'click', 'close', 'coming', 'complete', 'confirm', 'continue', 'copy', 'create', 'current', 'data', 'delete', 'depth', 'details', 'done', 'down', 'earn', 'edit', 'email', 'empty', 'enter', 'error', 'exchange', 'failed', 'file', 'filter', 'for', 'from', 'governance', 'has', 'have', 'help', 'hide', 'history', 'home', 'if', 'in', 'into', 'is', 'it', 'its', 'key', 'keys', 'language', 'last', 'latest', 'learn', 'live', 'loading', 'log', 'login', 'logout', 'market', 'markets', 'message', 'missing', 'more', 'new', 'next', 'no', 'not', 'of', 'off', 'on', 'open', 'or', 'order', 'other', 'out', 'over', 'overview', 'page', 'participate', 'players', 'please', 'platform', 'predict', 'prediction', 'previous', 'public', 'real', 'record', 'remove', 'required', 'reset', 'result', 'results', 'rewards', 'save', 'search', 'select', 'settings', 'show', 'soon', 'start', 'status', 'structured', 'submit', 'success', 'the', 'their', 'this', 'through', 'to', 'track', 'try', 'up', 'update', 'use', 'used', 'using', 'value', 'view', 'warning', 'when', 'with', 'without' ]); const DEFAULT_ALLOWED_ENGLISH_TERMS = new Set(['api']); function resolveAllowedEnglishTerms(options, projectConfig) { const terms = new Set(DEFAULT_ALLOWED_ENGLISH_TERMS); const sources = [ options && options.allowedEnglishTerms, projectConfig && projectConfig.allowedEnglishTerms ]; for (const source of sources) { if (Array.isArray(source)) { source.forEach(term => { if (typeof term === 'string' && term.trim()) { terms.add(term.trim().toLowerCase()); } }); } } return terms; } function normalizeLanguage(language) { return String(language || '').toLowerCase().split(/[-_]/)[0]; } function toAllowedTermSet(terms) { return resolveAllowedEnglishTerms({ allowedEnglishTerms: terms }); } function stripNonLanguageTokens(value) { return String(value || '') .replace(URL_PATTERN, ' ') .replace(EMAIL_PATTERN, ' ') .replace(/<[^>]+>/g, ' ') .replace(/\{\{[^}]+\}\}|\{[^}]+\}|%[sdifjoO]/g, ' '); } function isIgnoredEnglishToken(word, allowedTerms) { const normalized = word.toLowerCase().replace(/^['-]+|['-]+$/g, ''); if (!normalized || normalized.length <= 2) return true; if (allowedTerms.has(normalized)) return true; if (/^[A-Z0-9_-]{2,}$/.test(word)) return true; if (/\d/.test(word)) return true; return false; } function analyzeEnglishContent(value, options = {}) { const allowedTerms = toAllowedTermSet(options.allowedEnglishTerms); const words = stripNonLanguageTokens(value).match(/[A-Za-z][A-Za-z'-]*/g) || []; const countedWords = []; const englishWords = []; words.forEach(word => { if (isIgnoredEnglishToken(word, allowedTerms)) return; const normalized = word.toLowerCase().replace(/^['-]+|['-]+$/g, ''); countedWords.push(normalized); if (ENGLISH_WORDS.has(normalized)) { englishWords.push(normalized); } }); const totalWordCount = countedWords.length; const englishWordCount = englishWords.length; const englishPercentage = totalWordCount === 0 ? 0 : Number(((englishWordCount / totalWordCount) * 100).toFixed(2)); return { englishPercentage, englishWordCount, totalWordCount, englishWords: [...new Set(englishWords)].slice(0, 8) }; } function hasSecretLikeValue(value, keyPath = '') { const valueStr = String(value || ''); if ( PRIVATE_KEY_PATTERN.test(valueStr) || BEARER_TOKEN_PATTERN.test(valueStr) || CREDENTIAL_ASSIGNMENT_PATTERN.test(valueStr) || JWT_PATTERN.test(valueStr) ) { return true; } return CREDENTIAL_KEY_PATTERN.test(keyPath) && OPAQUE_SECRET_PATTERN.test(valueStr); } function detectTranslationContentRisks(value, options = {}) { const valueStr = String(value || ''); const issues = []; if (URL_PATTERN.test(valueStr)) { issues.push({ type: 'url', reason: 'Contains a URL; verify it is intentional and localized where needed.' }); } if (EMAIL_PATTERN.test(valueStr)) { issues.push({ type: 'email', reason: 'Contains an email address; verify it is intentional public contact content.' }); } if (hasSecretLikeValue(valueStr, options.keyPath)) { issues.push({ type: 'secret', reason: 'Looks like a credential or secret value, not ordinary translated content.' }); } const sourceLanguage = normalizeLanguage(options.sourceLanguage || 'en'); const targetLanguage = normalizeLanguage(options.targetLanguage); if (targetLanguage && targetLanguage !== sourceLanguage) { const threshold = Number.isFinite(Number(options.englishThresholdPercent)) ? Number(options.englishThresholdPercent) : DEFAULT_ENGLISH_THRESHOLD_PERCENT; const english = analyzeEnglishContent(valueStr, options); if ( english.englishPercentage > threshold && english.englishWordCount >= 3 ) { issues.push({ type: 'english_content', reason: `Possible untranslated English content (${english.englishPercentage}% English words, threshold ${threshold}%).`, englishPercentage: english.englishPercentage, englishThresholdPercent: threshold, englishWordCount: english.englishWordCount, totalWordCount: english.totalWordCount, englishWords: english.englishWords }); } } return issues; } module.exports = { DEFAULT_ENGLISH_THRESHOLD_PERCENT, analyzeEnglishContent, detectTranslationContentRisks, hasSecretLikeValue, resolveAllowedEnglishTerms };