Reactのカレンダーで、週の始まりの曜日を地域に合わせる方法
Intl.Locale の getWeekInfo() を使うと、地域ごとの週の始まりの曜日と週末の曜日を、ブラウザから受け取れます。日本やアメリカは日曜始まり、イギリスは月曜始まり、エジプトは土曜始まりです。曜日の並びを自分で対応表に書いておかずに済むので、React のカレンダーをどの地域の人が開いても、その地域でふだん使う並びで表示できます。
使うときに気をつけることは2つでした。曜日の番号が Date の getDay() と違うこと、そして古いブラウザでは getWeekInfo() の代わりに weekInfo というプロパティになっていることです。
検証環境は Ubuntu 24.04、Node.js 22.23.4 です。react / react-dom 19.3.1、@types/react 19.3.0、typescript 7.0.2、esbuild 0.28.2 を使い、.tsx を tsc で型チェックしてから esbuild でビルドしました。ブラウザは Playwright 1.63.0 で動かした Chromium 153(ヘッドレス)だけで、ブラウザの言語設定は Playwright の locale で切り替えています。Firefox、Safari、実機のスマートフォン、サーバーでの描画(SSR)、アラビア語などの右から左に書く言語での表の並び(dir="rtl")は試していません。
getWeekInfo() が返すもの
new Intl.Locale('en-GB').getWeekInfo() のように、言語タグから作った Intl.Locale で呼び出します。Chromium 153 で、いくつかの言語タグを試した結果です。
ja {"firstDay":7,"weekend":[6,7]}
en {"firstDay":7,"weekend":[6,7]}
de {"firstDay":1,"weekend":[6,7]}
ar {"firstDay":6,"weekend":[5,6]}
en-US {"firstDay":7,"weekend":[6,7]}
en-GB {"firstDay":1,"weekend":[6,7]}
en-US-u-fw-mon {"firstDay":1,"weekend":[6,7]}
en-GB-u-fw-sun {"firstDay":7,"weekend":[6,7]}
ja-JP-u-fw-mon {"firstDay":1,"weekend":[6,7]}
fa-IR {"firstDay":6,"weekend":[5]}
ar-SA {"firstDay":7,"weekend":[5,6]}
en-MV {"firstDay":5,"weekend":[6,7]}
firstDay が週の始まりの曜日、weekend が週末の曜日の配列です。MDN の getWeekInfo() の説明にあるとおり、曜日は、1 が月曜、7 が日曜という番号で表されます。週末は土日とは限りません。fa-IR(イラン)は金曜だけ、ar-SA(サウジアラビア)は金曜と土曜でした。
結果は、言語の部分より地域の部分で決まります。en-US と en-GB は同じ英語でも、日曜始まりと月曜始まりに分かれました。地域のない ja や en でも値は返り、どちらも日曜始まりでした。
言語タグの末尾に -u-fw-mon を付けると、地域の決まりより、こちらの指定が優先されました。en-US-u-fw-mon は月曜始まり、en-GB-u-fw-sun は日曜始まりです。アプリの設定で「週の始まり」を選べるようにするなら、この形で言語タグに足せます。
MDN の説明には minimalDays(年や月の最初の週に必要な最小の日数)も載っていますが、Chromium 153 が返したオブジェクトには入っていませんでした。月のカレンダーを並べるだけなら使いません。
曜日の番号を getDay() の番号に直す
getWeekInfo() の曜日は 1〜7 で、日曜が 7 です。一方、Date の getDay() は 0〜6 で、日曜が 0 です。月曜から土曜は同じ番号なので、7 で割った余りを取れば日曜だけが 0 になります。
古いブラウザへの備えも含めて、weekInfo.ts にまとめました。
// weekInfo.ts
// 古いブラウザ(Chrome 99〜129 など)は、getWeekInfo() の代わりに weekInfo というプロパティを持つ
type LocaleWithWeekInfo = Intl.Locale & { weekInfo?: Intl.WeekInfo };
// どちらもないブラウザで使う値(この例では日曜始まり、土日が週末)
const FALLBACK: Intl.WeekInfo = { firstDay: 7, weekend: [6, 7] };
export function getWeekInfo(tag: string): Intl.WeekInfo {
const locale: LocaleWithWeekInfo = new Intl.Locale(tag);
if (typeof locale.getWeekInfo === 'function') return locale.getWeekInfo();
if (locale.weekInfo) return locale.weekInfo;
return FALLBACK;
}
// getWeekInfo の曜日(1 が月曜、7 が日曜)を、Date#getDay の番号(0 が日曜、6 が土曜)に直す
export const toJsDay = (isoDay: number) => isoDay % 7;
TypeScript 7.0.2 の型定義(lib に esnext を指定)には getWeekInfo() と Intl.WeekInfo(firstDay と weekend)がありますが、古い weekInfo プロパティはありません。そのため、weekInfo を持つかもしれない型を自分で足しています。
古いブラウザでは weekInfo プロパティになっている
MDN の互換性データでは、getWeekInfo() は Chrome と Edge が130から、Firefox が153から、Safari が17から対応しています。それより前の Chrome(99〜129)、Safari(15.4〜16)、Node.js(18〜23)には getWeekInfo() がなく、週の情報を weekInfo というプロパティで返します。MDN によると、アクセスするたびに新しいオブジェクトを返すので、プロパティからメソッドに変わりました。
Node.js 22 は、まさにこの古い形でした。getWeekInfo はなく、weekInfo で値が返ります。上の getWeekInfo 関数を Node.js 22 で動かすと、weekInfo のほうを使って値を返しました。
typeof getWeekInfo: undefined / weekInfo: {"firstDay":1,"weekend":[6,7],"minimalDays":4}
ja-JP {"firstDay":7,"weekend":[6,7],"minimalDays":1}
en-GB {"firstDay":1,"weekend":[6,7],"minimalDays":4}
ar-EG {"firstDay":6,"weekend":[5,6],"minimalDays":1}
toJsDay: 1->1 2->2 3->3 4->4 5->5 6->6 7->0
Node.js 22 の weekInfo には minimalDays が入っていました。最後の行は、toJsDay で 7 だけが 0 になることを確かめたものです。
Chromium 153 では反対に、weekInfo はなく getWeekInfo() だけがありました。そこで Playwright で、ページを開く前に getWeekInfo を消して weekInfo だけを持たせたブラウザと、getWeekInfo を消しただけで weekInfo もないブラウザを作って試しました。前者では getWeekInfo() があるときと同じ月曜始まりの表になり、後者では FALLBACK の日曜始まりで表示され、エラーは出ませんでした(どちらも言語設定は en-GB)。
月カレンダーの並びと週末の色に使う
曜日の並び、曜日の見出し、表に並べる日付は、React に関係のない関数にしました。
// calendar.ts
import { toJsDay } from './weekInfo';
// 週の始まりの曜日から並べた、7つの曜日の番号(Date#getDay の番号)
export function weekdayOrder(firstDay: number): number[] {
const start = toJsDay(firstDay);
return Array.from({ length: 7 }, (_, i) => (start + i) % 7);
}
// 曜日の見出し。2026年11月1日は日曜日なので、1日 + 曜日の番号 の日付を表示用に書式化する
export function weekdayLabels(locale: string, order: number[]): string[] {
const fmt = new Intl.DateTimeFormat(locale, { weekday: 'short' });
return order.map((d) => fmt.format(new Date(2026, 10, 1 + d)));
}
// month は 0 始まり。表の左上の日から 6週ぶんの日付を返す
export function monthWeeks(year: number, month: number, firstDay: number): Date[][] {
const offset = (new Date(year, month, 1).getDay() - toJsDay(firstDay) + 7) % 7;
return Array.from({ length: 6 }, (_, w) =>
Array.from({ length: 7 }, (_, d) => new Date(year, month, 1 - offset + w * 7 + d)),
);
}
monthWeeks の offset は、1日より前に並ぶ、前の月の日の数です。週の始まりの曜日を toJsDay で getDay() の番号に直してから、1日の曜日との差を取っています。曜日の名前は、Intl.DateTimeFormat の weekday: 'short' で、同じ言語タグを使って作ります。
コンポーネントでは、getWeekInfo の結果から並びと週末を決めます。週末は、getDay() の番号の Set にしておき、見出しと日付のマスの両方で weekend クラスを付けるのに使います。
// MonthCalendar.tsx
import { useMemo } from 'react';
import { getWeekInfo, toJsDay } from './weekInfo';
import { monthWeeks, weekdayLabels, weekdayOrder } from './calendar';
type Props = { locale: string; year: number; month: number };
export function MonthCalendar({ locale, year, month }: Props) {
const { firstDay, weekend } = useMemo(() => getWeekInfo(locale), [locale]);
const order = useMemo(() => weekdayOrder(firstDay), [firstDay]);
const labels = useMemo(() => weekdayLabels(locale, order), [locale, order]);
const weeks = useMemo(() => monthWeeks(year, month, firstDay), [year, month, firstDay]);
// 週末の曜日を Date#getDay の番号の集合にしておく
const weekendDays = useMemo(() => new Set(weekend.map(toJsDay)), [weekend]);
const title = new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'long' })
.format(new Date(year, month, 1));
return (
<table className="calendar">
<caption>{title}</caption>
<thead>
<tr>
{order.map((d, i) => (
<th key={d} scope="col" className={weekendDays.has(d) ? 'weekend' : undefined}>
{labels[i]}
</th>
))}
</tr>
</thead>
<tbody>
{weeks.map((week) => (
<tr key={week[0].toDateString()}>
{week.map((date) => {
const classes = [
date.getMonth() !== month && 'outside',
weekendDays.has(date.getDay()) && 'weekend',
].filter(Boolean).join(' ');
return (
<td key={date.toDateString()} className={classes || undefined}>
{date.getDate()}
</td>
);
})}
</tr>
))}
</tbody>
</table>
);
}
getWeekInfo() は呼ぶたびに新しいオブジェクトを返します(Chromium 153 で l.getWeekInfo() === l.getWeekInfo() は false でした)。useMemo で locale が変わったときだけ呼ぶようにしておくと、weekend の配列も描画のたびに作り直されず、それを使う weekendDays の useMemo も働きます。
CSS は、.weekend の文字をピンク、.outside(隣の月の日)の文字を灰色にしただけです。.outside を後に書いているので、隣の月の週末は灰色になります。曜日を英語で見比べられるよう、エジプトは en-EG にして、3つの言語タグで並べた結果です。

en-EG(エジプト)は土曜始まりで、週末の金曜と土曜が表の右端と左端に分かれました。週末が並びの両端にあるとは限らないので、「右の2列を週末にする」のような書き方はできません。曜日ごとに weekend に含まれるかを調べる必要があります。
ブラウザの言語設定から地域を決める
どの地域の決まりを使うかは、ブラウザの言語設定(navigator.language)から決めました。選び直せるように、<select> も付けています。
// App.tsx
import { useState } from 'react';
import { MonthCalendar } from './MonthCalendar';
const CHOICES = ['ja-JP', 'en-US', 'en-GB', 'ar-EG'];
export function App() {
// 最初はブラウザの言語設定に合わせる
const [locale, setLocale] = useState(() => navigator.language);
const options = CHOICES.includes(locale) ? CHOICES : [locale, ...CHOICES];
return (
<main>
<label>
地域
<select value={locale} onChange={(e) => setLocale(e.target.value)}>
{options.map((tag) => (
<option key={tag} value={tag}>{tag}</option>
))}
</select>
</label>
<MonthCalendar locale={locale} year={2026} month={10} />
</main>
);
}
Playwright でブラウザの言語設定を変えて開き、見出しと1行目を読み取った結果です。* は隣の月の日です。見出しの (週末) と日付の ! は、weekend クラスが付いたことを表します。
ja-JP {"lang":"ja-JP","caption":"2026年11月","head":"日(週末) 月 火 水 木 金 土(週末)","row1":"1! 2 3 4 5 6 7!"} options: ja-JP,en-US,en-GB,ar-EG
en-US {"lang":"en-US","caption":"November 2026","head":"Sun(週末) Mon Tue Wed Thu Fri Sat(週末)","row1":"1! 2 3 4 5 6 7!"} options: ja-JP,en-US,en-GB,ar-EG
en-GB {"lang":"en-GB","caption":"November 2026","head":"Mon Tue Wed Thu Fri Sat(週末) Sun(週末)","row1":"26* 27* 28* 29* 30* 31*! 1!"} options: ja-JP,en-US,en-GB,ar-EG
ar-EG {"lang":"ar-EG","caption":"نوفمبر ٢٠٢٦","head":"السبت(週末) الأحد الاثنين الثلاثاء الأربعاء الخميس الجمعة(週末)","row1":"31*! 1 2 3 4 5 6!"} options: ja-JP,en-US,en-GB,ar-EG
en-MV {"lang":"en-MV","caption":"November 2026","head":"Fri Sat(週末) Sun(週末) Mon Tue Wed Thu","row1":"30* 31*! 1! 2 3 4 5"} options: en-MV,ja-JP,en-US,en-GB,ar-EG
選択肢にない en-MV(モルディブ)で開くと、選択肢の先頭に en-MV が足され、金曜始まりの表になりました。ja-JP で開いてから <select> で en-GB を選ぶと、見出しが Mon から始まる並びに変わり、1行目が10月26日からになりました。
ar-EG では、曜日の名前と見出しの年月がアラビア語になりました。一方、日付のマスは getDate() の数をそのまま出しているので、算用数字(0〜9)のままです。マスの数字も Intl.DateTimeFormat('ar-EG', { day: 'numeric' }) で書式化すると、Chromium 153 では1日が「١」、30日が「٣٠」のように、見出しの年(٢٠٢٦)と同じ書き方の数字になりました。





