デジタル庁デザインシステム:国際化日付処理の@internationalized/date活用
はじめに
グローバル化が進む現代のWebアプリケーション開発において、日付処理は最も複雑な課題の一つです。異なるタイムゾーン、ロケール、暦体系に対応しながら、ユーザーフレンドリーな日付選択UIを提供する必要があります。デジタル庁デザインシステムでは、この課題を解決するために @internationalized/date ライブラリを活用しています。
本記事では、デジタル庁デザインシステムの日付ピッカーコンポーネントにおける @internationalized/date の実装パターンとベストプラクティスを詳しく解説します。
@internationalized/dateの特徴と利点
@internationalized/date は、国際化対応の日付処理を簡素化するために設計されたライブラリです。以下のような特徴を持っています:
- タイムゾーン対応: 自動的にローカルタイムゾーンを処理
- 多言語対応: 様々なロケールと暦体系をサポート
- 不変データ構造: 日付オブジェクトは不変で安全な操作を提供
- React Aria連携: React Aria Componentsとシームレスに連携
デジタル庁デザインシステムでの実装例
基本的な日付ピッカーコンポーネント
import { CalendarDate, getLocalTimeZone, today } from '@internationalized/date';
import { useState } from 'react';
export const DatePickerExample = () => {
const [selectedDate, setSelectedDate] = useState<CalendarDate | null>(null);
const handleDateChange = (date: CalendarDate | null) => {
setSelectedDate(date);
};
return (
<DatePicker onDateChange={handleDateChange}>
<DatePicker.Year />
<DatePicker.Month />
<DatePicker.Day />
</DatePicker>
);
};
カレンダー連携機能
import { CalendarDate, getLocalTimeZone, today } from '@internationalized/date';
import { useState } from 'react';
export const DatePickerWithCalendar = () => {
const [yearInput, setYearInput] = useState('');
const [monthInput, setMonthInput] = useState('');
const [dayInput, setDayInput] = useState('');
const handleCalendarChange = (newDate: CalendarDate | null) => {
if (newDate) {
setYearInput(String(newDate.year));
setMonthInput(String(newDate.month).padStart(2, '0'));
setDayInput(String(newDate.day).padStart(2, '0'));
} else {
setYearInput('');
setMonthInput('');
setDayInput('');
}
};
return (
<div className="date-picker-container">
<DatePicker.Year value={yearInput} onChange={setYearInput} />
<DatePicker.Month value={monthInput} onChange={setMonthInput} />
<DatePicker.Day value={dayInput} onChange={setDayInput} />
<CalendarPicker onDateSelect={handleCalendarChange} />
</div>
);
};
主要なAPIと使用方法
CalendarDateクラス
CalendarDate は日付を表現する主要なクラスです:
import { CalendarDate } from '@internationalized/date';
// 現在の日付を取得
const today = new CalendarDate(2025, 9, 2);
// 日付の操作(不変なので新しいインスタンスが返る)
const tomorrow = today.add({ days: 1 });
const nextMonth = today.add({ months: 1 });
// 日付の比較
const isAfter = today.compare(tomorrow) > 0;
タイムゾーン処理
import { getLocalTimeZone, today } from '@internationalized/date';
// ローカルタイムゾーンの今日の日付を取得
const localToday = today(getLocalTimeZone());
// 特定のタイムゾーンの日付を取得
const tokyoDate = today('Asia/Tokyo');
日付のパースとフォーマット
import { parseDate } from '@internationalized/date';
// 文字列から日付をパース
const parsedDate = parseDate('2025-09-02');
// 日付の検証
const isValid = !isNaN(parsedDate.year);
実装パターンとベストプラクティス
1. 状態管理のパターン
import { CalendarDate } from '@internationalized/date';
import { useCallback, useState } from 'react';
export const useDatePicker = (initialDate?: CalendarDate) => {
const [date, setDate] = useState<CalendarDate | null>(
initialDate || null
);
const updateDate = useCallback((newDate: CalendarDate | null) => {
setDate(newDate);
}, []);
const clearDate = useCallback(() => {
setDate(null);
}, []);
return {
date,
updateDate,
clearDate,
hasDate: date !== null
};
};
2. バリデーションの実装
import { CalendarDate } from '@internationalized/date';
export const validateDate = (
date: CalendarDate | null,
options?: {
minDate?: CalendarDate;
maxDate?: CalendarDate;
required?: boolean;
}
): string[] => {
const errors: string[] = [];
if (options?.required && !date) {
errors.push('日付は必須です');
}
if (date && options?.minDate && date.compare(options.minDate) < 0) {
errors.push(`日付は ${formatDate(options.minDate)} 以降でなければなりません`);
}
if (date && options?.maxDate && date.compare(options.maxDate) > 0) {
errors.push(`日付は ${formatDate(options.maxDate)} 以前でなければなりません`);
}
return errors;
};
3. アクセシビリティ対応
import { CalendarDate } from '@internationalized/date';
export const getAccessibilityProps = (date: CalendarDate | null) => {
return {
'aria-label': date
? `選択された日付: ${formatDateForScreenReader(date)}`
: '日付が選択されていません',
'aria-required': true,
'aria-invalid': !date
};
};
const formatDateForScreenReader = (date: CalendarDate): string => {
return `${date.year}年 ${date.month}月 ${date.day}日`;
};
パフォーマンス最適化
メモ化による再レンダリング防止
import { CalendarDate } from '@internationalized/date';
import { useMemo } from 'react';
export const useMemoizedDateOperations = (date: CalendarDate | null) => {
const formattedDate = useMemo(() => {
if (!date) return '';
return `${date.year}-${String(date.month).padStart(2, '0')}-${String(date.day).padStart(2, '0')}`;
}, [date]);
const isWeekend = useMemo(() => {
if (!date) return false;
// 週末判定ロジック
return false;
}, [date]);
return { formattedDate, isWeekend };
};
デバウンス処理
import { CalendarDate } from '@internationalized/date';
import { useCallback } from 'react';
import { debounce } from 'lodash-es';
export const useDebouncedDateChange = (
onChange: (date: CalendarDate | null) => void,
delay: number = 300
) => {
const debouncedOnChange = useCallback(
debounce((newDate: CalendarDate | null) => {
onChange(newDate);
}, delay),
[onChange, delay]
);
return debouncedOnChange;
};
テスト戦略
単体テストの例
import { CalendarDate } from '@internationalized/date';
import { render, screen, fireEvent } from '@testing-library/react';
import { DatePicker } from './DatePicker';
describe('DatePicker', () => {
it('should handle date selection correctly', () => {
const mockOnChange = jest.fn();
render(<DatePicker onChange={mockOnChange} />);
const yearInput = screen.getByLabelText('年');
fireEvent.change(yearInput, { target: { value: '2025' } });
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({ year: 2025 })
);
});
it('should validate invalid dates', () => {
const { getByText } = render(<DatePicker />);
const yearInput = screen.getByLabelText('年');
fireEvent.change(yearInput, { target: { value: 'invalid' } });
expect(getByText('有効な年を入力してください')).toBeInTheDocument();
});
});
トラブルシューティング
よくある問題と解決策
| 問題 | 原因 | 解決策 |
|---|---|---|
| タイムゾーンの不一致 | サーバーとクライアントのタイムゾーン差異 | getLocalTimeZone() で統一 |
| 日付のパース失敗 | フォーマットの不一致 | parseDate で明示的なフォーマット指定 |
| パフォーマンス問題 | 過剰な再レンダリング | メモ化とデバウンスの導入 |
| アクセシビリティ問題 | スクリーンリーダー対応不足 | ARIA属性の適切な設定 |
デバッグ手法
import { CalendarDate } from '@internationalized/date';
export const debugDate = (date: CalendarDate | null) => {
if (!date) {
console.log('Date is null');
return;
}
console.log('Date details:', {
year: date.year,
month: date.month,
day: date.day,
era: date.era,
calendar: date.calendar.identifier
});
};
まとめ
デジタル庁デザインシステムにおける @internationalized/date の活用は、国際化対応の日付処理を効率的かつ堅牢に実現するための優れたアプローチです。以下のポイントが重要です:
- タイムゾーン対応: 自動的なローカルタイムゾーン処理
- 不変性: 安全な日付操作の保証
- 多言語サポート: 様々なロケールへの対応
- React Aria連携: アクセシビリティの確保
このアプローチを採用することで、ユーザーエクスペリエンスの向上とメンテナンス性の確保を両立できます。実際のプロジェクトでは、ここで紹介したパターンとベストプラクティスを参考に、独自の要件に合わせたカスタマイズを行うことをお勧めします。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



