Skip to content

Comments

feat: content localization for dashboards, charts, and filters#37790

Open
YuriyKrasilnikov wants to merge 78 commits intoapache:masterfrom
YuriyKrasilnikov:feat/dashboard-content-localization
Open

feat: content localization for dashboards, charts, and filters#37790
YuriyKrasilnikov wants to merge 78 commits intoapache:masterfrom
YuriyKrasilnikov:feat/dashboard-content-localization

Conversation

@YuriyKrasilnikov
Copy link

@YuriyKrasilnikov YuriyKrasilnikov commented Feb 8, 2026

SUMMARY

Adds content localization to Apache Superset. Users can translate dashboard titles, chart names, descriptions, metric labels, axis titles, annotation names, and more directly in the UI. Viewers see content in their language automatically.

Backend:

  • translations JSON column on Dashboard and Slice models via LocalizableMixin
  • Two API response modes: localized by default, editor mode via ?include_translations=true
  • GET /api/v1/localization/available_locales endpoint
  • XSS sanitization, BCP 47 locale validation, configurable size limits
  • {{ current_user_locale() }} Jinja macro for locale-aware SQL queries
  • Export/import support with backward compatibility
  • Slice.data property includes translations for /api/v1/explore/ endpoint

Frontend — Properties Modals (globe icon next to text fields):

  • Dashboard title and description
  • Chart name and description
  • Native filter name

Frontend — Inline Editors (globe icon next to titles in edit mode):

  • Dashboard title (Header)
  • Chart title in Explore (ExploreChartHeader)
  • Chart name override on dashboards (SliceHeader)
  • Tab names on dashboards (Tab)
  • Shared hooks: useAvailableLocales (cached locale fetch), useTranslatableTitle (locale-switching logic)

Frontend — Chart Controls in Explore (globe icon inside control inputs):

  • Adhoc metric labels via LocaleSwitcher in MetricEditPopover
  • Adhoc column labels via LocaleSwitcher in ColumnSelectPopover (Custom SQL)
  • Axis titles (X/Y) via TranslatableTextControl on Timeseries, MixedTimeseries, Bubble, BoxPlot, Gantt, Histogram, Waterfall
  • BigNumber subtitle and compare suffix
  • Waterfall labels (increase, decrease, total)
  • Annotation layer names on Timeseries and MixedTimeseries

Frontend — Chart Visualizations (automatic):
Translated metric labels display in 20 chart types: Table, Pivot Table, Pie, Funnel, Gauge, Radar, Treemap, Sunburst, Bubble, Heatmap, Graph, Tree, Sankey, BoxPlot, Gantt, BigNumber (Total, Trendline, PeriodOverPeriod), Timeseries, MixedTimeseries. Column labels in Table and Pivot Table.

Embedded SDK:

  • initialLocale parameter in embedDashboard() for setting locale at initialization
  • setLocale(locale) method for dynamic locale switching

Gated behind ENABLE_CONTENT_LOCALIZATION feature flag (default: off).

Related SIP: #37789

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

BEFORE: No content localization — all dashboard/chart/filter names are single-language.

AFTER: LocaleSwitcher inline dropdown appears next to translatable fields in edit mode:

┌──────────────────────────────────────────────────────────┐
│ Sales Dashboard                            [🌐 ▾ 2]     │
└──────────────────────────────────────────────────────────┘
                                              │ click
                                              ▼
                                    ┌──────────────────┐
                                    │ ✓ DEFAULT         │
                                    │───────────────────│
                                    │ ✓ 🇩🇪 Deutsch      │
                                    │   🇫🇷 Français     │
                                    │ ✓ 🇪🇸 Español      │
                                    └──────────────────┘

TESTING INSTRUCTIONS

  1. Enable feature flag: FEATURE_FLAGS = {"ENABLE_CONTENT_LOCALIZATION": True}
  2. Configure languages in LANGUAGES dict
  3. Run superset db upgrade
  4. Properties modals: Open dashboard → Edit → ... → Edit properties → click globe in Title field → select language → type translation → Save
  5. Inline editors: In dashboard edit mode, click globe next to dashboard title, chart name, or tab name → select language → edit → Save
  6. Explore inline title: Open chart in Explore → click globe next to chart title → translate → Save
  7. Metric labels: Click metric pill → globe in popover title → translate label → Run Query → verify translated label in chart
  8. Axis titles: Customize tab → X/Y Axis Title fields have globe → translate → verify in chart
  9. Locale switching: Switch UI language → verify all translated content appears
  10. Feature flag OFF: No globe visible, PUT with translations rejected

Unit tests:

cd superset-frontend && npx jest --testPathPatterns="TranslationEditor|PropertiesModal|FiltersConfigModal|SliceHeader|ExploreChartHeader|Tab.test"

Backend tests:

pytest tests/unit_tests/localization/

ADDITIONAL INFORMATION

  • Has associated issue: SIP: Content Localization for Dashboards, Charts, and Filters
  • Required feature flags: ENABLE_CONTENT_LOCALIZATION
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided: Migration adds a nullable JSON column to dashboards and slices — near-instant on PostgreSQL (metadata-only), under 1s on MySQL. No data backfill or table scan. Zero downtime — existing rows get NULL (= no translations).
  • Introduces new feature or API
  • Removes existing feature or API

Add infrastructure for localizing user-generated content (dashboard
titles, chart names, filter labels) based on viewer's UI language.

Changes:
- Add ENABLE_CONTENT_LOCALIZATION feature flag (default: False)
- Add translations JSON column to dashboards and slices tables
- Add translations to export_fields for import/export support
- Add unit tests for feature flag and migration

The translations column stores content translations in JSON format:
{
    "field_name": {
        "locale": "translated_value"
    }
}
Implement LocalizableMixin to support localization of user-generated
content (dashboard titles, chart names) based on viewer's locale.

- Add LocalizableMixin with get_localized(), set_translation(),
  get_available_locales() methods
- Integrate mixin into Dashboard and Slice models
- Add 25 unit tests for mixin and model localization
- Fallback chain: exact locale -> base language -> original value

Example translations format:
{
  "dashboard_title": {"de": "Verkaufs-Dashboard", "fr": "Tableau de bord"}
}
Add locale detection and content localization to API responses:
- Locale detection from session, Accept-Language header, or config default
- Dashboard schema returns localized title, description, and native filter names
- Chart schema returns localized slice_name and description
- Fallback chain: exact locale → base language → original value

All changes gated behind ENABLE_CONTENT_LOCALIZATION feature flag.
Add unit tests verifying translations field works correctly in
dashboard and chart export/import operations:
- Export includes translations when present
- Export omits translations when None
- Import preserves translations
- Import without translations works (backward compatible)
- Roundtrip export→import preserves translations exactly

Update shared test fixtures with translations examples (de, fr).
Add Jinja template function for locale-aware SQL queries:
- SELECT * FROM table WHERE language = '{{ current_user_locale() }}'
- {% if current_user_locale() == 'de' %}...{% endif %}

Returns user's locale string (never None, falls back to config default).
Adds locale to cache key by default for correct per-locale caching.
Add 'translations' to OVERWRITE_INSPECT_FIELDS for concurrent editing.
When two users edit translations simultaneously, the second user sees
a diff dialog showing old vs new translations before overwriting.

Refactored getOverwriteItems to properly serialize object values,
enabling correct comparison for JSON object fields like translations.
Add REST API endpoint for TranslationEditor UI to query available
locales from LANGUAGES config:

- LocalizationRestApi with GET /available_locales
- LocaleSchema, AvailableLocalesResponseSchema for response
- get_available_locales_data() pure function for testability
- Locales sorted by code, includes default_locale
- 12 unit tests covering all business logic
Add ?include_translations query parameter to dashboard and chart GET APIs:
- Default mode: returns localized field values, excludes translations dict
- Editor mode (?include_translations=true): returns original values + full translations dict

Add feature flag validation on PUT schemas:
- DashboardPutSchema and ChartPutSchema accept translations field
- Rejects translations when ENABLE_CONTENT_LOCALIZATION=False with 400 error
- Validates via @validates_schema decorator in schema layer

Tests: 24 new tests (14 for include_translations, 10 for feature flag PUT)
Strip HTML tags from translation values on PUT to prevent XSS attacks.
Translations are stored as plain text; React escapes on render.

- Add sanitization module with sanitize_translation_value() and
  sanitize_translations() using nh3 library
- Add @post_load sanitization in DashboardPutSchema and ChartPutSchema
- Add 23 unit tests covering XSS vectors and schema integration
- Fix mypy type annotation in api.py
Validate translations dict before storage:
- Field names: string, 1-50 chars
- Locale codes: BCP 47 (pt-BR) and POSIX (pt_BR) formats
- Translation values: string, max 10000 chars
- Total unique locales: max 50 per entity
- JSON size: max 1MB

Config constants added:
- CONTENT_LOCALIZATION_MAX_LOCALES
- CONTENT_LOCALIZATION_MAX_TEXT_LENGTH
- CONTENT_LOCALIZATION_MAX_FIELD_LENGTH
- CONTENT_LOCALIZATION_MAX_JSON_SIZE

Validation runs in @validates_schema after feature flag check,
before XSS sanitization.
Implements priority chain for chart names in dashboard context:
1. Override translation (json_metadata.slice_name_overrides[uuid].translations)
2. Override name (position_json.meta.sliceNameOverride)
3. Chart translation (chart.translations["slice_name"])
4. Chart original name (chart.slice_name)

Changes:
- Add slice_name_utils.py with get_localized_chart_name() and localize_chart_names()
- Add slice_name_overrides field to DashboardJSONMetadataSchema
- Integrate _localize_chart_names() in DashboardGetResponseSchema._apply_localization()
- Add 21 unit tests for utility functions
- Add 3 integration tests for schema localization
- Add new Localization.ts with core types:
  - FieldTranslations, Translations, LocalizableEntity
  - LocaleInfo, AvailableLocalesResponse
- Update Chart.ts: add translations, available_locales to Chart and Slice
- Update Dashboard.ts: add translations, available_locales to Dashboard
- Update dashboard/types.ts: add translations to DashboardInfo and Slice
- Add GlobalOutlined icon to AntdEnhanced.tsx
- TranslationButton: button with globe icon and translation count,
  opens translation editor modal
- TranslationField: single locale row with label, input, remove button,
  used inside TranslationEditorModal
- 10 unit tests (5 TranslationButton + 5 TranslationField)
- index.ts re-exports for TranslationEditor module
- TranslationEditorModal: modal for editing per-field translations
  with add/remove language, edit values, save/cancel workflow
- Per-field sections with original value, locale inputs, language select
- Strips empty values on save, deep-copies on open, discards on cancel
- 9 unit tests covering render, edit, add, remove, save, cancel
- Re-export TranslationEditorModal and TranslatableField from index.ts
…e aria-labels

Replace data-test attributes with aria-label for accessibility.
Switch tests from getByTestId to role-based selectors (getByRole, getByDisplayValue).
…Modal

Add EnableContentLocalization to FeatureFlag enum.
Wire TranslationButton and TranslationEditorModal into the General
Information section of PropertiesModal, gated by the feature flag.
Fetch available locales from /api/v1/localization/available_locales.
Pass ?include_translations=true on dashboard GET for editor mode.
Include translations in PUT payload on save.
Add content localization support to the Explore chart properties modal,
gated by EnableContentLocalization feature flag.

When enabled:
- Fetch chart with ?include_translations=true to get original values
- Fetch available locales from /api/v1/localization/available_locales
- Show TranslationButton in General settings section
- TranslationEditorModal as sibling to StandardModal via Fragment
- Include translations in PUT payload on save
- Translatable fields: slice_name, description

Tests: 4 new (button hidden/visible, opens modal, save payload)
Fix pre-existing flaky test: toBeVisible → toBeInTheDocument for modal
header (ant-zoom-appear-prepare animation timing)
Add content localization support to native filters in
FiltersConfigModal. When ENABLE_CONTENT_LOCALIZATION flag is on,
a TranslationButton appears next to the filter name input, opening
TranslationEditorModal for managing filter name translations.

- Add translations field to Filter type in @superset-ui/core
- Add translations field to NativeFiltersFormItem interface
- Pass translations through transformFormInput in filterTransformer
- Register translations as hidden FormItem so form.validateFields()
  includes it in save pipeline
- Fetch available locales from /api/v1/localization/available_locales
- 4 new tests: button hidden/visible by flag, modal open, save payload
Add dynamic locale switching for embedded dashboards via the
Switchboard API, following the same pattern as setThemeMode.

SDK: setLocale(locale) emits 'setLocale' event to iframe.
Frontend: Switchboard handler fetches language pack, reconfigures
translation singleton, updates dayjs locale, and reloads page
to re-fetch server-side localized content.
- Add docs/docs/configuration/content-localization.mdx with full user guide
- Add Content Localization section to UPDATING.md
- Add @docs annotation to ENABLE_CONTENT_LOCALIZATION feature flag
- Add docs link to feature-flags.json entry
Add Playwright E2E tests covering dashboard translation CRUD, locale
switching, available locales endpoint, and UI integration with the
TranslationEditor modal.

New files:
- playwright/helpers/api/dashboard.ts — Dashboard API CRUD helpers
- playwright/helpers/api/localization.ts — Localization API helper
- playwright/components/modals/TranslationEditorModal.ts — Modal POM
- playwright/tests/experimental/localization/dashboard-translations.spec.ts

Changes:
- DashboardPage: add enterEditMode() and clickEditProperties() methods
- modals/index.ts: export TranslationEditorModal
- Fix TS2352 in PropertiesModal test mocks (double assertion via unknown)
- Fix locale detection priority to match implementation:
  explicit param > session locale > Accept-Language > config default
- Fix dashboard translation UI flow: requires edit mode first
- Fix API example: localization driven by session locale, not Accept-Language
…cher

Replace the modal-based translation editing workflow with an inline
locale switcher dropdown rendered as Input suffix. Users can now switch
between DEFAULT text and per-locale translations directly in the form
field without opening a separate modal.

- Add LocaleSwitcher component with antd Dropdown
- Add utils.ts with deepCopyTranslations, stripEmptyValues, countFieldTranslations
- Integrate into Dashboard PropertiesModal, Chart PropertiesModal, FiltersConfigForm
- Delete TranslationEditorModal, TranslationButton, TranslationField components
- Update unit tests and Playwright E2E tests for new selectors
…ntent-localization

# Conflicts:
#	UPDATING.md
#	superset-frontend/src/dashboard/types.ts
…docs

Remove redundant text label (e.g. "DE") from LocaleSwitcher trigger —
show only flag/globe icon + badge + caret. Update content localization
docs to reflect inline LocaleSwitcher replacing the old Translations button.
… Table charts

- Sankey: use getLocalizedMetricLabel for tooltip metric display
- Pivot Table: merge localizedMetricLabelMap into verboseMap for metric display
- Add unit tests for metric localization in both charts
Add buildLocalizedColumnLabelMap for groupbyRows and groupbyColumns,
merge into enhancedVerboseMap alongside metric labels. Column names
display as localized headers via namesMapping → displayHeaderCell.
- Timeseries: "Total" in tooltip total row
- MixedTimeseries: "zoom area"/"restore zoom" in toolbox
- BoxPlot: tooltip stat labels (Max, Mean, Median, Quartiles, etc.)
- BigNumber POP: comparison period titles (Range, Year, Month, Week)
- BigNumber Trendline: use displayLabel instead of raw metricName
- Heatmap: use localizedMetricLabel instead of colnames[2]
- Radar: pass localizedMetricLabelMap to renderNormalizedTooltip
…chart

- Import buildLocalizedMetricLabelMap for both primary and secondary metrics
- Apply localized labels in tooltip seriesName and legend formatter
- Add unit test verifying localized metric labels in legend
…and Tree charts

- BoxPlot: localize metric labels in category names (xAxis + tooltip)
- Graph: use getLocalizedMetricLabel for tooltip metric display
- Tree: use getLocalizedMetricLabel for tooltip metric display
- BoxPlot: wrap zoom area/restore zoom strings with t()
- Add unit tests for all three charts
Add TranslatableTextControl component that extends TextControl with
inline translation editing via LocaleSwitcher dropdown. When the
ENABLE_CONTENT_LOCALIZATION feature flag is active, users can provide
per-locale translations for text form fields directly in the control panel.

Add getLocalizedFormDataValue utility that resolves a translated value
from formData.translations for a given field and locale, with base
language fallback (e.g. "de-AT" falls back to "de").

Update axis title controls (x_axis_title, y_axis_title) to use
TranslatableTextControl in shared titleControls, Histogram, and
Timeseries Bar control panels.

Resolve localized axis titles in Timeseries transformProps so that
charts render translated xAxis/yAxis names based on the viewer's locale.
Resolve localized axis titles (x, primary y, secondary y) in
MixedTimeseries transformProps via getLocalizedFormDataValue, matching
the pattern established in Timeseries charts.

Update yAxisTitleSecondary control from TextControl to
TranslatableTextControl so users can provide per-locale translations
for the secondary y-axis title.
Resolve localized axis titles in Bubble transformProps via
getLocalizedFormDataValue for x_axis_label and y_axis_label controls.

Update both controls from TextControl to TranslatableTextControl so
users can provide per-locale translations for axis titles.
…d Histogram charts

Resolve translated axis titles using getLocalizedFormDataValue with
locale-based fallback (e.g. de-AT → de) in BoxPlot, Gantt, and
Histogram transformProps. Localized values replace raw formData strings
in ECharts axis name config and padding offset calculations.

All three control panels already use TranslatableTextControl for
x_axis_title / y_axis_title fields.
Localize all user-configurable text labels in the Waterfall chart:
axis titles (xAxisLabel, yAxisLabel) and series labels (increaseLabel,
decreaseLabel, totalLabel). The totalMark data sentinel is kept
separate from the localized display label to avoid corrupting the
data pipeline while showing translated text in legend, tooltip, and
x-axis formatter.

Control panel: 5 controls changed from TextControl to
TranslatableTextControl (x_axis_label, y_axis_label, increase_label,
decrease_label, total_label).
…gNumber charts

Localize user-configurable text in all three BigNumber variants:
- BigNumberTotal: subtitle with legacy subheader fallback
- BigNumberWithTrendline: subtitle + comparison suffix (shown in
  formatted subheader as "percentChange compareSuffix")
- BigNumberPeriodOverPeriod: subtitle

Control panel: subtitle shared control changed from TextControl to
TranslatableTextControl (affects all 3 variants), compare_suffix
in BigNumberWithTrendline also changed to TranslatableTextControl.
Gantt chart tooltip displayed raw metric labels (`getMetricLabel`) for
tooltip metrics. When a metric has per-locale translations, the tooltip
now shows the localized label while preserving the original label as the
data key for column lookup in `dimensionNames`.

Uses `buildLocalizedMetricLabelMap` (same pattern as Timeseries) to
build an original→localized map, then resolves display labels in the
tooltip formatter via `localizedMetricLabelMap[label] ?? label`.

Column labels in the same tooltip are unaffected — they are datasource
metadata and do not require content localization.
…d MixedTimeseries charts

Add support for localizing user-defined annotation layer names in chart
legend, tooltip, and annotation labels (Formula, Interval, Event, and
Timeseries annotation types).

Render pipeline:
- Add `translations` field to `BaseAnnotationLayer` type for per-locale
  name storage (e.g., `{ name: { de: "Umsatzziel" } }`)
- Create `getLocalizedAnnotationName()` utility that resolves localized
  names with exact locale match and base-language fallback, delegating
  to `getLocalizedFormDataValue` for DRY locale resolution
- Update all four annotation transformers (`transformFormulaAnnotation`,
  `transformIntervalAnnotation`, `transformEventAnnotation`,
  `transformTimeseriesAnnotation`) to accept optional `localizedName`
  parameter: `series.name` uses localized text for display while
  `series.id` preserves original name for stable ECharts animation keys
- Update `extractAnnotationLabels()` to accept `locale` parameter
- Update Timeseries and MixedTimeseries `transformProps` to pre-resolve
  localized annotation names and pass them through the render pipeline
- Replace O(n) tooltip annotation filtering with O(1) Set lookup using
  localized names for correct matching

Editor UI:
- Create `TranslatableNameField` component for annotation layer name
  editing with LocaleSwitcher and TranslationInput support
- Two-component architecture: outer gate checks feature flag (renders
  plain TextControl when off, no Redux dependency), inner component
  uses hooks for locale state management
- Integrate into `AnnotationLayer.tsx`: add `translations` to state,
  props, and `annotationFields` for persistence in chart params
…ions to POST/Copy schemas

Extract duplicated translations validation and XSS sanitization from
ChartPutSchema and DashboardPutSchema into a shared TranslatableSchemaMixin.

Add translations support to ChartPostSchema, DashboardPostSchema, and
DashboardCopySchema so that translations can be provided at creation
and copy time, not only on update.

All five schemas now inherit from TranslatableSchemaMixin which provides:
- translations Dict field (optional, nullable)
- @validates_schema: feature flag check + structure validation
- @post_load: XSS sanitization of translation values
…odal

Add locale switching and translation persistence to the chart save/overwrite
workflow. When content localization is enabled, users can enter translations
for the chart name directly in the Save modal.

Components:
- TranslatableSliceNameField: functional component rendering Input with
  LocaleSwitcher suffix (feature flag OFF → plain Input, no overhead)
- SaveModal: loads existing translations via ?include_translations=true,
  passes translations through updateSlice/createSlice to the API
- getSlicePayload: accepts optional translations parameter

Tests: 8 new behavioral tests (5 SaveModal + 3 getSlicePayload),
1 existing test updated for new createSlice signature.
When saving a dashboard via inline edit (overwriteDashboard), translations
stored in Redux are now included in the PUT payload. This ensures that
translations edited via PropertiesModal in apply-only mode are not lost
when the user subsequently saves the dashboard.

Changes:
- Add translations to DashboardSaveData interface and overwrite PUT body
- Bridge PropertiesModal translations through handleOnPropertiesChange
  to Redux via dashboardInfoChanged
- Add PropertiesChanges.translations field so Header receives them
- Add dashboardInfo.translations to overwriteDashboard dependency array

When translations are undefined (normal load without PropertiesModal),
they are omitted from the PUT payload, so the backend preserves existing
DB values.
When a dashboard is copied via "Save As", translations are now
carried through the full data flow: SaveModal passes translations
from dashboardInfo, saveDashboardRequest includes them in the POST
payload, and the backend DashboardDAO.copy_dashboard stores them
on the new dashboard.

If the frontend does not provide translations (e.g. PropertiesModal
was never opened), the backend defaults to copying translations
from the original dashboard, consistent with how params are copied.
Metric and column label localization is handled on the frontend
in transformProps, not in ChartEntityResponseSchema. Remove tests
that incorrectly expected backend schema to localize form_data
metric/column labels.
…on tests

Add missing getLocalizedMetricLabel and getLocalizedFormDataValue to
BigNumber test mocks. Fix ChartProps generic type in chart plugin tests
to use explicit SqlaFormData, add as-const to aggregate literals, type
tooltip access in Sankey, accept null in addToasts, and add required
formData fields in TranslatableTextControl tests.
- TranslatableTextControl: replace broken prevValueRef with useEffect
  sync, matching BoundsControl pattern (external value changes ignored)
- LocaleSwitcher: keyboard toggle now uses handleOpenChange instead of
  direct setState, so onDropdownOpenChange fires for keyboard users
- LocaleProvider: use finally instead of catch-only for setIsLoading,
  preventing stuck loading state on same-locale setLocale calls
- LocaleController: store initializeLocale promise in pendingLocaleChange
  to prevent race condition with early setLocale calls
- schema_mixin: allow translations=null to bypass feature flag check,
  since null means "no translations" and needs no validation
@YuriyKrasilnikov
Copy link
Author

Response to CodeAnt AI reviews (Feb 17 + Feb 18)

Verified all 16 findings against the actual code. 5 were real bugs and have been fixed in 90614091ba. 11 are false positives.


Fixed (5) — commit 90614091ba

1. TranslatableTextControl prevValueRef broken pattern (index.tsx:201-207)

Real bug. prevValueRef.current is updated on line 204 BEFORE the displayValue check on line 206, so prevValueRef.current !== value is always false after the ref update → displayValue always equals localValue → external value changes (undo, chart switch) are ignored.

Replaced with useEffect sync — the established Superset pattern for functional components (BoundsControl.tsx:79-81).

2. LocaleSwitcher keyboard toggle bypassing handleOpenChange (LocaleSwitcher.tsx:233)

Real bug. setDropdownOpen(prev => !prev) bypasses handleOpenChange, so onDropdownOpenChange callback never fires for keyboard users. Two consumers rely on this callback for focus management: AdhocMetricEditPopoverTitle (uses dropdownOpenRef to track state) and DndColumnSelectPopoverTitle (calls inputRef.current?.focus() on close).

One-line fix: handleOpenChange(!dropdownOpen) — same handler for mouse and keyboard, matching Superset's DrillBySubmenu pattern.

3. LocaleProvider isLoading stuck on success (LocaleProvider.tsx:122-124)

Real bug. setIsLoading(true) at line 118, but only reset in the catch path. When controller.setLocale(currentLocale) → controller early returns (line 132-134 in LocaleController) → onChange not fired → isLoading stuck true.

Changed catch to finally — the dominant Superset pattern (3 of 4 loading state examples: DrillDetailPane, SamplesPane, UploadDataModel).

4. LocaleController initializeLocale race condition (LocaleController.ts:88)

Real bug. Constructor fires this.initializeLocale(initialLocale) without storing the promise in pendingLocaleChange. If setLocale() is called during init, the guard at line 137 (if (this.pendingLocaleChange)) does nothing → two concurrent fetches race → locale can revert to initial.

One-line fix: this.pendingLocaleChange = this.initializeLocale(initialLocale) — leverages the existing dedup mechanism. Same pattern as preamble.ts storing init promises.

5. schema_mixin translations=null validation (schema_mixin.py:69)

Real bug (partial). The check if "translations" not in data misses the case where translations is explicitly null — Marshmallow passes None as the value when allow_none=True. The feature flag check at line 72 would block null when the feature is disabled, even though null means "no translations."

However, the crash claim is incorrect — validate_translations(None) handles None safely (early return at validation.py:128-129).

Fixed: if "translations" not in data or data["translations"] is None: return


False positives (11)

6. getLocalizedFormDataValue empty string truthy check (getLocalizedFormDataValue.ts:41)

Not a bug. Empty string is not a valid translation value — the entire pipeline enforces this:

  • Frontend: stripEmptyValues() (utils.ts:30-44) removes empty strings before API call
  • Backend: validate_translations() (validation.py:155) enforces min_length=1
  • Backend: sanitize_translations() filters empty values after HTML stripping

The truthy check is intentional and consistent across getLocalizedFormDataValue, getLocalizedValue, countFieldTranslations, and buildLocalizedMetricLabelMap.

7 + 13. MixedTimeseries yAxisTitleSecondary key casing (transformProps.ts:245) — flagged twice across two reviews

Not a bug. The suggestion assumes translations are keyed by snake_case (y_axis_title_secondary), but that's incorrect.

The control panel defines name: 'yAxisTitleSecondary' (camelCase) at controlPanel.tsx:486. convertKeysToCamelCase (convertKeysToCamelCase.ts:22-33) is shallow — it uses mapKeys on top-level formData keys only. The translations dict is a nested value, so its internal keys are NOT converted. Both storage (TranslatableTextControl writes to translations[control.name]) and lookup (getLocalizedFormDataValue(translations, 'yAxisTitleSecondary')) use the same camelCase key from the control's name property.

8. countFieldTranslations counting DEFAULT_LOCALE_KEY (utils.ts:53)

Not a bug. DEFAULT_LOCALE_KEY = 'default' is a UI-only sentinel used in LocaleSwitcher to represent "editing the default column value." It is never stored in the translations JSON column in the database. The database format is {field: {locale_code: value}} where locale codes are real codes like "en", "de", "ru". The default key cannot appear in the translations map — stripEmptyValues() at save time and validate_translations() at API level both operate on real locale codes only.

9 + 15. Object spread undefined — FiltersConfigForm (FiltersConfigForm.tsx:644) + DndColumnSelectPopoverTitle (DndColumnSelectPopoverTitle.tsx:160) — flagged twice across two reviews

Not a bug. Object spread with undefined is explicitly safe per ECMAScript spec. CopyDataProperties (ECMA-262, §14.18) step 3: if source is undefined or null, return target. ({ ...undefined, de: "German" }){ de: "German" }.

Pre-existing Superset code uses the same pattern: useStoredSidebarWidth.ts:30,45 (Jul 2022) — useRef<Record<string, number>>() with no initial value, then { ...widthsMapRef.current, [id]: updatedWidth } where .current can be undefined.

The nested level in both components already has an explicit guard (?.label ?? {}, ?.name ?? {}), showing awareness of the undefined case — handled where the spec does NOT provide safety (property access on undefined).

10. PropertiesModal useEffect stale dependency (PropertiesModal/index.tsx:308)

Not a bug. The effect at line 294 intentionally loads chart data once when the modal opens (on slice.slice_id change). userLocale comes from Redux state.common.locale which is set in preamble.ts at app init and does not change during a session — changing locale in Superset requires a page reload. Adding fetchChartProperties to the deps would cause unnecessary re-fetches for a value that is effectively constant. The code passes react-hooks/exhaustive-deps in CI.

11. dashboards/schemas.py delpop for guest user fields (schemas.py:273-275)

Pre-existing code — authored in commit 386d4e054 (Dec 2023), not part of this PR. The fields are always present in the serialized dict:

  • owners: SQLAlchemy relationship() (line 151 in dashboard.py) — always exists
  • changed_by_name: @property (line 260) — always returns str
  • changed_by: inherited from AuditMixin — always exists

Marshmallow @post_dump includes all declared schema fields when the source object attributes exist. KeyError is impossible. The scenario of instantiating with only=("id", "dashboard_title") doesn't happen — this schema is used exclusively by the dashboard REST API at full field set.

12. SaveModal.tsx name overwrite race condition (SaveModal.tsx:176-178)

Not a practical issue. The theoretical race window is ~300-700ms (awaiting loadDashboard + loadTabs + translations fetch). The pre-existing code in the same componentDidMount (lines 150-157, commit 386d4e054, Dec 2023) has the identical pattern for the dashboard field — setState after async fetch without loading gate.

The name field already shows the correct value from props.sliceName at mount. The API response replaces it with the original (non-localized) name from include_translations=true — this is intentional for the editor mode. chart.slice_name || this.state.newSliceName also preserves user input if the API returns empty.

14. LocaleSwitcher warning for regional locales (LocaleSwitcher.tsx:83-84)

Not a bug. The scenario requires a hyphen-format locale like de-AT, but Superset LANGUAGES config (config.py:421-440) uses base codes and underscore variants only: en, de, pt_BR, zh_TW, etc. The session["locale"] value (initialization/__init__.py:994) is always a key from LANGUAGES — never hyphen-separated.

More importantly, the proposed fix would make things worse. The LocaleSwitcher warning correctly reflects what the user actually sees: both LocaleSwitcher (exact match) and getLocalizedValue (split('-') only) behave identically for underscore-separated locales — 'pt_BR'.split('-')[0] = 'pt_BR' (no fallback). Adding base-language fallback only to LocaleSwitcher would make the warning say "translation exists" while the rendered text shows the default value.

The backend get_translation() (locale_utils.py:123) handles both separators (for sep in ("-", "_")), but this is a separate backend-frontend consistency question, not a LocaleSwitcher bug.

16. sanitize_translations(None) crash in @post_load (schema_mixin.py:91)

Not a bug. sanitize_translations() explicitly accepts None:

  1. Type signature (sanitization.py:64-66): translations: dict[str, dict[str, str]] | None
  2. Guard clause (sanitization.py:85-86): if translations is None: return None
  3. Docstring example (sanitization.py:82-83): >>> sanitize_translations(None)None
  4. Dedicated test (xss_sanitization_test.py:119-121): assert sanitize_translations(None) is None

The call chain is: Marshmallow deserializes "translations": nulldata["translations"] = None@post_load calls sanitize_translations(None) → returns Nonedata["translations"] stays None. No TypeError possible.

…ntent-localization

# Conflicts:
#	UPDATING.md
#	superset-frontend/playwright/helpers/api/chart.ts
#	superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
#	superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts
#	superset-frontend/src/explore/components/PropertiesModal/index.tsx
…Explore

Add LocaleSwitcher to all inline title editors using shared hooks:
- useAvailableLocales: cached fetch of available locales
- useTranslatableTitle: locale-switching logic for inline editors

Inline editors now support content translations:
- sliceNameOverride on dashboard chart cards
- Dashboard tab names
- Chart title in Explore header
- Dashboard title in edit mode
…eSwitcher without existing translations

Slice.data property did not include the translations field, so the
/api/v1/explore/ endpoint never returned translations for charts.
This prevented the inline LocaleSwitcher from rendering in Explore.

Also remove the translations !== undefined guard from useTranslatableTitle
so the LocaleSwitcher renders even for entities that have no translations
yet, allowing users to add the first translation.

Update content localization docs to cover all translatable content types
including inline editors, axis titles, metric/column labels, annotation
names, and the initialLocale param in Embedded SDK.
@YuriyKrasilnikov
Copy link
Author

@mistercrunch @michael-s-molina @betodealmeida @eschutho @sadpandajoe

This PR implements the complete Content Localization feature (SIP-201 #37789) — enabling translation of user-created content (chart names, dashboard titles, metric labels, axis titles, annotations) without requiring dashboard duplication per language.

The feature is fully gated behind the ENABLE_CONTENT_LOCALIZATION feature flag with zero impact on existing deployments when disabled.

Current state

The implementation spans ~20k lines across 220 files (68 commits). This volume is not practical to review as a single PR.

Proposed approach: 7 stacked PRs

I propose splitting this into a series of independently reviewable PRs, each delivering standalone value:

# Scope Depends on Value
1 Backend: models, migration, LocalizableMixin, API (available_locales, ?include_translations), XSS sanitization, validation, Jinja current_user_locale() API contract and data layer
2 Frontend core: LocaleSwitcher, useTranslatableTitle, TranslationInput, PropertiesModals (Dashboard, Chart, Native Filter), inline title editors 1 Translation management UI
3a Metric labels: buildLocalizedMetricLabelMap + 20 chart types (Table, Timeseries, Pie, Bar, Funnel, Gauge, Radar, Sunburst, Treemap, Sankey, Pivot, MixedTimeseries, BoxPlot, Graph, Tree, Gantt, BigNumber) 2 Localized metrics in visualizations
3b Column labels: buildLocalizedColumnLabelMap + Table, Pivot Table, adhoc columns 2 Localized column headers
3c Axis titles & labels: TranslatableTextControl, getLocalizedFormDataValue + axis titles (Timeseries, MixedTimeseries, Bubble, BoxPlot, Gantt, Histogram), BigNumber subtitle, Waterfall labels 2 Localized axes and chart-specific text
3d Annotations: getLocalizedAnnotationName + Timeseries, MixedTimeseries 2 Localized annotation layer names
4 Embedded SDK + E2E: setLocale(), initialLocale, reactive LocaleController, Playwright tests 2 SDK support and test coverage

PRs 3a–3d and 4 have no dependencies on each other and can be reviewed and merged in any order once PR 2 is in.

Questions

  1. Is this decomposition acceptable, or would you prefer a different grouping?
  2. Should I proceed with splitting now, or wait for SIP-201 formal approval first?
  3. Would a running demo instance be useful for evaluating the feature before code review?

The complete implementation is functional on this branch for early evaluation if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Related to the REST API dashboard Namespace | Anything related to the Dashboard doc Namespace | Anything related to documentation embedded hold:sip! i18n:general Related to translations packages plugins risk:db-migration PRs that require a DB migration size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants