feat(table): locale-aware CSV and TSV exports - #10661
Conversation
CSV and TSV downloads need the browser decimal separator on the wire. Keep locale optional so older clients can still send only format. Co-authored-by: Cursor <cursoragent@cursor.com>
CSV and TSV need a shared field and decimal separator policy so adapters can format numbers without rewriting completed text. Default-table exports stay locale-neutral unless a dialect is provided.
Pandas and Polars already wrap Narwhals, so locale decimal formatting belongs in one writer. Adapters only prepare index and nested columns.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
All contributors have signed the CLA ✍️ ✅ |
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
Pull request overview
This PR adds locale-aware numeric formatting for CSV/TSV table exports by passing a browser-resolved locale (specifically the decimal separator) from the frontend to the backend export pipeline, while keeping JSON/Parquet/Markdown locale-neutral and preserving legacy behavior when locale is omitted.
Changes:
- Introduces a shared “delimited dialect” model (field + decimal separators) and uses it for both file downloads and clipboard copy requests.
- Updates table manager implementations (default/pandas/polars/narwhals) plus the narwhals CSV utility to support locale-specific decimal separators (no thousands grouping; semicolon CSV for comma-decimal locales by default).
- Adds backend + frontend test coverage for locale resolution, dialect selection, and export output parity across CSV/TSV vs locale-neutral formats.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/_utils/test_narwhals_utils.py | Adds CSV export tests for comma and Unicode decimal separators. |
| tests/_plugins/ui/_impl/utils/test_dataframe_utils.py | Adds download-as tests covering pt-BR CSV/TSV behavior, legacy behavior, and locale-neutral JSON. |
| tests/_plugins/ui/_impl/test_table.py | Verifies locale is optional in args parsing and that table downloads honor locale for CSV/TSV only. |
| tests/_plugins/ui/_impl/tables/test_polars_table.py | Adds to_delimited_str tests for polars manager across decimal/exponent/non-finite/unicode cases. |
| tests/_plugins/ui/_impl/tables/test_pandas_table.py | Adds to_delimited_str tests for pandas manager across decimal/exponent/non-finite/unicode cases. |
| tests/_plugins/ui/_impl/tables/test_narwhals_table.py | New tests for narwhals table manager delimited exports and locale neutrality of CSV. |
| tests/_plugins/ui/_impl/tables/test_delimited.py | New tests for dialect resolution rules and locale validation. |
| tests/_plugins/ui/_impl/tables/test_default_table.py | Adds default table delimited export tests (no grouping, quoting rules, unicode decimal, neutrality). |
| tests/_plugins/ui/_impl/dataframes/test_dataframe.py | Adds dataframe UI element download tests ensuring locale-aware CSV/TSV and locale-neutral JSON/Parquet. |
| packages/openapi/src/api.ts | Regenerates/moves OpenAPI component type ordering (no functional change). |
| packages/openapi/api.yaml | Regenerates/moves OpenAPI component ordering (no functional change). |
| marimo/_utils/narwhals_utils.py | Extends dataframe_to_csv with optional dialect support and decimal separator formatting path. |
| marimo/_plugins/ui/_impl/utils/dataframe.py | Threads optional locale through download_as and unifies CSV/TSV export via delimited serialization. |
| marimo/_plugins/ui/_impl/tables/table_manager.py | Adds to_delimited_str default API to TableManager interface. |
| marimo/_plugins/ui/_impl/tables/polars_table.py | Routes delimited export through dataframe_to_csv with dialect and improves error handling for narwhals compute errors. |
| marimo/_plugins/ui/_impl/tables/pandas_table.py | Routes delimited export through narwhals CSV utility, including index-as-columns handling. |
| marimo/_plugins/ui/_impl/tables/narwhals_table.py | Implements to_delimited_str and updates CSV path to use dialect defaults. |
| marimo/_plugins/ui/_impl/tables/delimited.py | New module defining locale/dialect types, dialect resolution, and numeric formatting helpers. |
| marimo/_plugins/ui/_impl/tables/default_table.py | Implements delimited export with numeric decimal replacement using resolved dialect. |
| marimo/_plugins/ui/_impl/table.py | Extends download args with optional locale and forwards it into export pipeline. |
| marimo/_plugins/ui/_impl/dataframes/dataframe.py | Forwards optional locale into dataframe export pipeline. |
| frontend/src/components/data-table/schemas.ts | Extends RPC schema/types to accept optional locale. |
| frontend/src/components/data-table/export-locale.ts | New helper to resolve decimal separator from Intl.NumberFormat(...).formatToParts. |
| frontend/src/components/data-table/export-actions.tsx | Uses useLocale() to attach locale to CSV/TSV download/copy requests and factors request-building helpers. |
| frontend/src/components/data-table/tests/export-locale.test.ts | Adds tests for locale resolution and schema acceptance of optional locale. |
| frontend/src/components/data-table/tests/export-actions.test.tsx | Adds tests ensuring parity between download and clipboard requests (locale included only for CSV/TSV). |
| frontend/plugins.openapi.yaml | Updates generated frontend OpenAPI schema to include optional locale for download_as inputs. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
All reported issues were addressed
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Coverage Report for ./frontend
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||
Locale-aware delimited serialization bypassed Polars normalization, changed pandas index headers and Windows line endings, and rejected empty configured separators. Keep locale policy in the table layer while moving serialization primitives below it. Return structured locale validation errors without hiding unrelated export failures.
There was a problem hiding this comment.
All reported issues were addressed across 16 files (changes from recent commits).
You’re at about 92% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Structured export errors must stop file and clipboard actions before empty URLs reach the browser. Polars also needs a safe fallback for nested containers that cannot use its native string cast. Preserve the fast scalar path, serialize complex nested cells as JSON, and expose numeric narrowing through a TypeGuard.
There was a problem hiding this comment.
All reported issues were addressed across 6 files (changes from recent commits).
You’re at about 92% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Download and clipboard rows exposed the same accessible name, which let the download error regression test pass through either action. Give each action a distinct name so assistive technology and tests can select it reliably. Pandas-native CSV output also follows the platform line separator. Compare the compatibility tests with pandas output so the Windows job does not assume LF.
There was a problem hiding this comment.
5.6 Sol on Codex
I found one pandas export regression to address.
The updated frontend sends a locale for every CSV and TSV request, including en-US. That means pandas exports now use PandasTableManager.to_delimited_str. For a dataframe with an unnamed, non-default index, this method calls with_index_as_columns(), which gives the index a synthetic name.
Before this PR, pandas produces:
,value
10,1.5With this PR, the same export produces:
Index0,value
10,1.5Unnamed MultiIndex levels similarly become Index0, Index1, and so on. This changes the exported column schema even though the feature should only change field and decimal separators.
Summary
localeis optional so older clients keep today's comma/dot CSV. An explicitdownload_csv_separatorstill wins.;as the default CSV field separator. No thousands grouping.Closes #10658