1/* Shared enhancement. Core content and links remain available without JavaScript. */ 2(() => { 3 'use strict'; 4 document.documentElement.classList.add('js'); 5 const supported = new Set(['ko', 'en']); 6 const storage = { 7 get() { try { return localStorage.getItem('updatesLanguagePreference'); } catch { return null; } }, 8 set(value) { 9 try { 10 localStorage.setItem('updatesLanguagePreference', value); 11 localStorage.setItem('language', value); 12 } catch { /* Storage is optional. */ } 13 } 14 }; 15 const requested = new URLSearchParams(location.search).get('lang'); 16 let deviceLanguage = ''; 17 try { 18 deviceLanguage = navigator.languages?.[0] || navigator.language || navigator.userLanguage || ''; 19 } catch { /* Device language is optional. */ } 20 const detected = /^ko(?:-|$)/i.test(String(deviceLanguage)) ? 'ko' : 'en'; 21 let language = supported.has(requested) ? requested : (supported.has(storage.get()) ? storage.get() : detected); 22 let toastTimer; 23 const labels = (ko, en) => language === 'en' ? en : ko; 24 25 function notify(ko, en, duration = 4000) { 26 const node = document.getElementById('toast'); 27 if (!node) return; 28 clearTimeout(toastTimer); 29 node.textContent = labels(ko, en); 30 node.hidden = false; 31 toastTimer = setTimeout(() => { node.hidden = true; }, duration); 32 } 33 34 // Only trusted, supplied privacy strings use rich text. URLs and tags are restricted. 35 function safePolicyFragment(value) { 36 const template = document.createElement('template'); 37 template.innerHTML = String(value); 38 const allowed = new Set(['BR', 'A', 'STRONG', 'EM', 'SPAN']); 39 [...template.content.querySelectorAll('*')].forEach(node => { 40 if (!allowed.has(node.tagName)) { node.replaceWith(...node.childNodes); return; } 41 const href = node.getAttribute('href'); 42 [...node.attributes].forEach(attr => node.removeAttribute(attr.name)); 43 if (node.tagName === 'A' && href) { 44 try { 45 const url = new URL(href); 46 if (url.protocol === 'https:' || url.protocol === 'mailto:') { 47 node.setAttribute('href', url.href); 48 node.className = 'policy-link'; 49 if (url.protocol === 'https:') { 50 node.target = '_blank'; node.rel = 'noopener noreferrer'; 51 } 52 } 53 } catch { /* Leave malformed links as inert text. */ } 54 } 55 }); 56 return template.content; 57 } 58 59 function applyLanguage(next, updateUrl = false, persistChoice = false) { 60 language = supported.has(next) ? next : 'en'; 61 document.documentElement.lang = language; 62 if (persistChoice) storage.set(language); 63 document.querySelectorAll('[data-ko][data-en]').forEach(el => { 64 el.textContent = el.getAttribute(`data-${language}`); 65 }); 66 document.querySelectorAll('[data-ko-placeholder]').forEach(el => { 67 el.setAttribute('placeholder', el.getAttribute(`data-${language}-placeholder`) || ''); 68 }); 69 document.querySelectorAll('[data-ko-label]').forEach(el => { 70 el.setAttribute('aria-label', el.getAttribute(`data-${language}-label`) || ''); 71 }); 72 if (window.YEE_POLICY) { 73 document.querySelectorAll('[data-policy-key]').forEach(el => { 74 const value = window.YEE_POLICY[language]?.[el.dataset.policyKey]; 75 if (typeof value === 'string') el.replaceChildren(safePolicyFragment(value)); 76 }); 77 } 78 document.querySelectorAll('[data-language-toggle]').forEach(button => { 79 button.setAttribute('aria-label', labels('Switch to English', 'íêµì´ë¡ ì í')); 80 button.querySelector('[data-language-label]').textContent = labels('EN', 'KO'); 81 }); 82 document.querySelectorAll('a[data-local-link]').forEach(link => { 83 try { 84 const url = new URL(link.getAttribute('href'), location.href); 85 if (url.origin === location.origin) { 86 url.searchParams.set('lang', language); 87 link.setAttribute('href', url.pathname + url.search + url.hash); 88 } 89 } catch { /* Relative navigation is still usable. */ } 90 }); 91 const page = document.body.dataset.page; 92 const titles = { 93 home: labels('YeeStudio â ìì ìì´ëì´, ë í¸ë¦¬í ì¼ì', 'YeeStudio â Small ideas. Better everyday.'), 94 privacy: labels('YeeStudio â ê°ì¸ì ë³´ì²ë¦¬ë°©ì¹¨', 'YeeStudio â Privacy Policy'), 95 error: `${document.body.dataset.error || '404'} â YeeStudio` 96 }; 97 document.title = titles[page] || 'YeeStudio'; 98 const description = page === 'privacy' 99 ? labels('YeeStudio ì±ê³¼ ì¹ ìë¹ì¤ì ê°ì¸ì ë³´ ì²ë¦¬ë°©ì¹¨ì íì¸íì¸ì.', 'Read the privacy policy for YeeStudio apps and web services.') 100 : page === 'home' 101 ? labels('ì¼ìì ìì ë¶í¸í¨ì í´ê²°íë YeeStudioì ì±ê³¼ ì¹ ë구를 ë§ëë³´ì¸ì.', 'Thoughtful apps and web tools that make everyday life a little easier.') 102 : labels('ìì²í íì´ì§ë¥¼ ì´ ì ììµëë¤. íì¼ë¡ ëìê°ê±°ë 문ìí´ ì£¼ì¸ì.', 'This page is unavailable. Head home or contact us for help.'); 103 document.querySelector('meta[name="description"]')?.setAttribute('content', description); 104 document.querySelector('meta[property="og:description"]')?.setAttribute('content', description); 105 document.querySelector('meta[property="og:title"]')?.setAttribute('content', document.title); 106 if (updateUrl) {
107 try { 108 const url = new URL(location.href); url.searchParams.set('lang', language); 109 history.replaceState(null, '', url.pathname + url.search + url.hash); 110 } catch { /* Local file previews may restrict history changes. */ } 111 } 112 document.dispatchEvent(new CustomEvent('yee:languagechange', { detail: { language } })); 113 } 114 115 document.querySelectorAll('[data-language-toggle]').forEach(button => { 116 button.addEventListener('click', () => applyLanguage(language === 'ko' ? 'en' : 'ko', true, true)); 117 }); 118 const menuButton = document.querySelector('.menu-toggle'); 119 const menu = document.getElementById('mobile-menu'); 120 function closeMenu(returnFocus = false) { 121 if (!menu || !menuButton) return; 122 menu.hidden = true; menuButton.setAttribute('aria-expanded', 'false'); 123 if (returnFocus) menuButton.focus(); 124 } 125 menuButton?.addEventListener('click', () => { 126 const opening = menu.hidden; 127 menu.hidden = !opening; 128 menuButton.setAttribute('aria-expanded', String(opening)); 129 }); 130 menu?.querySelectorAll('a').forEach(link => link.addEventListener('click', () => closeMenu())); 131 document.addEventListener('keydown', event => { 132 if (event.key === 'Escape' && menu && !menu.hidden) closeMenu(true); 133 }); 134 document.addEventListener('click', event => { 135 if (menu && !menu.hidden && !event.target.closest('.site-header')) closeMenu(); 136 }); 137 const desktop = matchMedia('(min-width: 901px)'); 138 desktop.addEventListener?.('change', event => { if (event.matches) closeMenu(); }); 139 const header = document.querySelector('.site-header'); 140 const top = document.querySelector('.back-to-top'); 141 let scrollFrame; 142 function updateScroll() { 143 scrollFrame = null; 144 header?.classList.toggle('is-scrolled', scrollY > 12); 145 top?.classList.toggle('is-visible', scrollY > 650); 146 } 147 window.addEventListener('scroll', () => { 148 if (!scrollFrame) scrollFrame = requestAnimationFrame(updateScroll); 149 }, { passive: true }); 150 updateScroll(); 151 152 document.querySelectorAll('[data-copy]').forEach(button => { 153 button.addEventListener('click', async () => { 154 const value = button.dataset.copy || ''; 155 try { 156 if (!navigator.clipboard?.writeText) throw new Error('Clipboard unavailable'); 157 await navigator.clipboard.writeText(value); 158 notify('ì´ë©ì¼ 주ì를 ë³µì¬íì´ì.', 'Email address copied.'); 159 } catch { 160 // Do not pretend success or read the clipboard. Offer selectable source text instead. 161 const address = document.querySelector('.contact-email'); 162 if (address) { 163 const range = document.createRange(); range.selectNodeContents(address); 164 const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); 165 } 166 notify('ìë ë³µì¬ê° ì íëì´ ìì´ì. íìë ì´ë©ì¼ 주ì를 ì§ì ë³µì¬í´ 주ì¸ì.', 'Automatic copying is unavailable. Please copy the displayed email address.', 6500); 167 } 168 }); 169 }); 170 document.querySelectorAll('[data-print]').forEach(button => button.addEventListener('click', () => window.print())); 171 window.YeeRenewal = Object.freeze({ 172 getLanguage: () => language, t: labels, notify, applyLanguage, 173 motion: () => matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' 174 }); 175 applyLanguage(language, false, false); 176})();
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.