로케일과 날짜 형식

사용자의 언어로 날짜를 표시하고, `Intl`이 만들지 못하는 라벨은 직접 만들어요.

locale에 BCP 47 태그를 넣으면 차트의 모든 날짜 라벨을 Intl.DateTimeFormat이 만들어요. 설치할 번들도, 불러올 로케일 파일도 없어요. 태그가 없으면 내장 영어 라벨을 써요.

다른 언어로 날짜 표시하기

locale은 앱이 이미 쓰는 태그를 그대로 받아요. 설정에서 바꿀 것은 그것뿐이에요.

ProjectChart.tsx
import { useState } from 'react';
import { ReactGanttChart, type Task } from '@jaeungkim/gantt-chart';
import '@jaeungkim/gantt-chart/style.css';

const initialTasks: Task[] = [
  { id: 'design', name: 'Design', parentId: null, sequence: '1',
    startDate: '2026-09-01', endDate: '2026-09-12' },
  { id: 'build', name: 'Build', parentId: null, sequence: '2',
    startDate: '2026-09-15', endDate: '2026-10-10' },
];

export function ProjectChart() {
  const [tasks, setTasks] = useState(initialTasks);

  return (
    <ReactGanttChart
      tasks={tasks}
      onTasksChange={setTasks}
      locale="ko-KR"
      showTaskList
      height={420}
    />
  );
}

태그는 차트가 그리는 모든 날짜에 적용돼요. 모든 포매터는 timeZone: 'UTC'로 만들어져서 라벨이 보는 사람의 시간대로 밀리지 않아요.

작업 목록에는 로케일이 필요 없어요. 앱 데이터인 task.name을 그대로 보여주거든요.

편집할 수 있는 Start와 End는 포매터를 거치지 않아요. 네이티브 <input type="date">라서 태그와 상관없이 YYYY-MM-DD예요. 어떤 필드가 입력을 받는지는 상세 패널에서 확인해요.

빈 문자열은 태그가 없는 것과 같아요. "en_US"처럼 구조가 잘못된 태그도 내장 라벨로 돌아가요. Intl이 예외를 던지고, 차트가 그 예외를 잡아 태그마다 페이지 로드당 한 번 경고를 남겨요.

"xx-YY"처럼 형식은 맞지만 알 수 없는 태그는 예외를 던지지 않아요. Intl이 런타임 로케일을 대신 쓰니 이런 오타는 아무 표시도 남지 않아요.

Intl에는 분기 필드가 없어서 quarter 헤더는 차트가 직접 조립해요. 연도 문자열이 숫자로 끝나면 Q3을 앞에, 그렇지 않으면 뒤에 붙여서 Q3 2025와 2025년 Q3이 나와요.

직접 만든 라벨로 바꾸기

formats는 배율을 키로 하는 레코드예요. 배율마다 라벨 슬롯 세 개 중 원하는 것만 넣고, 생략한 슬롯은 아래 계층의 라벨을 그대로 써요.

슬롯채우는 라벨받는 값
tick하단 헤더 행의 셀마다 라벨 하나그 셀의 시작 날짜
header상단 헤더 행의 그룹마다 라벨 하나그룹의 시작 날짜
tooltip그 밖의 모든 날짜. 호버 툴팁, 스크린 리더가 읽는 막대 이름, 상세 패널의 읽기 전용 Start와 End, 드래그 날짜 표시한 번에 날짜 하나

슬롯은 값이 있는 첫 계층에서 각각 따로 결정돼요. formats[scale][slot]이 먼저이고, 그다음이 locale, 마지막이 내장 영어 라벨이에요. 그래서 locale="ko-KR"과 함께 formats.quarter.header를 넘겨도 tick과 tooltip은 한국어로 남아요.

4월에 시작하는 회계 연도에는 Intl이 만들지 못하는 분기 라벨이 필요해요.

fiscal.ts
import type { GanttFormatOverrides } from '@jaeungkim/gantt-chart';

// Module scope, so the object identity never changes between renders
export const formats: GanttFormatOverrides = {
  quarter: {
    header: (d) => {
      // The Dayjs is in UTC mode, so d.month() is 0 to 11 in UTC
      const fiscalMonth = (d.month() + 9) % 12;
      const fiscalYear = d.month() >= 3 ? d.year() : d.year() - 1;
      return `FY${fiscalYear} Q${Math.floor(fiscalMonth / 3) + 1}`;
    },
  },
};

formats={formats}로 넘겨요. header 슬롯은 그룹의 시작 날짜를 받으니 이 재정의 함수는 항상 1월, 4월, 7월, 10월만 봐요.

포매터에 넘어오는 Dayjs는 UTC 모드예요. d.format('HH:mm'), d.year(), d.month(), d.day()가 모두 UTC 값을 반환해요. 현지 시각으로 바꾸는 포매터는 자기 셀과 어긋나는 라벨을 만들어요.

locale은 dayjs까지 닿지 않아서 d.format('MMMM')은 태그와 상관없이 September를 반환해요.

formats는 모듈 스코프에 선언하거나 useMemo로 감싸요. 렌더링마다 새 객체 리터럴을 만들면 라벨 뒤의 포매터를 전부 다시 만들고, 차트는 이 상황을 경고하지 않아요.

이웃한 그룹의 라벨이 같으면 더 넓은 셀 하나로 합쳐져요. 두 그룹에 같은 문자열을 반환하는 재정의도 그 둘을 하나로 합쳐요.

이동이나 크기 조절 중에는 드래그하는 구간의 각 끝이 그 끝이 놓인 눈금 셀에 표시돼요. 이때는 tooltip 슬롯을 압축한 형태를 써요. 재정의한 값은 압축하지 않고 반환한 그대로 표시해요.

주 시작 요일 바꾸기

firstDayOfWeek는 일요일 0부터 토요일 6까지의 숫자예요. 설정하면 week 배율의 상단 헤더 행이 월 대신 그 요일에 시작하는 주 단위로 묶여요.

차트는 값이 아니라 prop이 있는지를 보니 firstDayOfWeek={0}도 주 단위 그룹을 켜요. 다른 배율에서는 아무 일도 일어나지 않아요. 검증도 없어요. 값이 그대로 나머지 연산에 들어가서 7은 0처럼, -1은 6처럼 동작해요.

주 단위로 묶인 헤더도 재정의는 formats.week.header에서 읽어요. 레코드의 키가 화면에 보이는 배율이기 때문이에요. 여기에 formats.day.header를 써도 효과가 없어요.

음영은 별개예요. 어떤 날을 음영 처리할지는 workingWeekdays와 holidays가 정해요. 타임라인에서 확인해요.

영어로 남는 문자열

세 prop이 바꾸는 것은 날짜뿐이에요. 차트가 그리는 나머지 문자열은 모두 하드코딩된 영어예요.

문자열나타나는 곳
Add task작업 목록 아래 버튼
Start, End, Duration, Progress, Depends on상세 패널의 필드 캡션
12h, 3d호버 툴팁과 상세 패널의 기간 축약 표기
Gantt chart, Task name, Timeline, Resize task list, Task details, Close task details차트와 두 열, 스플리터, 상세 패널과 닫기 버튼의 접근성 이름
summary, to, % complete막대 접근성 이름 안의 연결어
<scale> scale키보드로 배율을 바꾼 뒤 나오는 알림
<name> deleted, <name> cannot be changed, <name> moved to 2 of 5 under <parent>키보드로 편집한 뒤 나오는 알림

이 문자열에서 현지화된 tooltip 포매터를 거치는 것은 안에 있는 날짜뿐이에요. 알림 자체는 키보드와 스크린 리더에서 다뤄요.

renderDetail은 캡션과 닫기 버튼을 포함해 상세 패널 본문 전체를 바꿔요. 그래서 그 안의 문구는 앱에서 번역할 수 있어요. 패널 자체의 접근성 이름은 영어로 남아요.

차트는 배율 선택기를 그리지 않아요. 선택기와 그 옵션 텍스트는 앱에서 만들고 번역해요. 명령형 API에서 확인해요.

적용 전에 확인하기

  • locale과 formats는 날짜를 보여주는 방식만 바꾸고 파싱 방식은 바꾸지 않아요. startDate와 endDate는 태그와 상관없이 UTC로 읽어요. 작업 데이터에서 확인해요.
  • 위치와 라벨은 UTC이고, 현지 시각으로 그리는 prop은 없어요. 현지 시각이 필요하면 넘기기 전에 날짜 문자열을 직접 옮겨요.
  • 오른쪽에서 왼쪽으로 읽는 레이아웃은 지원하지 않아요. dir을 설정하지도 읽지도 않으니 앱에서 차트를 직접 뒤집어야 해요.
  • 숫자와 복수형 형식은 지원하지 않아요. Intl.NumberFormat, Intl.RelativeTimeFormat, Intl.PluralRules를 만들지 않아서 진행률은 숫자 뒤에 % complete를 붙여 읽어 줘요.

prop과 기본값은 GanttProps, 배율별 내장 형식 문자열은 배율과 테마 타입에서 확인해요.