reopt designreopt design
DocsExploreToolsPricingBuilder
Start
Overview
Start
Next.js 설치
Manual install
Core Concepts
아키텍처
Composition Patterns
Accessibility
Keyboard Patterns
Styling
로컬라이제이션
Theme System
Advanced Patterns
Build & Operate
Skills
AI Integration
CLI (opt surface add)
Dependency Graph
Tools
Canvas Catalog
Theme Builder
Form Builder
Templates
Templates
Releases
Release Notes
Oopt-ui
reopt designreopt design

A design system for the AI era

  • Docs
  • Pricing
  • Releases
  • GitHub
  • Terms of Service
  • Privacy Policy

© 2026 reopt-ai. All rights reserved.

Core Concepts
  1. Docs
  2. /
  3. Core Concepts
  4. /
  5. 로컬라이제이션

로컬라이제이션

라벨 카탈로그와 Intl 포맷 두 축으로 패키지 로케일을 커스터마이즈하는 방법, 그리고 패키지별 지원 현황을 정리합니다.

reopt design · Updated Jul 25, 2026

두 개의 축

reopt 패키지의 로케일 커스터마이즈는 서로 다른 두 축으로 나뉩니다. 둘 중 하나만 바꾸면 화면이 반쪽만 번역됩니다.

1. 라벨 카탈로그

버튼, 빈 상태 문구, aria-label처럼 패키지가 소유한 정적 문자열. 패키지가 기본 카탈로그를 내장하고, 소비자는 messages 또는 labels prop으로 부분 덮어쓰기를 합니다. 모든 머지는 얕은 병합이라 넘기지 않은 키는 기본값이 그대로 남습니다.

2. Intl 포맷

날짜, 시각, 요일·월 이름, 숫자, 상대 시간처럼 런타임에 계산되는 값. 이건 카탈로그가 아니라 BCP-47 locale 문자열과 timeZone이 결정합니다. 카탈로그를 한국어로 바꿔도 locale을 넘기지 않으면 날짜는 브라우저 로케일을 따릅니다.

라벨 카탈로그를 쓰는 이유는 번들 때문입니다. 패키지는 번역 런타임을 싣지 않고 plain object만 export 하므로, 쓰지 않는 로케일은 트리셰이킹으로 사라집니다.

패키지별 지원 현황

현재 구현 수준입니다. 카탈로그는 ko/en 두 벌이 모두 export되고 타입 계약이 있는 상태, 부분은 카탈로그가 한쪽만 있거나 일부 문자열이 아직 하드코딩된 상태, props만은 컴포넌트 prop으로 문자열을 받지만 미리 만들어 둔 카탈로그가 없는 상태입니다.

패키지수준카탈로그 export주입 지점기본 언어Intl
@reopt-ai/opt-calendar카탈로그koMessages / enMessagesmessages · locale · timeZone영어DateTimeFormat 전면
@reopt-ai/opt-editor카탈로그koMessages / enMessagesmessages영어없음
@reopt-ai/opt-chat카탈로그ko*Labels / en*Labels컴포넌트별 labels한국어RelativeTimeFormat · NumberFormat
@reopt-ai/opt-devtool부분koDevtoolLabels / enDevtoolLabelslabels한국어없음
@reopt-ai/opt-datagrid부분koLabelslabels영어없음 (컬럼 formatter 담당)
@reopt-ai/opt-ui부분ko/en 3쌍 (combobox · starRating · breadcrumb)컴포넌트별 labels혼재 (영어 기본 · 한국어 fallback 33개 파일)ko-KR + KST 고정
@reopt-ai/opt-chartsprops만없음컴포넌트별 labels영어없음
@reopt-ai/opt-ui-primitivesprops만없음aria-label 등 소비자 제공—NumberFormat(undefined) 자동
@reopt-ai/opt-shell미지원없음레시피가 자식 컴포넌트에 위임—없음

기본 언어가 패키지마다 다릅니다. opt-calendar · opt-editor · opt-datagrid는 영어가 기본이고, opt-chat · opt-devtool은 한국어가 기본입니다. 한 화면에 여러 패키지를 얹으면 아무것도 주입하지 않은 상태에서 언어가 섞이므로, 앱 진입점에서 한 번에 카탈로그를 정하세요.

opt-calendar — 카탈로그 + locale + 시간대

로케일 지원이 가장 넓은 패키지입니다. 라벨은 messages, 요일·월·시각 표기는 locale, 절대 시각 계산은 spec의 timeZone이 각각 담당합니다. 세 값은 독립이라 "한국어 라벨 + 미국 시간대" 같은 조합도 됩니다.

tsx
import { Calendar, koMessages } from "@reopt-ai/opt-calendar";

<Calendar
  store={store}
  // 1. 라벨 카탈로그 (부분 덮어쓰기 가능 — Partial<CalendarMessages>)
  messages={koMessages}
  // 2. 요일/월/시각 표기 (Intl.DateTimeFormat에 그대로 전달)
  locale="ko-KR"
  // 3. 주 시작 요일 — 로케일과 별개 정책
  weekStartsOn={1}
/>;

// 일부 키만 바꾸기
<Calendar store={store} messages={{ today: "오늘", newEvent: "일정 추가" }} />;

시간대는 spec 레벨입니다. CalendarSpec에 timeZone을 두지 않으면 Intl.DateTimeFormat().resolvedOptions().timeZone으로 브라우저 시간대를 채웁니다. 이벤트마다 timeZone을 따로 줄 수도 있습니다.

formatCount(template, count)는 {count} 보간과 {count, plural, one {…} other {…}} 한 가지 형태만 지원합니다. 러시아어처럼 복수형이 3갈래 이상인 언어는 이 헬퍼로 처리되지 않으니, 앱의 ICU 런타임에서 문자열을 만들어 넘기세요.

Calendar 플레이그라운드에서 직접 전환locale · 시간대 · 주 시작 요일을 실행 중인 캘린더에서 바꿔 봅니다.

opt-editor — 네임스페이스 키 + t()

에디터 메시지는 blockToolbar.delete 처럼 점으로 구분된 평면 키입니다. 값 안의 {변수}는 t(key, vars)가 치환합니다.

tsx
import { Editor, koMessages, useEditorMessages } from "@reopt-ai/opt-editor";

// 주입 — Editor / EditorProvider 어느 쪽이든 messages prop
<Editor store={store} catalog={catalog} messages={koMessages} />;

// 커스텀 블록 안에서 읽기
function MyBlockToolbar() {
  const t = useEditorMessages();
  return <button>{t("blockToolbar.convertTo", { label: "인용" })}</button>;
}

누락된 키는 영어(enMessages)로 폴백하므로 부분 번역 카탈로그를 그대로 넘겨도 화면이 비지 않습니다. Provider 밖에서 useEditorMessages()를 호출해도 영어로 동작합니다.

opt-chat — 컴포넌트별 라벨 그룹

opt-chat은 하나의 큰 카탈로그 대신 컴포넌트 단위 라벨 그룹을 export합니다(koMessageLabels, koPromptInputLabels, koConversationLabels …). 기본값은 한국어이고, 영어로 쓰려면 en*Labels를 명시적으로 넘깁니다.

tsx
import {
  PromptInput,
  Message,
  enPromptInputLabels,
  enMessageLabels,
} from "@reopt-ai/opt-chat";

<PromptInput labels={enPromptInputLabels} onSubmit={handleSubmit} />;
<Message labels={{ ...enMessageLabels, retry: "Try again" }} />;

시간 표기는 Commit의 locale prop(기본 ko-KR)이 Intl.RelativeTimeFormat으로 내려갑니다. 라벨만 영어로 바꾸고 locale을 그대로 두면 "3일 전"이 남습니다.

opt-datagrid · opt-devtool

두 패키지 모두 labels prop 한 곳으로 모여 있고, 값 안의 {count}, {title}, {row}는 패키지가 보간합니다. 기본 언어는 반대입니다 — datagrid는 영어, devtool은 한국어.

tsx
import { DataGrid, koLabels } from "@reopt-ai/opt-datagrid";
import { OptDevtool, enDevtoolLabels } from "@reopt-ai/opt-devtool";

// 영어 기본 → 한국어 카탈로그 주입
<DataGrid columns={columns} rows={rows} labels={koLabels} />;

// 한국어 기본 → 영어 카탈로그 주입
<OptDevtool labels={enDevtoolLabels} />;

opt-datagrid의 영어 기본값은 컴포넌트 내부 상수라 심볼로 export되지 않습니다. 영어로 쓸 땐 labels를 아예 넘기지 마세요.

opt-ui — 컴포넌트별 labels prop

opt-ui는 아직 통합 카탈로그가 없습니다. 대신 63개 컴포넌트 파일이 각각 labels prop과 XxxLabels 타입을 노출하고, 내부에서 { ...DEFAULT_LABELS, ...labels }로 병합합니다. 미리 만들어 둔 카탈로그는 Combobox · StarRating · Breadcrumb 3쌍뿐입니다.

tsx
import { Pagination, ComboboxEmpty, enComboboxLabels } from "@reopt-ai/opt-ui";

// 함수형 라벨도 있다 — 숫자 위치를 언어마다 다르게 둘 수 있게
<Pagination
  currentPage={page}
  totalPages={20}
  onPageChange={setPage}
  labels={{
    previous: "이전 페이지",
    next: "다음 페이지",
    count: (start, end, total) => `전체 ${total}건 중 ${start}–${end}`,
  }}
/>;

// 한국어 기본값을 영어 카탈로그로 교체
<ComboboxEmpty>{enComboboxLabels.empty}</ComboboxEmpty>;

주의: Shell 33개 파일은 기본값이 한국어입니다 (AuthForm, ActivityFeed, TaskList, WizardForm, SettingsForm …). 영어 제품을 만든다면 이 컴포넌트들엔 labels를 반드시 넘겨야 합니다.

날짜·숫자 헬퍼(formatOptShortDate, formatOptTime, formatOptNumber)는 OPT_UI_DEFAULT_LOCALE = "ko-KR"과 KST에 고정되어 있습니다. Node와 브라우저의 ICU 빌드 차이로 SSR hydration이 어긋나지 않게 의도적으로 고정한 값이라, 다른 로케일이 필요하면 이 헬퍼를 쓰지 말고 앱에서 Intl.DateTimeFormat을 직접 호출하세요.

Next.js 앱에 한 번만 배선하기

패키지 카탈로그는 앱의 i18n 라이브러리와 무관합니다. 앱 로케일을 한 번 읽어 각 패키지 카탈로그로 매핑하는 지점을 하나만 두세요. 이 사이트의 플레이그라운드가 쓰는 방식과 같습니다.

tsx
"use client";

import { useLocale } from "next-intl";
import { koMessages as koCalendar } from "@reopt-ai/opt-calendar";
import { koMessages as koEditor } from "@reopt-ai/opt-editor";
import { koLabels as koGrid } from "@reopt-ai/opt-datagrid";

export function ProductScreen() {
  const locale = useLocale(); // "ko" | "en"
  const isKo = locale === "ko";

  return (
    <>
      {/* 영어가 기본인 패키지 — 한국어일 때만 카탈로그를 넘긴다 */}
      <Calendar
        store={calendarStore}
        messages={isKo ? koCalendar : undefined}
        locale={isKo ? "ko-KR" : "en-US"}
      />
      <Editor
        store={editorStore}
        catalog={catalog}
        messages={isKo ? koEditor : undefined}
      />
      <DataGrid
        rows={rows}
        columns={columns}
        labels={isKo ? koGrid : undefined}
      />
    </>
  );
}

undefined를 넘기면 패키지 기본값이 그대로 쓰이므로 분기마다 카탈로그를 두 벌 만들 필요가 없습니다. 반대로 기본이 한국어인 패키지(opt-chat, opt-devtool)는 영어일 때 en*Labels를 넘기는 방향으로 뒤집습니다.

ko·en 외의 로케일

패키지가 싣고 다니는 카탈로그는 ko·en 두 벌뿐이지만, 타입이 공개되어 있어 어떤 로케일이든 직접 만들 수 있습니다. 타입을 만족시키면 키 누락은 컴파일 단계에서 걸립니다.

ts
import type { CalendarMessages } from "@reopt-ai/opt-calendar";
import type { EditorMessages } from "@reopt-ai/opt-editor";

// 전체를 채우면 Partial이 아닌 완전한 카탈로그가 된다
export const jaCalendarMessages: CalendarMessages = {
  calendarLabel: "カレンダー",
  today: "今日",
  newEvent: "予定を追加",
  // … 나머지 키도 타입이 강제한다
};

// 일부만 바꾸려면 Partial
export const jaEditorMessages: Partial<EditorMessages> = {
  "blockToolbar.delete": "ブロックを削除",
};

Intl 쪽은 카탈로그와 별개로 BCP-47 태그만 바꾸면 됩니다 — locale="ja-JP"를 넘기면 요일·월·시각 표기는 브라우저 ICU가 처리합니다.

아직 커스텀되지 않는 값

정직하게 남겨 둡니다. 아래 값들은 현재 prop으로 바꿀 수 없습니다.

위치하드코딩된 값회피법
opt-editor 슬래시 커맨드 keywords블록 13종의 검색 키워드가 한국어 고정 (["제목", "헤딩"] 등)표시 이름(blockType.*)은 번역되므로 목록은 정상. 검색어만 한국어로 매칭됨
opt-chat Reasoning"생각하는 중…", "잠시 생각함", "N초 동안 생각함"라벨 카탈로그에 없음. 다른 문구가 필요하면 Reasoning 파트를 직접 구성
opt-chat Context 비용 표시Intl.NumberFormat("en-US") 고정USD 통화 표기라 의도된 고정
opt-ui 포맷 헬퍼ko-KR · KST 고정SSR 결정성을 위한 의도적 고정. 앱에서 Intl 직접 사용
전체RTL(오른쪽→왼쪽) 레이아웃dir="rtl" 대응 스타일이 없음. 아랍어·히브리어는 미지원

라벨 계약을 지키는 규칙

하드코딩이 다시 늘어나지 않도록 ESLint가 두 룰로 감시합니다. 둘 다 Shell·Surface 파일에만 적용되고 warn 레벨입니다.

룰검사 내용
opt-ui/require-labels사용자에게 보이는 문자열을 가진 컴포넌트는 labels prop을 노출해야 함
opt-ui/require-labels-no-hardcodeJSX 텍스트에 하드코딩된 문자열 금지 — Labels 패턴으로 옮길 것

의도된 예외는 주석으로 사유를 남깁니다. 사유가 없으면 require-suppress-explanation에 걸립니다.

tsx
// eslint-disable-next-line opt-ui/require-labels-no-hardcode -- 브랜드명은 번역 대상이 아니다
<span>reopt design</span>
PreviousStylingdata-attribute 셀렉터, className 오버라이드, 다크모드, 애니메이션Core Concepts
Go to Styling
NextTheme SystemCompound Theme 아키텍처, 5개 프리셋 × light/dark 모드, CSS 변수 체계Core Concepts