feat(i18n): add Spanish locale with full EN/ES translations#9635
feat(i18n): add Spanish locale with full EN/ES translations#9635dfliess wants to merge 11 commits into
Conversation
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.
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.
AdityaHegde
left a comment
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
Missing pluralization.
There was a problem hiding this comment.
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)", |
There was a problem hiding this comment.
This is missing pluralization as well.
There was a problem hiding this comment.
Done, plural variant ("+1 other" / "+N others").
|
|
||
| $: ({ isLoading, isError, isSuccess, error } = $query); | ||
|
|
||
| const kindTitleMap: Record<string, () => string> = { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}", |
There was a problem hiding this comment.
These are changing from Successfully invited... => Invited...
There was a problem hiding this comment.
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(", ") })} |
There was a problem hiding this comment.
This is missing pluralization
There was a problem hiding this comment.
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()} |
There was a problem hiding this comment.
Public and private cases loose highligh
There was a problem hiding this comment.
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 })} |
There was a problem hiding this comment.
Another case, formatted end looses highlight
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
Project title aria-label missing from this and next change
There was a problem hiding this comment.
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 { | |||
There was a problem hiding this comment.
reuse translateV1TimeGrain from new-grains.ts
There was a problem hiding this comment.
Done, removed the local duplicate and reused translateV1TimeGrain.
| @@ -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, | |||
There was a problem hiding this comment.
This key should be TimeComparisonOption
There was a problem hiding this comment.
Done, keyed by TimeComparisonOption; the map covers all enum members, so the exhaustive Record compiles.
…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
|
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. |
Adds Spanish (ES) as a second locale to Rill, building on the Paraglide scaffolding from #9570.
What's included
es.jsonwith ~1,740 keys, full parity withen.jsondocument-locale,luxon-locale,normalize-locale,escape-html, catalog integrity testpreferred_localecolumn, migration 0096, proto field, API handler); on next login the saved locale is applied automaticallyHow it works
localStorage→preferredLanguage→baseLocale(web-admin);preferredLanguage→baseLocale(web-local)UpdateUserPreferencesto save the choice+layout.tsreadspreferredLocalefromGetCurrentUserand applies it viasetLocale@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