From 592b6b1254ce6d9a0b92838f0149e710941d5396 Mon Sep 17 00:00:00 2001 From: user Date: Mon, 27 Apr 2026 21:00:57 +0900 Subject: [PATCH] feat: localize interview questions (ko/en), send X-Lang header --- internal/httpapi/diagnostic.go | 9 +++++++++ internal/interview/catalog.go | 37 ++++++++++++++++++++++++++-------- internal/interview/service.go | 2 +- internal/interview/types.go | 1 + internal/webapp/static/app.js | 6 ++++-- internal/webapp/static/i18n.js | 27 +++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 11 deletions(-) diff --git a/internal/httpapi/diagnostic.go b/internal/httpapi/diagnostic.go index e1e5820..980b7bf 100644 --- a/internal/httpapi/diagnostic.go +++ b/internal/httpapi/diagnostic.go @@ -13,6 +13,7 @@ type createDiagnosticSessionRequest struct { TargetRole string `json:"target_role"` Stack []string `json:"stack"` InterviewTimeline string `json:"interview_timeline"` + Lang string `json:"lang"` } type submitDiagnosticAnswerRequest struct { @@ -27,11 +28,19 @@ func (h Handler) createDiagnosticSession(w http.ResponseWriter, r *http.Request) return } + lang := req.Lang + if lang == "" { + lang = r.Header.Get("X-Lang") + } + if lang == "" { + lang = "en" + } session, err := h.diagnostic.CreateSession(r.Context(), interview.CreateSessionInput{ UserID: req.UserID, TargetRole: req.TargetRole, Stack: req.Stack, InterviewTimeline: req.InterviewTimeline, + Lang: lang, }) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) diff --git a/internal/interview/catalog.go b/internal/interview/catalog.go index 8475184..055df5f 100644 --- a/internal/interview/catalog.go +++ b/internal/interview/catalog.go @@ -2,28 +2,49 @@ package interview import "tutor/internal/workflows" -func BackendDeveloperQuestions() []Question { - return []Question{ +var questionPrompts = map[string]map[string]string{ + "ko": { + "backend-http-idempotency": "HTTP 메서드가 멱등성을 가지려면 어떤 조건이 필요하며, 재시도 시 왜 중요한가요?", + "backend-db-index-tradeoff": "데이터베이스 인덱스를 추가하면 API가 어떻게 개선되며, 어떤 트레이드오프가 발생할 수 있나요?", + "backend-cache-invalidation": "API 응답을 캐싱할지 어떻게 결정하며, 오래된 데이터는 어떻게 처리하나요?", + }, + "en": { + "backend-http-idempotency": "What makes an HTTP method idempotent, and why does that matter for retries?", + "backend-db-index-tradeoff": "When would adding a database index improve an API, and what tradeoffs can it introduce?", + "backend-cache-invalidation": "How would you decide whether to cache an API response, and how would you handle stale data?", + }, +} + +func BackendDeveloperQuestions(lang string) []Question { + if lang == "" { + lang = "en" + } + base := []Question{ { - ID: "backend-http-idempotency", - Prompt: "What makes an HTTP method idempotent, and why does that matter for retries?", + ID: "backend-http-idempotency", Concepts: []workflows.ConceptRef{ {ID: "http-idempotency", Label: "HTTP idempotency", Track: BackendDeveloperTrack}, }, }, { - ID: "backend-db-index-tradeoff", - Prompt: "When would adding a database index improve an API, and what tradeoffs can it introduce?", + ID: "backend-db-index-tradeoff", Concepts: []workflows.ConceptRef{ {ID: "database-indexes", Label: "Database indexes", Track: BackendDeveloperTrack}, }, }, { - ID: "backend-cache-invalidation", - Prompt: "How would you decide whether to cache an API response, and how would you handle stale data?", + ID: "backend-cache-invalidation", Concepts: []workflows.ConceptRef{ {ID: "cache-invalidation", Label: "Cache invalidation", Track: BackendDeveloperTrack}, }, }, } + for i := range base { + if p, ok := questionPrompts[lang][base[i].ID]; ok { + base[i].Prompt = p + } else if p, ok := questionPrompts["en"][base[i].ID]; ok { + base[i].Prompt = p + } + } + return base } diff --git a/internal/interview/service.go b/internal/interview/service.go index 067b998..45f785f 100644 --- a/internal/interview/service.go +++ b/internal/interview/service.go @@ -44,7 +44,7 @@ func (s *Service) CreateSession(_ context.Context, input CreateSessionInput) (Se TargetRole: input.TargetRole, Stack: append([]string(nil), input.Stack...), InterviewTimeline: input.InterviewTimeline, - Questions: BackendDeveloperQuestions(), + Questions: BackendDeveloperQuestions(input.Lang), CreatedAt: time.Now().UTC(), } if s.memory != nil { diff --git a/internal/interview/types.go b/internal/interview/types.go index fe16d5a..e276a4e 100644 --- a/internal/interview/types.go +++ b/internal/interview/types.go @@ -47,6 +47,7 @@ type CreateSessionInput struct { TargetRole string Stack []string InterviewTimeline string + Lang string } type SubmitAnswerInput struct { diff --git a/internal/webapp/static/app.js b/internal/webapp/static/app.js index 4ab75b3..e9ba8c4 100644 --- a/internal/webapp/static/app.js +++ b/internal/webapp/static/app.js @@ -89,6 +89,7 @@ els.sessionForm.addEventListener("submit", async (event) => { .map((item) => item.trim()) .filter(Boolean), interview_timeline: value("#timeline"), + lang: localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko", }; try { @@ -216,7 +217,7 @@ function renderSession() { "aria-pressed", String(state.selectedQuestion?.id === question.id) ); - button.innerHTML = `${escapeHTML(question.id)}${escapeHTML(question.prompt)}`; + button.innerHTML = `${escapeHTML(question.id)}${escapeHTML(tq(question.id) || question.prompt)}`; button.addEventListener("click", () => { state.selectedQuestion = question; els.answerText.value = ""; @@ -465,7 +466,8 @@ els.logoutButton.addEventListener("click", () => { async function request(url, options = {}) { const token = localStorage.getItem("tutor_token"); - const headers = { "Content-Type": "application/json" }; + const lang = localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko"; + const headers = { "Content-Type": "application/json", "X-Lang": lang }; if (token) headers["Authorization"] = `Bearer ${token}`; const response = await fetch(url, { headers, ...options }); const body = await response.json(); diff --git a/internal/webapp/static/i18n.js b/internal/webapp/static/i18n.js index ecc3e65..8c1b116 100644 --- a/internal/webapp/static/i18n.js +++ b/internal/webapp/static/i18n.js @@ -181,6 +181,33 @@ window.t = function (key, ...args) { return typeof text === "function" ? text(...args) : text; } +var questionTexts = { + ko: { + "backend-http-idempotency": + "HTTP 메서드가 멱등성을 가지려면 어떤 조건이 필요하며, 재시도 시 왜 중요한가요?", + "backend-db-index-tradeoff": + "데이터베이스 인덱스를 추가하면 API가 어떻게 개선되며, 어떤 트레이드오프가 발생할 수 있나요?", + "backend-cache-invalidation": + "API 응답을 캐싱할지 어떻게 결정하며, 오래된 데이터는 어떻게 처리하나요?", + }, + en: { + "backend-http-idempotency": + "What makes an HTTP method idempotent, and why does that matter for retries?", + "backend-db-index-tradeoff": + "When would adding a database index improve an API, and what tradeoffs can it introduce?", + "backend-cache-invalidation": + "How would you decide whether to cache an API response, and how would you handle stale data?", + }, +}; + +window.tq = function (id) { + const lang = + localStorage.getItem("tutor_lang") || + document.documentElement.lang || + "ko"; + return questionTexts[lang]?.[id] ?? questionTexts["en"]?.[id] ?? ""; +}; + window.updateStaticText = function () { const lang = localStorage.getItem("tutor_lang") ||