Recipe

Ship one locale across a dashboard

Give a chart, a grid, a map and a Gantt every locale setting they have, translate every dictionary, switch to German, and 9 of 15 values on screen still read as English. The fix is one module that owns the tag, the formatters and the direction, with a thin adapter per product.

A chart, a grid, a map and a Gantt switched between English, German and Arabic, two waysOpen in new tab
Rebuilds this page in your assistant. It reads current API guidance from our MCP server rather than from its training data.

Built with ApexCharts.js, ApexGrid, ApexMaps, ApexGantt

A dashboard with a chart, a grid, a map and a Gantt has four locale systems, plus a fifth nobody configured: the browser's. Each library's locale option translates that library's own strings, and does it well. None of them formats your data, and two of them format it for the browser instead of for the page. So the language switch belongs in one module that owns the tag, the formatters and the direction, with a thin adapter per product.

Everything below was measured in Chromium on apexcharts 8.8.1, apex-grid 4.0.1, apexmaps 1.3.0 and apexgantt 3.22.0, with supporting runs on apextree 2.3.0, apexsankey 1.14.0 and apexstock 0.6.1. Each case ran under an en-US and a de-DE browser locale, with and without dir="rtl" on <html>. The demo above runs the same four products and reads its own text back out of the DOM.

npm install apexcharts apex-grid apexmaps apexgantt dayjs

What the harness establishes, before any of the reasoning:

  • With every product given every locale setting it has and every dictionary translated, switching the demo to German left 9 of 15 checked values reading exactly as they did in English, and Arabic left 10.
  • chart.setLocale() redraws nothing. The chart's internal month names change and the axis keeps the old ones until something else redraws it.
  • ApexGrid's currency and date columns format for the browser's locale. The page's language never reaches them, and the date column has no setting that would.
  • On an RTL page, ApexGantt can be half mirrored in two opposite ways, neither of which throws.

What does each locale option cover?

Their own strings, in four different shapes. That is the whole of it.

OptionDictionaries shippedFormats numbers for youSwitch at runtime
ApexChartschart.locales (an array of named month, day and toolbar tables) and chart.defaultLocale49 locale filesNo: 2000000.00 on the value axis in every localesetLocale(), plus a redraw (below)
ApexGridlocaleText (a flat map of string keys)English and SpanishCurrency and date columns, for the browser's localeAssign localeText again
ApexMapslocale (a nested object of strings and templates)NoneIts own compact English: 99k to 235k, 1.2MupdateOptions({ locale }), in place
ApexGanttlocale.pack, dateLocale (a dayjs object), messages, tag, directionJapanese, marked draftThrough tag, where a message template asks for itupdate({ locale })

The supporting runs fill in the rest of the family. ApexTree and ApexSankey each take locale.direction and locale.messages, and the ApexTree docs say a new locale needs a new instance. ApexStock 0.6.1 declares no locale or direction option in its types, has no localization page, and a chart.locales passed through to it did not reach the axis: German registered, Mar 02 rendered.

The shapes are why one config object cannot drive them all, and the dictionary column is why "use the bundled locale" stops working at the second language. A German dashboard writes its grid, map and Gantt strings by hand.

Why does the chart ignore setLocale?

Because on 8.8.1 setLocale() changes the chart's locale and stops there. Measured on a chart registered with English and German:

chart.setLocale('de')
chart.w.globals.locale.shortMonths[2]   // 'Mär'
// x axis still reads 02 Mar, 16 Mar, 30 Mar
// toolbar titles still read Zoom In, Zoom Out

After 1.5 seconds, nothing had moved. Toggling a series, which is what a legend click does, redrew the chart and the German labels appeared. That is the worst version of the bug: the switch looks broken, then fixes itself when the reader touches something. Three more measured facts complete the picture:

  • Registering locales replaces English. With locales: [de], setLocale('en') throws Wrong locale name provided. Register every language the page offers, English included.
  • updateOptions({ chart: { defaultLocale } }) alone does not switch. The labels stayed English.
  • A repeated updateOptions({}) redraws once. The first call after a setLocale('de') applied German; the next, after setLocale('en'), left the German labels where they were.

The pairing that worked on every switch, in both directions, and kept a zoomed window (1 February to 1 April) intact through German and Arabic:

chart.setLocale(name)
await chart.updateOptions({ chart: { defaultLocale: name } }, false, false)

The same redraw re-runs any formatter that reads the current locale, which is how the module below gets its numbers onto the axis.

Why do the grid's numbers follow the browser?

Because localeText is a dictionary for the grid's chrome, and the column types format through Intl against the runtime locale. One row, rendered by apex-grid 4.0.1 under two browser locales:

Column typeen-US browserde-DE browserFollows localeTextFollows column locale
number1234567.8911234567.891NoNo
currency€1,234,567.891.234.567,89 €NoYes
dateMar 4, 202604.03.2026NoNo

With the bundled Spanish dictionary, the header controls read Filtrar columna and Menú de columna while the cells kept the browser's English formatting. So an English page in a German browser shows German money, and a German page in a German browser looks correct. The second case is the one that survives review, because it is what a German developer sees.

The column locale is documented for currency formatting and reaches nothing else; the date column has no locale setting. Both gaps close with a cellTemplate that calls your own formatter, which also keeps sorting and filtering on the raw value:

grid.columns = [
  { key: 'revenue', type: 'currency', currency: 'EUR', locale: loc.tag },
  { key: 'orders', type: 'number', cellTemplate: (p) => loc.count(p.value) },
  { key: 'last', type: 'date', cellTemplate: (p) => loc.date(p.value) },
]
grid.localeText = loc.gridStrings

Direction needs nothing. The grid reads its computed direction, so a dir on <html> mirrored the column order under both browsers.

What does the map leave in English?

Everything that is not one of its own control strings. The locale option retitled the zoom buttons, and updateOptions({ locale }) retitled them again in place without replacing the SVG. Untouched by it, measured on 1.3.0:

  • The legend ranges, 197k to 521k, in compact English under either browser.
  • The generated screen-reader summary: Values range from 98765.40 to 2345678.25, 172 areas have no data.
  • Region names, which come from the geometry pack. Germany is data, not a UI string.

Three options reach those: legend.formatter for the ranges, tooltip.formatter for the values and names, and a string a11y.description for the summary. One thing does not. Each legend chip carries a title built from the default format, and with a German legend.formatter the visible chip read 197.413 € bis 520.756 € while its title still read 197k to 521k (2 features). No 1.3.0 option reaches that title.

Which way does time run in an RTL dashboard?

Choose it, because the products will not agree by themselves.

ApexCharts kept its SVG at a computed direction: ltr inside an RTL page, with the value axis on the left and the legend in its original order. Its types have reversed on the y axis and none on the x axis, so a datetime axis runs left to right in every language. That is the fixed point.

ApexGantt, on the same RTL page, three ways:

locale.directionTask listTime runsContainer direction
unset, so 'ltr'right, inheritedleft to rightrtl
'auto'leftright to leftltr, with Latin task names
'rtl'rightright to leftrtl

The first two are half mirrored, in opposite directions. 'ltr' is the default and writes no dir attribute, so the layout inherits the page's direction while the timeline does not. 'auto' writes dir="auto" onto the container, and in HTML that means "infer from the first strong character of the text", not "inherit". With Latin task names and English column titles it resolved to ltr. In the demo's Arabic mode, with the column titles translated, the same setting produced a fully mirrored chart: whether 'auto' works depends on the text, not on the page. ApexTree behaved the same way, mirroring its layout while its container computed ltr around Latin labels, and ApexSankey's 'auto' also wrote dir="auto" and mirrored its flow.

So pass a direction explicitly, and decide which. With a time-series chart on the page, time can only run one way across it if it runs left to right, so the demo mirrors the grid, the map legend and the page chrome and keeps the Gantt's time with the chart. A page without a time-series chart can mirror the Gantt fully with 'rtl'. Two traps on the way:

  • Put an explicit ltr on a wrapper. ApexGantt manages its container's dir attribute and removed one set there when its own direction was 'ltr'. A dir="ltr" on the parent element held.
  • Switching to RTL at runtime scrolls the bars away. Built in RTL, all six bars were in view. Switched to RTL by update(), with 'rtl' or 'auto', none were: the timeline sat at its far end, two years past the work. scrollToTask(id) or setState({ scroll: { horizontal: 0 } }) straight after the update brought all six back.

Which digits does an Arabic page use?

Decide that once too. Intl for ar-EG prints Arabic-Indic digits, so the grid read ١٨٬٢١٣. ApexGantt's dates come from dayjs, and its Arabic locale printed 2 مارس 2026 with Latin digits. Left alone, one screen carries both numbering systems.

The Gantt's read-only dates take a dayjs pattern, not a formatter, so the direction of travel is toward the Gantt: pin Latin digits with a Unicode extension on the tag, ar-EG-u-nu-latn, and every Intl formatter in the module follows it. If Arabic-Indic digits matter more than the Gantt's date column, format that column yourself instead.

The module

One object per language, built from a tag. Every adapter reads it and nothing else, so the digits, separators, month names and direction on screen come from one place:

function makeLocale(tag, { dir, chartLocale, dayjsLocale, strings }) {
  const money = new Intl.NumberFormat(tag, { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 })
  const compact = new Intl.NumberFormat(tag, { style: 'currency', currency: 'EUR', notation: 'compact' })
  const count = new Intl.NumberFormat(tag)
  const date = new Intl.DateTimeFormat(tag, { dateStyle: 'medium', timeZone: 'UTC' })
  const region = new Intl.DisplayNames([tag], { type: 'region' })
  return {
    tag, dir, chartLocale, dayjsLocale, strings,
    timeDir: 'ltr', // see "Which way does time run"
    money: (v) => money.format(v),
    compact: (v) => compact.format(v),
    count: (v) => count.format(v),
    date: (v) => date.format(new Date(v)),
    country: (iso2) => region.of(iso2),
  }
}

async function applyLocale(loc) {
  document.documentElement.lang = loc.tag
  document.documentElement.dir = loc.dir

  // ApexCharts: formatters read `current`, so the redraw re-runs them.
  chart.setLocale(loc.chartLocale)
  await chart.updateOptions({ chart: { defaultLocale: loc.chartLocale } }, false, false)

  // ApexGrid: chrome from the dictionary, values from the module.
  grid.columns = gridColumns(loc)
  grid.localeText = loc.strings.grid

  // ApexMaps: its strings, plus the ranges, values and summary it cannot know.
  await map.updateOptions({
    locale: loc.strings.map,
    legend: { formatter: (i) => `${loc.money(i.from)} ${loc.strings.to} ${loc.money(i.to)}` },
    a11y: { description: loc.strings.mapLabel },
  })

  // ApexGantt: explicit direction, on a wrapper when it is ltr.
  ganttWrapper.setAttribute('dir', loc.timeDir)
  await gantt.update({
    locale: {
      tag: loc.tag,
      dateLocale: loc.dayjsLocale,
      messages: loc.strings.gantt,
      direction: loc.timeDir,
    },
  })
}

current = makeLocale(...) before applyLocale(current) is the only assignment a language switch makes. In the demo that pattern took the German and Arabic runs from 9 and 10 unchanged values to one, the legend chip title.

Two ApexGantt message keys are worth knowing because they carry numbers. progressValue defaults to '{value}%', which printed 62.5% under a German tag; '{value, number} %' printed 62,5 %. barAriaLabel takes a template such as '{name}: {start} bis {end}', which turned the bar's accessible name from March 2, 2026 to March 20, 2026 into 2. März 2026 bis 20. März 2026.

When does none of this apply?

When the page has one language and you set it at construction. Every runtime defect above (the lazy setLocale, the scrolled-away Gantt, the repeated updateOptions) belongs to switching, and a page that is built in German from the start never switches. The formatting gaps do not go away with it, though: the grid's currency and date columns still format for the browser, and the chart's value axis is still unformatted, so the module's formatters are worth keeping even when it is only ever called once.

It also does not apply to text that is your data rather than a library's. Series names, column headers and task names are yours in every design, and no locale option will translate them.

Which plan covers this?

What you needPackagePlan
Charts, locale files, formattersapexchartsCommunity and up
The grid, localeText, column typesapex-gridCommunity and up
The choropleth map, locale, legend formatterapexmapsCommunity and up
The Gantt and its localizationapexganttPremium and OEM

Checked against the one entitlements table the pricing page is generated from. ApexCharts.js, ApexGrid and ApexMaps start at Community; ApexGantt starts at Premium. Community is free only for organizations under $2M USD in annual revenue, budget or funding, and a paid licence above it. Nothing in the family is open source. ApexGantt renders in full without a key, watermarked, so everything here can be tried before any of that matters. The pricing page has the matrix.

ApexGantt localization in full: locale packs, the 124 message keys, templates and RTL

See the pieces running

Reference documentation

Frequently Asked Questions

Why does ApexCharts setLocale() not change the axis labels?

On apexcharts 8.8.1 setLocale() updates the chart's locale and redraws nothing: after setLocale('de') the chart's internal short month names read Mär while the axis still read 02 Mar, 1.5 seconds later. The next redraw applies it, which can be the reader clicking the legend. Follow it with chart.updateOptions({ chart: { defaultLocale: name } }, false, false). That switched every time in both directions and kept a zoomed window intact. A repeated updateOptions({}) is not enough: it redrew once and did nothing on the next switch.

Why does ApexGrid show currency in the wrong format after I set localeText?

Because localeText translates the grid's own strings, and the currency and date columns format through Intl against the browser's locale. On apex-grid 4.0.1 one currency cell read €1,234,567.89 under an en-US browser and 1.234.567,89 € under de-DE, whatever localeText held. A column locale such as 'de-DE' fixes the currency column. The date column has no locale setting and the number column is printed unformatted (1234567.891), so give both a cellTemplate that calls your own Intl formatter.

How do I mirror a dashboard for Arabic or Hebrew?

Set dir on the page and pass every product an explicit direction, because they do not agree by themselves. ApexGrid follows the computed direction with no option. ApexCharts stays left to right: its SVG computed direction ltr inside an RTL page, and 8.8.1 has no reversed x axis, so a datetime axis always runs left to right. ApexGantt's default writes no dir, so its task list moved right while time kept running left to right; 'auto' writes dir="auto", which the browser resolves from the text, and with Latin task names that mirrored the timeline but not the task list. Decide which way time runs on the page and pass 'rtl' or 'ltr' explicitly.

Why did my Gantt bars disappear after switching to RTL?

On apexgantt 3.22.0 switching an existing chart to right-to-left with update() left none of six bars in view: the timeline sat at its far end, two years past the work. A chart built in RTL from the start opened correctly. Calling scrollToTask(id), or setState({ scroll: { horizontal: 0 } }), straight after the update brought all six back.

Should an Arabic dashboard use Arabic-Indic or Latin digits?

Pick one, because the products will otherwise use both. Intl for ar-EG prints Arabic-Indic digits, so a grid read ١٨٬٢١٣, while ApexGantt formats its read-only dates through dayjs, whose Arabic locale printed 2 مارس 2026 with Latin digits. The Gantt takes a pattern there rather than a formatter, so the simplest consistent page pins Latin digits with the tag ar-EG-u-nu-latn, which every Intl formatter in a locale module then follows.

Related

See one product localized on its own

The ApexGantt demo switches between English, French, German, Arabic, Hebrew and the Japanese pack.

Get started