PageSourceSearch

https://www.mtgproxy.net/src/LanguageManager.js

js mtgproxy.net collected 2026-09-28 07:07:35 UTC 21,494 bytes, 536 lines download raw bytes

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.