// i18n — UI string dictionary + helpers for EN / KO / JA. // Content fields (bio, description, etc.) are stored per-language in data.js // as { en, ko, ja } objects; use L(v) to read them. const LANG_KEY = "jimi_lang_v1"; const SUPPORTED_LANGS = ["en", "ko", "ja"]; const LANG_LABELS = { en: "EN", ko: "한", ja: "日" }; const LANG_NAMES = { en: "English", ko: "한국어", ja: "日本語" }; const LANG_LOCALES = { en: "en-US", ko: "ko-KR", ja: "ja-JP" }; // Language detection, split in two so we can tell "auto" apart from "manual". // detectFromBrowser() — walks navigator.languages, picks ko/ja/en, else en. // detectLang() — explicit saved choice wins, otherwise browser. function detectFromBrowser() { const prefs = navigator.languages && navigator.languages.length ? navigator.languages : [navigator.language || "en"]; for (const pref of prefs) { const lc = String(pref).toLowerCase(); if (lc.startsWith("ko")) return "ko"; if (lc.startsWith("ja")) return "ja"; if (lc.startsWith("en")) return "en"; } return "en"; } function detectLang() { try { const saved = localStorage.getItem(LANG_KEY); if (saved && SUPPORTED_LANGS.includes(saved)) return saved; } catch (e) {} return detectFromBrowser(); } function hasLangOverride() { try { const saved = localStorage.getItem(LANG_KEY); return !!(saved && SUPPORTED_LANGS.includes(saved)); } catch (e) { return false; } } function clearLangOverride() { try { localStorage.removeItem(LANG_KEY); } catch (e) {} location.reload(); } let CURRENT_LANG = detectLang(); try { document.documentElement.lang = CURRENT_LANG; } catch (e) {} const I18N = { en: { "nav.home": "Home", "nav.artists": "Artists", "nav.exhibitions": "Exhibitions", "nav.news": "News", "nav.about": "About", "nav.contact": "Contact", "filter.all": "All", "status.current": "Current", "status.upcoming": "Upcoming", "status.past": "Past", "title.home": "Contemporary Art, New York", "title.artists": "Artists", "title.exhibitions": "Exhibitions", "title.news": "News", "title.about": "About", "title.contact": "Contact", "title.artist": "Artist", "title.exhibition": "Exhibition", "eyebrow.now_on_view": "Now on view", "eyebrow.upcoming_recent": "Upcoming & recent", "eyebrow.artists": "Artists", "eyebrow.roster": "Gallery roster", "eyebrow.program": "Program", "eyebrow.announcements": "Announcements & press", "eyebrow.visit": "Visit & get in touch", "eyebrow.about_gallery": "About the gallery", "eyebrow.about_exhibition": "About the exhibition", "eyebrow.works_in_exhibition": "Works in the exhibition", "eyebrow.selected_works": "Selected works", "eyebrow.exhibitions": "Exhibitions", "cta.view_exhibition": "View exhibition →", "cta.about_prefix": "About", "cta.all_artists": "← All artists", "cta.all_exhibitions": "← All exhibitions", "misc.not_found": "Not found", "misc.newsletter": "Newsletter", "misc.staff_login": "Staff login", "misc.rights_suffix": "All rights reserved.", "footer.hours": "Hours", "footer.contact": "Contact", "footer.follow": "Follow", "footer.instagram": "Instagram", "contact.gallery": "Gallery", "contact.press_sales": "Press & sales", "admin.sidebar.dashboard": "Dashboard", "admin.sidebar.artists": "Artists", "admin.sidebar.exhibitions": "Exhibitions", "admin.sidebar.news": "News", "admin.sidebar.settings": "Settings", "admin.sidebar.view_site": "↗ View public site", "admin.sidebar.export": "⬇ Export data", "admin.sidebar.import": "⬆ Import data", "admin.sidebar.logout": "Log out", "admin.sidebar.reset": "Load demo data", "admin.sidebar.language": "Language", "lang.auto_hint": "Back to auto-detect", "admin.confirm.import": "This will replace all current data. Continue?", "admin.toast.exported": "Data exported", "admin.toast.imported": "Data imported", "admin.error.invalid_file": "Invalid file — expected a Jimi Gallery JSON export.", "admin.login.title": "Admin", "admin.login.password": "Password", "admin.login.submit": "Sign in", "admin.login.incorrect": "Incorrect password.", "admin.login.hint": "Demo password:", "admin.page.dashboard": "Dashboard", "admin.page.dashboard_sub": "Overview of gallery content", "admin.page.artists": "Artists", "admin.page.artists_sub": "Manage the gallery roster", "admin.page.exhibitions": "Exhibitions", "admin.page.exhibitions_sub": "Current, upcoming, and past", "admin.page.news": "News", "admin.page.news_sub": "Announcements and press", "admin.page.settings": "Settings", "admin.page.settings_sub": "Gallery information shown across the site", "admin.btn.new_artist": "+ New artist", "admin.btn.new_exhibition": "+ New exhibition", "admin.btn.new_post": "+ New post", "admin.btn.edit": "Edit", "admin.btn.delete": "Delete", "admin.btn.view": "View", "admin.btn.save": "Save", "admin.btn.save_changes": "Save changes", "admin.btn.cancel": "Cancel", "admin.btn.upload": "Upload", "admin.btn.clear": "Clear", "admin.btn.add_work": "+ Add work", "admin.btn.manage": "Manage", "admin.dash.current_upcoming": "Current & upcoming", "admin.dash.recent_news": "Recent news", "admin.dash.nothing_scheduled": "Nothing scheduled.", "admin.stat.artists": "Artists", "admin.stat.exhibitions": "Exhibitions", "admin.stat.current_upcoming": "Current / Upcoming", "admin.stat.news": "News items", "admin.table.name": "Name", "admin.table.born": "Born", "admin.table.works": "Works", "admin.table.title": "Title", "admin.table.artists": "Artists", "admin.table.dates": "Dates", "admin.table.status": "Status", "admin.table.date": "Date", "admin.empty.artists": "No artists yet.", "admin.empty.exhibitions": "No exhibitions yet.", "admin.empty.news": "No news yet.", "admin.modal.edit_artist": "Edit artist", "admin.modal.new_artist": "New artist", "admin.modal.edit_exhibition": "Edit exhibition", "admin.modal.new_exhibition": "New exhibition", "admin.modal.edit_post": "Edit post", "admin.modal.new_post": "New post", "admin.confirm.delete_artist": "Delete this artist? Works will be removed too.", "admin.confirm.delete_exhibition": "Delete this exhibition?", "admin.confirm.delete_post": "Delete this post?", "admin.confirm.reset": "Replace current data with the demo set? This cannot be undone.", "admin.toast.saved": "Saved", "admin.toast.artist_removed": "Artist removed", "admin.toast.exhibition_removed": "Exhibition removed", "admin.toast.post_removed": "Post removed", "admin.toast.settings_saved": "Settings saved", "admin.toast.data_reset": "Demo data loaded", "admin.form.name": "Name", "admin.form.born": "Born", "admin.form.lives": "Lives & works", "admin.form.portrait": "Portrait image", "admin.form.biography": "Biography", "admin.form.selected_works": "Selected works", "admin.form.title": "Title", "admin.form.status": "Status", "admin.form.venue": "Venue", "admin.form.start_date": "Start date", "admin.form.end_date": "End date", "admin.form.hero_image": "Hero image", "admin.form.artists": "Artists", "admin.form.artists_hint": "(hold ⌘/Ctrl to select multiple)", "admin.form.description": "Description", "admin.form.press_note": "Press note (optional)", "admin.form.image": "Image", "admin.form.date": "Date", "admin.form.excerpt": "Excerpt", "admin.form.body": "Body", "admin.form.gallery_name": "Gallery name", "admin.form.tagline": "Tagline", "admin.form.address": "Address", "admin.form.phone": "Phone", "admin.form.email": "Email", "admin.form.hours": "Hours", "admin.form.instagram": "Instagram handle", "admin.form.about": "About copy", "admin.form.year": "Year", "admin.form.medium": "Medium", "admin.form.dimensions": "Dimensions", "admin.works_count": "{n} works", "admin.paste_or_upload": "Paste image URL or upload from your computer", "admin.no_image": "No image selected", "admin.preview_unavailable": "Preview unavailable", "admin.ml_hint": "Each language tab is saved independently." }, ko: { "nav.home": "홈", "nav.artists": "작가", "nav.exhibitions": "전시", "nav.news": "소식", "nav.about": "소개", "nav.contact": "연락", "filter.all": "전체", "status.current": "진행 중", "status.upcoming": "예정", "status.past": "지난", "title.home": "현대미술, 뉴욕", "title.artists": "작가", "title.exhibitions": "전시", "title.news": "소식", "title.about": "소개", "title.contact": "연락", "title.artist": "작가", "title.exhibition": "전시", "eyebrow.now_on_view": "현재 전시", "eyebrow.upcoming_recent": "예정 · 최근 전시", "eyebrow.artists": "작가", "eyebrow.roster": "갤러리 로스터", "eyebrow.program": "프로그램", "eyebrow.announcements": "공지 · 보도", "eyebrow.visit": "방문 · 문의", "eyebrow.about_gallery": "갤러리 소개", "eyebrow.about_exhibition": "전시 소개", "eyebrow.works_in_exhibition": "출품작", "eyebrow.selected_works": "주요 작품", "eyebrow.exhibitions": "전시", "cta.view_exhibition": "전시 보기 →", "cta.about_prefix": "작가 소개 —", "cta.all_artists": "← 작가 목록", "cta.all_exhibitions": "← 전시 목록", "misc.not_found": "찾을 수 없음", "misc.newsletter": "뉴스레터", "misc.staff_login": "직원 로그인", "misc.rights_suffix": "모든 권리 보유.", "footer.hours": "운영 시간", "footer.contact": "연락처", "footer.follow": "소셜", "footer.instagram": "인스타그램", "contact.gallery": "갤러리", "contact.press_sales": "보도 · 판매", "admin.sidebar.dashboard": "대시보드", "admin.sidebar.artists": "작가", "admin.sidebar.exhibitions": "전시", "admin.sidebar.news": "소식", "admin.sidebar.settings": "설정", "admin.sidebar.view_site": "↗ 공개 사이트 보기", "admin.sidebar.export": "⬇ 데이터 내보내기", "admin.sidebar.import": "⬆ 데이터 가져오기", "admin.sidebar.logout": "로그아웃", "admin.sidebar.reset": "데모 데이터 설정하기", "admin.sidebar.language": "언어", "lang.auto_hint": "자동 감지로 돌아가기", "admin.confirm.import": "현재 데이터 전체가 가져온 파일로 교체됩니다. 계속하시겠습니까?", "admin.toast.exported": "데이터를 내보냈습니다", "admin.toast.imported": "데이터를 가져왔습니다", "admin.error.invalid_file": "파일 형식이 올바르지 않습니다 — 지미 갤러리 JSON 내보내기 파일이어야 합니다.", "admin.login.title": "관리자", "admin.login.password": "비밀번호", "admin.login.submit": "로그인", "admin.login.incorrect": "비밀번호가 올바르지 않습니다.", "admin.login.hint": "데모 비밀번호:", "admin.page.dashboard": "대시보드", "admin.page.dashboard_sub": "갤러리 콘텐츠 개요", "admin.page.artists": "작가", "admin.page.artists_sub": "갤러리 로스터 관리", "admin.page.exhibitions": "전시", "admin.page.exhibitions_sub": "진행 중 · 예정 · 지난 전시", "admin.page.news": "소식", "admin.page.news_sub": "공지와 보도", "admin.page.settings": "설정", "admin.page.settings_sub": "사이트 전반에 표시되는 갤러리 정보", "admin.btn.new_artist": "+ 새 작가", "admin.btn.new_exhibition": "+ 새 전시", "admin.btn.new_post": "+ 새 글", "admin.btn.edit": "편집", "admin.btn.delete": "삭제", "admin.btn.view": "보기", "admin.btn.save": "저장", "admin.btn.save_changes": "변경 저장", "admin.btn.cancel": "취소", "admin.btn.upload": "업로드", "admin.btn.clear": "지우기", "admin.btn.add_work": "+ 작품 추가", "admin.btn.manage": "관리", "admin.dash.current_upcoming": "진행 중 · 예정", "admin.dash.recent_news": "최근 소식", "admin.dash.nothing_scheduled": "예정된 항목이 없습니다.", "admin.stat.artists": "작가", "admin.stat.exhibitions": "전시", "admin.stat.current_upcoming": "진행 중 / 예정", "admin.stat.news": "소식", "admin.table.name": "이름", "admin.table.born": "출생", "admin.table.works": "작품", "admin.table.title": "제목", "admin.table.artists": "작가", "admin.table.dates": "기간", "admin.table.status": "상태", "admin.table.date": "일자", "admin.empty.artists": "등록된 작가가 없습니다.", "admin.empty.exhibitions": "등록된 전시가 없습니다.", "admin.empty.news": "등록된 소식이 없습니다.", "admin.modal.edit_artist": "작가 편집", "admin.modal.new_artist": "새 작가", "admin.modal.edit_exhibition": "전시 편집", "admin.modal.new_exhibition": "새 전시", "admin.modal.edit_post": "글 편집", "admin.modal.new_post": "새 글", "admin.confirm.delete_artist": "이 작가를 삭제하시겠습니까? 작품도 함께 제거됩니다.", "admin.confirm.delete_exhibition": "이 전시를 삭제하시겠습니까?", "admin.confirm.delete_post": "이 글을 삭제하시겠습니까?", "admin.confirm.reset": "현재 데이터를 데모 데이터로 설정하시겠습니까? 되돌릴 수 없습니다.", "admin.toast.saved": "저장됨", "admin.toast.artist_removed": "작가가 삭제되었습니다", "admin.toast.exhibition_removed": "전시가 삭제되었습니다", "admin.toast.post_removed": "글이 삭제되었습니다", "admin.toast.settings_saved": "설정이 저장되었습니다", "admin.toast.data_reset": "데모 데이터가 설정되었습니다", "admin.form.name": "이름", "admin.form.born": "출생", "admin.form.lives": "거주 · 작업", "admin.form.portrait": "초상 이미지", "admin.form.biography": "작가 소개", "admin.form.selected_works": "주요 작품", "admin.form.title": "제목", "admin.form.status": "상태", "admin.form.venue": "장소", "admin.form.start_date": "시작일", "admin.form.end_date": "종료일", "admin.form.hero_image": "대표 이미지", "admin.form.artists": "작가", "admin.form.artists_hint": "(⌘/Ctrl 키로 여러 명 선택)", "admin.form.description": "소개", "admin.form.press_note": "보도 문구 (선택)", "admin.form.image": "이미지", "admin.form.date": "일자", "admin.form.excerpt": "요약", "admin.form.body": "본문", "admin.form.gallery_name": "갤러리명", "admin.form.tagline": "태그라인", "admin.form.address": "주소", "admin.form.phone": "전화", "admin.form.email": "이메일", "admin.form.hours": "운영 시간", "admin.form.instagram": "인스타그램 핸들", "admin.form.about": "소개 본문", "admin.form.year": "연도", "admin.form.medium": "매체", "admin.form.dimensions": "크기", "admin.works_count": "작품 {n}점", "admin.paste_or_upload": "이미지 URL을 붙여넣거나 파일을 업로드하세요", "admin.no_image": "이미지 없음", "admin.preview_unavailable": "미리보기 불가", "admin.ml_hint": "언어 탭별로 독립 저장됩니다." }, ja: { "nav.home": "ホーム", "nav.artists": "アーティスト", "nav.exhibitions": "展覧会", "nav.news": "ニュース", "nav.about": "ギャラリー", "nav.contact": "お問い合わせ", "filter.all": "すべて", "status.current": "開催中", "status.upcoming": "予定", "status.past": "過去", "title.home": "コンテンポラリーアート、ニューヨーク", "title.artists": "アーティスト", "title.exhibitions": "展覧会", "title.news": "ニュース", "title.about": "ギャラリー", "title.contact": "お問い合わせ", "title.artist": "アーティスト", "title.exhibition": "展覧会", "eyebrow.now_on_view": "開催中", "eyebrow.upcoming_recent": "予定・最近の展覧会", "eyebrow.artists": "アーティスト", "eyebrow.roster": "ギャラリー所属作家", "eyebrow.program": "プログラム", "eyebrow.announcements": "お知らせ・プレス", "eyebrow.visit": "ご来廊・お問い合わせ", "eyebrow.about_gallery": "ギャラリー概要", "eyebrow.about_exhibition": "展覧会について", "eyebrow.works_in_exhibition": "出品作品", "eyebrow.selected_works": "主要作品", "eyebrow.exhibitions": "展覧会", "cta.view_exhibition": "展覧会を見る →", "cta.about_prefix": "作家について —", "cta.all_artists": "← アーティスト一覧", "cta.all_exhibitions": "← 展覧会一覧", "misc.not_found": "見つかりません", "misc.newsletter": "ニュースレター", "misc.staff_login": "スタッフログイン", "misc.rights_suffix": "All rights reserved.", "footer.hours": "営業時間", "footer.contact": "お問い合わせ", "footer.follow": "フォロー", "footer.instagram": "Instagram", "contact.gallery": "ギャラリー", "contact.press_sales": "プレス・販売", "admin.sidebar.dashboard": "ダッシュボード", "admin.sidebar.artists": "アーティスト", "admin.sidebar.exhibitions": "展覧会", "admin.sidebar.news": "ニュース", "admin.sidebar.settings": "設定", "admin.sidebar.view_site": "↗ 公開サイトを見る", "admin.sidebar.export": "⬇ データをエクスポート", "admin.sidebar.import": "⬆ データをインポート", "admin.sidebar.logout": "ログアウト", "admin.sidebar.reset": "デモデータを読み込む", "admin.sidebar.language": "言語", "lang.auto_hint": "自動検出に戻す", "admin.confirm.import": "現在のデータがすべて置き換えられます。続行しますか?", "admin.toast.exported": "データをエクスポートしました", "admin.toast.imported": "データをインポートしました", "admin.error.invalid_file": "ファイル形式が正しくありません — ジミ・ギャラリーのJSONエクスポートが必要です。", "admin.login.title": "管理画面", "admin.login.password": "パスワード", "admin.login.submit": "サインイン", "admin.login.incorrect": "パスワードが正しくありません。", "admin.login.hint": "デモ用パスワード:", "admin.page.dashboard": "ダッシュボード", "admin.page.dashboard_sub": "ギャラリーコンテンツの概要", "admin.page.artists": "アーティスト", "admin.page.artists_sub": "所属作家の管理", "admin.page.exhibitions": "展覧会", "admin.page.exhibitions_sub": "開催中・予定・過去の展覧会", "admin.page.news": "ニュース", "admin.page.news_sub": "お知らせとプレス", "admin.page.settings": "設定", "admin.page.settings_sub": "サイト全体に表示されるギャラリー情報", "admin.btn.new_artist": "+ 新規アーティスト", "admin.btn.new_exhibition": "+ 新規展覧会", "admin.btn.new_post": "+ 新規投稿", "admin.btn.edit": "編集", "admin.btn.delete": "削除", "admin.btn.view": "表示", "admin.btn.save": "保存", "admin.btn.save_changes": "変更を保存", "admin.btn.cancel": "キャンセル", "admin.btn.upload": "アップロード", "admin.btn.clear": "クリア", "admin.btn.add_work": "+ 作品を追加", "admin.btn.manage": "管理", "admin.dash.current_upcoming": "開催中・予定", "admin.dash.recent_news": "最新ニュース", "admin.dash.nothing_scheduled": "予定された項目はありません。", "admin.stat.artists": "アーティスト", "admin.stat.exhibitions": "展覧会", "admin.stat.current_upcoming": "開催中 / 予定", "admin.stat.news": "ニュース", "admin.table.name": "名前", "admin.table.born": "生年", "admin.table.works": "作品", "admin.table.title": "タイトル", "admin.table.artists": "アーティスト", "admin.table.dates": "会期", "admin.table.status": "状態", "admin.table.date": "日付", "admin.empty.artists": "登録済みのアーティストはありません。", "admin.empty.exhibitions": "登録済みの展覧会はありません。", "admin.empty.news": "登録済みのニュースはありません。", "admin.modal.edit_artist": "アーティストを編集", "admin.modal.new_artist": "新規アーティスト", "admin.modal.edit_exhibition": "展覧会を編集", "admin.modal.new_exhibition": "新規展覧会", "admin.modal.edit_post": "投稿を編集", "admin.modal.new_post": "新規投稿", "admin.confirm.delete_artist": "このアーティストを削除しますか?作品も併せて削除されます。", "admin.confirm.delete_exhibition": "この展覧会を削除しますか?", "admin.confirm.delete_post": "この投稿を削除しますか?", "admin.confirm.reset": "現在のデータをデモデータに置き換えますか?元に戻せません。", "admin.toast.saved": "保存しました", "admin.toast.artist_removed": "アーティストを削除しました", "admin.toast.exhibition_removed": "展覧会を削除しました", "admin.toast.post_removed": "投稿を削除しました", "admin.toast.settings_saved": "設定を保存しました", "admin.toast.data_reset": "デモデータを読み込みました", "admin.form.name": "名前", "admin.form.born": "生年", "admin.form.lives": "拠点・制作地", "admin.form.portrait": "ポートレイト画像", "admin.form.biography": "作家紹介", "admin.form.selected_works": "主要作品", "admin.form.title": "タイトル", "admin.form.status": "状態", "admin.form.venue": "会場", "admin.form.start_date": "開始日", "admin.form.end_date": "終了日", "admin.form.hero_image": "メインビジュアル", "admin.form.artists": "アーティスト", "admin.form.artists_hint": "(⌘/Ctrlで複数選択)", "admin.form.description": "解説", "admin.form.press_note": "プレスノート (任意)", "admin.form.image": "画像", "admin.form.date": "日付", "admin.form.excerpt": "要約", "admin.form.body": "本文", "admin.form.gallery_name": "ギャラリー名", "admin.form.tagline": "タグライン", "admin.form.address": "住所", "admin.form.phone": "電話", "admin.form.email": "メール", "admin.form.hours": "営業時間", "admin.form.instagram": "Instagramハンドル", "admin.form.about": "ギャラリー紹介", "admin.form.year": "制作年", "admin.form.medium": "技法", "admin.form.dimensions": "サイズ", "admin.works_count": "作品 {n}点", "admin.paste_or_upload": "画像URLを貼り付けるか、ファイルをアップロード", "admin.no_image": "画像が選択されていません", "admin.preview_unavailable": "プレビュー表示不可", "admin.ml_hint": "言語タブごとに個別に保存されます。" } }; function t(key, vars) { const dict = I18N[CURRENT_LANG] || I18N.en; let v = dict[key]; if (v == null) v = I18N.en[key]; if (v == null) return key; if (vars) { for (const k in vars) v = v.replace("{" + k + "}", vars[k]); } return v; } // Localized content getter. Accepts either: // - a plain string (returned as-is, for legacy/single-lang data) // - an object { en, ko, ja } (returns CURRENT_LANG value, falling back) function L(v) { if (v == null) return ""; if (typeof v === "string") return v; if (typeof v !== "object") return String(v); return v[CURRENT_LANG] || v.en || v.ko || v.ja || Object.values(v)[0] || ""; } function setLang(lang) { if (!SUPPORTED_LANGS.includes(lang) || lang === CURRENT_LANG) return; try { localStorage.setItem(LANG_KEY, lang); } catch (e) {} location.reload(); } function getLang() { return CURRENT_LANG; } function getLocale() { return LANG_LOCALES[CURRENT_LANG] || "en-US"; } function langSwitcherHtml(variant) { const cls = variant === "sidebar" ? "lang-switcher sidebar" : "lang-switcher"; const override = hasLangOverride(); return `