Skip to content

Conversation

@useon
Copy link
Contributor

@useon useon commented Jan 27, 2026

🚩 연관 이슈

closed #408

📝 작업 내용

먼저 병합된 자동화를 이용해서 실제 i18n 국제화 작업을 진행했습니다.
헤더에 언어 선택을 할 수 있는 셀렉터를 추가하였습니다!

언어 셀렉터 추가

헤더가 있는 모든 페이지에서 언어 셀렉터를 통해 언어를 선택할 수 있습니다.

image

언어 설정에 따라 키에 따라 설정된 밸류 값으로 바로 변경되는 것을 확인하실 수 있습니다!
설정에 따라 url이 변경이 되며, localStorage에도 언어 설정 값을 넣어 다음번 방문에도 이전에 사용했던 설정값대로 사용할 수 있도록 했습니다.

자동화를 적용해보며 공유하고 싶은 내용

어디까지나 변환을 도와주는 도구로서 자동화를 이용해야 하고, 자동화 적용시에는 검토가 필요합니다.

자동 변환이 안 되거나 검토가 필요한 부분

  1. 컴포넌트 외부 상수/props 기본값 번역

    • 자동화를 적용하며 컴포넌트의 구조를 변경하여 prop의 기본값은 내부에서 결정하도록 하고, 상수를 바로 ui에 사용하는 경우 미리 해당 한글 키를 각 json 파일에 정의하고 받는 값 자체를 수동으로 t()로 감싸 처리했습니다.
  2. 숫자/문자 추출
    image
    -30초, +30초 등 각각 키로 추출되는 것을 한글인 '초'만 추출하고 싶었습니다. 하지만 아래의 이유들로 제외하였습니다.

    1. 현재 프로젝트는 동적 데이터는 템플릿 리터럴로 관리하고 있기 때문에 하드코딩된 숫자로 인한 키가 엄청 많아지는 문제가 발생할 가능성이 낮다고 판단했습니다.
    2. 기능 구현으로 인한 스크립트 복잡성 증가 대비, 현재 얻을 수 있는 이득이 크지 않다고 생각했습니다. (줄어드는 키가 10개 미만이었음)
  3. 컴포넌트 내 ui에 사용되지 않는 한글인 경우
    검토를 통해 ui에 드러나지 않지만 한글 문자열을 사용한 경우 불필요하게 key값에 추가될 수 있기 때문에 번역 대상이 아니면 수동으로 변환된 내용을 되돌려야 합니다.

  4. 중첩된 HTML 구조
    이는 현재 각각 문자열에 대응하여 자동 변환의 대상이지만 어순이 중요한 경우 Trans 형태로 수동 변환합니다.

중첩인 경우 Trans 컴포넌트로 수정

아래 케이스는 자동화로 단순 분리하면 번역 어순이 깨질 수 있어 수동으로 i18next-react의 Trans 컴포넌트로 처리해야 합니다.
[참고] https://react.i18next.com/latest/trans-component#typescript

적용 대상

  • 문장 중간에 아이콘/이미지/버튼가 중첩 HTML로 구성되어 있어 독립적이지 않고 자연스러운 어순을 위해 함께 키로 관리해야 하는 경우

예시 코드

<h3 className="whitespace-pre-line">
  <Trans
    i18nKey="토론자가 작전 시간을\n요청하면 <0/>\n버튼을 눌러 시간을 사용해요"
    components={[
      <img key="timeout" src={timeoutButton} alt={t('작전 시간 사용')} />,
    ]}
  />
</h3>
  1. **문장 안에 들어갈 컴포넌트 위치를 <0/>, <1/>처럼 표시합니다.
  2. Trans 컴포넌트의 components 배열에 실제 React 요소를 순서대로 전달합니다.
  3. 렌더링 시 줄바꿈이 필요하면 br태그 대신 \n을 쓰고 부모에 whitespace-pre-line스타일을 적용합니다.
  4. 번역 key - value는 아래와 같이 관리합니다.
  • ko
"토론자가 작전 시간을\n요청하면 <0/>\n버튼을 눌러 시간을 사용해요": "토론자가 작전 시간을\n요청하면 <0/>\n버튼을 눌러 시간을 사용해요"
  • en
"토론자가 작전 시간을\n요청하면 <0/>\n버튼을 눌러 시간을 사용해요": "If a debater requests Prep Time\npress <0/>\nto use time"

🏞️ 스크린샷 (선택)

2026-01-28.5.34.53.mov

🗣️ 리뷰 요구사항 (선택)

한글 하드코딩 문자열이 있던 컴포넌트를 대상으로 전부 변환이 일어났기 때문에 파일 체인지가 엄청 많아서 보기 힘드실 수 있습니다 🥲
커밋 단위로 보시는 것을 추천드립니다 .. (단순 변환 및 훅, import 추가는 빠르게 넘기시길 !)
파일이 많이 바뀌었고 엄청난 병합들을 해결해서 제가 확인한다고 열심히 확인했지만!!
혹시나 전체적으로 번역이 안되는 부분, 동작이 안되는 부분이 있는지 함께 봐주신다면 감사하겠습니다!

Summary by CodeRabbit

  • New Features

    • 한국어·영어 번역 리소스 대량 추가(각 언어 다수 키) 및 전역 다국어 지원 도입
    • 헤더의 언어 선택기 추가 및 언어별 라우팅/경로 생성 기능 도입
  • UI 변경

    • 다수 컴포넌트의 텍스트를 번역 키로 전환해 로컬라이즈된 문구 확대 적용
    • 접근성 레이블·버튼·모달 등 번역 적용 및 일부 레이아웃·표현 보완
  • Removed

    • GoogleButton 컴포넌트 제거
  • Chores

    • i18n 초기화·테스트용 번역 응답 핸들러 추가, 공유 URL 정규화, 사용자 에이전트 검사 유틸 제거

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@AGENT.md`:
- Around line 109-113: 문서의 예시에서 사용된 navigateWithLang import 경로가 실제 구현과 불일치하므로,
AGENT.md의 코드 블록에서 사용한 navigateWithLang 참조를 실제 모듈명인 langNavigation으로 변경하거나(예:
import { navigateWithLang } from '../util/langNavigation') 문서 본문에 현재 파일/함수명이
navigateWithLang 대신 langNavigation임을 명확히 표기해 예시와 구현이 일치하도록 수정하세요; 참조 대상
함수명(navigateWithLang, langNavigation)을 찾아 예시와 설명을 동기화하면 됩니다.

In `@src/components/BackActionHandler.tsx`:
- Around line 14-23: The strict equality check in handleBackAction can fail when
window.location.pathname has a trailing slash (e.g., "/en/") while
buildLangPath('/', lang) returns "/en"; normalize the pathname before comparing:
use the existing path normalization helper (e.g., stripDefaultLangFromPath or
trim trailing slashes) to compute a normalizedPathname from
window.location.pathname and then compare normalizedPathname === rootPath;
update the handleBackAction callback to use the normalized pathname prior to
calling getAccessToken and navigate (referencing handleBackAction,
buildLangPath, getAccessToken, navigate, and stripDefaultLangFromPath).

In `@src/page/TableSharingPage/TableSharingPage.tsx`:
- Around line 81-84: The Promise callback currently calls closeModal() and then
does throw new Error(t('공유받은 테이블을 저장하지 못했어요.')), which creates an unhandled
promise rejection that React Error Boundary won't catch; update the promise
chain around the function that contains this callback (referencing closeModal
and the i18n t(...) message) to handle errors explicitly — either append a
.catch(...) to the promise to log and surface the error (or call a provided
error handler), or set an error state (e.g., setError(...)) and navigate/render
your error page so the Error Boundary or app-level error UI can display the
failure instead of throwing inside the .then() callback.

In `@src/page/VoteParticipationPage/VoteParticipationPage.tsx`:
- Around line 176-178: In VoteParticipationPage update the paragraph element
that currently uses the typo'd Tailwind className "text-bas mt-2" to the correct
"text-base mt-2" (locate the <p> element in the VoteParticipationPage component
that renders "(제출 후에는 변경이 불가능 합니다.)" and fix the className string).
🧹 Nitpick comments (9)
src/components/GoToDebateEndButton/GoToDebateEndButton.tsx (1)

21-25: handleClick 함수의 불필요한 매개변수

tableId는 이미 컴포넌트 props에서 스코프 내에 있으므로, 함수 매개변수로 다시 받을 필요가 없습니다.

♻️ 간소화 제안
-  const handleClick = (tableId: number) => {
+  const handleClick = () => {
     const currentLang = i18n.resolvedLanguage ?? i18n.language;
     const lang = isSupportedLang(currentLang) ? currentLang : DEFAULT_LANG;
     navigate(buildLangPath(`/table/customize/${tableId}/end`, lang));
   };

그리고 호출부도 수정:

-      onClick={() => handleClick(tableId)}
+      onClick={handleClick}
src/page/DebateVoteResultPage/DebateVoteResultPage.tsx (1)

43-50: handleGoHomeuseCallback으로 메모이제이션하는 것을 권장합니다.

handleGoToEndPageuseCallback으로 감싸져 있지만 handleGoHome은 그렇지 않습니다. handleGoHomeVoteDetailResult 컴포넌트에 prop으로 전달되므로(line 164), 매 렌더링마다 새 함수 참조가 생성되어 자식 컴포넌트의 불필요한 리렌더링을 유발할 수 있습니다.

♻️ 메모이제이션 적용 제안
-  const handleGoHome = () => {
-    navigate(rootPath);
-  };
+  const handleGoHome = useCallback(() => {
+    navigate(rootPath);
+  }, [navigate, rootPath]);
src/layout/components/header/StickyTriSectionHeader.tsx (2)

59-62: 언어 결정 로직을 커스텀 훅으로 추출 고려

현재 언어를 결정하고 언어 인식 경로를 생성하는 이 패턴이 다른 컴포넌트에서도 반복될 수 있습니다. 재사용성을 위해 커스텀 훅으로 추출하는 것을 고려해 볼 수 있습니다.

// 예시: useCurrentLang 훅
function useCurrentLang() {
  const { i18n } = useTranslation();
  const currentLang = i18n.resolvedLanguage ?? i18n.language;
  return isSupportedLang(currentLang) ? currentLang : DEFAULT_LANG;
}

현재 구현도 정상 동작하므로 향후 리팩토링 시 고려하시면 됩니다.


114-136: isLoggedIn() 호출 결과를 변수로 추출 고려

isLoggedIn() 함수가 동일 렌더링 내에서 여러 번 호출됩니다 (lines 118, 119, 127). 함수가 단순히 토큰 존재 여부를 확인하는 것이라면 성능 영향은 미미하지만, 가독성과 의도 명확성을 위해 변수로 추출하는 것을 고려해 볼 수 있습니다.

♻️ 제안하는 리팩토링
 case 'auth':
+  const loggedIn = isLoggedIn();
   return (
     <button
       className="flex h-full items-center justify-center p-[4px]"
-      aria-label={isLoggedIn() ? t('로그아웃') : t('로그인')}
-      title={isLoggedIn() ? t('로그아웃') : t('로그인')}
+      aria-label={loggedIn ? t('로그아웃') : t('로그인')}
+      title={loggedIn ? t('로그아웃') : t('로그인')}
       key={`${iconName}-${index}`}
       onClick={() => {
         // 전체 화면 상태에서 홈으로 이동하는 경우, 전체 화면 비활성화
         if (isFullscreen) {
           setFullscreen(false);
         }

-        if (isLoggedIn()) {
+        if (loggedIn) {
           logoutMutate();
         } else {
           openModal();
         }
       }}
     >
src/page/TableComposition/components/TimerCreationContent/TimerCreationContent.tsx (2)

48-52: STANCE_RECORD 값들이 번역되지 않아 i18n 적용이 일관되지 않습니다.

SPEECH_TYPE_RECORD의 값들은 t()로 감싸서 번역하고 있지만, STANCE_RECORD의 값들은 번역 처리 없이 직접 사용되고 있습니다. 특히 라인 252에서 STANCE_RECORD['NEUTRAL']이 번역 없이 사용됩니다.

제안하는 수정

라인 252를 수정하여 일관된 번역 적용:

-      { value: 'NEUTRAL', label: STANCE_RECORD['NEUTRAL'] },
+      { value: 'NEUTRAL', label: t(STANCE_RECORD['NEUTRAL']) },

또는 STANCE_RECORD를 별도의 유틸리티 파일로 분리하여 SPEECH_TYPE_RECORD와 동일한 패턴으로 관리하는 것을 권장합니다.


258-266: BellTypeToString 값들도 번역 처리가 필요합니다.

bellOptions에서 사용되는 BellTypeToString 값들과 라인 864에서 표시되는 BellTypeToString[bell.type]이 번역 없이 사용되고 있습니다. 다른 UI 문자열들과 일관성을 위해 번역 처리가 필요합니다.

제안하는 수정
  const bellOptions: DropdownMenuItem<BellType>[] = useMemo(
    () => [
-      { value: 'BEFORE_END', label: BellTypeToString['BEFORE_END'] },
-      { value: 'AFTER_END', label: BellTypeToString['AFTER_END'] },
-      { value: 'AFTER_START', label: BellTypeToString['AFTER_START'] },
+      { value: 'BEFORE_END', label: t(BellTypeToString['BEFORE_END']) },
+      { value: 'AFTER_END', label: t(BellTypeToString['AFTER_END']) },
+      { value: 'AFTER_START', label: t(BellTypeToString['AFTER_START']) },
    ],
-    [],
+    [t],
  );

라인 864도 동일하게 수정:

-  {BellTypeToString[bell.type]}
+  {t(BellTypeToString[bell.type])}
src/apis/axiosInstance.ts (1)

7-12: 현재 언어 계산 로직을 getCurrentLang로 통일해 중복 제거 권장

Line 75-77의 언어 판별 로직은 중앙화 유틸을 쓰면 규칙 변경 시 단일 지점에서 관리됩니다.

♻️ 제안 리팩터링
 import i18n from '../i18n';
 import {
   buildLangPath,
-  DEFAULT_LANG,
-  isSupportedLang,
+  getCurrentLang,
 } from '../util/languageRouting';
 
 ...
-        const currentLang = i18n.resolvedLanguage ?? i18n.language;
-        const lang = isSupportedLang(currentLang) ? currentLang : DEFAULT_LANG;
+        const lang = getCurrentLang(i18n);
         window.location.href = buildLangPath('/home', lang);

Also applies to: 75-77

src/constants/debate_template.ts (1)

16-20: baseUrl 해결 로직이 중복됩니다.

src/util/arrayEncoding.tscreateTableShareUrl과 동일한 baseUrl 폴백 및 정규화 로직이 중복되어 있습니다. 공통 유틸리티 함수로 추출하면 유지보수성이 향상됩니다.

♻️ baseUrl 해결 로직 추출 제안

src/util/languageRouting.ts 또는 별도 유틸리티에 공통 함수 추가:

// src/util/urlUtils.ts
export function resolveBaseUrl(baseUrl?: string): string {
  const resolved = baseUrl && baseUrl.trim() !== '' ? baseUrl : window.location.origin;
  return resolved.replace(/\/+$/, '');
}
src/page/LandingPage/hooks/useLandingPageHandlers.ts (1)

18-23: 콜백을 useCallback으로 래핑하여 불필요한 뮤테이션 재생성을 방지하세요.

useLogout(() => navigate(homePath))는 매 렌더링마다 새로운 뮤테이션 객체를 생성합니다. 콜백을 useCallback으로 메모이제이션하고 homePath를 의존성으로 지정하면 언어 변경 시에만 콜백이 업데이트되어 렌더링 성능이 개선됩니다.

const logoutCallback = useCallback(() => navigate(homePath), [homePath, navigate]);
const { mutate: logoutMutate } = useLogout(logoutCallback);

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/page/TableComposition/components/TimerCreationContent/TimerCreationContent.tsx (2)

147-373: ⚠️ Potential issue | 🟠 Major

TIME_BASED에서 발언 유형 입력이 무시될 수 있습니다.
TIME_BASED 초기값이 표준 키(예: OPEN_DEBATE 등)와 매칭되면 currentSpeechType !== 'CUSTOM' 상태가 되어, 사용자가 입력한 speechTypeTextValue가 저장/검증에서 무시됩니다. TIME_BASED는 입력값을 항상 우선하도록 분기를 정리하는 게 안전합니다.

✅ 제안 수정
   const handleSubmit = useCallback(() => {
+    const isTimeBased = timerType === 'TIME_BASED';
     const totalTime = minutes * 60 + seconds;
     const totalTimePerTeam = teamMinutes * 60 + teamSeconds;
     const totalTimePerSpeaking = speakerMinutes * 60 + speakerSeconds;
@@
-    if (currentSpeechType === 'CUSTOM') {
+    if (currentSpeechType === 'CUSTOM' || isTimeBased) {
       // 텍스트 길이 유효성 검사
       if (speechTypeTextValue.length > 10) {
         errors.push(t('발언 유형은 최대 10자까지 입력할 수 있습니다.'));
       }
@@
-    if (currentSpeechType === 'CUSTOM') {
+    if (isTimeBased) {
+      speechTypeToSend = speechTypeTextValue;
+      stanceToSend = 'NEUTRAL';
+    } else if (currentSpeechType === 'CUSTOM') {
       speechTypeToSend = speechTypeTextValue;
       stanceToSend = timerType === 'TIME_BASED' ? 'NEUTRAL' : stance;
     } else {
       speechTypeToSend = SPEECH_TYPE_RECORD[currentSpeechType];
       stanceToSend = currentSpeechType === 'TIMEOUT' ? 'NEUTRAL' : stance;
     }

253-260: ⚠️ Potential issue | 🟡 Minor

'중립' 라벨이 번역되지 않아 언어 전환 시 고정됩니다.

stanceOptions에서 NEUTRAL 라벨만 t()가 적용되지 않아 다국어 전환 시 갱신되지 않습니다. 다른 옵션들(speechTypeOptions, bellOptions)과의 일관성을 위해 t(STANCE_RECORD['NEUTRAL'])로 감싸고 useMemo의 의존성 배열에 t를 추가해야 합니다.

✅ 제안 수정
 const stanceOptions: DropdownMenuItem<Stance>[] = useMemo(
   () => [
     { value: 'PROS', label: prosTeamName },
     { value: 'CONS', label: consTeamName },
-    { value: 'NEUTRAL', label: STANCE_RECORD['NEUTRAL'] },
+    { value: 'NEUTRAL', label: t(STANCE_RECORD['NEUTRAL']) },
   ],
-  [prosTeamName, consTeamName],
+  [prosTeamName, consTeamName, t],
 );

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/mocks/handlers/global.ts`:
- Line 7: The MSW handler matching /\/locales\/[^/]+\/translation\.json$/
currently returns an empty object which causes i18n.t() to fall back to keys;
update the handler in src/mocks/handlers/global.ts (the http.get(...) handler)
to return actual translation fixtures for the requested locale instead of {} —
e.g., parse the locale from the request URL and respond with a translations
object (imported JSON or a small in-test map) so components using t() (e.g.,
TableListPage and calls to t(...)) receive real strings during tests.
🧹 Nitpick comments (1)
src/page/TableComposition/TableCompositionPage.test.tsx (1)

24-31: 테스트 라우트를 실제 lang‑prefix 라우팅 구조와 맞추기

라우터는 LanguageWrapper를 통해 모든 경로를 /:lang/... 구조로 관리하고 있습니다. 실제 앱은 buildLangPath()/ko/composition?mode=add 같은 lang-prefix URL을 생성하지만, 테스트 래퍼는 여전히 /composition으로 설정되어 있습니다. 테스트가 실제 라우팅 구조를 더 정확히 반영하도록 다음과 같이 수정하는 것이 좋습니다:

+import { DEFAULT_LANG } from '../../util/languageRouting';

 function TestWrapper({
   children,
-  initialEntries = ['/composition?mode=add'],
+  initialEntries = [`/${DEFAULT_LANG}/composition?mode=add`],
 }: {
   children: React.ReactNode;
   initialEntries?: string[];
 }) {
@@
-            <Route path="/composition" element={children} />
+            <Route path="/:lang/composition" element={children} />

테스트 파일 내 다른 모든 initialEntries도 동일한 규칙으로 업데이트하세요 (예: initialEntries={['/composition?mode=edit&tableId=1&type=CUSTOMIZE']}도 해당).

@useon useon requested a review from i-meant-to-be February 5, 2026 19:49
Copy link
Contributor

@i-meant-to-be i-meant-to-be left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이제 다듬을 부분은 거의 다 처리한 것 같네요. 개발 외 작업도 할 게 많은 태스크였을 텐데, 쭉 잘 템포 유지하느라 고생 많으셨습니다!

@useon useon merged commit 5a0ae1b into develop Feb 10, 2026
2 checks passed
@useon useon deleted the feat/#408 branch February 10, 2026 12:45
@useon useon deployed to DEPLOY_DEV February 10, 2026 12:45 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 개발

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[FEAT] 헤더에 언어 선택 드롭다운 추가 및 i18n 자동 변환 스크립트 적용

2 participants