Skip to content

Internationalize the default welcome page#15974

Merged
jdaugherty merged 12 commits into
apache:8.0.xfrom
codeconsole:feat/forge-welcome-page-i18n
Jul 12, 2026
Merged

Internationalize the default welcome page#15974
jdaugherty merged 12 commits into
apache:8.0.xfrom
codeconsole:feat/forge-welcome-page-i18n

Conversation

@codeconsole

@codeconsole codeconsole commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

The application generated by grails-forge ships a language selector in its navbar, but index.gsp was hardcoded English — switching languages changed nothing on the page.

Changes

  • gsp/index.gsp template: every user-facing string now renders through <g:message> (headings, card titles, table headers, aria-labels). Product names (Grails, Spring Boot, Groovy, JVM) stay literal.
  • All 18 bundled locales (i18n/messages*.properties + English defaults): 31 new welcome.* keys per bundle, translated for cs, da, de, es, fr, it, ja, nb, nl, pl, pt_BR, pt_PT, ru, sk, sv, th and zh_CN. The controller count renders through a MessageFormat choice, or a genitive phrasing where a language's plural rules can't be expressed as a choice (cs, pl, ru, sk).

Tests

  • GrailsGspSpec."test default index page is internationalized" — the generated page uses message codes and no hardcoded user-facing English remains.
  • GrailsDefaultPluginsSpec."test every i18n bundle translates the welcome page" — every locale bundle carries the welcome keys, with byte-intact spot checks of the Russian/Japanese/Thai/Chinese titles surviving the template pipeline.
  • Full :grails-forge-core:test run green.

All 18 locales were also verified end-to-end in a generated running application (response charset=UTF-8, strict UTF-8 byte validation, translated content matching the bundles).

Note

Correct rendering of non-Latin-1 locales also depends on the SiteMesh 3.3.0-M3 upgrade (companion PR to follow once M3 syncs to Maven Central), which fixes the response charset in the view-resolver integration. Without it, decorated pages fall back to the container's locale-derived charset.

Follow-up: bundle backfill and MessageFormat repairs (second/third commits)

Validating the bundles surfaced pre-existing i18n debt, fixed here so every locale is complete and renders correctly:

  • Missing keys backfilled: fr and zh_CN lacked 32 keys each (all default.* scaffolding labels + all typeMismatch messages), pt_PT and ru lacked the 22 default.* labels, and every non-English bundle lacked the bare typeMismatch key — those locales silently fell back to English. Backfilled following each file's established vocabulary and conventions.
  • 17 broken MessageFormat patterns repaired: unescaped apostrophes in arg-bearing messages corrupted rendered output — six French validation messages (rendered nest for n'est), two Italian range/size messages (swallowed their {3}/{4} bounds), nine Thai typeMismatch messages (showed literal {0}).
  • Two typos fixed (pl, ru), and two labels localized after translation review (th servlet container, zh_CN profile).
  • New GrailsDefaultPluginsSpec feature pins bundle integrity: every locale declares every key, and every parameterized message formats without leftover placeholders.

Follow-up: the cups return, filterable cards, and a controllers nav dropdown (fourth/fifth commits)

  • Welcome hero: the classic three-cups mark is back as the page's signature — a contained charcoal panel holding the welcome copy, the full mark inlined as a theme-adaptive SVG with a warm halo in the brand accent, one load animation (disabled under prefers-reduced-motion), cups receding to a faint backdrop on mobile. With the copy in the hero, Runtime versions joins the other status cards as a uniform four-card row.
  • Filterable controllers/plugins: both cards gain a funnel dropdown with a live name filter (auto-focus, engaged-state toggle, collapsing namespace groups, localized empty states — welcome.filter.name/welcome.filter.none in all 18 bundles). The Available Controllers card now mirrors the Installed plugins card (default padding, h6 title, count badge) instead of its oversized p-md-5/h2 layout.
  • Theme fix: count badges swap hardcoded text-bg-light (a white glare in dark mode) for adaptive neutral pills. The unused welcome.controllers.detected key is removed from all bundles.
  • Layout navbar: a controllers dropdown on every page, linking each controller's default action, labeled with the localized welcome.artefact.controllers key.
  • GrailsGspSpec pins the hero, filters, badges, and nav dropdown; full :grails-forge-core:test green.

Follow-up: layout chrome internationalized (sixth commit)

The layout wrapping every generated page still hardcoded English — the three footer cards, the light/dark/auto theme selector, and the loading spinner — so they stayed untranslated on all pages regardless of the selected language. All now render through layout.* message codes translated in the 18 bundles (including alt text and aria-labels), with a spec asserting no user-facing English remains hardcoded in the layout.

Follow-up: actuators card and a pinned default language (seventh/eighth commits)

  • Actuators card on the welcome page (the application-status data already lives in the page's cards): lists the exposed actuator web endpoints with links under the configured management base path, below Installed plugins. Renders only when Spring Boot Actuator is present and exposes endpoints — the supplier type is resolved via ClassUtils.isPresent, so removing the actuator dependency can't break the page. welcome.actuators added to all 18 bundles.
  • The language menu pins the default language above a divider — a user who switches to a language they cannot read always has a recognizable way back.
  • The navbar stays minimal: controllers, language and theme only — pinned by GrailsGspSpec (the intermediate commit adding status/artefacts/plugins navbar dropdowns is reverted by the follow-up).

Follow-up: profile skeleton parity (tenth commit)

The profile-based create-app generates from grails-profiles skeletons, which carry their own copies of the welcome templates and had received none of this work — only forge-generated applications got the new page. All 22 shared files (index.gsp, layout, welcome.css/js, 18 bundles) are now synced byte-identical into the web/base skeletons, and a new GrailsGspSpec feature pins the mirror: any future change to one side without the other fails the build.

The generated application ships a language selector in its navbar, but
index.gsp was hardcoded English, so switching languages changed nothing
on the page. Render every user-facing string through <g:message> and add
the welcome.* keys (31 per bundle) to all 18 bundled locales: cs, da,
de, es, fr, it, ja, nb, nl, pl, pt_BR, pt_PT, ru, sk, sv, th and zh_CN,
plus the English defaults. The controller count renders through a
MessageFormat choice (or a genitive phrasing where a language's plural
rules cannot be expressed as a choice), and product names (Grails,
Spring Boot, Groovy, JVM) stay literal.

Correct rendering of non-Latin-1 locales also depends on the SiteMesh
3.3.0-M3 upgrade, which fixes the response charset in the view-resolver
integration; without it the page falls back to the container's
locale-derived charset.
@codeconsole codeconsole marked this pull request as draft July 11, 2026 22:07
messages_th left the servlet container label in English while every
other technical term in the bundle is transliterated into Thai script;
messages_zh_CN left the profile row label untranslated while every
sibling row in the Application card is localized.
Validating the bundles surfaced long-standing gaps and defects in the
original translations:

- fr and zh_CN were missing 32 keys each (all default.* scaffolding
  labels and all typeMismatch messages), pt_PT and ru were missing the
  22 default.* labels, and every non-English bundle lacked the bare
  typeMismatch key — those locales silently fell back to English.
  Backfill them following each file's established vocabulary, register
  and date-format conventions.

- 17 messages corrupted their output at render time because an
  unescaped apostrophe inside an argument-bearing MessageFormat pattern
  starts a quoted region: six French validation messages dropped the
  apostrophe ("n'est" rendered as "nest"), two Italian range/size
  messages swallowed their {3}/{4} bounds entirely, and nine Thai
  typeMismatch messages quoted '{0}' so users saw a literal {0} instead
  of the property name. Escape the French/Italian apostrophes as '' and
  unquote the Thai placeholders.

- Fix two typos: pl "poprawnyą liczbą" and ru "Предыдушая страница".

A new GrailsDefaultPluginsSpec feature pins both properties for good:
every locale must declare every key, and every parameterized message
must format without leftover placeholders.
@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 51.0371%. Comparing base (08bd0bf) to head (e03e68c).
⚠️ Report is 30 commits behind head on 8.0.x.

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #15974        +/-   ##
==================================================
+ Coverage     49.6709%   51.0371%   +1.3662%     
- Complexity      17017      17413       +396     
==================================================
  Files            2004       2012         +8     
  Lines           93896      94202       +306     
  Branches        16448      16460        +12     
==================================================
+ Hits            46639      48078      +1439     
+ Misses          40078      38919      -1159     
- Partials         7179       7205        +26     

see 76 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The classic three-cups mark, removed with the January welcome-page
redesign, returns as the page's signature: a contained charcoal hero
holding the welcome copy, with the full cups mark inlined as an SVG
(theme-adaptive via currentColor) and a warm halo in the brand accent
color sampled from the shipped logo. The panel stays dark in both themes
so the white mark always reads; motion is a single load moment, disabled
under prefers-reduced-motion, and the cups recede to a faint backdrop on
small screens.

With the welcome copy in the hero, the Runtime versions card joins
Application, Server and Artefact counts as a uniform four-card status
row. The Available Controllers card loses its outsized padding and h2
and mirrors the Installed plugins card exactly: h6 title, count badge,
and a name-filter dropdown. Both controllers and plugins are now
filterable through a funnel button whose menu holds a search input —
live filtering, auto-focus on open, an engaged state on the toggle
while filtered, namespace groups that collapse when emptied, and
localized empty-state text (welcome.filter.name / welcome.filter.none
added to all 18 bundles).

The plugin-count badge swaps its hardcoded text-bg-light — a white glare
in dark mode — for a theme-adaptive neutral pill, and the controllers
card adopts the same badge in place of the "detected" sentence, whose
welcome.controllers.detected key is removed from all bundles rather than
shipped dead.
Every page generated with the default layout now carries a navbar
dropdown listing the application's controllers, each entry linking to
the controller's default action (namespaced controllers are prefixed
with their namespace). The label reuses the localized
welcome.artefact.controllers key, the menu scrolls past 60vh, and the
item renders only when the application has controllers.

GrailsGspSpec pins the dropdown, the welcome-page hero, the per-card
name filters, and the theme-adaptive count badges.
The layout that wraps every generated page still carried hardcoded
English: the three footer cards (Grails Guides, Documentation, Join the
Community), the light/dark/auto theme selector with its toggle label,
and the loading spinner's screen-reader text. Since the layout renders
on every page — scaffolded views included — these stayed English no
matter which language was selected.

Render all of it through new layout.* message codes, translated in all
18 bundles, including image alt text and the theme toggle's aria-label.
GrailsGspSpec pins the message codes and asserts no user-facing English
remains hardcoded in the layout.
…ult language

Bring the default layout's navbar to feature parity with established
Grails applications: an Application Status dropdown (server, host,
environment, profile and app version, the Grails/Groovy/JVM/Spring
Boot/Spring versions, and the reloading state), an Artefacts dropdown
with the artefact counts, and an Installed Plugins dropdown listing
every plugin with the same live name filter the welcome page cards use.
All labels render through localized message codes, reusing the
welcome.* vocabulary where it exists (layout.status, layout.artefacts
and layout.actuators added to all 18 bundles).

An Actuators dropdown lists the exposed actuator web endpoints, linking
each one under the configured management base path. It only renders
when Spring Boot Actuator is present and exposes endpoints, and the
supplier type is resolved via ClassUtils.isPresent so applications that
remove the actuator dependency don't break the layout.

The language menu now pins the default language above a divider,
excluded from the alphabetical list below: a user who switches to a
language they cannot read must always find a recognizable way back.

The left navigation collapses behind a toggler on small screens; the
language and theme selectors stay visible.
The status, artefacts, actuators and plugins dropdowns added to the
navbar in the previous commit duplicated data the welcome page's cards
already present — the navbar goes back to controllers, language and
theme only, with the pinned default language kept in the locale menu.

The one thing the welcome page was actually missing is an Actuators
card: it lists the exposed actuator web endpoints with links under the
configured management base path, rendered below the installed plugins.
It only appears when Spring Boot Actuator is present and exposes
endpoints, with the supplier type resolved via ClassUtils.isPresent so
applications that remove the actuator dependency don't break the page.
The unused layout.status and layout.artefacts keys are removed and
layout.actuators becomes welcome.actuators in all 18 bundles.
The runtime-versions card gains a Spring Security row, resolved
reflectively through ClassUtils.isPresent like the actuators card, so
applications without the dependency render nothing and applications
with it report the version alongside the other framework versions.
The profile-based create-app generates from grails-profiles skeletons,
which carry their own copies of the welcome templates and had not
received any of the i18n or redesign work — only forge-generated
applications got the new page. Sync index.gsp, the layout, welcome.css,
welcome.js and all 18 message bundles into the web and base skeletons,
byte-identical to the forge templates.

A new GrailsGspSpec feature pins the mirror: every shared template must
stay byte-identical between grails-forge-core's resources and the
profile skeletons, so the two generators can no longer drift apart
silently.
The pl, pt_BR and pt_PT bundles opened with personal attribution
comments naming individual translators with their e-mail addresses.
Generated applications should not ship third-party personal data in
their source, and translation credit belongs in the project history,
not in every application created from the templates. Removed from the
forge templates and the profile skeleton alike.
@codeconsole codeconsole marked this pull request as ready for review July 12, 2026 01:26
@codeconsole codeconsole requested a review from jdaugherty July 12, 2026 01:26
Comment thread grails-forge/grails-forge-core/src/main/resources/i18n/messages_sk.properties Outdated
Comment thread grails-forge/grails-forge-core/src/main/resources/gsp/main.gsp Outdated
Comment thread grails-forge/grails-forge-core/src/main/resources/gsp/index.gsp Outdated
- Drop the shadowed duplicate typeMismatch key from the Slovak bundle
  (Properties keeps only the last value) and guard every bundle against
  duplicate keys in the i18n integrity spec
- Update the stale welcome.js comment: the initial sort call was removed
  because the server already renders rows sorted by name ascending
- Pin the language dropdown to the configured default locale
  (spring.web.locale, falling back to English) instead of hardcoding it
- Link actuator endpoints against management.server.port and
  management.server.base-path when a separate management port is set
- Note in every bundle that 'Actuators' is the Spring Boot product name
  and deliberately untranslated

All changes mirrored byte-identically into the grails-profiles skeletons.
@codeconsole codeconsole requested a review from jdaugherty July 12, 2026 02:15
<p class="lead text-body-secondary">
Congratulations, you have successfully started a Grails application.
<%-- WELCOME HERO --%>
<section class="welcome-hero position-relative overflow-hidden rounded-3 p-4 p-md-5 mb-4">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

On the design front, we'll probably want more feedback. For now, since we need this to get the 8.0.0-M3 out, I'm ok merging. We will probably have to readdress if anyone disagrees.

@jdaugherty

Copy link
Copy Markdown
Contributor

As discussed, this is required for the 8.0.0-M3 stage. @jamesfredley @matrei please review the design changes later and let's re-address any concerns in a follow-up PR.

@jdaugherty jdaugherty merged commit c03073e into apache:8.0.x Jul 12, 2026
54 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants