Skip to content

Commit e2f2acf

Browse files
jammyclaude
andcommitted
feat: migrate profileQuestions constants to i18n keys
- Replace hardcoded Korean strings in PROFILE_QUESTIONS with i18n key references (titleKey, subtitleKey, labelKey) - Rename getProfileLabel → getProfileLabelKey to return translation key - Update OnboardingModal and ProfilePage to use t() for all question/option text - Add 25 onboarding keys (age, occupation, experience, goal) to ko/en/zh translation files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0818c96 commit e2f2acf

6 files changed

Lines changed: 135 additions & 53 deletions

File tree

packages/frontend/src/components/OnboardingModal.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,10 @@ export function OnboardingModal() {
157157
className="text-center"
158158
>
159159
<h2 className="text-xl font-bold text-[var(--theme-dashboard-title)]">
160-
{currentQuestion.title}
160+
{t(currentQuestion.titleKey)}
161161
</h2>
162162
<p className="text-sm text-[var(--theme-dashboard-text-muted)] mt-1">
163-
{currentQuestion.subtitle}
163+
{t(currentQuestion.subtitleKey)}
164164
</p>
165165
</motion.div>
166166
</AnimatePresence>
@@ -195,7 +195,7 @@ export function OnboardingModal() {
195195
<span className={`text-sm font-medium ${
196196
selectedValue === option.value ? 'text-orange-700' : 'text-[var(--theme-dashboard-title)]'
197197
}`}>
198-
{option.label}
198+
{t(option.labelKey)}
199199
</span>
200200
</motion.button>
201201
))}
@@ -213,7 +213,7 @@ export function OnboardingModal() {
213213
className="flex items-center gap-1 px-4 py-3 text-[var(--theme-dashboard-text-muted)] border-2 border-[var(--theme-dashboard-card-border)] rounded-xl font-medium hover:bg-[var(--theme-dashboard-section-header-bg)] transition-colors"
214214
>
215215
<ChevronLeft className="w-4 h-4" />
216-
이전
216+
{t('common.previous')}
217217
</motion.button>
218218
)}
219219

@@ -237,16 +237,16 @@ export function OnboardingModal() {
237237
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
238238
className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full"
239239
/>
240-
저장 중...
240+
{t('common.loading')}
241241
</span>
242242
) : isLastStep ? (
243243
<>
244244
<Sparkles className="w-4 h-4" />
245-
시작하기
245+
{t('home.start_learning')}
246246
</>
247247
) : (
248248
<>
249-
다음
249+
{t('common.next')}
250250
<ChevronRight className="w-4 h-4" />
251251
</>
252252
)}
@@ -259,7 +259,7 @@ export function OnboardingModal() {
259259
onClick={handleSkip}
260260
className="text-xs text-[var(--theme-dashboard-text-muted)] hover:text-[var(--theme-dashboard-title)] transition-colors"
261261
>
262-
나중에 설정하기
262+
{t('onboarding.skip_for_now')}
263263
</button>
264264
</div>
265265
</motion.div>

packages/frontend/src/constants/profileQuestions.ts

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,22 @@
55
* USAGE:
66
* - OnboardingModal: 최초 설문
77
* - ProfilePage: 조회/수정
8+
*
9+
* NOTE: title/subtitle/label 은 i18n 키로 저장.
10+
* 컴포넌트에서 t(question.titleKey) 형태로 사용.
811
*/
912

1013
// 설문 항목 타입
1114
export interface ProfileQuestion {
1215
key: ProfileQuestionKey;
13-
title: string;
14-
subtitle: string;
16+
titleKey: string;
17+
subtitleKey: string;
1518
options: ProfileOption[];
1619
}
1720

1821
export interface ProfileOption {
1922
value: string;
20-
label: string;
23+
labelKey: string;
2124
emoji: string;
2225
}
2326

@@ -32,61 +35,60 @@ export type ProfileQuestionKey =
3235
export const PROFILE_QUESTIONS: readonly ProfileQuestion[] = [
3336
{
3437
key: 'ageGroup',
35-
title: '나이대가 어떻게 되세요?',
36-
subtitle: '맞춤형 학습 콘텐츠를 위해 필요해요',
38+
titleKey: 'onboarding.age_question',
39+
subtitleKey: 'onboarding.age_subtitle',
3740
options: [
38-
{ value: '10s', label: '10대', emoji: '🎒' },
39-
{ value: '20s', label: '20대', emoji: '🎓' },
40-
{ value: '30s', label: '30대', emoji: '💼' },
41-
{ value: '40s+', label: '40대 이상', emoji: '🌟' },
41+
{ value: '10s', labelKey: 'onboarding.age_10s', emoji: '🎒' },
42+
{ value: '20s', labelKey: 'onboarding.age_20s', emoji: '🎓' },
43+
{ value: '30s', labelKey: 'onboarding.age_30s', emoji: '💼' },
44+
{ value: '40s+', labelKey: 'onboarding.age_40s_plus', emoji: '🌟' },
4245
],
4346
},
4447
{
4548
key: 'occupation',
46-
title: '현재 어떤 일을 하고 계세요?',
47-
subtitle: '학습 목표에 맞는 추천을 드릴게요',
49+
titleKey: 'onboarding.occupation_question',
50+
subtitleKey: 'onboarding.occupation_subtitle',
4851
options: [
49-
{ value: 'student_middle', label: '중학생', emoji: '📚' },
50-
{ value: 'student_high', label: '고등학생', emoji: '📝' },
51-
{ value: 'student_univ', label: '대학생', emoji: '🎓' },
52-
{ value: 'job_seeker', label: '취업 준비생', emoji: '🔍' },
53-
{ value: 'worker', label: '직장인', emoji: '💻' },
54-
{ value: 'other', label: '기타', emoji: '✨' },
52+
{ value: 'student_middle', labelKey: 'onboarding.student_middle', emoji: '📚' },
53+
{ value: 'student_high', labelKey: 'onboarding.student_high', emoji: '📝' },
54+
{ value: 'student_univ', labelKey: 'onboarding.student_univ', emoji: '🎓' },
55+
{ value: 'job_seeker', labelKey: 'onboarding.job_seeker', emoji: '🔍' },
56+
{ value: 'worker', labelKey: 'onboarding.worker', emoji: '💻' },
57+
{ value: 'other', labelKey: 'onboarding.other', emoji: '✨' },
5558
],
5659
},
5760
{
5861
key: 'programmingExp',
59-
title: '프로그래밍 경험이 있으신가요?',
60-
subtitle: '수준에 맞는 설명을 제공해 드릴게요',
62+
titleKey: 'onboarding.exp_question',
63+
subtitleKey: 'onboarding.exp_subtitle',
6164
options: [
62-
{ value: 'none', label: '처음이에요', emoji: '🌱' },
63-
{ value: 'less_1y', label: '1년 미만', emoji: '🌿' },
64-
{ value: '1_3y', label: '1~3년', emoji: '🌳' },
65-
{ value: '3y_plus', label: '3년 이상', emoji: '🏆' },
65+
{ value: 'none', labelKey: 'onboarding.exp_none', emoji: '🌱' },
66+
{ value: 'less_1y', labelKey: 'onboarding.exp_less_1y', emoji: '🌿' },
67+
{ value: '1_3y', labelKey: 'onboarding.exp_1_3y', emoji: '🌳' },
68+
{ value: '3y_plus', labelKey: 'onboarding.exp_3y_plus', emoji: '🏆' },
6669
],
6770
},
6871
{
6972
key: 'learningGoal',
70-
title: '어떤 목표로 학습하시나요?',
71-
subtitle: '목표에 맞는 학습 경로를 추천해 드릴게요',
73+
titleKey: 'onboarding.goal_question',
74+
subtitleKey: 'onboarding.goal_subtitle',
7275
options: [
73-
{ value: 'basics', label: '기초부터 탄탄히', emoji: '📖' },
74-
{ value: 'job_prep', label: '취업/이직 준비', emoji: '🎯' },
75-
{ value: 'skill_up', label: '실력 향상', emoji: '📈' },
76-
{ value: 'curiosity', label: '호기심/재미', emoji: '🎮' },
76+
{ value: 'basics', labelKey: 'onboarding.goal_basics', emoji: '📖' },
77+
{ value: 'job_prep', labelKey: 'onboarding.goal_job_prep', emoji: '🎯' },
78+
{ value: 'skill_up', labelKey: 'onboarding.goal_skill_up', emoji: '📈' },
79+
{ value: 'curiosity', labelKey: 'onboarding.goal_curiosity', emoji: '🎮' },
7780
],
7881
},
7982
] as const;
8083

8184
/**
82-
* value로 label 찾기 (프로필 표시용)
85+
* value로 labelKey 찾기 (컴포넌트에서 t(key) 로 번역)
8386
*/
84-
export function getProfileLabel(key: ProfileQuestionKey, value: string): string {
87+
export function getProfileLabelKey(key: ProfileQuestionKey, value: string): string {
8588
const question = PROFILE_QUESTIONS.find((q) => q.key === key);
8689
if (!question) return value;
87-
8890
const option = question.options.find((o) => o.value === value);
89-
return option?.label || value;
91+
return option?.labelKey ?? value;
9092
}
9193

9294
/**
@@ -95,9 +97,8 @@ export function getProfileLabel(key: ProfileQuestionKey, value: string): string
9597
export function getProfileEmoji(key: ProfileQuestionKey, value: string): string {
9698
const question = PROFILE_QUESTIONS.find((q) => q.key === key);
9799
if (!question) return '';
98-
99100
const option = question.options.find((o) => o.value === value);
100-
return option?.emoji || '';
101+
return option?.emoji ?? '';
101102
}
102103

103104
/**

packages/frontend/src/features/profile/ProfilePage.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
} from '@/components/ui/dialog';
2727
import {
2828
PROFILE_QUESTIONS,
29-
getProfileLabel,
29+
getProfileLabelKey,
3030
getProfileEmoji,
3131
type ProfileQuestionKey,
3232
} from '@/constants/profileQuestions';
@@ -385,16 +385,16 @@ function getErrorMessage(error: unknown, fallback: string): string {
385385
>
386386
<div>
387387
<p className="text-xs text-[var(--theme-dashboard-text-muted)] mb-1">
388-
{question.title.replace('?', '')}
388+
{t(question.titleKey).replace('?', '')}
389389
</p>
390390
{value ? (
391391
<p className="text-sm font-medium text-[var(--theme-dashboard-title)] flex items-center gap-2">
392392
<span>{getProfileEmoji(question.key, value)}</span>
393-
<span>{getProfileLabel(question.key, value)}</span>
393+
<span>{t(getProfileLabelKey(question.key, value))}</span>
394394
</p>
395395
) : (
396396
<p className="text-sm text-[var(--theme-dashboard-text-muted)]">
397-
아직 설정하지 않았어요
397+
{t('learning_profile.not_set')}
398398
</p>
399399
)}
400400
</div>
@@ -414,13 +414,13 @@ function getErrorMessage(error: unknown, fallback: string): string {
414414
<div className="p-4 bg-gradient-to-br from-orange-50 to-amber-50 rounded-xl border-2 border-orange-200">
415415
<div className="flex items-center justify-between mb-3">
416416
<p className="text-sm font-semibold text-[var(--theme-dashboard-title)]">
417-
{question.title}
417+
{t(question.titleKey)}
418418
</p>
419419
<button
420420
onClick={() => setEditingKey(null)}
421421
className="text-xs text-[var(--theme-dashboard-text-muted)] hover:text-[var(--theme-dashboard-title)] transition-colors"
422422
>
423-
취소
423+
{t('common.cancel')}
424424
</button>
425425
</div>
426426
<div className="grid grid-cols-2 gap-2">
@@ -442,7 +442,7 @@ function getErrorMessage(error: unknown, fallback: string): string {
442442
>
443443
<span className="text-lg">{option.emoji}</span>
444444
<span className="text-sm font-medium text-[var(--theme-dashboard-title)]">
445-
{option.label}
445+
{t(option.labelKey)}
446446
</span>
447447
{value === option.value && (
448448
<Check className="w-4 h-4 text-orange-500 ml-auto" />

packages/frontend/src/locales/en/translation.json

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,34 @@
493493
"minimal": "Minimal"
494494
},
495495
"onboarding": {
496-
"txt_939764": "Skip for now"
496+
"txt_939764": "Skip for now",
497+
"age_question": "How old are you?",
498+
"age_subtitle": "Needed for personalized learning content",
499+
"age_10s": "Teens",
500+
"age_20s": "20s",
501+
"age_30s": "30s",
502+
"age_40s_plus": "40s and above",
503+
"occupation_question": "What do you currently do?",
504+
"occupation_subtitle": "We'll recommend courses based on your goals",
505+
"student_middle": "Middle school student",
506+
"student_high": "High school student",
507+
"student_univ": "University student",
508+
"job_seeker": "Job seeker",
509+
"worker": "Working professional",
510+
"other": "Other",
511+
"exp_question": "Do you have programming experience?",
512+
"exp_subtitle": "We'll tailor explanations to your level",
513+
"exp_none": "Complete beginner",
514+
"exp_less_1y": "Less than 1 year",
515+
"exp_1_3y": "1–3 years",
516+
"exp_3y_plus": "3+ years",
517+
"goal_question": "What is your learning goal?",
518+
"goal_subtitle": "We'll recommend a learning path for your goal",
519+
"goal_basics": "Build a solid foundation",
520+
"goal_job_prep": "Job / career change prep",
521+
"goal_skill_up": "Improve my skills",
522+
"goal_curiosity": "Curiosity / fun",
523+
"skip_for_now": "Skip for now"
497524
},
498525
"gamification": {
499526
"txt_c09ebd": "Start your streak",

packages/frontend/src/locales/ko/translation.json

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,34 @@
493493
"minimal": "미니멀"
494494
},
495495
"onboarding": {
496-
"txt_939764": "나중에 하기"
496+
"txt_939764": "나중에 하기",
497+
"age_question": "나이대가 어떻게 되세요?",
498+
"age_subtitle": "맞춤형 학습 콘텐츠를 위해 필요해요",
499+
"age_10s": "10대",
500+
"age_20s": "20대",
501+
"age_30s": "30대",
502+
"age_40s_plus": "40대 이상",
503+
"occupation_question": "현재 어떤 일을 하고 계세요?",
504+
"occupation_subtitle": "학습 목표에 맞는 추천을 드릴게요",
505+
"student_middle": "중학생",
506+
"student_high": "고등학생",
507+
"student_univ": "대학생",
508+
"job_seeker": "취업 준비생",
509+
"worker": "직장인",
510+
"other": "기타",
511+
"exp_question": "프로그래밍 경험이 있으신가요?",
512+
"exp_subtitle": "수준에 맞는 설명을 제공해 드릴게요",
513+
"exp_none": "처음이에요",
514+
"exp_less_1y": "1년 미만",
515+
"exp_1_3y": "1~3년",
516+
"exp_3y_plus": "3년 이상",
517+
"goal_question": "어떤 목표로 학습하시나요?",
518+
"goal_subtitle": "목표에 맞는 학습 경로를 추천해 드릴게요",
519+
"goal_basics": "기초부터 탄탄히",
520+
"goal_job_prep": "취업/이직 준비",
521+
"goal_skill_up": "실력 향상",
522+
"goal_curiosity": "호기심/재미",
523+
"skip_for_now": "나중에 하기"
497524
},
498525
"gamification": {
499526
"txt_c09ebd": "스트릭 시작하기",

packages/frontend/src/locales/zh/translation.json

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,34 @@
493493
"minimal": "简约"
494494
},
495495
"onboarding": {
496-
"txt_939764": "稍后再说"
496+
"txt_939764": "稍后再说",
497+
"age_question": "您的年龄段是?",
498+
"age_subtitle": "用于提供个性化学习内容",
499+
"age_10s": "10多岁",
500+
"age_20s": "20多岁",
501+
"age_30s": "30多岁",
502+
"age_40s_plus": "40岁及以上",
503+
"occupation_question": "您目前从事什么工作?",
504+
"occupation_subtitle": "我们将根据您的目标推荐课程",
505+
"student_middle": "初中生",
506+
"student_high": "高中生",
507+
"student_univ": "大学生",
508+
"job_seeker": "求职者",
509+
"worker": "职场人",
510+
"other": "其他",
511+
"exp_question": "您有编程经验吗?",
512+
"exp_subtitle": "我们将根据您的水平提供解释",
513+
"exp_none": "完全零基础",
514+
"exp_less_1y": "不到1年",
515+
"exp_1_3y": "1~3年",
516+
"exp_3y_plus": "3年以上",
517+
"goal_question": "您的学习目标是什么?",
518+
"goal_subtitle": "我们将为您推荐合适的学习路径",
519+
"goal_basics": "打好基础",
520+
"goal_job_prep": "求职/转行准备",
521+
"goal_skill_up": "提升技能",
522+
"goal_curiosity": "兴趣/娱乐",
523+
"skip_for_now": "稍后再说"
497524
},
498525
"gamification": {
499526
"txt_c09ebd": "开始连续打卡",

0 commit comments

Comments
 (0)