Skip to content

i18n audit — TwoMore

Status: Archived

Generated: 2026-05-12. Scope: packages/features/**/*.tsx, packages/app/src/presentation/**/*.tsx, packages/app/src/config/i18n/ko/**/*.ts. Excludes tests, dev-panel, node_modules, dist, generated types.


Section 1 — Hardcoded UI strings

Strings rendered to the user that bypass t(). Each row names the file, line, the exact hardcoded text, and the recommended fix.

#FileLine(s)Hardcoded textRecommended fix
1packages/features/clubs/src/join-club-screen.tsx26'이미 가입된 클럽이에요.'t().clubs.alreadyMember (key exists)
2packages/features/clubs/src/join-club-screen.tsx28'유효하지 않은 초대 코드예요. 다시 확인해보세요.'t().clubs.invalidInviteCode (key exists)
3packages/features/clubs/src/join-club-screen.tsx30'가입 중 오류가 발생했어요. 잠시 후 다시 시도해보세요.'t().errors.joinClubFailed (key exists)
4packages/features/clubs/src/join-club-screen.tsx107'확인 중...' / '가입하기' (ternary)t().clubs.clubNameChecking / t().clubs.join (both keys exist)
5packages/features/sessions/src/participants-sheet.tsx138'이 참가자' (nameMap fallback used inside an Alert.alert dialog)Add t().sessionDetail.unknownParticipant (new key: '참가자')
6packages/features/profile/src/announcements-screen.tsx21–46Entire ANNOUNCEMENTS array: 3 items each with title, date, and multi-paragraph body[]Move to i18n namespace announcements.* or a CMS/remote data source. Placeholder content rendered live to users.
7packages/features/activity/src/shared/pickup-filters.tsx29–35TIER_OPTIONS labels: '브론즈', '실버', '골드', '플래티넘', '다이아', '마스터', '그랜드마스터'Use t().ranking.tier.bronzegrandmaster (keys exist in ranking.ts). Current duplication risks label drift if tier names ever change.
8packages/features/activity/src/shared/pickup-filters.tsx40–45FEE_OPTIONS labels: '0원', '1만원', '2만원', '3만원', '5만원', '10만원'Low risk (numbers are locale-invariant), but unify via a formatter: formatKRW(0), formatKRW(10000) etc. from date-format.ts or a new format-currency.ts helper.
9packages/features/activity/src/shared/pickup-filters.tsx113–129REGION_OPTIONS labels: all 17 Korean 시/도 names hard-wired as label: '서울' etc.Region names are the same in all languages and match REGION_LABELS in packages/app/src/utils/korean-phone.ts. Use REGION_LABELS as the single source of truth rather than a second array.

Total: 9 findings across 3 files.

Notes:

  • Items 1–4 are the highest-priority fixes — all four required keys already exist in i18n, so there is no new key work, just a call-site substitution.
  • Item 5 requires one new key.
  • Item 6 is an architectural decision (i18n vs CMS); the content is currently static placeholder text so it is low-urgency but visually misleading in production.
  • Items 7–9 are data-constant consolidation tasks, not copy mistakes; functional risk is low.

Section 2 — Unnatural Korean translations

HIGH severity

Issues that produce noticeably un-natural or register-inconsistent copy that native Korean speakers will flag.


H-1. common.ts — English "strike/Strike" mixed into Korean UI sentences (5 keys)

File: packages/app/src/config/i18n/ko/common.ts, lines 564–575 (trust.* namespace)

KeyCurrent valueProblem
trust.attendanceDashboardSubtitle'클럽 멤버들의 참석률과 누적 strike을 한눈에'English "strike" mid-Korean sentence with incorrect Korean particle (should be after open syllable)
trust.strikeCount(n) => `Strike ${n}`All-English label rendered in Korean UI
trust.excuseSheetTitle(name) => `${name}의 strike 기록`English "strike" in Korean possessive phrase
trust.recentStrike'최근 strike'English noun in Korean adjective phrase
trust.strikeRecordsEmpty'인정할 strike 기록이 없어요'English noun in Korean sentence
trust.strikeRecordsSubtitle'각 기록을 인정하면 해당 strike이 즉시 사라져요'English "strike" + incorrect particle (should be after open syllable)

Recommended fix: Replace "strike" with 스트라이크 throughout (Koreanized loanword, widely understood) or restructure around 불참 기록 (more natural, avoids loanword entirely).

Example rewrites:

  • attendanceDashboardSubtitle'클럽 멤버들의 참석률과 누적 불참 기록을 한눈에'
  • strikeCount(n) => `불참 기록 ${n}건`
  • excuseSheetTitle(name) => `${name}의 불참 기록`
  • recentStrike'최근 불참 기록'
  • strikeRecordsEmpty'인정할 불참 기록이 없어요'
  • strikeRecordsSubtitle'각 기록을 인정하면 해당 페널티가 즉시 사라져요'

H-2. settings.tsclubMembers.confirmRemoveTitle uses 습니까 (stiff formal) instead of app-wide ~아/어요 register

File: packages/app/src/config/i18n/ko/settings.ts, line 910

Current: (name) => `정말 ${name}님을 추방하시겠습니까?`

The entire app uses the casual-polite ~아/어요 register (Toss UX benchmark). 습니까 is a formal interrogative register used in official documents or broadcasting. It produces jarring tonal whiplash on a confirmation dialog. Every other confirmation dialog in the file uses ~을까요? or ~아/어요.

Recommended fix: (name) => `정말 ${name}님을 추방할까요?`


H-3. settings.tssettingsScreen.rowDeveloperModeSubtitle uses 합니다 (formal declarative)

File: packages/app/src/config/i18n/ko/settings.ts, line 777

Current: '테스트 시나리오 패널을 활성화합니다.'

합니다 is the formal register. Every other settings subtitle in the file uses ~아/어요 or a noun phrase. Even though this string is gated behind a developer setting, it is visible to any user who enables developer mode from settings.

Recommended fix: '테스트 시나리오 패널을 활성화해요.'


H-4. onboarding.tsplayStylePickup and doneFindPickup use "픽업" instead of canonical app term "번개"

File: packages/app/src/config/i18n/ko/onboarding.ts, lines 16 and 23

KeyCurrentProblem
onboarding.playStylePickup'픽업'Uses English loanword. The description key playStylePickupDesc correctly says '자유롭게 번개 경기를 즐겨요', creating label/description inconsistency.
onboarding.playStyleBothDesc'클럽과 픽업 모두 즐겨요'Same inconsistency.
onboarding.doneSubtitle'이제 클럽을 찾거나 픽업 경기를 시작해 보세요!'"픽업 경기" is not the app term.
onboarding.doneFindPickup'픽업 만들기'CTA that should read '번개 만들기' to match t().sessions.pickupForm.title.

The CLAUDE.md rule states: "Terminology → pickup (not meets/bunggae) in code. Korean (번개) stays in i18n only." Onboarding is i18n — these should say 번개.

Recommended fix:

  • playStylePickup'번개'
  • playStyleBothDesc'클럽과 번개 모두 즐겨요'
  • doneSubtitle'이제 클럽을 찾거나 번개 경기를 시작해 보세요!'
  • doneFindPickup'번개 만들기'

MEDIUM severity

Issues that are naturalness imperfections or minor inconsistencies a careful Korean speaker would notice.


M-1. sessions.tsmatchReferee.transferSheetTitle missing space before 주세요

File: packages/app/src/config/i18n/ko/sessions.ts, line 665

Current: '양도할 사람을 선택해주세요' Expected: '양도할 사람을 선택해 주세요'

Korean National Institute of Korean Language (국립국어원) prescribes a space before 주세요 when it follows a verb stem (보조 동사 띄어쓰기). Every other 주세요 in the file and across other namespaces correctly has the space. This is a single-character typo.


M-2. matches.tsmatchHistory.withPartner hardcodes particle without 과/와 alternation

File: packages/app/src/config/i18n/ko/matches.ts, line 296

Current: (name: string) => `(${name}과)`

Korean particle alternation: consonant-final syllable → , vowel-final syllable → . Hardcoding produces (지훈과) (correct) but also (지수과) (incorrect; should be (지수와)). This is a grammatical error that will appear in roughly half of all partner display contexts.

Recommended fix: Use a helper that checks the final character: withJosaGwa(name) from a utility or write inline name.endsWith vowel ? '와' : '과' check. A simple heuristic: check (name.charCodeAt(name.length - 1) - 0xAC00) % 28 === 0 (no final consonant → vowel-final → ).


M-3. clubs.tsanalytics.noShowRate uses English loanword 노쇼

File: packages/app/src/config/i18n/ko/clubs.ts, line 238

Current: '노쇼율'

노쇼 (no-show) is an English loanword in a data analytics label visible only to 총무. The more natural Korean alternative is 불참율 (not-attending rate), consistent with the domain vocabulary used throughout the trust and attendance namespaces. 불참 is already used in trust.strikeRecordNoShow: '불참' (common.ts line 572).

Recommended fix: '불참율'


M-4. home.tsheroExtras.badgeWaitlisted uses loanword 웨이팅 instead of canonical domain term

File: packages/app/src/config/i18n/ko/home.ts, line 268

Current: '웨이팅'

"웨이팅" is an English loanword for "waiting." The app's status system uses waitlisted as the RSVP status, and the canonical Korean for this is 대기 (as used in status.ts and other places). The session detail screen and RSVP flow use 대기 명단 / 대기 consistently.

Recommended fix: '대기' (matching RsvpStatusBadge label for waitlisted)


M-5. sessions.tssessionDetail.deleteSessionAlertBody uses 시겠어요 (formal interrogative) vs app-wide ~할까요? pattern

File: packages/app/src/config/i18n/ko/sessions.ts, line 637

Current: '정말 삭제하시겠어요?'

시겠어요 is a slightly formal interrogative (checking intent via the honorific auxiliary ). The rest of the app's destructive confirmation patterns use ~할까요? ('정말 삭제할까요?') which is casual-polite and matches the Toss tone. The word 정말 already signals seriousness without needing 시겠.

Recommended fix: '정말 삭제할까요?'


M-6. clubs.tspartnerSuggester.breakdownElo uses academic term 근접도

File: packages/app/src/config/i18n/ko/clubs.ts, line 635

Current: 'ELO 근접도'

근접도 (proximity/closeness measure) is formal/academic vocabulary. In context this label appears next to a numeric score in the partner suggester. ELO 유사도 (similarity) or ELO 차이 (difference) would be more natural to a general audience.

Recommended fix: 'ELO 유사도' (or '레이팅 근접' if a noun phrase is preferred)


LOW severity

Minor issues that are technically defensible but could be improved for consistency.


L-1. sessions.ts and home.ts — English abbreviation "RSVP" used in several visible labels

Files: packages/app/src/config/i18n/ko/sessions.ts (line 33: rsvpDeadline), packages/app/src/config/i18n/ko/home.ts (lines 75, 267, 296: rsvpDeadlineSoon, badgeRsvpPending)

Examples:

  • sessions.rsvpDeadline: 'RSVP 마감' → could be '참가 마감'
  • home.beltChip.rsvpDeadlineSoon: 'RSVP 마감 임박' → could be '참가 마감 임박'
  • home.heroExtras.badgeRsvpPending: 'RSVP 미정' → could be '참가 미정'

"RSVP" is understood by the target demographic of Korean tennis club managers who often use the term, but the app's terminology guide uses 참가 for RSVP actions. This is a consistency note rather than a correctness error.


L-2. ranking.tsleaderboard: '리더보드' could be '순위표'

File: packages/app/src/config/i18n/ko/ranking.ts, line 44

Current: '리더보드'

The CLAUDE.md terminology note states: "Korean term note: leaderboard copy uses 순위 (not 랭킹)." 리더보드 is an English loanword for "leaderboard." The surrounding strings consistently use 순위 (globalElo: '전체 순위', clubElo: '클럽 순위'). '순위표' would be fully consistent.

This is low-severity because 리더보드 appears only as a tab label and is widely understood.


L-3. settings.tsdeleteAccountScreen uses a fragmented sentence pattern for the confirmation instruction

File: packages/app/src/config/i18n/ko/settings.ts, lines ~835–841

The pattern splits the instruction into confirmInstructionFull ('탈퇴를 원하시면 아래에 ') + inline rendering of confirmPhrase ('탈퇴합니다') + confirmInstructionSuffix ('를 입력해 주세요').

The split is a localization anti-pattern (word order varies by language). For Korean-only strings this is harmless, but it also creates two strings that share a grammatical dependency. A single parameterized key would be cleaner:

confirmInstruction: (phrase: string) => 탈퇴를 원하시면 아래에 '${phrase}'를 입력해 주세요``


Total: HIGH 4 issue clusters (10 individual keys). MEDIUM 6. LOW 3.


Section 3 — Top-5 high-traffic namespace summaries

home namespace (packages/app/src/config/i18n/ko/home.ts)

Size: ~829 lines. Overall quality: Excellent. The most comprehensive namespace in the codebase. Register is consistently casual-polite ~아/어요. Micro-copy for chips, banners, hero cards, and live-session states is well-crafted.

Strengths:

  • Session state micro-copy (live, locked, recruiting, proximity labels) is natural and concise.
  • Weather descriptions use appropriate colloquial tone ('비 맞을 수 있어요', '겨울 코트').
  • Empty states and friend-activity meta text read like a real Korean product.

Issues found:

  • L-1 (low): rsvpDeadlineSoon, badgeRsvpPending use English "RSVP" (lines 75, 267, 296).
  • M-4 (medium): badgeWaitlisted: '웨이팅' — loanword; should be '대기' (line 268).

No structural or HIGH issues.


sessions namespace (packages/app/src/config/i18n/ko/sessions.ts)

Size: ~773 lines, covering sessions, wizard, RSVP, session status, session summary, recurrence, session form, pickup form, session templates, match board, create-session screen, session detail, scorecard, match referee, and match rules.

Overall quality: Good. Dense and well-organized. Minor inconsistencies in register and spacing.

Strengths:

  • matchRules sub-namespace is thorough — all chip labels, wizard steps, and summary format strings are localized.
  • Session wizard copy (continueDraft, titleOptionalHint) is natural.
  • RSVP and capacity strings are clear.

Issues found:

  • M-1 (medium): matchReferee.transferSheetTitle missing space: 선택해주세요선택해 주세요 (line 665).
  • M-5 (medium): sessionDetail.deleteSessionAlertBody: '정말 삭제하시겠어요?'시겠어요 vs 할까요? register (line 637).
  • L-1 (low): rsvpDeadline: 'RSVP 마감' — English abbreviation (line 33).
  • Defensible: styleSocial: '친목'친선 is more tennis-specific but 친목 is correct Korean.

scorecard sub-namespace (within sessions.ts)

Location: packages/app/src/config/i18n/ko/sessions.ts, lines 646–656.

Size: 10 keys. Overall quality: Clean and minimal.

Strengths:

  • Tab labels (대진표, LIVE, 라운드 결과) are idiomatic.
  • Empty states ('진행 중인 경기가 없어요') are natural.

Issues found:

  • tabLive: 'LIVE' — all-caps English abbreviation. This is consistent with industry convention (Toss, Kakao) for real-time indicators, so this is an intentional choice, not an error.
  • No naturalness issues.

No issues.


matchReferee sub-namespace (within sessions.ts)

Location: packages/app/src/config/i18n/ko/sessions.ts, lines 659–678.

Size: 14 keys. Overall quality: Good. The action labels (심판 자원하기, 심판 양도, 심판 그만하기) are natural and tennis-specific.

Strengths:

  • Toast feedback strings (assignedSuccess, releasedSuccess, proposedSuccess) are concise and use natural Korean.
  • Error messages are specific and actionable.

Issues found:

  • M-1 (medium): transferSheetTitle: '양도할 사람을 선택해주세요' — missing space before 주세요 (line 665). Only issue in the namespace.

clubDetail / clubs namespace (packages/app/src/config/i18n/ko/clubs.ts)

Size: ~823 lines, covering clubs, club creation, club detail, members, dues, analytics, attendance, partner suggester, club settings, and related screens.

Overall quality: Good. Comprehensive. The analytics and admin-only sub-namespaces are less polished than the public-facing ones.

Strengths:

  • Member management copy (confirmRemoveMember, roleOwner, roleAdmin, roleMember) is clear.
  • Club creation wizard steps are natural.
  • Dues management strings are specific and actionable.

Issues found:

  • M-3 (medium): analytics.noShowRate: '노쇼율' — English loanword; prefer '불참율' (line 238).
  • M-6 (medium): partnerSuggester.breakdownElo: 'ELO 근접도' — academic term; prefer 'ELO 유사도' (line 635).
  • H-2 (high — in settings.ts but covers a clubs flow): clubMembers.confirmRemoveTitle uses 습니까 formal register (settings.ts line 910).

End of audit.

Markdown remains the source of truth. Run yarn docs:check before handoff.