Skip to content

feat(i18n): add Spanish locale with full EN/ES translations#9635

Open
dfliess wants to merge 11 commits into
rilldata:mainfrom
dfliess:i18n-es-locale
Open

feat(i18n): add Spanish locale with full EN/ES translations#9635
dfliess wants to merge 11 commits into
rilldata:mainfrom
dfliess:i18n-es-locale

Conversation

@dfliess

@dfliess dfliess commented Jul 1, 2026

Copy link
Copy Markdown

Adds Spanish (ES) as a second locale to Rill, building on the Paraglide scaffolding from #9570.

What's included

  1. Spanish message cataloges.json with ~1,740 keys, full parity with en.json
  2. Localized web-common — all user-facing strings in shared components (~216 files)
  3. Localized web-admin — all user-facing strings in cloud admin (~231 files)
  4. LanguageSwitcher component — dropdown in the avatar menu to switch EN/ES, with spec and fixtures
  5. Locale utilitiesdocument-locale, luxon-locale, normalize-locale, escape-html, catalog integrity test
  6. User preferred locale — persists the user's language choice server-side (preferred_locale column, migration 0096, proto field, API handler); on next login the saved locale is applied automatically

How it works

  • Locale detection: localStoragepreferredLanguagebaseLocale (web-admin); preferredLanguagebaseLocale (web-local)
  • LanguageSwitcher calls UpdateUserPreferences to save the choice
  • +layout.ts reads preferredLocale from GetCurrentUser and applies it via setLocale
  • All imports use @rilldata/web-common/lib/i18n/gen/messages (matching feat: localization support #9570's structure)

Relation to #9621

This PR supersedes #9621. Rebased on current main (which includes #9570), dropped the framework scaffold (now upstream), kept only translations + language addition as requested in the review.

Test plan

  • go build ./... passes
  • Catalog integrity tests pass (key parity, no empties, parameter match, no duplicates)
  • Manual: switch locale via LanguageSwitcher in avatar menu, verify Spanish renders

dfliess added 6 commits July 1, 2026 10:39
Add es.json with full Spanish translations for all 1,733 user-facing
strings. Update inlang settings to include "es" in the locales array.
Merge Rill's existing organizations_overview_page_title key into both
catalogs.
Replace ~1,700 hard-coded English strings across shared viewer
components, features, and layout with m.key() calls backed by the
EN/ES message catalogs.
Replace hard-coded English strings across web-admin features
(projects, alerts, reports, branches, organizations, etc.) with
m.key() calls backed by the EN/ES message catalogs.
Add LanguageSwitcher dropdown component, locale utility modules
(document-locale, luxon-locale, normalize-locale, escape-html),
i18n init barrel, and catalog integrity test suite.
Add preferred_locale column (migration 0096), proto field, Go
handlers, and frontend plumbing so users can persist their language
choice via the API. On page load the +layout.ts resolver applies
the stored preference before rendering.

Proto generated files regenerated against current main.
Add canvas_tab_group, canvas_add_widget_to_tab,
canvas_add_widget_below_tabs, and edit_publish_merge_deploy_failed
to both EN and ES catalogs. These strings were introduced on main
after the original i18n branch diverged.
Comment thread admin/database/postgres/migrations/0096.sql Outdated
Comment thread web-common/src/components/i18n/LanguageSwitcher.svelte Outdated
Comment thread web-common/src/lib/i18n/locale-utils.ts
Comment thread web-common/src/lib/i18n/__tests__/catalog-integrity.spec.ts Outdated
dfliess added 4 commits July 2, 2026 10:23
Address review: keep the language choice in localStorage only (paraglide
localStorage strategy) instead of persisting it on the user record. Reverts
the preferred_locale column, proto field and admin handlers, and removes the
persistLocale plumbing from LanguageSwitcher/AvatarButton and the initial
locale resolver from the web-admin layout. The manual switcher is now
cloud-only: Rill Developer keeps upstream's browser-language detection and
no longer renders LanguageSwitcher.
Address review: collapse document-locale, escape-html, luxon-locale and
normalize-locale into a single locale-utils module re-exported from the
i18n index. normalize-locale is dropped entirely: its only consumer was
the removed backend preference resolver.
Address review: move the catalog checks from a vitest spec into
scripts/i18n-guard.js and generalize them to every file under messages/:
union of keys across locales with per-file missing-key reporting, per-key
parameter superset validation, duplicate top-level keys, and empty texts.
Messages can be plain strings or variant arrays and variants are checked
for internal consistency (selectors, match branches and placeholders must
resolve to declared inputs/locals). Catalog errors are exact and now fail
the quality pipeline; the hardcoded-string heuristic stays warning-level
until --strict.
…ches

Address review: replace singular/plural key pairs and inline count
conditionals with paraglide variant messages (declarations/selectors/match,
backed by Intl.PluralRules). Converted: dimension filter chips, chat
thinking durations, project status (parse errors, tables/views, compute
units), relative time, user management counts (users, groups, members,
projects, invite/added notifications including a two-selector variant),
env var key errors, canvas color palette, pivot drag labels and pivot
error counts. This also fixes Spanish forms that were grammatically wrong
for count=1 and restores upstream singular forms the extraction had
flattened.

Also repair issues surfaced while verifying: message keys referenced but
missing from the catalogs (github connect pages, workspace onboarding,
dashboards empty state), parameter-name mismatches (billing plan renewal,
invite error notifications, logs connection error), the welcome-message
fragment, the missing second time grain, All time casing, and two
pre-existing type errors caught by tsc-with-whitelist.
@dfliess dfliess requested a review from AdityaHegde July 2, 2026 09:44

@AdityaHegde AdityaHegde left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the quick set of changes. This is a massive PR so it will take time for me to fully review. Here are a few more changes needed.

}
}
],
"users_already_member": "{emails} already a member of this organization",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Missing pluralization.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, converted to a plural variant ("is already a member" / "are already members"), and the call site passes the count now.

"status_label_status": "Status",
"status_learn_about_external_olap": "Learn about connecting external OLAP engines",
"status_learn_more": "Learn more ->",
"status_levels_selected": "{first}, +{count} other(s)",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is missing pluralization as well.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, plural variant ("+1 other" / "+N others").


$: ({ isLoading, isError, isSuccess, error } = $query);

const kindTitleMap: Record<string, () => string> = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

To ensure this doesnt go out of sync, lets do someting like,

type ProjectPageKindParam = "report" | "dashboard" | "alert";
const kindTitleMap: Record<ProjectPageKindParam, () => string> = {...}

This will throw a lint error if we add more kinds but not add maps here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, added ProjectPageKindParam and typed the map as Record<ProjectPageKindParam, () => string>, dropping the fallback so lint catches missing entries. Thanks, nice suggestion.

],
"selectors": ["countPlural"],
"match": {
"countPlural=one": "Invited {count} person as {role}",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These are changing from Successfully invited... => Invited...

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, restored "Successfully invited..." for the org-level AddUsersDialog via its own key. The project-level invite forms originally said "Invited...", so they keep the existing key.

{failedInvites.length === 1
? `Failed to invite ${failedInvites[0]}`
: `Failed to invite: ${failedInvites.join(", ")}`}
{m.users_failed_invite({ emails: failedInvites.join(", ") })}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is missing pluralization

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, plural variant matching the original forms ("Failed to invite X" / "Failed to invite: X, Y"). Also converted the sibling users_failed_add_groups and users_failed_invite_users, which had the same pattern.

{#if isPublic}
This project is currently <strong>Public</strong>. Anyone with the URL can
view this project.
{m.settings_project_visibility_public()}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Public and private cases loose highligh

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, restored the highlights (kept in the catalog value since they wrap static text).

If you cancel your plan, you'll still be able to access your account
through
<span class="font-semibold">{cycleEndFormatted}.</span>
{m.billing_cancel_plan_desc({ date: cycleEndFormatted })}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Another case, formatted end looses highlight

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, the formatted date is injected wrapped in the span (period included, as in the original).

aria-label="Project title"
>
Welcome to <span class="text-accent-primary-action">{project}</span>
<h1 class="text-4xl font-semibold text-fg-secondary">

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Project title aria-label missing from this and next change

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, restored on both headings, as a translatable key.

@@ -49,6 +53,21 @@

const { rillTime } = featureFlags;

/** Map a V1TimeGrain to its translated display name. */
function getTranslatedGrain(grain: V1TimeGrain | AvailableTimeGrain): string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

reuse translateV1TimeGrain from new-grains.ts

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, removed the local duplicate and reused translateV1TimeGrain.

Comment thread web-common/src/lib/time/config.ts Outdated
@@ -691,24 +692,40 @@ export const TIME_GRAIN: Record<V1TimeGrain, TimeGrain> = {
};

/** The default configurations for time comparisons. */
export const TIME_COMPARISON = {
export const TIME_COMPARISON: Record<
string,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This key should be TimeComparisonOption

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, keyed by TimeComparisonOption; the map covers all enum members, so the exhaustive Record compiles.

@nishantmonu51 nishantmonu51 added Type:Feature New feature request Size:XL Very large change: 2,000+ lines labels Jul 2, 2026
…sh Spanish catalog

Review feedback:
- Convert remaining count-based messages to plural variants
  (status_levels_selected, users_already_member, users_failed_invite,
  users_failed_add_groups, users_failed_invite_users, groups_total_count)
- Restore texts changed during extraction (Enter a search term,
  Successfully invited..., lowercase user/users descriptions)
- Restore lost ellipses, aria-labels, grain capitalization and inline
  markup, injecting escaped params via {@html} where markup wraps data
- Type kindTitleMap as Record<ProjectPageKindParam, () => string>, reuse
  translateV1TimeGrain, key TIME_COMPARISON by TimeComparisonOption, and
  use alert_edit for the Edit alert trigger

Fidelity sweep over the full diff:
- Split shared keys that flattened distinct source texts (custom range,
  download PNG, search placeholders, no-results, alert/report created-by
  metadata, branch/subpath labels, learn more, loading tables, delete org
  label, filters-only switch)
- Restore typographic apostrophes, Unicode ellipsis, exact wording, and
  the interleaved GitHub user/repo inline components in retry-auth
- Localize hardcoded Viewer role params; fix duplicated Copy in
  copy-to-clipboard tooltips

Spanish catalog:
- Fix gender/number agreement and impersonal se-constructions; align
  tu/usted with each section's dominant register
- Unify terminology: dashboard (was tablero/panel), Visualizador,
  ranking, minigrafico
- Fix mistranslations and gender-dependent billing messages
@dfliess

dfliess commented Jul 2, 2026

Copy link
Copy Markdown
Author

Thanks a lot for the thorough review, really appreciate the time given the size of this PR.

All comments are addressed in 66de2da. Since most of them were fidelity regressions (lost ellipses/markup/aria-labels, altered texts, flattened plurals), we also swept the entire diff for the same classes of issues and fixed ~35 more occurrences of the same patterns (shared keys flattening distinct source texts, typographic apostrophes, a couple of inline components left dangling outside their sentence, etc., grouped in the commit message). We also did a native-speaker pass over the Spanish catalog.

Happy to adjust anything or split changes out if it makes reviewing easier, glad to collaborate however works best for you.

@dfliess dfliess requested a review from AdityaHegde July 2, 2026 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Size:XL Very large change: 2,000+ lines Type:Feature New feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants