Internationalize the default welcome page#15974
Merged
jdaugherty merged 12 commits intoJul 12, 2026
Merged
Conversation
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.
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 Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ 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 🚀 New features to boost your workflow:
|
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.
jdaugherty
reviewed
Jul 12, 2026
- 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.
jdaugherty
reviewed
Jul 12, 2026
| <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"> |
Contributor
There was a problem hiding this comment.
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.
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The application generated by grails-forge ships a language selector in its navbar, but
index.gspwas hardcoded English — switching languages changed nothing on the page.Changes
gsp/index.gsptemplate: 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.i18n/messages*.properties+ English defaults): 31 newwelcome.*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 aMessageFormatchoice, 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.:grails-forge-core:testrun 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-M3upgrade (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:
frandzh_CNlacked 32 keys each (alldefault.*scaffolding labels + alltypeMismatchmessages),pt_PTandrulacked the 22default.*labels, and every non-English bundle lacked the baretypeMismatchkey — those locales silently fell back to English. Backfilled following each file's established vocabulary and conventions.MessageFormatpatterns repaired: unescaped apostrophes in arg-bearing messages corrupted rendered output — six French validation messages (renderednestforn'est), two Italian range/size messages (swallowed their{3}/{4}bounds), nine ThaitypeMismatchmessages (showed literal{0}).pl,ru), and two labels localized after translation review (thservlet container,zh_CNprofile).GrailsDefaultPluginsSpecfeature 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)
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.welcome.filter.name/welcome.filter.nonein all 18 bundles). The Available Controllers card now mirrors the Installed plugins card (default padding, h6 title, count badge) instead of its oversizedp-md-5/h2 layout.text-bg-light(a white glare in dark mode) for adaptive neutral pills. The unusedwelcome.controllers.detectedkey is removed from all bundles.welcome.artefact.controllerskey.GrailsGspSpecpins the hero, filters, badges, and nav dropdown; full:grails-forge-core:testgreen.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)
ClassUtils.isPresent, so removing the actuator dependency can't break the page.welcome.actuatorsadded to all 18 bundles.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-appgenerates fromgrails-profilesskeletons, 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 theweb/baseskeletons, and a newGrailsGspSpecfeature pins the mirror: any future change to one side without the other fails the build.