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.
| Option | Dictionaries shipped | Formats numbers for you | Switch at runtime | |
|---|---|---|---|---|
| ApexCharts | chart.locales (an array of named month, day and toolbar tables) and chart.defaultLocale | 49 locale files | No: 2000000.00 on the value axis in every locale | setLocale(), plus a redraw (below) |
| ApexGrid | localeText (a flat map of string keys) | English and Spanish | Currency and date columns, for the browser's locale | Assign localeText again |
| ApexMaps | locale (a nested object of strings and templates) | None | Its own compact English: 99k to 235k, 1.2M | updateOptions({ locale }), in place |
| ApexGantt | locale.pack, dateLocale (a dayjs object), messages, tag, direction | Japanese, marked draft | Through tag, where a message template asks for it | update({ 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')throwsWrong 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 asetLocale('de')applied German; the next, aftersetLocale('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 type | en-US browser | de-DE browser | Follows localeText | Follows column locale |
|---|---|---|---|---|
number | 1234567.891 | 1234567.891 | No | No |
currency | €1,234,567.89 | 1.234.567,89 € | No | Yes |
date | Mar 4, 2026 | 04.03.2026 | No | No |
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.
Germanyis 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.direction | Task list | Time runs | Container direction |
|---|---|---|---|
unset, so 'ltr' | right, inherited | left to right | rtl |
'auto' | left | right to left | ltr, with Latin task names |
'rtl' | right | right to left | rtl |
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
ltron a wrapper. ApexGantt manages its container'sdirattribute and removed one set there when its own direction was'ltr'. Adir="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)orsetState({ 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 need | Package | Plan |
|---|---|---|
| Charts, locale files, formatters | apexcharts | Community and up |
The grid, localeText, column types | apex-grid | Community and up |
The choropleth map, locale, legend formatter | apexmaps | Community and up |
| The Gantt and its localization | apexgantt | Premium 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 RTLSee 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.