Skip to content

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

Draft
pranavmanglik wants to merge 4 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 4 commits intoreflex-dev:mainfrom
pranavmanglik:main

Conversation

@pranavmanglik
Copy link

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
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.

@pranavmanglik
Copy link
Author

I am a bit new to this, so if something is broken. Then please drop a comment and I will try to solve it out.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR adds a locale prop to rx.plotly and a Config.plotly_locale app-wide default so Plotly charts can render number separators, date formats, and modebar tooltips in non-English locales. The approach injects locale data inline via Plotly's config.locales prop through a thin React wrapper (_RxPlotLocale) added via add_custom_code.

Key issues found:

  • Sub-class regression (P0): _render() now unconditionally rewrites every component's element name to _RxPlotLocale, which internally hardcodes React.createElement(Plot, ...). All sub-classes (PlotlyBasic, PlotlyCartesian, PlotlyGeo, etc.) lose their lightweight dist variants and silently render with the full Plot bundle instead.
  • Missing React hook imports in sub-classes (P0): Sub-class add_imports() overrides return only their own imports without calling super(), so useState, useEffect, and React are never imported for those components — the inherited _RxPlotLocale custom code that uses them will fail at runtime.
  • Fetch path breaks in production (P1): Locale files are fetched from /node_modules/plotly.js-locales/${key}.js, which is accessible in dev mode but not after a production build. Locales will silently fall back to English in production.
  • Path traversal + new Function eval (P1): Since locale supports state variables (locale=AppState.locale), a user-controlled value could craft a path like ../../some/path to fetch and execute arbitrary local file content via new Function. A strict format allowlist should gate the URL construction.
  • if not props.get("locale") treats explicit "" as unset (P1): A developer passing locale="" to opt out of the global Config.plotly_locale would have the global value applied anyway since empty string is falsy.
  • pyi_hashes.json updates ~80 unrelated .pyi stubs — worth confirming these are a legitimate side-effect of the code generation tooling and not hiding unintended changes.

Confidence Score: 1/5

  • Not safe to merge — two P0 regressions break all Plotly sub-classes and the locale fetch will not work in production builds.
  • The _render() override unconditionally routes all sub-classes through _RxPlotLocale → Plot, silently discarding their lightweight bundle variants. Sub-classes also miss the required React hook imports. The /node_modules/ fetch path means locale support does not work at all after reflex build. Combined with a path-traversal risk for user-controlled locales, these issues block a safe merge.
  • reflex/components/plotly/plotly.py requires the most attention — specifically the _render() tag override, the sub-class add_imports() chain, the locale fetch URL strategy, and the locale key validation.

Important Files Changed

Filename Overview
reflex/components/plotly/plotly.py Core change file — adds locale prop, _RxPlotLocale JS wrapper, and global config fallback. Has critical regressions: _render() unconditionally rewrites tag to _RxPlotLocale which hardcodes Plot, breaking all sub-classes (PlotlyBasic, PlotlyCartesian, etc.); sub-classes also miss React hook imports because their add_imports() overrides don't call super. Fetch path /node_modules/... will 404 in production. Also contains a path-traversal + new Function eval risk for user-controlled locale values.
reflex/config.py Adds plotly_locale: str = "" to BaseConfig. Field is functional but lacks the inline # docstring comment used by every other field in the class, and is missing a blank separator line before the next field.
tests/components/plotly/test_plotly.py New test file with 5 tests covering the locale prop default, German/Chinese locale codes, and Config field. Tests bypass create() (no plotly dep needed) and are logically sound. Two unused imports (SimpleNamespace, patch) should be removed.
pyi_hashes.json Hash entries for ~80 .pyi files were updated, most unrelated to plotly (e.g. radix themes, el elements, layout). This suggests the .pyi stubs were regenerated as a side effect and may include changes beyond the scope of this PR. Worth confirming these are legitimately auto-generated and not hiding unintended stub changes.

Sequence Diagram

sequenceDiagram
    participant Dev as Developer (rxconfig.py)
    participant Py as plotly.py (Python)
    participant Render as _render()
    participant JS as _RxPlotLocale (JS)
    participant Fetch as fetch(/node_modules/...)
    participant Plot as Plot (react-plotly.js)

    Dev->>Py: rx.plotly(data=fig, locale="de")
    Py->>Py: create() — apply Config.plotly_locale fallback if locale not set
    Py->>Render: _render()
    Render->>Render: tag.set(name="_RxPlotLocale")
    Note over Render: ⚠ Sub-classes (PlotlyBasic etc.) also hit this path, hardcoding Plot

    Render-->>JS: JSX: <_RxPlotLocale locale="de" config={...} .../>

    JS->>JS: useState / useEffect (mount)
    JS->>Fetch: fetch(/node_modules/plotly.js-locales/de.js)
    Note over Fetch: ⚠ 404 in production — node_modules not served
    Fetch-->>JS: CJS locale module text
    JS->>JS: new Function("module","exports", code) — eval locale data
    JS->>JS: mergedConfig = { locale: "de", locales: { de: localeData }, ...config }
    JS->>Plot: React.createElement(Plot, { ...rest, config: mergedConfig })
    Plot-->>JS: Rendered chart with German formatting
Loading

Comments Outside Diff (1)

  1. reflex/components/plotly/plotly.py, line 416-422 (link)

    P0 Sub-classes missing React hook imports

    PlotlyBasic (and every other sub-class) overrides add_imports() returning only CREATE_PLOTLY_COMPONENT, without calling super().add_imports(). But the inherited add_custom_code() still emits _RxPlotLocale, which uses useState, useEffect, and React. Without those imports, the generated JS module for these sub-classes will throw ReferenceError: useState is not defined at runtime.

    The parent's React-hook imports need to be included:

    def add_imports(self) -> ImportDict | list[ImportDict]:
        return [CREATE_PLOTLY_COMPONENT, *super().add_imports()]

    This applies to PlotlyBasic, PlotlyCartesian, PlotlyGeo, PlotlyGl3d, PlotlyGl2d, PlotlyMapbox, PlotlyFinance, and PlotlyStrict.

Last reviewed commit: "feat(plotly): add lo..."

# 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
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
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
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
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
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
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
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
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