feat: localize interview questions (ko/en), send X-Lang header
This commit is contained in:
@@ -13,6 +13,7 @@ type createDiagnosticSessionRequest struct {
|
|||||||
TargetRole string `json:"target_role"`
|
TargetRole string `json:"target_role"`
|
||||||
Stack []string `json:"stack"`
|
Stack []string `json:"stack"`
|
||||||
InterviewTimeline string `json:"interview_timeline"`
|
InterviewTimeline string `json:"interview_timeline"`
|
||||||
|
Lang string `json:"lang"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type submitDiagnosticAnswerRequest struct {
|
type submitDiagnosticAnswerRequest struct {
|
||||||
@@ -27,11 +28,19 @@ func (h Handler) createDiagnosticSession(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
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{
|
session, err := h.diagnostic.CreateSession(r.Context(), interview.CreateSessionInput{
|
||||||
UserID: req.UserID,
|
UserID: req.UserID,
|
||||||
TargetRole: req.TargetRole,
|
TargetRole: req.TargetRole,
|
||||||
Stack: req.Stack,
|
Stack: req.Stack,
|
||||||
InterviewTimeline: req.InterviewTimeline,
|
InterviewTimeline: req.InterviewTimeline,
|
||||||
|
Lang: lang,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, http.StatusBadRequest, err.Error())
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
|
|||||||
@@ -2,28 +2,49 @@ package interview
|
|||||||
|
|
||||||
import "tutor/internal/workflows"
|
import "tutor/internal/workflows"
|
||||||
|
|
||||||
func BackendDeveloperQuestions() []Question {
|
var questionPrompts = map[string]map[string]string{
|
||||||
return []Question{
|
"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",
|
ID: "backend-http-idempotency",
|
||||||
Prompt: "What makes an HTTP method idempotent, and why does that matter for retries?",
|
|
||||||
Concepts: []workflows.ConceptRef{
|
Concepts: []workflows.ConceptRef{
|
||||||
{ID: "http-idempotency", Label: "HTTP idempotency", Track: BackendDeveloperTrack},
|
{ID: "http-idempotency", Label: "HTTP idempotency", Track: BackendDeveloperTrack},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "backend-db-index-tradeoff",
|
ID: "backend-db-index-tradeoff",
|
||||||
Prompt: "When would adding a database index improve an API, and what tradeoffs can it introduce?",
|
|
||||||
Concepts: []workflows.ConceptRef{
|
Concepts: []workflows.ConceptRef{
|
||||||
{ID: "database-indexes", Label: "Database indexes", Track: BackendDeveloperTrack},
|
{ID: "database-indexes", Label: "Database indexes", Track: BackendDeveloperTrack},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "backend-cache-invalidation",
|
ID: "backend-cache-invalidation",
|
||||||
Prompt: "How would you decide whether to cache an API response, and how would you handle stale data?",
|
|
||||||
Concepts: []workflows.ConceptRef{
|
Concepts: []workflows.ConceptRef{
|
||||||
{ID: "cache-invalidation", Label: "Cache invalidation", Track: BackendDeveloperTrack},
|
{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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (s *Service) CreateSession(_ context.Context, input CreateSessionInput) (Se
|
|||||||
TargetRole: input.TargetRole,
|
TargetRole: input.TargetRole,
|
||||||
Stack: append([]string(nil), input.Stack...),
|
Stack: append([]string(nil), input.Stack...),
|
||||||
InterviewTimeline: input.InterviewTimeline,
|
InterviewTimeline: input.InterviewTimeline,
|
||||||
Questions: BackendDeveloperQuestions(),
|
Questions: BackendDeveloperQuestions(input.Lang),
|
||||||
CreatedAt: time.Now().UTC(),
|
CreatedAt: time.Now().UTC(),
|
||||||
}
|
}
|
||||||
if s.memory != nil {
|
if s.memory != nil {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ type CreateSessionInput struct {
|
|||||||
TargetRole string
|
TargetRole string
|
||||||
Stack []string
|
Stack []string
|
||||||
InterviewTimeline string
|
InterviewTimeline string
|
||||||
|
Lang string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubmitAnswerInput struct {
|
type SubmitAnswerInput struct {
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ els.sessionForm.addEventListener("submit", async (event) => {
|
|||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
interview_timeline: value("#timeline"),
|
interview_timeline: value("#timeline"),
|
||||||
|
lang: localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko",
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -216,7 +217,7 @@ function renderSession() {
|
|||||||
"aria-pressed",
|
"aria-pressed",
|
||||||
String(state.selectedQuestion?.id === question.id)
|
String(state.selectedQuestion?.id === question.id)
|
||||||
);
|
);
|
||||||
button.innerHTML = `<span class="question-id">${escapeHTML(question.id)}</span>${escapeHTML(question.prompt)}`;
|
button.innerHTML = `<span class="question-id">${escapeHTML(question.id)}</span>${escapeHTML(tq(question.id) || question.prompt)}`;
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
state.selectedQuestion = question;
|
state.selectedQuestion = question;
|
||||||
els.answerText.value = "";
|
els.answerText.value = "";
|
||||||
@@ -465,7 +466,8 @@ els.logoutButton.addEventListener("click", () => {
|
|||||||
|
|
||||||
async function request(url, options = {}) {
|
async function request(url, options = {}) {
|
||||||
const token = localStorage.getItem("tutor_token");
|
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}`;
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||||
const response = await fetch(url, { headers, ...options });
|
const response = await fetch(url, { headers, ...options });
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
|
|||||||
@@ -181,6 +181,33 @@ window.t = function (key, ...args) {
|
|||||||
return typeof text === "function" ? text(...args) : text;
|
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 () {
|
window.updateStaticText = function () {
|
||||||
const lang =
|
const lang =
|
||||||
localStorage.getItem("tutor_lang") ||
|
localStorage.getItem("tutor_lang") ||
|
||||||
|
|||||||
Reference in New Issue
Block a user