TypeError: can't access property "toLocaleLowerCase", unitText is undefined

Author: ex1stCreated Sep 10, 2026Updated Sep 13, 2026

Reduced Test Case

Do you understand that if a reduced test case is not provided, we will intentionally delay triaging of your ticket?

  • I understand

Which connector are you using (React/Angular/etc)?

No connector (vanilla JS)

Bug Description

TypeError in locale todayHint/prevHint/nextHint when a custom view's duration has no single-unit denominator (e.g. { month: 12 })

Description

When a custom view is configured with a duration that reduces to a non-unit value (e.g. a 12-month "year" view defined as duration: { month: 12 } instead of duration: { year: 1 }), clicking the today, prev, or next toolbar buttons while that view is active throws a TypeError inside locale hint functions, e.g.:

TypeError: can't access property "toLocaleLowerCase", unitText is undefined
    at todayHint (locales/es.js:28)
    at formatWithOrdinals (main.js:2755)
    at buttonHint (main.js:7598)

Reproduction

javascript
new Calendar(el, {
  locale: 'es',
  views: {
    dayGridYear: {
      type: 'dayGrid',
      duration: { month: 12 }, // 12 !== 1, so it never resolves to a single unit
    },
  },
  initialView: 'dayGridYear',
  headerToolbar: { left: 'prev,today,next', center: 'title', right: 'dayGridMonth,dayGridYear' },
});

Click Today (or Prev/Next) while dayGridYear is the active view → crash.

Root cause

In buildViewSpec(), viewSpec.singleUnit is only set when greatestDurationDenominator(duration).value === 1:

javascript
if (denom.value === 1) {
    singleUnit = durationUnit;
    ...
}

For duration: { month: 12 }, the denominator is { unit: 'month', value: 12 }, so singleUnit stays ''.

Later, in the toolbar button-building code (around main.js:7590-7607), the today/prev/next hint callback computes unitText as:

javascript
buttonHint = (currentUnit) => {
    return formatWithOrdinals(buttonInput.hint || calendarOptions[name + 'Hint'], [
        calendarOptions[currentUnit + 'TextLong'] ||
            calendarOptions[currentUnit + 'Text'],
        currentUnit
    ], buttonText);
};

When currentUnit is '' (empty singleUnit), this evaluates to calendarOptions['TextLong'] || calendarOptions['Text'], both of which are undefined. So unitText is undefined when passed to the locale's todayHint/prevHint/nextHint.

Most locale packs (e.g. es, and likely others following the same pattern) assume unitText is always a string and call .toLocaleLowerCase() on it unconditionally:

javascript
// locales/es.js
todayHint(unitText, unit) {
    return (unit === 'day') ? 'Hoy' :
        ((unit === 'week') ? 'Esta' : 'Este') + ' ' + unitText.toLocaleLowerCase();
},

→ crash.

Notably, getButtonState() (used elsewhere, main.js:8684-8722) already guards against this exact case:

javascript
const currentUnit = currentData.viewSpec.singleUnit;
const currentHintOrdinal = [
    currentUnit ? getSingleUnitText(currentUnit, options) : '',
    currentUnit,
];

but the toolbar-button-building code path (used when actually rendering the today/prev/next buttons, around main.js:7596-7607) does not have the same guard.

Suggested fix

Apply the same currentUnit ? ... : '' guard in the toolbar button hint callback, so unitText is always a string:

javascript
buttonHint = (currentUnit) => {
    return formatWithOrdinals(buttonInput.hint || calendarOptions[name + 'Hint'], [
        currentUnit
            ? (calendarOptions[currentUnit + 'TextLong'] || calendarOptions[currentUnit + 'Text'] || '')
            : '',
        currentUnit
    ], buttonText);
};

This keeps behavior consistent with getButtonState()/getSingleUnitText() and prevents locale hint functions from ever receiving undefined as unitText.

Environment

  • fullcalendar (vanilla JS) v7.1.0
  • Reproduced with locale es, but any locale whose todayHint/prevHint/nextHint calls a string method on unitText unconditionally is affected.

Source: fullcalendar/fullcalendar