From c750cfd6d97ac7e067f6188ac816425e341087a9 Mon Sep 17 00:00:00 2001 From: Haewon Kam Date: Thu, 10 Sep 2026 16:38:41 +0900 Subject: [PATCH] =?UTF-8?q?i18n(supporters):=20=EC=A0=84=EC=97=AD=20?= =?UTF-8?q?=EC=96=B8=EC=96=B4=20=EC=A0=84=ED=99=98(KO/EN)=EC=9D=84=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=ED=95=98=EB=82=98=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC,=20=EC=98=81=EC=96=B4=20=ED=99=88=20/en=20=EC=8B=A0?= =?UTF-8?q?=EC=84=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 헤더 lang-switch: 현재 페이지의 상대 언어 페이지로 이동(/ ↔ /en, /plan ↔ /en/plan, 그 외는 언어 홈). 구조 기준선 갱신(haewon 승인) - 영어 페이지에서는 메뉴·CTA·배너·뒤로가기·푸터(지원 고지·부작용 고지·링크·주소)까지 함께 전환. lib/i18n.ts, data/factSheetEn.json - /en 영어 홈: 히어로, Start Here 카드, 병원 숫자, 오는 길(공항 경로·진료시간), 병원 사실, 원칙. 한국어 글은 번역 없이 링크만 안내 - 플래너의 페이지 내 언어 토글 제거, Step 1은 국적만. sitemap·hreflang 추가 Co-Authored-By: Claude Fable 5.1 --- supporters/scripts/gate/layout-baseline.json | 6 +- supporters/src/components/Planner.astro | 10 +-- supporters/src/data/factSheetEn.json | 20 +++++ supporters/src/data/planStrings.json | 12 +-- supporters/src/layouts/Base.astro | 60 +++++++++---- supporters/src/lib/i18n.ts | 46 ++++++++++ supporters/src/pages/en/index.astro | 92 ++++++++++++++++++++ supporters/src/pages/index.astro | 2 +- supporters/src/pages/sitemap.xml.ts | 1 + supporters/src/styles/global.css | 3 + 10 files changed, 217 insertions(+), 35 deletions(-) create mode 100644 supporters/src/data/factSheetEn.json create mode 100644 supporters/src/lib/i18n.ts create mode 100644 supporters/src/pages/en/index.astro diff --git a/supporters/scripts/gate/layout-baseline.json b/supporters/scripts/gate/layout-baseline.json index 094e2d3..cd72c76 100644 --- a/supporters/scripts/gate/layout-baseline.json +++ b/supporters/scripts/gate/layout-baseline.json @@ -16,6 +16,7 @@ "a", "a", "a", + "a.lang-switch", "a.cta", "main", "section.hero-light", @@ -257,7 +258,6 @@ "a", "a", "a", - "a", "div", "p", "img", @@ -287,6 +287,7 @@ "a", "a", "a", + "a.lang-switch", "a.cta", "div.backbar-wrap", "div.backbar.wrap-wide", @@ -485,7 +486,6 @@ "a", "a", "a", - "a", "div", "p", "img", @@ -515,6 +515,7 @@ "a", "a", "a", + "a.lang-switch", "a.cta", "div.backbar-wrap", "div.backbar.wrap-wide", @@ -739,7 +740,6 @@ "a", "a", "a", - "a", "div", "p", "img", diff --git a/supporters/src/components/Planner.astro b/supporters/src/components/Planner.astro index a6ad803..492df1f 100644 --- a/supporters/src/components/Planner.astro +++ b/supporters/src/components/Planner.astro @@ -19,10 +19,6 @@ const nHotels = PL.places.filter((x) => x.category === 'hotel').length;
{t.eyebrow}

{t.h_hero}

{t.title} {t.lede}

-
{nProcs}{t.stat_procs}{t.stat_procs_d}
@@ -53,7 +49,6 @@ const nHotels = PL.places.filter((x) => x.category === 'hotel').length; const PREFS = ['sightseeing', 'shopping', 'food', 'kpop', 'care']; const root = document.getElementById('planner')!; const pageLang = (root.dataset.lang === 'en' ? 'en' : 'ko') as Lang; - const pageUrl = (l: Lang) => (l === 'en' ? '/en/plan' : '/plan'); const toastEl = document.getElementById('toast')!; const sticky = document.getElementById('sticky')!; const reservationUrl: string = (factSheet as any).reservationUrl || (factSheet as any).url || '/visit'; @@ -101,8 +96,8 @@ const nHotels = PL.places.filter((x) => x.category === 'hotel').length;
${cardHead(1, t.h_step1, t.step1)} -
+

${esc(t.h_step1_hint)}

${cardHead(2, t.h_step2, t.step2)} @@ -263,9 +258,8 @@ const nHotels = PL.places.filter((x) => x.category === 'hotel').length; // ---------- 이벤트 ---------- document.addEventListener('click', (e) => { - const el = (e.target as HTMLElement).closest('[data-action],[data-lang]'); + const el = (e.target as HTMLElement).closest('[data-action]'); if (!el) return; - if (el.dataset.lang) { e.preventDefault(); const target = el.dataset.lang as Lang; if (target !== state.lang) { location.href = `${pageUrl(target)}#${encodeHash({ p: state.proc || undefined, d: state.date || undefined, l: target, n: state.nat || undefined, pref: state.prefs })}`; } return; } const a = el.dataset.action; if (a === 'proc') { state.proc = el.dataset.id!; render(); if (state.date) document.getElementById('result')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); } else if (a === 'pref') { const id = el.dataset.id!; state.prefs = state.prefs.includes(id) ? state.prefs.filter((x) => x !== id) : [...state.prefs, id]; render(); } diff --git a/supporters/src/data/factSheetEn.json b/supporters/src/data/factSheetEn.json new file mode 100644 index 0000000..826fcfd --- /dev/null +++ b/supporters/src/data/factSheetEn.json @@ -0,0 +1,20 @@ +{ + "_comment": "영어 페이지용 병원 사실 번역. 값의 원본은 factSheet.json 이고 여기에는 영어 표기만 둔다. 숫자·전화·URL 은 factSheet 에서 그대로 읽는다. 빈 값은 영어 페이지에서 '확인' 대신 항목을 숨긴다.", + "name": "View Plastic Surgery Clinic", + "shortName": "View Plastic Surgery", + "kind": "plastic surgery clinic", + "areaLabel": "Sinnonhyeon Station, Gangnam, Seoul", + "address": { + "full": "107 Bongeunsa-ro, Gangnam-gu, Seoul (View Plastic Surgery Building)", + "mapQuery": "View Plastic Surgery Seoul Bongeunsa-ro 107" + }, + "transit": "Line 9, Sinnonhyeon Station, Exit 3. Within 50 m of the exit. The whole building is the clinic, so you can see it from the exit.", + "airport": "From Incheon Airport: take the AREX airport railroad to Gimpo Airport Station, then Line 9 express to Sinnonhyeon Station with no transfer. About 60 to 70 minutes. A taxi takes 60 to 90 minutes depending on traffic.", + "parking": "The street in front of the main entrance is one-way. Enter from the pharmacy alley and turn right at the crossroads. The car park is on the left.", + "hoursDays": { "월·화·수·목": "Mon to Thu", "금": "Fri", "토": "Sat", "일·공휴일": "Sun and public holidays" }, + "hoursClosed": "Closed", + "building": "19-storey clinic building (B4 to 15F). Consultation 4F to 5F, examination B1, surgery 11F to 14F, inpatient wards 8F and 10F, dental clinic 15F.", + "arrival": "Go to the information desk on the 1st floor first. Consultation is on 4F to 5F, VIP consultation on 3F, and pre-operative tests at the B1 examination center.", + "specialties": ["Breast augmentation", "Facial contouring", "Jaw surgery", "Eyelid surgery", "Rhinoplasty", "Liposuction and fat grafting", "Anti-aging and lifting", "Men's plastic surgery", "Dermatology", "Hair transplant"], + "languageNote": "Consultation is available in Korean. Ask the clinic about interpreter support when you book." +} diff --git a/supporters/src/data/planStrings.json b/supporters/src/data/planStrings.json index 67e5a4d..6a4207c 100644 --- a/supporters/src/data/planStrings.json +++ b/supporters/src/data/planStrings.json @@ -4,7 +4,7 @@ "eyebrow": "Recovery Planner", "title": "수술일을 정하셨다면, 여행 일정은 여기서 짜세요.", "lede": "시술과 수술일을 고르면 입국 권장일, 출국 가능 최소일, 회복기 동안 외출할 수 있는 날과 그날 갈 만한 곳을 정리해 드립니다. 계산은 이 화면 안에서만 이루어지고 어디에도 저장되지 않습니다.", - "step1": "언어와 국적", + "step1": "국적", "step2": "시술과 수술일", "step3": "여유 시간에 하고 싶은 것", "step4": "일정표", @@ -76,7 +76,7 @@ "provisionalNote": "\"병원 확인 전\"으로 표시된 값은 병원 공개 안내에 없어 일반 기준으로 넣은 잠정값입니다. 상담에서 본인 일정으로 확정하세요.", "noRemoval": "제거 방문 없음", "h_hero": "Plan Your Recovery Stay", - "h_step1": "Language & Nationality", + "h_step1": "Nationality", "h_step2": "Procedure & Surgery Date", "h_step3": "Your Free Time", "h_step4": "Your Schedule", @@ -104,7 +104,8 @@ "empty_title": "시술과 수술일을 고르면 여기에 일정표가 나옵니다", "empty_sub": "수술일은 예상일이어도 됩니다.", "stickyView": "일정표 보기", - "stickyStay": "{a} → {b} · {n}박" + "stickyStay": "{a} → {b} · {n}박", + "h_step1_hint": "국적을 고르면 시차와 무비자 체류 한도를 함께 봅니다." }, "en": { "eyebrow": "Recovery Planner", @@ -182,7 +183,7 @@ "provisionalNote": "Values marked \"Provisional\" are not in the clinic's published guidance and use general estimates. Confirm your own dates at consultation.", "noRemoval": "No removal visit", "h_hero": "Plan Your Recovery Stay", - "h_step1": "Language & Nationality", + "h_step1": "Nationality", "h_step2": "Procedure & Surgery Date", "h_step3": "Your Free Time", "h_step4": "Your Schedule", @@ -210,6 +211,7 @@ "empty_title": "Pick a procedure and a surgery date to see your schedule here", "empty_sub": "An estimated date is fine.", "stickyView": "See schedule", - "stickyStay": "{a} → {b} · {n} nights" + "stickyStay": "{a} → {b} · {n} nights", + "h_step1_hint": "Your nationality sets the jet-lag buffer and the visa-free stay check." } } diff --git a/supporters/src/layouts/Base.astro b/supporters/src/layouts/Base.astro index 15b26f7..0add854 100644 --- a/supporters/src/layouts/Base.astro +++ b/supporters/src/layouts/Base.astro @@ -1,6 +1,8 @@ --- import '../styles/global.css'; import { SITE_NAME, SITE_TAGLINE, INDEXABLE, fact, site as S, surface, reservationUrl, editor, has, v } from '../lib'; +import { UI, switchHref } from '../lib/i18n'; +import EN from '../data/factSheetEn.json'; const ed = editor(); const f = fact as Record; @@ -18,6 +20,12 @@ const { title, description, jsonLd, ogImage = S.buildingImage?.src || '/favicon. const site = Astro.site?.toString().replace(/\/$/, '') ?? ''; const cleanPath = Astro.url.pathname.replace(/\/index\.html$/, '/').replace(/\.html$/, ''); const canonical = (new URL(cleanPath, Astro.site).toString().replace(/\/$/, '') || site); +const u = UI[lang]; +const other = lang === 'ko' ? 'en' : 'ko'; +const switchTo = switchHref(Astro.url.pathname, other); +const clinicName = lang === 'en' ? EN.shortName : v(f.shortName, '병원'); +const homeHref = lang === 'en' ? '/en' : '/'; +const isHome = ['/', '', '/index.html', '/index', '/en', '/en/index.html', '/en/index'].includes(Astro.url.pathname.replace(/\/$/, '') || '/'); const ld = jsonLd ? JSON.stringify({ '@context': 'https://schema.org', ...(Array.isArray(jsonLd) ? { '@graph': jsonLd } : jsonLd) }) : null; --- @@ -44,32 +52,44 @@ const ld = jsonLd ? JSON.stringify({ '@context': 'https://schema.org', ...(Array {ld &&