Skip to content

feat(plotly): add locale prop and Config.plotly_locale for i18n chart formatting#6193

Draft
pranavmanglik wants to merge 5 commits intoreflex-dev:mainfrom
pranavmanglik:main
Draft

feat(plotly): add locale prop and Config.plotly_locale for i18n chart formatting#6193
pranavmanglik wants to merge 5 commits intoreflex-dev:mainfrom
pranavmanglik:main

Conversation

@pranavmanglik
Copy link
Copy Markdown

feat(plotly): add locale prop and Config.plotly_locale for i18n chart formatting

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

New Feature Submission:

  • Does your submission pass the tests?
  • Have you linted your code locally prior to submission?

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

Summary

Adds a locale prop to rx.plotly (and all plotly subcomponents) so that charts automatically follow user language and regional formatting — number separators, date axis labels, month/weekday names, and modebar button tooltips. Also adds plotly_locale to rx.Config as an app-wide default.

Problem

rx.plotly had no way to configure the Plotly.js locale. All charts rendered with en-US defaults regardless of the user's language — decimal separators, date axis tick labels, month/weekday names, and modebar button tooltips were always in English with no first-class Reflex API to change this.

Developers targeting non-English audiences had to ship their own JS shims to work around this.

Usage

# per-chart locale
rx.plotly(data=fig, locale="de")      # German
rx.plotly(data=fig, locale="zh-CN")   # Simplified Chinese
rx.plotly(data=fig, locale="fr")      # French
rx.plotly(data=fig, locale="pt-BR")   # Brazilian Portuguese

app-wide default in rxconfig.py

config = rx.Config(
app_name="myapp",
plotly_locale="de",
)

also works via environment variable

REFLEX_PLOTLY_LOCALE=de reflex run

state-driven — switches locale without page reload

rx.plotly(data=fig, locale=AppState.locale)

Per-chart locale= takes precedence over Config.plotly_locale. If neither is set, Plotly's built-in "en" defaults apply — zero behaviour change for existing apps.

What locale affects

Element en (before) de (after)
Modebar zoom in "Zoom in" "Hineinzoomen"
Modebar zoom out "Zoom out" "Herauszoomen"
Modebar download "Download plot as a PNG" "Graphen als PNG herunterladen"
Modebar reset axes "Reset axes" "Achsen zurücksetzen"
Month names January … December Januar … Dezember
Weekday names Mon … Sun Mo … So
Date format %m/%d/%Y %d.%m.%Y
Decimal / thousands separator 1,234.56 1.234,56

Test results

tests/components/plotly/test_plotly.py::test_plotly_locale_default PASSED
tests/components/plotly/test_plotly.py::test_plotly_locale_de     PASSED
tests/components/plotly/test_plotly.py::test_plotly_locale_zh     PASSED
tests/components/plotly/test_plotly.py::test_config_has_plotly_locale   PASSED
tests/components/plotly/test_plotly.py::test_config_plotly_locale_default PASSED

5 passed, 2 warnings in 0.34s

Manually verified with locale="de" and locale="zh-CN" using reflex run. Confirmed German modebar tooltips ("Hineinzoomen", "Graphen als PNG herunterladen") and Chinese tooltips rendering correctly. Default chart (no locale set) behaviour is unchanged.

Supported locales

~70 locales from plotly.js-locales (MIT licensed), including: af, ar, bg, cs, da, de, de-ch, el, es, es-ar, et, fa, fi, fr, fr-ch, he, hi-in, hr, hu, id, it, ja, ko, lt, lv, nl, no, pl, pt-br, pt-pt, ro, ru, sk, sl, sr, sv, th, tr, uk, vi, zh-cn, zh-hk, zh-tw and more.

# feat(plotly): add `locale` prop and `Config.plotly_locale` for i18n chart formatting

All Submissions:

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

New Feature Submission:

  • Does your submission pass the tests?
  • Have you linted your code locally prior to submission?

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

Summary

Adds a locale prop to rx.plotly (and all plotly subcomponents) so that charts automatically follow user language and regional formatting — number separators, date axis labels, month/weekday names, and modebar button tooltips. Also adds plotly_locale to rx.Config as an app-wide default.

Problem

rx.plotly had no way to configure the Plotly.js locale. All charts rendered with en-US defaults regardless of the user's language — decimal separators, date axis tick labels, month/weekday names, and modebar button tooltips were always in English with no first-class Reflex API to change this.

Developers targeting non-English audiences had to ship their own JS shims to work around this.

Usage

# per-chart locale
rx.plotly(data=fig, locale="de")      # German
rx.plotly(data=fig, locale="zh-CN")   # Simplified Chinese
rx.plotly(data=fig, locale="fr")      # French
rx.plotly(data=fig, locale="pt-BR")   # Brazilian Portuguese

# app-wide default in rxconfig.py
config = rx.Config(
    app_name="myapp",
    plotly_locale="de",
)

# also works via environment variable
# REFLEX_PLOTLY_LOCALE=de reflex run

# state-driven — switches locale without page reload
rx.plotly(data=fig, locale=AppState.locale)

Per-chart locale= takes precedence over Config.plotly_locale. If neither is set, Plotly's built-in "en" defaults apply — zero behaviour change for existing apps.

What locale affects

Element en (before) de (after)
Modebar zoom in "Zoom in" "Hineinzoomen"
Modebar zoom out "Zoom out" "Herauszoomen"
Modebar download "Download plot as a PNG" "Graphen als PNG herunterladen"
Modebar reset axes "Reset axes" "Achsen zurücksetzen"
Month names January … December Januar … Dezember
Weekday names Mon … Sun Mo … So
Date format %m/%d/%Y %d.%m.%Y
Decimal / thousands separator 1,234.56 1.234,56

Implementation

Uses Plotly.js's built-in config.locales inline injection:

config={{ locale: "de", locales: { de: localeData } }}

This avoids Plotly.register() and any dynamic JS imports entirely. The locale file is fetched at runtime via fetch() from the plotly.js-locales package, parsed with a new Function CJS sandbox, and passed inline through the config prop. This sidesteps all Vite/bundler CJS→ESM conversion issues.

A thin React wrapper _RxPlotLocale (injected via add_custom_code) handles the async fetch and forwards all other props to <Plot> unchanged. Since tag = "Plot" causes Reflex to auto-import Plot from react-plotly.js, the rendered element name is overridden in _render() via tag.set(name="_RxPlotLocale").

Unknown locale codes fall back gracefully to "en" with a console warning:

[rx.plotly] Locale "xx" could not be loaded: HTTP 404.
Check https://www.npmjs.com/package/plotly.js-locales for supported codes.

Files changed

File Change
reflex/components/plotly/plotly.py Add locale: Var[str] prop; add _rxLoadLocale + _RxPlotLocale to add_custom_code; add plotly.js-locales to lib_dependencies; override rendered tag name in _render; read global config fallback in create()
reflex/config.py Add plotly_locale: str = "" to BaseConfig
tests/components/plotly/test_plotly.py New test file — 5 tests covering locale prop, default value, and config field

Test results

tests/components/plotly/test_plotly.py::test_plotly_locale_default PASSED
tests/components/plotly/test_plotly.py::test_plotly_locale_de     PASSED
tests/components/plotly/test_plotly.py::test_plotly_locale_zh     PASSED
tests/components/plotly/test_plotly.py::test_config_has_plotly_locale   PASSED
tests/components/plotly/test_plotly.py::test_config_plotly_locale_default PASSED

5 passed, 2 warnings in 0.34s

Manually verified with locale="de" and locale="zh-CN" using reflex run. Confirmed German modebar tooltips ("Hineinzoomen", "Graphen als PNG herunterladen") and Chinese tooltips rendering correctly. Default chart (no locale set) behaviour is unchanged.

Supported locales

~70 locales from [plotly.js-locales](https://www.npmjs.com/package/plotly.js-locales) (MIT licensed), including:
af, ar, bg, cs, da, de, de-ch, el, es, es-ar, et, fa, fi, fr, fr-ch, he, hi-in, hr, hu, id, it, ja, ko, lt, lv, nl, no, pl, pt-br, pt-pt, ro, ru, sk, sl, sr, sv, th, tr, uk, vi, zh-cn, zh-hk, zh-tw and more.

@pranavmanglik
Copy link
Copy Markdown
Author

Here, if you are using de locale, then "Zoom" is same as in English. I have checked that in /tmp/plotly_locale_test/.web/node_modules/plotly.js-locales.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR adds first-class i18n support to rx.plotly by introducing a locale prop and Config.plotly_locale global default. The _RxPlotLocale JS wrapper handles async CDN locale fetching, correctly stores Promises in cache (fixing the race condition from prior review), validates locale codes via regex, and forwards _plotComponent so subclasses use their correct dist variant. However, an unrelated change flips createPlotlyComponent's is_default from TrueFalse, which would break all eight Plotly subclasses at runtime.

Confidence Score: 3/5

Not safe to merge as-is — an unrelated is_default change breaks all Plotly dist subclasses at runtime.

The locale feature itself is well-structured (race-condition fixed, path traversal guarded, graceful fallback), but the PR flips createPlotlyComponent's is_default from True to False — breaking PlotlyBasic, PlotlyCartesian, PlotlyGeo, PlotlyGl3d, PlotlyGl2d, PlotlyMapbox, PlotlyFinance, and PlotlyStrict. The two P2 issues (CSP eval concern, unused npm package) don't block merge on their own.

reflex/components/plotly/plotly.py — the is_default=False change at CREATE_PLOTLY_COMPONENT must be reviewed and most likely reverted.

Important Files Changed

Filename Overview
reflex/components/plotly/plotly.py Core change — adds locale prop, _RxPlotLocale JS wrapper, CDN locale fetch, and _render() override; introduces an unrelated is_default regression that breaks all Plotly subclasses at runtime.
reflex/config.py Adds plotly_locale: str = '' to BaseConfig; missing descriptive comment and blank-line separation consistent with other fields.
tests/components/plotly/test_plotly.py New test file with 5 tests; contains unused SimpleNamespace and patch imports.
tests/components/plotly/init.py Empty init.py — correct and straightforward.
pyi_hashes.json Auto-generated .pyi stub hash updates.

Comments Outside Diff (1)

  1. reflex/components/plotly/plotly.py, line 411 (link)

    P1 Unrelated is_default flip breaks all Plotly subclasses

    The diff changes createPlotlyComponent's is_default from TrueFalse, unrelated to the locale feature. This transforms the generated JS import from a default to a named import:

    Before: import createPlotlyComponent from 'react-plotly.js/factory';
    After: import { createPlotlyComponent } from 'react-plotly.js/factory';

    react-plotly.js/factory exports createPlotlyComponent as a default export. With Vite's CJS interop, { createPlotlyComponent } would resolve to undefined, breaking PlotlyBasic, PlotlyCartesian, PlotlyGeo, PlotlyGl3d, PlotlyGl2d, PlotlyMapbox, PlotlyFinance, and PlotlyStrict at runtime.

Reviews (2): Last reviewed commit: "fix(plotly): correct locale integration ..." | Re-trigger Greptile

# Render through _RxPlotLocale wrapper which handles locale loading.
# `tag = "Plot"` above tells Reflex to auto-import Plot from react-plotly.js;
# we override the rendered element name here so the JSX uses _RxPlotLocale.
tag = tag.set(name="_RxPlotLocale")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 _RxPlotLocale hardcodes Plot — all sub-classes broken

_render() is now inherited by every Plotly sub-class (PlotlyBasic, PlotlyCartesian, PlotlyGeo, etc.) and unconditionally rewrites the element name to _RxPlotLocale. But the _RxPlotLocale JS function hardcodes React.createElement(Plot, ...), where Plot is the default import from react-plotly.js (the full, heavy bundle).

Before this PR each sub-class rendered its own dynamic component (BasicPlotlyPlot, CartesianPlotlyPlot, etc.). After this PR they all render through _RxPlotLocale → Plot, ignoring their lightweight dist variants entirely.

The _render() override should be guarded so it only applies when the component is actually the base Plotly class and a locale is set, or the _RxPlotLocale wrapper needs to accept the inner component as a prop so sub-classes can pass their specific dynamic component name:

function _RxPlotLocale({ locale, config, _plotComponent, ...rest }) {
    const PlotComponent = _plotComponent || Plot;
    ...
    return React.createElement(PlotComponent, { ...rest, config: mergedConfig });
}

function _rxLoadLocale(locale) {
const key = locale.toLowerCase();
if (_rxLocaleCache[key]) return Promise.resolve(_rxLocaleCache[key]);
const url = `/node_modules/plotly.js-locales/${key}.js`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Locale fetch from /node_modules/ will 404 in production

The fetch URL is constructed as /node_modules/plotly.js-locales/${key}.js. During reflex run (dev mode) the frontend dev-server may serve node_modules, but after reflex build the production output is a static bundle — there is no /node_modules/ directory served at that path. Every locale fetch will return a 404, the catch branch will fire, and all non-English locales will silently fall back to English in production.

The locale files should either be:

  1. Copied to the public/static directory at build time and served from there, or
  2. Imported at bundle time (e.g., as JSON) so Vite/webpack can include them in the bundle.

Using a CDN URL (e.g. https://cdn.jsdelivr.net/npm/plotly.js-locales@3.3.1/${key}.js) is another option but adds an external network dependency.

from reflex.config import get_config

# Apply global plotly_locale from rxconfig.py if no per-chart locale given.
if not props.get("locale"):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Explicit locale="" is treated as "not set", overriding with global config

if not props.get("locale") is falsy for both None (locale not supplied at all) and "" (explicitly passed as empty to opt-out). If Config.plotly_locale = "de" and a developer writes rx.plotly(data=fig, locale="") to force English on a specific chart, the global default would still be applied.

The check should differentiate between "not supplied" and "explicitly empty":

Suggested change
if not props.get("locale"):
if "locale" not in props:

# List of plugin types to disable in the app.
disable_plugins: list[type[Plugin]] = dataclasses.field(default_factory=list)

plotly_locale: str = ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing docstring comment for plotly_locale

Every other field in BaseConfig has a # <description> comment. This field was added without one, and it's also missing the blank line that separates logical sections from neighbouring fields (consistent with the custom instruction rule on code readability).

Suggested change
plotly_locale: str = ""
# The default BCP-47 locale code for all rx.plotly charts (e.g. "de", "fr", "zh-CN").
# Can be overridden per-chart with the locale= prop. Also settable via REFLEX_PLOTLY_LOCALE.
plotly_locale: str = ""

Rule Used: Add blank lines between logical sections of code f... (source)

Learnt From
reflex-dev/flexgen#2170

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +1 to +3
"""Tests for rx.plotly locale support."""
from types import SimpleNamespace
from unittest.mock import patch
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unused imports in test file

SimpleNamespace and patch are imported but never referenced in any of the five tests.

Suggested change
"""Tests for rx.plotly locale support."""
from types import SimpleNamespace
from unittest.mock import patch
"""Tests for rx.plotly locale support."""
from reflex.components.plotly.plotly import Plotly
from reflex.vars.base import LiteralVar

Comment on lines +240 to +262
function _rxLoadLocale(locale) {
const key = locale.toLowerCase();
if (_rxLocaleCache[key]) return Promise.resolve(_rxLocaleCache[key]);
const url = `/node_modules/plotly.js-locales/${key}.js`;
return fetch(url)
.then(r => {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.text();
})
.then(code => {
const mod = { exports: {} };
new Function("module", "exports", code)(mod, mod.exports);
_rxLocaleCache[key] = mod.exports;
return mod.exports;
})
.catch(e => {
console.warn(
"[rx.plotly] Locale \\"" + locale + "\\" could not be loaded: " + e.message +
". Check https://www.npmjs.com/package/plotly.js-locales for supported codes."
);
return null;
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Path traversal + new Function eval with user-controlled locale

The PR description explicitly supports state-driven locales (locale=AppState.locale). If AppState.locale is derived from user input and is not validated, a crafted value like ../../some/path would change the fetch URL to /node_modules/plotly.js-locales/../../some/path.js. The fetched content is then passed directly to new Function("module", "exports", code) and executed — essentially running arbitrary file content as JavaScript.

Before building the URL, the locale key should be validated against a strict allowlist pattern (e.g., allowing only [a-z]{2,3} optionally followed by -[a-z]{2,4}) and rejected with a console warning if it does not match.

Comment on lines +238 to +262
const _rxLocaleCache = {};

function _rxLoadLocale(locale) {
const key = locale.toLowerCase();
if (_rxLocaleCache[key]) return Promise.resolve(_rxLocaleCache[key]);
const url = `/node_modules/plotly.js-locales/${key}.js`;
return fetch(url)
.then(r => {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.text();
})
.then(code => {
const mod = { exports: {} };
new Function("module", "exports", code)(mod, mod.exports);
_rxLocaleCache[key] = mod.exports;
return mod.exports;
})
.catch(e => {
console.warn(
"[rx.plotly] Locale \\"" + locale + "\\" could not be loaded: " + e.message +
". Check https://www.npmjs.com/package/plotly.js-locales for supported codes."
);
return null;
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 _rxLocaleCache race condition — duplicate in-flight fetches

The cache check if (_rxLocaleCache[key]) only guards against re-fetching after a promise has already resolved. If two _RxPlotLocale components with the same non-English locale mount simultaneously, both call _rxLoadLocale before either resolves, find no cache entry, and issue duplicate network requests.

Storing the Promise in the cache rather than the resolved value fixes this — any subsequent call for the same key returns the same pending promise immediately.

@pranavmanglik pranavmanglik marked this pull request as draft March 19, 2026 13:44
@pranavmanglik pranavmanglik marked this pull request as ready for review March 31, 2026 15:21
@pranavmanglik pranavmanglik marked this pull request as draft March 31, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant