(function (root, factory) { const service = factory(); if (typeof module === 'object' && module.exports) module.exports = service; if (root) root.ChannelLanguageDetection = service; })(typeof globalThis !== 'undefined' ? globalThis : this, function () { const LANGUAGES = { fr: { code: 'fr', label: 'French' }, en: { code: 'en', label: 'English' }, es: { code: 'es', label: 'Spanish' } }; const UNKNOWN = { code: 'unknown', label: 'Unknown', confidence: 0, status: 'unknown' }; const EXPLICIT_CODES = { FR: 'fr', SFR: 'fr', 'FR-CAR': 'fr', EN: 'en', UK: 'en', US: 'en', AU: 'en', ES: 'es' }; const WORDS = { fr: ['french', 'francais', 'france'], en: ['english', 'anglais', 'england', 'british'], es: ['spanish', 'espagnol', 'espanol', 'spain'] }; function normalized(value) { return String(value || '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); } function addEvidence(scores, evidence, code, weight, type, value) { if (!code) return; scores[code] = (scores[code] || 0) + weight; evidence.push({ code, weight, type, value: String(value || '') }); } function findExplicitPrefix(name) { const match = String(name || '').trim().match(/^(?:\|\s*([A-Z-]{2,8})\s*\||\[\s*([A-Z-]{2,8})\s*\]|([A-Z-]{2,8})\s*\|)/i); return match ? (match[1] || match[2] || match[3]).toUpperCase() : null; } function scoreText(scores, evidence, value, weight, type, includeCountryCodes) { const text = normalized(value); if (!text) return; Object.entries(WORDS).forEach(([code, terms]) => { if (terms.some(term => new RegExp(`(?:^|[^a-z])${term}(?:$|[^a-z])`, 'i').test(text))) { addEvidence(scores, evidence, code, weight, type, value); } }); if (includeCountryCodes) { const tokens = text.toUpperCase().split(/[^A-Z]+/).filter(Boolean); const mapped = tokens.map(token => EXPLICIT_CODES[token]).filter(Boolean); [...new Set(mapped)].forEach(code => addEvidence(scores, evidence, code, weight, type, value)); } } function detectLanguage(variant, context = {}) { const scores = {}; const evidence = []; const prefix = findExplicitPrefix(variant.name); addEvidence(scores, evidence, EXPLICIT_CODES[prefix], 60, 'name-prefix', prefix); scoreText(scores, evidence, variant.name, 45, 'name-token', false); scoreText(scores, evidence, variant.groupTitle, 35, 'group', true); scoreText(scores, evidence, context.sourceName || variant.sourceName, 20, 'source', true); scoreText(scores, evidence, `${variant.tvgId || ''} ${variant.tvgName || ''}`, 20, 'metadata', true); const ranked = Object.entries(scores).sort((a, b) => b[1] - a[1]); const total = ranked.reduce((sum, entry) => sum + entry[1], 0); const percentages = Object.fromEntries(ranked.map(([code, score]) => [code, Math.round(score / total * 100)])); if (!ranked.length || total < 20) { return { language: { ...UNKNOWN }, languageScores: percentages, languageEvidence: evidence }; } const [topCode, topScore] = ranked[0]; const topConfidence = percentages[topCode]; const secondConfidence = ranked[1] ? percentages[ranked[1][0]] : 0; if (topScore < 20 || topConfidence < 45 || topConfidence - secondConfidence < 10) { return { language: { ...UNKNOWN }, languageScores: percentages, languageEvidence: evidence }; } let status = 'low'; if (topScore >= 60 && topConfidence >= 75) status = 'high'; else if (topScore >= 35 && topConfidence >= 60) status = 'medium'; return { language: { ...LANGUAGES[topCode], confidence: topConfidence, status }, languageScores: percentages, languageEvidence: evidence }; } return { LANGUAGES, detectLanguage }; });