YuriyKrasilnikov opened a new issue, #37789: URL: https://github.com/apache/superset/issues/37789
## Motivation Apache Superset supports UI internationalization (gettext-based `.po` files), but **user-created content** — dashboard titles, chart names, filter labels, descriptions — remains single-language. Organizations serving multilingual audiences must either: - Duplicate dashboards per language (maintenance nightmare) - Accept that users see content in a foreign language - Build custom solutions outside Superset This is a solved problem in competing BI tools: | Tool | Content Localization | Approach | |------|---------------------|----------| | **Power BI** | Native | Metadata translations in semantic model + `USERCULTURE()` DAX function | | **Looker** | Native | `.strings.json` files with LookML field translations | | **Tableau** | Workaround | Parameter + translation tables + calculated fields | | **Metabase** | Embedded only (Pro) | CSV translation dictionary | | **Superset** | **Not supported** | SIP-60 proposed but not implemented | [SIP-60](https://github.com/apache/superset/issues/13442) proposed extending i18n to charts but was not implemented. This SIP takes a different architectural approach — JSON column storage instead of parallel i18n columns — and covers the full stack: data model, API, frontend editing, embedded SDK, SQL templating, and export/import. ### What gets translated | Entity | Translatable Fields | |--------|-------------------| | Dashboard | `dashboard_title` | | Chart | `slice_name`, `description` | | Native Filter | `name` | | Chart Name Override | `sliceNameOverride` (per-dashboard) | ## Proposed Change ### Feature Flag All functionality is gated behind `ENABLE_CONTENT_LOCALIZATION` (default: `False`). When disabled, the system behaves identically to current Superset — no schema changes visible, no API changes, no frontend UI. ### Data Model A `translations` JSON column is added to `dashboards` and `slices` tables: ```python class Dashboard(Model, LocalizableMixin): dashboard_title = Column(String(500)) # default/fallback text translations = Column(JSON, nullable=True) # {"dashboard_title": {"de": "...", "fr": "..."}} class Slice(Model, LocalizableMixin): slice_name = Column(String(250)) description = Column(Text) translations = Column(JSON, nullable=True) # {"slice_name": {"de": "..."}, "description": {"de": "..."}} ``` Native filter translations are stored in `json_metadata.native_filter_configuration[].translations`. `LocalizableMixin` provides `get_localized()`, `set_translation()`, and `get_available_locales()` methods with a three-step fallback chain: 1. Exact locale match (`pt-BR`) 2. Base language (`pt`) 3. Original column value ### API Two response modes controlled by `?include_translations=true`: **Default mode** — returns localized values based on user's locale: ``` GET /api/v1/dashboard/123 Accept-Language: de → {"dashboard_title": "Verkaufs-Dashboard", "available_locales": ["de", "fr"]} ``` **Editor mode** — returns original values + full translations dict: ``` GET /api/v1/dashboard/123?include_translations=true → {"dashboard_title": "Sales Dashboard", "translations": {"dashboard_title": {"de": "Verkaufs-Dashboard", "fr": "Tableau de bord"}}, "available_locales": ["de", "fr"]} ``` Locale detection priority: session locale → `Accept-Language` header → `BABEL_DEFAULT_LOCALE`. **Saving:** standard PUT with `translations` field: ``` PUT /api/v1/dashboard/123 {"translations": {"dashboard_title": {"de": "Verkaufs-Dashboard"}}} ``` ### Frontend: LocaleSwitcher An inline locale dropdown rendered as an Input suffix (similar to password eye icon). The user switches between DEFAULT text and per-locale translations directly in the form field. ``` ┌──────────────────────────────────────────────────────────┐ │ Sales Dashboard [🌐 ▾ 2] │ └──────────────────────────────────────────────────────────┘ │ click ▼ ┌──────────────────┐ │ ✓ DEFAULT │ │───────────────────│ │ ✓ 🇩🇪 Deutsch │ │ 🇫🇷 Français │ │ ✓ 🇪🇸 Español │ └──────────────────┘ ``` - **DEFAULT** = original column value (fallback for users without a matching translation) - **✓** = translation text is filled for this locale - **Badge (2)** = number of filled translations - **Yellow icon** = viewer's locale has no translation (attention indicator) Integrated into three editing surfaces: 1. **Dashboard Properties Modal** — Title field 2. **Chart Properties Modal** — Name and Description fields 3. **Filter Configuration** — Filter name field ### Jinja Macro `{{ current_user_locale() }}` returns the viewer's resolved locale in SQL templates, enabling locale-aware queries: ```sql SELECT product_name FROM products WHERE language = '{{ current_user_locale() }}' ``` The locale value is automatically included in the cache key. ### Embedded SDK `setLocale()` method for host applications to switch embedded dashboard language dynamically: ```js const dashboard = await embedDashboard({...}); dashboard.setLocale('de'); // triggers reload with new locale ``` ### Export/Import Translations are included in YAML exports. Missing `translations` field on import sets it to `null` (backward compatible). ```yaml dashboard_title: Sales Dashboard translations: dashboard_title: de: Verkaufs-Dashboard fr: Tableau de bord des ventes ``` ### Validation and Security - **XSS prevention**: all translation values sanitized on save (HTML tags stripped) - **Locale validation**: BCP 47 and POSIX formats accepted - **Size limits**: configurable via `CONTENT_LOCALIZATION_MAX_*` settings (locales: 50, text: 10K chars, JSON: 1MB) - **Feature flag guard**: PUT with `translations` rejected when flag is disabled ## New or Changed Public Interfaces ### REST API | Endpoint | Change | |----------|--------| | `GET /api/v1/dashboard/{id}` | Returns localized field values; adds `available_locales` field | | `GET /api/v1/dashboard/{id}?include_translations=true` | Returns original values + `translations` dict | | `PUT /api/v1/dashboard/{id}` | Accepts `translations` field | | `GET /api/v1/chart/{id}` | Same as dashboard | | `PUT /api/v1/chart/{id}` | Same as dashboard | | `GET /api/v1/localization/available_locales` | **New endpoint** — returns configured locales + default | ### Models | Model | Change | |-------|--------| | `Dashboard` | Added `translations` JSON column, `LocalizableMixin` | | `Slice` | Added `translations` JSON column, `LocalizableMixin` | ### Frontend Components | Component | Description | |-----------|-------------| | `LocaleSwitcher` | Inline dropdown in Input suffix for locale switching | | `TranslationTextAreaWrapper` | Wrapper for TextArea fields (no native suffix support) | ### Configuration | Setting | Default | Description | |---------|---------|-------------| | `ENABLE_CONTENT_LOCALIZATION` | `False` | Feature flag | | `CONTENT_LOCALIZATION_MAX_LOCALES` | 50 | Max locales per entity | | `CONTENT_LOCALIZATION_MAX_TEXT_LENGTH` | 10000 | Max chars per translation | | `CONTENT_LOCALIZATION_MAX_JSON_SIZE` | 1048576 | Max JSON payload (bytes) | ### Embedded SDK New method on `EmbeddedDashboard` type: ```ts setLocale(locale: string): void ``` ### Jinja New macro: `current_user_locale(add_to_cache_keys=True) → str` ## New Dependencies **None.** The implementation uses only existing dependencies: - SQLAlchemy JSON column type - Flask-Babel (already used for UI i18n) - antd Dropdown, Badge (already in superset-frontend) ## Migration Plan and Compatibility ### Database Migration Migration `1af0da0adfec` adds a nullable `translations` JSON column to `dashboards` and `slices` tables: ```python op.add_column("dashboards", sa.Column("translations", sa.JSON(), nullable=True)) op.add_column("slices", sa.Column("translations", sa.JSON(), nullable=True)) ``` - **Forward compatible**: `NULL` = no translations, existing behavior unchanged - **Rollback**: drops the columns - **No data migration needed**: existing entities simply have `NULL` translations ### Feature Flag Behavior | Flag State | API Behavior | Frontend | PUT with translations | |------------|-------------|----------|----------------------| | `False` (default) | No localization, no `available_locales` | No LocaleSwitcher visible | **Rejected** | | `True` | Full localization pipeline | LocaleSwitcher in edit modals | Accepted + validated | ### Export/Import Compatibility - Exports with translations: backward compatible (unknown fields ignored by older Superset) - Imports without translations: `NULL` (no translations) - Imports with translations: accepted if feature flag enabled ## Rejected Alternatives ### 1. Separate Translation Table (Normalized) ```sql CREATE TABLE content_translations ( content_type VARCHAR(50), content_id INTEGER, field_name VARCHAR(50), locale VARCHAR(10), value TEXT, UNIQUE(content_type, content_id, field_name, locale) ); ``` **Rejected because:** - Requires JOINs on every read (performance impact on dashboard load) - Additional table maintenance, more complex migrations - Cannot be self-contained in export/import YAML - JSON column achieves the same with single-query reads ### 2. Parallel i18n Columns (SIP-60 approach) ```python slice_name = Column(String(250)) slice_name_i18n = Column(JSON) # {"en": "Sales", "de": "Verkauf"} ``` **Rejected because:** - Doubles the number of translatable columns in the schema - Does not scale: adding a new translatable field requires a migration - Breaks existing field references in code - This proposal uses a single `translations` column for all field translations ### 3. Modal-based Translation Editor Initial implementation used a separate `TranslationEditorModal` with per-language text fields. Replaced with inline `LocaleSwitcher` because: - Users didn't know which language they were editing in the main input - Opening a modal for each field was cumbersome - Inline switching makes the active language always visible ## Reference Implementation - **Branch:** [`feat/dashboard-content-localization`](https://github.com/apache/superset/compare/master...feat/dashboard-content-localization) - **Phases:** 9 completed (Foundation → Backend → API → Export/Import → Security → Frontend → Integration → Embedded/Caching → Documentation → E2E → UX) - **Test coverage:** ~150 unit tests + 4 Playwright E2E scenarios - **Documentation:** `docs/docs/configuration/content-localization.mdx` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
