1// LanguageManager.js - Handles internationalization (i18n) for MTGProxy.net 2 3import { translations, availableLanguages, getCurrentLanguage, setCurrentLanguage, getCardLanguagePreference, setCardLanguagePreference } from './i18n.js'; 4import { trackLanguageChanged } from './utils/analytics.js'; 5 6export class LanguageManager { 7 constructor() { 8 // Language code mappings for button display (native abbreviations) 9 this.languageCodes = { 10 en: 'EN', 11 es: 'ES', 12 fr: 'FR', 13 de: 'DE', 14 it: 'IT', 15 pt: 'PT', 16 ja: 'æ¥', 17 ko: 'í', 18 ru: 'РУ', 19 zhs: 'ç®', 20 zht: 'ç¹', 21 ph: '<i class="ms ms-p"></i>' // Phyrexian mana symbol 22 }; 23 24 // Detect language from URL first, then fall back to localStorage 25 this.currentLanguage = this.detectLanguageFromURL() || getCurrentLanguage(); 26 this.init(); 27 } 28 29 detectLanguageFromURL() { 30 // Extract language from URL path (e.g., /es/, /fr/, /de/, /zhs/, /zht/, /ph/) 31 const pathMatch = window.location.pathname.match(/^\/(en|es|fr|de|it|pt|ja|ko|ru|zhs|zht|ph)/); 32 if (pathMatch && availableLanguages.includes(pathMatch[1])) { 33 const urlLang = pathMatch[1]; 34 // Sync URL language with localStorage 35 setCurrentLanguage(urlLang); 36 return urlLang; 37 } 38 return null; 39 } 40 41 init() { 42 // Set initial data-language attribute on body 43 document.body.setAttribute('data-language', this.currentLanguage); 44 45 this.populateLanguageSelector(); 46 this.setupEventListeners(); 47 this.updateAllUIText(); 48 this.updateLanguageButton(); 49 } 50 51 populateLanguageSelector() { 52 const languageDropdown = document.getElementById('languageDropdown'); 53 if (!languageDropdown) return; 54 55 languageDropdown.innerHTML = ''; 56
57 availableLanguages.forEach(lang => { 58 const option = document.createElement('div'); 59 option.className = 'language-option'; 60 option.dataset.lang = lang; 61 62 if (lang === this.currentLanguage) { 63 option.classList.add('selected'); 64 } 65 66 const code = this.languageCodes[lang] || lang.toUpperCase(); 67 68 // For Phyrexian, use HTML with mana symbol 69 if (lang === 'ph') { 70 option.innerHTML = code; 71 } else { 72 option.textContent = code; 73 } 74 75 option.addEventListener('click', () => { 76 this.switchLanguage(lang); 77 }); 78 79 languageDropdown.appendChild(option); 80 }); 81 } 82 83 setupEventListeners() { 84 // Event listeners for button clicks are now handled in the unified 85 // utility buttons script in index.html for mutual exclusivity 86 // This only sets up language option click handlers 87 } 88 89 closeLanguageSelector() { 90 const languageDropdown = document.getElementById('languageDropdown'); 91 if (languageDropdown) { 92 languageDropdown.style.display = 'none'; 93 } 94 } 95 96 switchLanguage(lang) { 97 if (!availableLanguages.includes(lang)) return; 98 99 // Track language change in GA4 100 const fromLang = this.currentLanguage; 101 trackLanguageChanged(fromLang, lang); 102 103 // Store preference in localStorage 104 setCurrentLanguage(lang); 105 106 // Store preference in cookie for server-side detection 107 document.cookie = `preferred_language=${lang}; path=/; max-age=31536000`; // 1 year 108 109 // Convert URL to new language 110 const currentPath = window.location.pathname; 111 const pathWithoutLang = currentPath.replace(/^\/(en|es|fr|de|it|pt|ja|ko|ru|zhs|zht|ph)/, ''); 112 const newPath = lang === 'en' ? pathWithoutLang || '/' : `/${lang}${pathWithoutLang || ''}`; 113 114 // Navigate to new URL (this will reload the page with correct SEO meta tags) 115 window.location.href = newPath; 116 } 117 118 updateLanguageButton() { 119 const languageSelectorBtn = document.getElementById('languageSelectorBtn'); 120 if (languageSelectorBtn) { 121 const code = this.languageCodes[this.currentLanguage] || 'EN'; 122 // Update data attribute for styling purposes 123 languageSelectorBtn.setAttribute('data-lang', this.currentLanguage); 124 // For Phyrexian, use HTML with mana symbol 125 if (this.currentLanguage === 'ph') { 126 languageSelectorBtn.innerHTML = code; 127 } else { 128 languageSelectorBtn.textContent = code; 129 } 130 } 131 } 132 133 updateAllUIText() { 134 const t = translations[this.currentLanguage]; 135 136 // Header 137 const tagline = document.getElementById('tagline'); 138 if (tagline) tagline.textContent = t.tagline; 139 140 // Deck Section 141 const deckSection = document.querySelector('.deck-section h2'); 142 if (deckSection) deckSection.textContent = t.deckList; 143 144 const deckInput = document.getElementById('deckInput'); 145 if (deckInput) deckInput.placeholder = t.deckPlaceholder; 146 147 const submitBtn = document.getElementById('submitBtn'); 148 if (submitBtn) submitBtn.textContent = t.generateCards; 149 150 const cardSearchInput = document.getElementById('cardSearchInput'); 151 if (cardSearchInput) cardSearchInput.placeholder = t.searchByName; 152 153 const advancedSearchBtn = document.getElementById('advancedSearchBtn'); 154 if (advancedSearchBtn) advancedSearchBtn.textContent = t.advancedSearch; 155 156 // Download buttons 157 const downloadBtns = document.querySelectorAll('#downloadBtn, #topDownloadBtn'); 158 downloadBtns.forEach(btn => { 159 if (btn) btn.textContent = t.downloadPDF; 160 }); 161 162 const printBtns = document.querySelectorAll('#printBtn, #topPrintBtn'); 163 printBtns.forEach(btn => { 164 if (btn) btn.textContent = t.printSheets; 165 }); 166 167 const advancedBtns = document.querySelectorAll('#advancedSettingsBtn, #topAdvancedSettingsBtn'); 168 advancedBtns.forEach(btn => { 169 if (btn) btn.textContent = t.advanced; 170 }); 171 172 const settingsBtns = document.querySelectorAll('#settingsDropdownBtn, #topSettingsDropdownBtn'); 173 settingsBtns.forEach(btn => { 174 if (btn) { 175 const text = btn.childNodes[0]; 176 if (text && text.nodeType === Node.TEXT_NODE) { 177 text.textContent = t.printSettings + '\n '; 178 } 179 } 180 }); 181 182 // Settings dropdown 183 this.updateSettingsDropdown(t); 184 185 // Advanced Settings Modal 186 this.updateAdvancedSettingsModal(t); 187 188 // Card count display 189 this.updateCardCount(); 190 191 // Update Advanced Search Modal if it's open 192 this.updateAdvancedSearchModalIfOpen(t); 193 194 // Update donation section 195 if (window.MTGProxyApp && window.MTGProxyApp.donationManager) { 196 window.MTGProxyApp.donationManager.updateDonationText(); 197 } 198 } 199 200 updateAdvancedSearchModalIfOpen(t) { 201 // Check if Advanced Search modal is open 202 const modal = document.querySelector('.advanced-search-modal'); 203 if (!modal) return; 204 205 // Update modal title 206 const modalTitle = modal.querySelector('.modal-header h2'); 207 if (modalTitle) modalTitle.textContent = t.advancedSearchTitle; 208 209 // Update field labels 210 const cardNameLabel = modal.querySelector('label[for="adv-card-name"]'); 211 if (cardNameLabel) cardNameLabel.textContent = t.cardName; 212
213 const setLabel = modal.querySelector('label[for="adv-set"]'); 214 if (setLabel) setLabel.textContent = t.setExpansion; 215 216 const oracleLabel = modal.querySelector('label[for="adv-oracle-text"]'); 217 if (oracleLabel) oracleLabel.textContent = t.oracleText; 218 219 // Update exact phrases hint 220 const exactPhrasesHint = modal.querySelector('.field-hint'); 221 if (exactPhrasesHint) exactPhrasesHint.textContent = t.exactPhrasesHint; 222 223 const typeLabel = modal.querySelector('label[for="adv-card-type"]'); 224 if (typeLabel) typeLabel.textContent = t.type; 225 226 // Update Format Legality section 227 const formatLegalityLabel = modal.querySelector('.advanced-search-field label'); 228 if (formatLegalityLabel && formatLegalityLabel.textContent.includes('Format') || formatLegalityLabel.textContent.includes('Légalité')) { 229 formatLegalityLabel.textContent = t.formatLegality; 230 } 231 232 // Update Status dropdown button 233 const statusBtn = modal.querySelector('#legalityStatusDropdownBtn'); 234 if (statusBtn) { 235 const textNode = Array.from(statusBtn.childNodes).find(node => node.nodeType === Node.TEXT_NODE); 236 if (textNode) textNode.textContent = t.status; 237 } 238 239 // Update Status dropdown options 240 const statusMenu = modal.querySelector('#legalityStatusDropdownMenu'); 241 if (statusMenu) { 242 const statusOptions = statusMenu.querySelectorAll('.dropdown-toggle-label span:first-child'); 243 if (statusOptions[0]) statusOptions[0].textContent = t.legal; 244 if (statusOptions[1]) statusOptions[1].textContent = t.notLegal; 245 if (statusOptions[2]) statusOptions[2].textContent = t.banned; 246 if (statusOptions[3]) statusOptions[3].textContent = t.restricted; 247 } 248 249 // Update Format dropdown button 250 const formatBtn = modal.querySelector('#formatLegalityDropdownBtn'); 251 if (formatBtn) { 252 const textNode = Array.from(formatBtn.childNodes).find(node => node.nodeType === Node.TEXT_NODE); 253 if (textNode) textNode.textContent = t.format; 254 } 255 256 // Update Format dropdown options 257 const formatMenu = modal.querySelector('#formatLegalityDropdownMenu'); 258 if (formatMenu) { 259 const formatOptions = formatMenu.querySelectorAll('.dropdown-toggle-label span:first-child'); 260 if (formatOptions[0]) formatOptions[0].textContent = t.formatCommander; 261 if (formatOptions[1]) formatOptions[1].textContent = t.formatStandard; 262 if (formatOptions[2]) formatOptions[2].textContent = t.formatModern; 263 if (formatOptions[3]) formatOptions[3].textContent = t.formatPioneer; 264 if (formatOptions[4]) formatOptions[4].textContent = t.formatHistoric; 265 if (formatOptions[5]) formatOptions[5].textContent = t.formatBrawl; 266 if (formatOptions[6]) formatOptions[6].textContent = t.formatStandardBrawl; 267 if (formatOptions[7]) formatOptions[7].textContent = t.formatPauper; 268 if (formatOptions[8]) formatOptions[8].textContent = t.formatLegacy; 269 if (formatOptions[9]) formatOptions[9].textContent = t.formatVintage; 270 if (formatOptions[10]) formatOptions[10].textContent = t.formatExplorer; 271 if (formatOptions[11]) formatOptions[11].textContent = t.formatTimeless; 272 if (formatOptions[12]) formatOptions[12].textContent = t.formatAlchemy; 273 if (formatOptions[13]) formatOptions[13].textContent = t.formatPremodern; 274 if (formatOptions[14]) formatOptions[14].textContent = t.formatOldschool; 275 if (formatOptions[15]) formatOptions[15].textContent = t.formatOathbreaker; 276 if (formatOptions[16]) formatOptions[16].textContent = t.formatPauperCommander; 277 if (formatOptions[17]) formatOptions[17].textContent = t.formatPremodernCommander; 278 if (formatOptions[18]) formatOptions[18].textContent = t.formatDuelCommander; 279 if (formatOptions[19]) formatOptions[19].textContent = t.formatGladiator; 280 if (formatOptions[20]) formatOptions[20].textContent = t.formatPenny; 281 if (formatOptions[21]) formatOptions[21].textContent = t.formatFuture; 282 } 283 284 // Update Colors label 285 const colorsLabels = modal.querySelectorAll('.advanced-search-field label'); 286 for (let label of colorsLabels) { 287 if (label.textContent === 'Colors' || label.textContent === 'Couleurs' || label.textContent === 'Colores') { 288 label.textContent = t.colors; 289 break; 290 } 291 } 292 293 // Update Color Identity label 294 for (let label of colorsLabels) { 295 if (label.textContent.includes('Identity') || label.textContent.includes('Identité') || label.textContent.includes('Identidad')) { 296 label.textContent = t.colorIdentity; 297 break; 298 } 299 } 300 301 // Update Mana Value label and comparison dropdown 302 const manaValueLabel = Array.from(modal.querySelectorAll('.advanced-search-field label')).find(l => 303 l.textContent === 'Mana Value' || l.textContent === 'Valeur de mana' || l.textContent === 'Valor de maná' 304 ); 305 if (manaValueLabel) manaValueLabel.textContent = t.manaValue; 306 307 // Update all comparison dropdowns 308 const comparisonSelects = modal.querySelectorAll('select[id$="-comparison"]');
309 comparisonSelects.forEach(select => { 310 const options = select.querySelectorAll('option'); 311 if (options[0]) options[0].textContent = t.equals; 312 if (options[1]) options[1].textContent = t.lessThan; 313 if (options[2]) options[2].textContent = t.lessThanOrEqual; 314 if (options[3]) options[3].textContent = t.greaterThan; 315 if (options[4]) options[4].textContent = t.greaterThanOrEqual; 316 }); 317 318 // Update Power, Toughness, Loyalty labels 319 const powerLabel = Array.from(modal.querySelectorAll('.advanced-search-field label')).find(l => 320 l.textContent === 'Power' || l.textContent === 'Force' || l.textContent === 'Fuerza' 321 ); 322 if (powerLabel) powerLabel.textContent = t.power; 323 324 const toughnessLabel = Array.from(modal.querySelectorAll('.advanced-search-field label')).find(l => 325 l.textContent === 'Toughness' || l.textContent === 'Endurance' || l.textContent === 'Resistencia' 326 ); 327 if (toughnessLabel) toughnessLabel.textContent = t.toughness; 328 329 const loyaltyLabel = Array.from(modal.querySelectorAll('.advanced-search-field label')).find(l => 330 l.textContent === 'Loyalty' || l.textContent === 'Loyauté' || l.textContent === 'Lealtad' 331 ); 332 if (loyaltyLabel) loyaltyLabel.textContent = t.loyalty; 333 334 // Update Sort By section 335 const sortByLabel = Array.from(modal.querySelectorAll('.advanced-search-field label')).find(l => 336 l.textContent === 'Sort By' || l.textContent === 'Trier par' || l.textContent === 'Ordenar por' 337 ); 338 if (sortByLabel) sortByLabel.textContent = t.sortBy; 339 340 const sortSelect = modal.querySelector('#adv-sort'); 341 if (sortSelect) { 342 const sortOptions = sortSelect.querySelectorAll('option'); 343 if (sortOptions[0]) sortOptions[0].textContent = t.sortName; 344 if (sortOptions[1]) sortOptions[1].textContent = t.sortSet; 345 if (sortOptions[2]) sortOptions[2].textContent = t.sortReleaseDate; 346 if (sortOptions[3]) sortOptions[3].textContent = t.sortSpoiledDate; 347 if (sortOptions[4]) sortOptions[4].textContent = t.sortRarity; 348 if (sortOptions[5]) sortOptions[5].textContent = t.sortColor; 349 if (sortOptions[6]) sortOptions[6].textContent = t.sortUSD; 350 if (sortOptions[7]) sortOptions[7].textContent = t.sortEUR; 351 if (sortOptions[8]) sortOptions[8].textContent = t.sortTIX; 352 } 353 354 const sortOrderLabel = Array.from(modal.querySelectorAll('.advanced-search-field label')).find(l => 355 l.textContent === 'Order' || l.textContent === 'Ordre' || l.textContent === 'Orden' 356 );
357 if (sortOrderLabel) sortOrderLabel.textContent = t.sortOrder; 358 359 const orderSelect = modal.querySelector('#adv-order'); 360 if (orderSelect) { 361 const orderOptions = orderSelect.querySelectorAll('option'); 362 if (orderOptions[0]) orderOptions[0].textContent = t.sortAsc; 363 if (orderOptions[1]) orderOptions[1].textContent = t.sortDesc; 364 } 365 366 // Update buttons 367 const searchBtn = modal.querySelector('.modal-footer button[type="submit"]'); 368 if (searchBtn) searchBtn.textContent = t.search; 369 370 const cancelBtn = modal.querySelector('.modal-footer button[type="button"]'); 371 if (cancelBtn) cancelBtn.textContent = t.cancel; 372 } 373 374 updateSettingsDropdown(t) { 375 const dropdownSections = document.querySelectorAll('#settingsDropdownMenu .dropdown-section'); 376 377 if (dropdownSections[0]) { 378 const title = dropdownSections[0].querySelector('.dropdown-section-title'); 379 if (title) title.textContent = t.printOptions; 380 381 const labels = dropdownSections[0].querySelectorAll('.dropdown-toggle-label span:first-child'); 382 if (labels[0]) labels[0].textContent = t.cutLines; 383 if (labels[1]) labels[1].textContent = t.printDoubleFaced; 384 if (labels[2]) labels[2].textContent = t.useA4; 385 if (labels[3]) labels[3].textContent = t.proxyCardBacks; 386 if (labels[4]) labels[4].textContent = t.testPrint; 387 } 388 389 if (dropdownSections[1]) { 390 const title = dropdownSections[1].querySelector('.dropdown-section-title'); 391 if (title) title.textContent = t.cornerFill; 392 393 const labels = dropdownSections[1].querySelectorAll('.dropdown-toggle-label span:first-child'); 394 if (labels[0]) labels[0].textContent = t.blackCorners; 395 if (labels[1]) labels[1].textContent = t.whiteCorners; 396 } 397 398 if (dropdownSections[2]) { 399 const title = dropdownSections[2].querySelector('.dropdown-section-title'); 400 if (title) title.textContent = t.deckOptions; 401 402 const labels = dropdownSections[2].querySelectorAll('.dropdown-toggle-label span:first-child'); 403 if (labels[0]) labels[0].textContent = t.ignoreBasicLands; 404 if (labels[1]) labels[1].textContent = t.printDecklist; 405 if (labels[2]) labels[2].textContent = t.includeSideboard; 406 } 407 408 if (dropdownSections[3]) { 409 const title = dropdownSections[3].querySelector('.dropdown-section-title'); 410 if (title) title.textContent = t.enhancements; 411 412 const labels = dropdownSections[3].querySelectorAll('.dropdown-toggle-label span:first-child'); 413 if (labels[0]) labels[0].textContent = t.proxyWatermark; 414 } 415 } 416 417 updateAdvancedSettingsModal(t) { 418 const modal = document.getElementById('advancedSettingsModal'); 419 if (!modal) return; 420 421 const title = modal.querySelector('.modal-header h3'); 422 if (title) title.textContent = t.advancedPrinterSettings; 423 424 const alignmentTitle = modal.querySelector('.advanced-header h4'); 425 if (alignmentTitle) alignmentTitle.textContent = t.printerAlignment; 426 427 const alignmentDesc = modal.querySelector('.advanced-description'); 428 if (alignmentDesc) alignmentDesc.textContent = t.printerAlignmentDesc; 429 430 const offsetGroups = modal.querySelectorAll('.offset-group'); 431 if (offsetGroups[0]) { 432 const title = offsetGroups[0].querySelector('h4'); 433 if (title) title.textContent = t.frontSheetPositioning; 434 435 const labels = offsetGroups[0].querySelectorAll('.input-group label'); 436 if (labels[0]) labels[0].textContent = t.horizontalOffset; 437 if (labels[1]) labels[1].textContent = t.verticalOffset; 438 439 const help = offsetGroups[0].querySelector('.offset-help'); 440 if (help) help.textContent = t.offsetHelp; 441 } 442 443 if (offsetGroups[1]) { 444 const title = offsetGroups[1].querySelector('h4'); 445 if (title) title.textContent = t.backSheetPositioning; 446 447 const labels = offsetGroups[1].querySelectorAll('.input-group label'); 448 if (labels[0]) labels[0].textContent = t.horizontalOffset; 449 if (labels[1]) labels[1].textContent = t.verticalOffset; 450 451 const help = offsetGroups[1].querySelector('.offset-help'); 452 if (help) help.textContent = t.offsetHelpBack; 453 } 454 455 const currentSettings = modal.querySelector('.current-settings h4'); 456 if (currentSettings) currentSettings.textContent = t.currentConfiguration; 457 458 const settingLabels = modal.querySelectorAll('.setting-label'); 459 if (settingLabels[0]) settingLabels[0].textContent = t.frontSheet; 460 if (settingLabels[1]) settingLabels[1].textContent = t.backSheet; 461 if (settingLabels[2]) settingLabels[2].textContent = t.netShift; 462 463 const note = modal.querySelector('.note'); 464 if (note) { 465 note.innerHTML = `<strong>Note:</strong> ${t.advancedNote}`; 466 } 467 468 const resetBtn = modal.querySelector('#advancedSettingsResetBtn'); 469 if (resetBtn) resetBtn.textContent = t.resetToZero; 470 471 const saveBtn = modal.querySelector('#advancedSettingsDoneBtn'); 472 if (saveBtn) saveBtn.textContent = t.save; 473 } 474 475 updateCardCount() { 476 const t = translations[this.currentLanguage]; 477 const cardCountEl = document.getElementById('cardCount'); 478 if (cardCountEl) { 479 const count = parseInt(cardCountEl.textContent) || 0; 480 const unit = count === 1 ? t.card : t.cards; 481 cardCountEl.textContent = `${count} ${unit}`; 482 } 483 } 484 485 updateSheetCount(count) { 486 const t = translations[this.currentLanguage]; 487 const sheetCountEl = document.getElementById('sheetCount'); 488 if (sheetCountEl) { 489 const unit = count === 1 ? t.sheet : t.sheets; 490 sheetCountEl.textContent = `${count} ${unit}`; 491 } 492 } 493 494 // Method to get translation for a specific key 495 t(key) { 496 return translations[this.currentLanguage][key] || translations.en[key] || key; 497 } 498 499 // Method to get no results message for a specific language code 500 getNoResultsMessage(langCode) {
501 const key = `no${langCode.charAt(0).toUpperCase() + langCode.slice(1).replace('s', 'S').replace('t', 'T')}Cards`; 502 const messages = { 503 en: this.t('noEnglishCards'), 504 es: this.t('noSpanishCards'), 505 fr: this.t('noFrenchCards'), 506 de: this.t('noGermanCards'), 507 it: this.t('noItalianCards'), 508 pt: this.t('noPortugueseCards'), 509 ja: this.t('noJapaneseCards'), 510 ko: this.t('noKoreanCards'), 511 ru: this.t('noRussianCards'), 512 zhs: this.t('noChineseSimplifiedCards'), 513 zht: this.t('noChineseTraditionalCards'), 514 ph: this.t('noPhyrexianCards') 515 }; 516 return messages[langCode] || `No ${langCode.toUpperCase()} cards found`; 517 } 518 519 // Card language preference methods 520 getCardLanguagePreference() { 521 return getCardLanguagePreference(); 522 } 523 524 setCardLanguagePreference(lang) { 525 return setCardLanguagePreference(lang); 526 } 527} 528 529// Global translation helper function 530export function t(key) { 531 if (window.MTGLanguageManager) { 532 return window.MTGLanguageManager.t(key); 533 } 534 // Fallback to English or the key itself 535 return translations.en[key] || key; 536}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.