From 5022fe597f6a3e2d60c5e50647aebb532c4cba88 Mon Sep 17 00:00:00 2001 From: thxforall <113906780+thxforall@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:25:56 +0900 Subject: [PATCH] =?UTF-8?q?feat(web):=20explore=20=EC=84=9C=ED=94=BC?= =?UTF-8?q?=EC=8A=A4=20i18n=20=E2=80=94=20Explore=20=EB=84=A4=EC=9E=84?= =?UTF-8?q?=EC=8A=A4=ED=8E=98=EC=9D=B4=EC=8A=A4=20ko/en=20=EB=B0=B0?= =?UTF-8?q?=EC=84=A0=20(#845)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit explore surface의 하드코딩 영문을 useTranslations("Explore")로 배선 (#838 홈 baseline 패턴 복제). messages/{ko,en}.json에 Explore 네임스페이스 신설(ko는 실제 한국어 초안), 키셋 1:1 파리티 + ICU 보간 정적 테스트를 i18n/__tests__에 추가. explore는 CSR(client mutator가 URL locale 추종) 이라 RSC serverApiGet locale 전달 스텝은 불필요(홈 전용 갭). 배선: ExploreClient(헤더/검색/에러·엠프티 상태/로딩), EntityTabBar(탭 라벨+aria), ComingSoonState(타이틀/설명/배지), SortDropdown(정렬 라벨), ExploreCardCell(alt/스팟 aria, ICU plural). 엠프티 상태 동적 카피는 ICU 이스케이프 함정을 피해 곡선따옴표(‘{query}’) 사용 + 보간 테스트로 가드. 스코프 결정: - STYLE_TAXONOMY 스타일칩/POPULAR_SEARCH_TAGS는 미번역 — 라벨이 검색 쿼리로 그대로 주입(setQuery)되는 검색 토큰/고유명사라 번역 시 백엔드 검색 동작이 바뀜. - SearchSuggestions(전역 검색 드롭다운)는 이번 스코프 제외 — 비지역화 루트 레이아웃 Header에도 마운트되어 NextIntlClientProvider 밖이라 useTranslations 배선 시 전역 크래시. 셸 프로바이더 커버리지 후속 필요. #880(#843 explore 필터 interim)과 ExploreClient.tsx·SortDropdown.tsx 공유 — 문자열만 t() 교체, 필터 로직/SORT_OPTIONS 상수 shape 미변경 (나중 머지 rebase 최소화). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../(shell)/explore/ExploreClient.tsx | 42 ++++++------ .../__tests__/explore-messages-parity.test.ts | 55 +++++++++++++++ .../components/explore/ComingSoonState.tsx | 39 +++-------- .../lib/components/explore/EntityTabBar.tsx | 21 +++--- .../components/explore/ExploreCardCell.tsx | 6 +- .../lib/components/explore/SortDropdown.tsx | 7 +- .../__tests__/EntityTabBar.locale.test.tsx | 34 ++++++++++ .../__tests__/ExploreCardCell.test.tsx | 25 +++++-- packages/web/messages/en.json | 68 +++++++++++++++++++ packages/web/messages/ko.json | 68 +++++++++++++++++++ 10 files changed, 298 insertions(+), 67 deletions(-) create mode 100644 packages/web/i18n/__tests__/explore-messages-parity.test.ts create mode 100644 packages/web/lib/components/explore/__tests__/EntityTabBar.locale.test.tsx diff --git a/packages/web/app/[locale]/(shell)/explore/ExploreClient.tsx b/packages/web/app/[locale]/(shell)/explore/ExploreClient.tsx index 1bdfc613e..7f3cb2358 100644 --- a/packages/web/app/[locale]/(shell)/explore/ExploreClient.tsx +++ b/packages/web/app/[locale]/(shell)/explore/ExploreClient.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useMemo, useRef, useCallback } from "react"; +import { useTranslations } from "next-intl"; import { Search, X, ChevronDown } from "lucide-react"; import { useDebounce, useRecentSearchesStore } from "@decoded/shared"; import { useExploreData } from "@/lib/hooks/useExploreData"; @@ -49,6 +50,7 @@ export function ExploreClient({ hasMagazine, initialQuery = "", }: Props) { + const t = useTranslations("Explore"); const debouncedQuery = useSearchStore((state) => state.debouncedQuery); const query = useSearchStore((state) => state.query); const setQuery = useSearchStore((state) => state.setQuery); @@ -238,9 +240,9 @@ export function ExploreClient({
{/* Page header */}
-

Explore

+

{t("header.title")}

- Discover looks, moods and items from the community. + {t("header.subtitle")}

@@ -256,7 +258,7 @@ export function ExploreClient({ onFocus={() => query.length > 0 && setShowSuggestions(true)} onKeyDown={handleKeyDown} data-testid="explore-search-input" - placeholder="Search people, shows, items..." + placeholder={t("search.placeholder")} className="w-full rounded-full border border-border bg-card py-2 pl-10 pr-10 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring" /> {query.length > 0 && ( @@ -264,7 +266,7 @@ export function ExploreClient({ onClick={handleClear} className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" type="button" - aria-label="Clear search" + aria-label={t("search.clearAria")} > @@ -320,7 +322,7 @@ export function ExploreClient({ : "border-border text-muted-foreground hover:bg-accent" )} > - + {contextOptions.map(([value, count]) => (
@@ -375,19 +377,17 @@ export function ExploreClient({
⚠️

- Failed to load posts + {t("loadError.title")}

- {error instanceof Error - ? error.message - : "Something went wrong while loading posts."} + {error instanceof Error ? error.message : t("loadError.body")}

)} @@ -397,14 +397,14 @@ export function ExploreClient({
🛠️

- Search is temporarily unavailable + {t("searchError.title")}

{(() => { const msg = error instanceof Error ? error.message - : "Please try again in a moment."; + : t("searchError.body"); return msg.length > 140 ? `${msg.slice(0, 140)}…` : msg; })()}

@@ -414,14 +414,14 @@ export function ExploreClient({ className="rounded-full border border-border bg-card px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent" type="button" > - Retry + {t("searchError.retry")}
@@ -435,13 +435,13 @@ export function ExploreClient({

{debouncedQuery.trim().length > 0 - ? `No results for '${debouncedQuery}'` - : "No posts found yet."} + ? t("empty.noResults", { query: debouncedQuery }) + : t("empty.noPosts")}

{debouncedQuery.trim().length > 0 - ? "Try different keywords or the suggestions below." - : "Check back later."} + ? t("empty.noResultsHint") + : t("empty.noPostsHint")}

{debouncedQuery.trim().length > 0 && ( <> @@ -462,7 +462,7 @@ export function ExploreClient({ onClick={handleClear} className="text-xs text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground" > - Clear search + {t("empty.clear")} )} @@ -490,7 +490,7 @@ export function ExploreClient({