From cca9b10a45166eee9b58a4f5523b730cfe44e58f Mon Sep 17 00:00:00 2001 From: Guillermo Pages Date: Sat, 9 Aug 2025 23:21:25 +0200 Subject: [PATCH] feat: add locale support --- src/App.tsx | 2 + src/components/calendar/Day/index.tsx | 5 +- src/components/calendar/Month/index.tsx | 9 +- src/components/calendar/Week/index.tsx | 5 +- src/components/calendar/Year/index.tsx | 4 +- src/components/calendar/utils.ts | 33 +++--- src/examples/LocaleSupport.tsx | 135 ++++++++++++++++++++++++ src/types/calendar.ts | 6 ++ 8 files changed, 180 insertions(+), 19 deletions(-) create mode 100644 src/examples/LocaleSupport.tsx diff --git a/src/App.tsx b/src/App.tsx index 85c5c43..74f2ab5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,12 +6,14 @@ import { DateRangeView } from './examples/DateRangeView'; import { InteractiveDateRange } from './examples/InteractiveDateRange'; import { CompactYear } from './examples/CompactYear'; import { ComprehensiveColorScheme } from './examples/ComprehensiveColorScheme'; +import { LocaleSupport } from './examples/LocaleSupport'; import styles from './App.module.scss'; const App: React.FC = () => { return (
+ diff --git a/src/components/calendar/Day/index.tsx b/src/components/calendar/Day/index.tsx index f5365bc..2396f97 100644 --- a/src/components/calendar/Day/index.tsx +++ b/src/components/calendar/Day/index.tsx @@ -18,7 +18,8 @@ export const Day: React.FC = ({ fontProportion = 100, magnify = false, activeColors, - colorScheme + colorScheme, + locale }) => { const isInteractive = Boolean(onSelect || onHover); @@ -102,7 +103,7 @@ export const Day: React.FC = ({ {headerStyle !== 'none' && (
- {getDayLabel(date, headerStyle)} + {getDayLabel(date, headerStyle, locale)}
)} diff --git a/src/components/calendar/Month/index.tsx b/src/components/calendar/Month/index.tsx index af27c59..c52f7da 100644 --- a/src/components/calendar/Month/index.tsx +++ b/src/components/calendar/Month/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { startOfMonth, startOfWeek, addDays, addWeeks, endOfMonth, format } from 'date-fns'; +import { enUS } from 'date-fns/locale'; import { MonthProps, HeaderStyle } from '../../../types/calendar'; import { Week } from '../Week'; import styles from './Month.module.scss'; @@ -20,7 +21,8 @@ export const Month: React.FC = ({ magnify = false, activeColors, activeDates, - colorScheme + colorScheme, + locale = enUS }) => { const monthStart = startOfMonth(date); const start = startOfWeek(monthStart, { weekStartsOn: 1 }); @@ -51,6 +53,7 @@ export const Month: React.FC = ({ activeColors={activeColors} activeDates={activeDates} colorScheme={colorScheme} + locale={locale} /> ); current = addWeeks(current, 1); @@ -58,7 +61,7 @@ export const Month: React.FC = ({ return (
-

{format(date, 'MMMM yyyy')}

+

{format(date, 'MMMM yyyy', { locale })}

= ({ [styles['Month__DayHeader--weekend']]: isWeekend })} > - {format(dayDate, 'EEEEE')} + {format(dayDate, 'EEEEE', { locale })}
); })} diff --git a/src/components/calendar/Week/index.tsx b/src/components/calendar/Week/index.tsx index f9f48f7..8f5322b 100644 --- a/src/components/calendar/Week/index.tsx +++ b/src/components/calendar/Week/index.tsx @@ -20,7 +20,8 @@ export const Week: React.FC = ({ magnify = false, activeColors, activeDates = [], - colorScheme + colorScheme, + locale }) => { const allDays = Array.from({ length: 7 }, (_, i) => { const date = addDays(startDate, i); @@ -63,6 +64,7 @@ export const Week: React.FC = ({ magnify={magnify} activeColors={activeColors} colorScheme={colorScheme} + locale={locale} />
); @@ -81,6 +83,7 @@ export const Week: React.FC = ({ magnify={magnify} activeColors={activeColors} colorScheme={colorScheme} + locale={locale} />
); diff --git a/src/components/calendar/Year/index.tsx b/src/components/calendar/Year/index.tsx index 81f6324..c62af01 100644 --- a/src/components/calendar/Year/index.tsx +++ b/src/components/calendar/Year/index.tsx @@ -17,7 +17,8 @@ export const Year: React.FC = ({ magnify = false, activeColors, activeDates, - colorScheme + colorScheme, + locale }) => { const months = Array.from({ length: 12 }, (_, index) => { const monthDate = new Date(year, index, 1); @@ -39,6 +40,7 @@ export const Year: React.FC = ({ activeColors={activeColors} activeDates={activeDates} colorScheme={colorScheme} + locale={locale} /> ); }); diff --git a/src/components/calendar/utils.ts b/src/components/calendar/utils.ts index 319d32a..fc3449c 100644 --- a/src/components/calendar/utils.ts +++ b/src/components/calendar/utils.ts @@ -1,5 +1,6 @@ import { HeaderStyle } from '../../types/calendar'; -import { format } from 'date-fns'; +import { format, Locale } from 'date-fns'; +import { enUS } from 'date-fns/locale'; export const getHeightByHeaderStyle = (headerStyle: HeaderStyle) => { const baseHeights: Record = { @@ -29,22 +30,30 @@ export const getMinWidthByHeaderStyle = (headerStyle: HeaderStyle) => { return `min-width: ${baseWidths[headerStyle]};`; }; -export const getDayLabel = (date: Date, headerStyle: HeaderStyle): string => { - const day = format(date, 'EEEE'); +export const getDayLabel = (date: Date, headerStyle: HeaderStyle, locale: Locale = enUS): string => { + const day = format(date, 'EEEE', { locale }); + switch (headerStyle) { case 'expanded': return day.toUpperCase(); case 'compacted': - return day.slice(0, 3).toUpperCase(); + // Use EEE format for proper 3-letter abbreviation in the locale + return format(date, 'EEE', { locale }).toUpperCase(); case 'tiny': - if (day === 'Thursday') return 'T'; - if (day === 'Tuesday') return 't'; - if (day === 'Saturday') return 's'; - if (day === 'Sunday') return 'S'; - if (day === 'Monday') return 'M'; - if (day === 'Wednesday') return 'W'; - if (day === 'Friday') return 'F'; - return day[0]; + // Use EEEEE format for single letter abbreviation when available + // Fallback to first letter for better locale support + const singleLetter = format(date, 'EEEEE', { locale }); + + // For English, handle special cases (Thursday/Tuesday, Saturday/Sunday) + if (locale.code === 'en-US' || locale.code === 'en') { + const dayName = format(date, 'EEEE', { locale }); + if (dayName === 'Thursday') return 'T'; + if (dayName === 'Tuesday') return 't'; + if (dayName === 'Saturday') return 's'; + if (dayName === 'Sunday') return 'S'; + } + + return singleLetter.toUpperCase(); default: return day; } diff --git a/src/examples/LocaleSupport.tsx b/src/examples/LocaleSupport.tsx new file mode 100644 index 0000000..215c05e --- /dev/null +++ b/src/examples/LocaleSupport.tsx @@ -0,0 +1,135 @@ +import React, { useState } from 'react'; +import { Year } from '../components/calendar/Year'; +import { Controls } from '../components/calendar/Controls'; +import { HeaderStyle, MonthCutoffType, DaySize } from '../types/calendar'; +import styles from './Examples.module.scss'; +import { Locale } from 'date-fns'; +import { + enUS, + es, + fr, + de, + it, + pt, + ja, + ko, + zhCN, + ru, + ar, + nl, + pl, + tr, + sv, + nb +} from 'date-fns/locale'; + +export const LocaleSupport: React.FC = () => { + const [dayHeaderStyle, setDayHeaderStyle] = useState('tiny'); + const [monthCutoff, setMonthCutoff] = useState('truncate'); + const [size, setSize] = useState('l'); + const [fontProportion, setFontProportion] = useState(100); + const [magnify, setMagnify] = useState(false); + const [selectedLocale, setSelectedLocale] = useState('enUS'); + + const locales: Record = { + enUS: { locale: enUS, name: 'English (US)' }, + es: { locale: es, name: 'Español' }, + fr: { locale: fr, name: 'Français' }, + de: { locale: de, name: 'Deutsch' }, + it: { locale: it, name: 'Italiano' }, + pt: { locale: pt, name: 'Português' }, + ja: { locale: ja, name: '日本語' }, + ko: { locale: ko, name: '한국어' }, + zhCN: { locale: zhCN, name: '中文 (简体)' }, + ru: { locale: ru, name: 'Русский' }, + ar: { locale: ar, name: 'العربية' }, + nl: { locale: nl, name: 'Nederlands' }, + pl: { locale: pl, name: 'Polski' }, + tr: { locale: tr, name: 'Türkçe' }, + sv: { locale: sv, name: 'Svenska' }, + nb: { locale: nb, name: 'Norsk' } + }; + + const activeDates = [ + new Date(2025, 0, 1), // New Year's Day + new Date(2025, 3, 21), // Spring + new Date(2025, 6, 14), // Bastille Day / Summer + new Date(2025, 9, 31), // Halloween + new Date(2025, 11, 25) // Christmas + ]; + + return ( +
+

International Locale Support

+

+ The calendar supports multiple languages through the date-fns locale system. + Day names, month names, and date formats are automatically localized. +

+ +
+ +
+ + + +
+ +
+ +
+

Localized Elements:

+
    +
  • Month names (e.g., January, Janvier, Januar, 一月)
  • +
  • Day names in headers (Monday, Lundi, Montag, 月曜日)
  • +
  • Day abbreviations (Mon, Lun, Mo, 月)
  • +
  • Single letter days (M, L, M, 月)
  • +
+ +

Header Style Examples:

+
    +
  • Expanded: Full day names (e.g., MONDAY, LUNDI, MONTAG)
  • +
  • Compacted: 3-letter abbreviations (e.g., MON, LUN, MO)
  • +
  • Tiny: Single letters with special handling for conflicts
  • +
  • None: No day headers, only numbers
  • +
+ +

+ Note: Some languages may show squares or incorrect characters if the required fonts + are not installed on your system (especially for Arabic, Chinese, Japanese, and Korean). +

+
+
+ ); +}; \ No newline at end of file diff --git a/src/types/calendar.ts b/src/types/calendar.ts index 9c25db9..00bc2b2 100644 --- a/src/types/calendar.ts +++ b/src/types/calendar.ts @@ -1,3 +1,5 @@ +import { Locale } from 'date-fns'; + export type DateRange = { startDate: Date | null; endDate: Date | null; @@ -71,6 +73,7 @@ export interface YearProps { activeColors?: ActiveColors; activeDates?: Date[]; colorScheme?: ColorScheme; + locale?: Locale; } export interface MonthProps { @@ -89,6 +92,7 @@ export interface MonthProps { activeColors?: ActiveColors; activeDates?: Date[]; colorScheme?: ColorScheme; + locale?: Locale; } export interface WeekProps { @@ -106,6 +110,7 @@ export interface WeekProps { activeColors?: ActiveColors; activeDates?: Date[]; colorScheme?: ColorScheme; + locale?: Locale; } export interface DayProps { @@ -120,4 +125,5 @@ export interface DayProps { magnify?: boolean; activeColors?: ActiveColors; colorScheme?: ColorScheme; + locale?: Locale; }