feat: allow customization of HTML lang attribute #41593
feat: allow customization of HTML lang attribute #41593
Conversation
…d browser translations Add per-app htmlLang setting in App Settings > General that sets the <html lang=""> attribute on published apps via react-helmet. Includes instance-level APPSMITH_DEFAULT_HTML_LANG env var with graceful fallback to "en" when unset. Adds notranslate directives and translate="no" to the app shell, editor UI containers, and widget iframe templates to prevent browsers and extensions from auto-translating content. Closes: appsmithorg/appsmith-ee#6642 Made-with: Cursor
WalkthroughThis change adds HTML language support and translation suppression: a new htmlLang field flows through configs, APIs, types, and UI settings; templates and generated HTML include translate="no" and Google notranslate directives; AppViewer applies the lang attribute when rendering. Changes
Sequence DiagramsequenceDiagram
actor User
participant Settings as GeneralSettings UI
participant FrontendAPI as ApplicationApi (client)
participant Server as Appsmith Server (ApplicationDetail)
participant Config as Appsmith Configs
participant Viewer as AppViewer/Helmet
participant Browser as Browser
User->>Settings: Enter/save HTML language code
Settings->>FrontendAPI: updateApplication(htmlLang)
FrontendAPI->>Server: persist applicationDetail.htmlLang
Server-->>FrontendAPI: confirmation with updated details
FrontendAPI-->>Settings: update UI state
Viewer->>Config: read defaultHtmlLang
Viewer->>Server: fetch currentApplicationDetails
Server-->>Viewer: return applicationDetail (htmlLang)
Viewer->>Viewer: choose lang = applicationDetail.htmlLang || defaultHtmlLang || "en"
Viewer->>Helmet: set htmlAttributes.lang
Browser->>Browser: render <html lang="..."> and disable auto-translation (translate="no")
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx (1)
498-517: Consider adding basic BCP 47 validation.The input accepts any free-form text, but the PR objective states it "accepts BCP 47 language codes." Invalid codes won't break functionality but won't help accessibility either. A simple regex check (e.g.,
/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/) with an error message would improve UX.💡 Optional: Add basic validation
+ const BCP47_REGEX = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/; + const [isHtmlLangValid, setIsHtmlLangValid] = useState(true); + const validateHtmlLang = (value: string) => { + if (!value.trim()) return true; // Empty is valid (falls back to default) + return BCP47_REGEX.test(value.trim()); + }; <Input id="t--general-settings-app-language" + isValid={isHtmlLangValid} + errorMessage={isHtmlLangValid ? undefined : "Invalid language code (e.g., en, de, fr-CA)"} label={createMessage(GENERAL_SETTINGS_APP_LANGUAGE_LABEL)} - onBlur={() => saveHtmlLang(htmlLang)} - onChange={(value: string) => setHtmlLang(value)} + onBlur={() => { + if (validateHtmlLang(htmlLang)) saveHtmlLang(htmlLang); + }} + onChange={(value: string) => { + setHtmlLang(value); + setIsHtmlLangValid(validateHtmlLang(value)); + }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx` around lines 498 - 517, The language input currently accepts any text; add basic BCP 47 validation in the GeneralSettings component by validating htmlLang against a regex (e.g., /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/) inside the onChange/onBlur/onKeyPress handlers for the Input used for htmlLang, set a local validation error state (e.g., htmlLangError) and display a small error Text beneath the Input using createMessage(GENERAL_SETTINGS_APP_LANGUAGE_TOOLTIP) style; prevent calling saveHtmlLang(htmlLang) when the value is invalid and ensure the Input shows the error state so users get immediate feedback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/client/public/index.html`:
- Around line 2-5: The root <html> element currently hardcodes lang="en" which
delays language configuration until client-side boot; update the server-rendered
template so the root <html> element uses the APPSMITH_DEFAULT_HTML_LANG
environment value with a server-side fallback to "en" (the same source used
later in the client script), ensuring the initial HTML response advertises the
configured language; make the change where the root <html> tag is defined in
index.html and keep the existing client-side assignment (lines ~271-272) as-is
to remain consistent.
---
Nitpick comments:
In
`@app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx`:
- Around line 498-517: The language input currently accepts any text; add basic
BCP 47 validation in the GeneralSettings component by validating htmlLang
against a regex (e.g., /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/) inside the
onChange/onBlur/onKeyPress handlers for the Input used for htmlLang, set a local
validation error state (e.g., htmlLangError) and display a small error Text
beneath the Input using createMessage(GENERAL_SETTINGS_APP_LANGUAGE_TOOLTIP)
style; prevent calling saveHtmlLang(htmlLang) when the value is invalid and
ensure the Input shows the error state so users get immediate feedback.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 632ba231-4d4c-4bee-abb4-ea8c444d719a
📒 Files selected for processing (18)
app/client/public/404.htmlapp/client/public/index.htmlapp/client/src/ce/api/ApplicationApi.tsxapp/client/src/ce/configs/index.tsapp/client/src/ce/configs/types.tsapp/client/src/ce/constants/messages.tsapp/client/src/entities/Application/types.tsapp/client/src/pages/AppIDE/components/AppSettings/AppSettings.tsxapp/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsxapp/client/src/pages/AppViewer/AppViewerHtmlTitle.tsxapp/client/src/pages/AppViewer/index.tsxapp/client/src/pages/Editor/PropertyPane/PropertyPaneTab.tsxapp/client/src/widgets/CustomWidget/component/index.tsxapp/client/src/widgets/ExternalWidget/component/index.tsxapp/client/src/widgets/wds/WDSCustomWidget/component/createHtmlTemplate.tsapp/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/ApplicationDetailCE.javadeploy/docker/fs/opt/appsmith/caddy-reconfigure.mjsdeploy/docker/fs/opt/appsmith/templates/loading.html
Replace the hardcoded lang="en" with the Caddy template expression so the initial HTML response advertises the configured language before client-side React boots. Made-with: Cursor
There was a problem hiding this comment.
♻️ Duplicate comments (1)
app/client/public/index.html (1)
2-2:⚠️ Potential issue | 🟠 MajorKeep the
"en"fallback at the render site.Line 2 and Lines 271-272 still collapse to an empty value when
APPSMITH_DEFAULT_HTML_LANGis unset, so the first HTML response can shiplang=""and the client config can exposedefaultHtmlLang: "". That makes the documented"en"default depend entirely on deploy-time env injection rather than the code path that renders it.What is the correct Caddy/templates syntax to render `{{env "APPSMITH_DEFAULT_HTML_LANG"}}` with a fallback of `"en"` when the environment variable is unset?Also applies to: 271-272
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/client/public/index.html` at line 2, Replace the bare template calls to {{env "APPSMITH_DEFAULT_HTML_LANG"}} in index.html with the Caddy template form that provides a fallback value, i.e. use the env function with a default: {{env "APPSMITH_DEFAULT_HTML_LANG" "en"}}; update the lang attribute at the top (the html tag) and the client-config insertion that sets defaultHtmlLang (the occurrences around lines 271-272) so they render "en" when APPSMITH_DEFAULT_HTML_LANG is unset.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@app/client/public/index.html`:
- Line 2: Replace the bare template calls to {{env
"APPSMITH_DEFAULT_HTML_LANG"}} in index.html with the Caddy template form that
provides a fallback value, i.e. use the env function with a default: {{env
"APPSMITH_DEFAULT_HTML_LANG" "en"}}; update the lang attribute at the top (the
html tag) and the client-config insertion that sets defaultHtmlLang (the
occurrences around lines 271-272) so they render "en" when
APPSMITH_DEFAULT_HTML_LANG is unset.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b39b056d-264f-46a9-94c3-d6743d19ddef
📒 Files selected for processing (1)
app/client/public/index.html
|
/build-deploy-preview skip-tests=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/22827220661. |
|
Deploy-Preview-URL: https://ce-41593.dp.appsmith.com |
There was a problem hiding this comment.
Review comments from Claude.
app/client/public/index.html — fallback gap in the <html lang> attribute
The <html> tag on line 2 uses:
<html lang='{{env "APPSMITH_DEFAULT_HTML_LANG"}}' translate="no">The template substitution in caddy-reconfigure.mjs replaces {{env "APPSMITH_DEFAULT_HTML_LANG"}} via:
(_, name) => (process.env[name] || extraEnv[name] || "")Since extraEnv.APPSMITH_DEFAULT_HTML_LANG is set to process.env.APPSMITH_DEFAULT_HTML_LANG || "en", the extraEnv fallback should kick in when the env var isn't set — so in a Docker-based deployment, the rendered HTML will correctly get lang="en". That part is fine.
However, in non-Docker environments (e.g. running the client dev server directly, or cloud deployments where caddy-reconfigure isn't the one serving index.html), the Caddy template expression may not be processed at all, which would leave lang='' verbatim in the HTML, or the browser receiving the unprocessed template string. It would be safer to set an explicit fallback at the template level itself. If Caddy's template syntax supports a default, something like:
{{env "APPSMITH_DEFAULT_HTML_LANG" | default "en"}}
would make the fallback self-contained and not dependent on the extraEnv JavaScript logic.
Similarly, in the JavaScript config injection block (line ~272), parseConfig('{{env "APPSMITH_DEFAULT_HTML_LANG"}}') || "" will resolve to "" rather than "en" when the env var isn't set and the template isn't processed. The triple fallback in AppViewerHtmlTitle (lang || defaultHtmlLang || "en") does save you at runtime, but it means the config carries an empty string that has to be silently corrected downstream.
AppViewerHtmlTitle.tsx — module-level getAppsmithConfigs() call
const { defaultHtmlLang } = getAppsmithConfigs();This is called at module load time, outside any React component or hook. If getAppsmithConfigs() reads from a mutable config store (which it does — it merges ENV_CONFIG and APPSMITH_FEATURE_CONFIGS), this value will be captured once when the module is first imported and won't reflect any subsequent config updates. For an env-var-backed config that doesn't change at runtime this is probably fine in practice, but it's inconsistent with how the rest of the codebase typically calls getAppsmithConfigs() inside the component or a hook. Worth moving it inside the component function to be consistent and future-safe.
GeneralSettings.tsx — no validation before saving
The saveHtmlLang callback does a trimmed === current guard which is good, but there's no check on whether the trimmed value is a plausible BCP 47 code before dispatching the update. A user can type something like "not a language" or paste a long string with spaces, and it will be persisted and then applied to the <html lang> attribute. Invalid lang values don't break anything today, but they degrade accessibility tool behavior and are meaningless for the feature's stated goal.
Even a lightweight check — ensuring the value matches something like /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/ before saving, with a small inline error message — would prevent accidental invalid saves. The onBlur/Enter flow means a user can type and walk away without realizing the input was invalid.
PropertyPaneTab.tsx — translate="no" on tabs
This adds translate="no" to the StyledTabs wrapper to prevent auto-translation of tab labels like "Content" and "Style". That's a targeted and reasonable fix for the editor. One thing worth confirming: does StyledTabs (from @appsmith/ads) forward arbitrary HTML attributes to the DOM, or could this silently get swallowed? If it's a styled-component or a custom component that doesn't spread props to the underlying DOM element, the attribute won't reach the browser and the protection won't work.
ApplicationDetailCE.java — no server-side validation
htmlLang is stored as a plain String with no length limit or format constraint. Since this flows from user input all the way into the <html lang> attribute of the served page, it's worth having at least a basic server-side sanitization (trim, max length, reject obviously invalid values) even if client-side validation is added too. Defense in depth applies here.
Overall
The feature direction is good and the code is generally clean. The main things I'd want addressed before merge are the index.html fallback gap (correctness risk for non-Docker deployments) and the missing input validation on the lang setting (both client and server side). The module-level config call in AppViewerHtmlTitle is a minor consistency issue.
| @@ -1,7 +1,8 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <html lang='{{env "APPSMITH_DEFAULT_HTML_LANG"}}' translate="no"> | |||
There was a problem hiding this comment.
The template has no inline fallback value. The caddy-reconfigure.mjs provides the "en" default only during Docker deployment. Tagging @ashit-rath to validate if this concern is not valid.
There was a problem hiding this comment.
Let's add an inline fallback value if we can. Better to be safe if the option is available
Users building apps in non-English languages (e.g. German) experience corrupted UI text because browsers and translation extensions see and auto-translate page content — sometimes German-to-German, sometimes misinterpreting English UI words like "Content" as the French adjective (translated to "Thrilled").
This PR fixes the problem at three layers:
Immediate defence: Adds translate="no" and to the app shell HTML, editor UI containers (PropertyPane tabs, App Settings), and widget iframe templates. This stops browsers from offering or performing auto-translation.
Per-app language setting: Adds an "HTML Language" field to App Settings > General. App builders can enter a BCP 47 language code (e.g. de, fr, ja) that gets persisted as htmlLang on ApplicationDetail and applied to the published app's attribute at runtime via react-helmet.
Instance-level default: Adds APPSMITH_DEFAULT_HTML_LANG environment variable for self-hosted admins who want all apps on their instance to default to a specific language. Falls back gracefully to "en" when the variable is not set.
Description
Tip
Add a TL;DR when the description is longer than 500 words or extremely technical (helps the content, marketing, and DevRel team).
Please also include relevant motivation and context. List any dependencies that are required for this change. Add links to Notion, Figma or any other documents that might be relevant to the PR.
Fixes #
6642Automation
/ok-to-test tags="@tag.All"
🔍 Cypress test results
Tip
🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
Workflow run: https://github.com/appsmithorg/appsmith/actions/runs/23129680993
Commit: 535615e
Cypress dashboard.
Tags:
@tag.AllSpec:
Mon, 16 Mar 2026 07:01:57 UTC
Communication
Should the DevRel and Marketing teams inform users about this change?
Summary by CodeRabbit
New Features
Behavior