From 6f24fce3a09f825a60786b2d683d973ac2e59ba5 Mon Sep 17 00:00:00 2001 From: Stacey Levine Date: Thu, 5 Mar 2026 18:36:35 -0500 Subject: [PATCH 1/2] feat: allow customization of HTML lang attribute to prevent unintended browser translations Add per-app htmlLang setting in App Settings > General that sets the 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 --- app/client/public/404.html | 3 +- app/client/public/index.html | 5 +- app/client/src/ce/api/ApplicationApi.tsx | 1 + app/client/src/ce/configs/index.ts | 6 +++ app/client/src/ce/configs/types.ts | 1 + app/client/src/ce/constants/messages.ts | 3 ++ app/client/src/entities/Application/types.ts | 1 + .../components/AppSettings/AppSettings.tsx | 2 +- .../components/GeneralSettings.tsx | 53 ++++++++++++++++++- .../pages/AppViewer/AppViewerHtmlTitle.tsx | 9 ++-- app/client/src/pages/AppViewer/index.tsx | 1 + .../Editor/PropertyPane/PropertyPaneTab.tsx | 6 ++- .../widgets/CustomWidget/component/index.tsx | 2 +- .../ExternalWidget/component/index.tsx | 2 +- .../component/createHtmlTemplate.ts | 2 +- .../domains/ce/ApplicationDetailCE.java | 3 ++ .../fs/opt/appsmith/caddy-reconfigure.mjs | 3 +- .../fs/opt/appsmith/templates/loading.html | 3 +- 18 files changed, 92 insertions(+), 14 deletions(-) diff --git a/app/client/public/404.html b/app/client/public/404.html index 3d7109f0d3e5..0f5f7978d0e0 100644 --- a/app/client/public/404.html +++ b/app/client/public/404.html @@ -1,8 +1,9 @@ - + + diff --git a/app/client/public/index.html b/app/client/public/index.html index 101041e32833..a822fdea1fcb 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -1,7 +1,8 @@ - + + { .APPSMITH_DISABLE_IFRAME_WIDGET_SANDBOX ? process.env.APPSMITH_DISABLE_IFRAME_WIDGET_SANDBOX.length > 0 : false, + defaultHtmlLang: process.env.APPSMITH_DEFAULT_HTML_LANG || "", pricingUrl: process.env.REACT_APP_PRICING_URL || "", customerPortalUrl: process.env.REACT_APP_CUSTOMER_PORTAL_URL || "", }; @@ -255,6 +257,10 @@ export const getAppsmithConfigs = (): AppsmithUIConfigs => { ENV_CONFIG.disableIframeWidgetSandbox || APPSMITH_FEATURE_CONFIGS?.disableIframeWidgetSandbox || false, + defaultHtmlLang: + ENV_CONFIG.defaultHtmlLang || + APPSMITH_FEATURE_CONFIGS?.defaultHtmlLang || + "", pricingUrl: ENV_CONFIG.pricingUrl || APPSMITH_FEATURE_CONFIGS?.pricingUrl || "", customerPortalUrl: diff --git a/app/client/src/ce/configs/types.ts b/app/client/src/ce/configs/types.ts index 8306e7cb5a04..2c021c5ea8a1 100644 --- a/app/client/src/ce/configs/types.ts +++ b/app/client/src/ce/configs/types.ts @@ -58,6 +58,7 @@ export interface AppsmithUIConfigs { }; appsmithSupportEmail: string; disableIframeWidgetSandbox: boolean; + defaultHtmlLang: string; pricingUrl: string; customerPortalUrl: string; } diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 9a9640abeb18..544a8616f064 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -1962,6 +1962,9 @@ export const GENERAL_SETTINGS_NAME_EMPTY_MESSAGE = () => export const GENERAL_SETTINGS_NAME_SPECIAL_CHARACTER_ERROR = () => "Only alphanumeric or '-()' are allowed"; export const GENERAL_SETTINGS_APP_ICON_LABEL = () => "App icon"; +export const GENERAL_SETTINGS_APP_LANGUAGE_LABEL = () => "HTML language"; +export const GENERAL_SETTINGS_APP_LANGUAGE_TOOLTIP = () => + "Sets the lang attribute on the published app. This tells browsers what language your content is in and prevents unwanted auto-translation. Use a BCP 47 code (e.g. en, de, fr, ja)."; export const GENERAL_SETTINGS_APP_URL_LABEL = () => "App slug"; export const GENERAL_SETTINGS_APP_URL_PLACEHOLDER = () => "app-url"; export const GENERAL_SETTINGS_APP_URL_PLACEHOLDER_FETCHING = () => diff --git a/app/client/src/entities/Application/types.ts b/app/client/src/entities/Application/types.ts index d9460656c9db..1cdd9fcc1f3c 100644 --- a/app/client/src/entities/Application/types.ts +++ b/app/client/src/entities/Application/types.ts @@ -38,6 +38,7 @@ export interface ApplicationPayload { appPositioning?: LayoutSystemTypeConfig; navigationSetting?: NavigationSetting; themeSetting?: ThemeSetting; + htmlLang?: string; }; collapseInvisibleWidgets?: boolean; evaluationVersion?: EvaluationVersion; diff --git a/app/client/src/pages/AppIDE/components/AppSettings/AppSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/AppSettings.tsx index c02a479c1508..8b3d94d418b6 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/AppSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/AppSettings.tsx @@ -206,7 +206,7 @@ function AppSettings() { DIVIDER_AND_SPACING_HEIGHT; return ( - +
{SectionHeadersConfig.map((config) => ( diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx index 9bdde85dbc28..5c1225f42c41 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx @@ -10,6 +10,8 @@ import { import type { UpdateApplicationPayload } from "ee/api/ApplicationApi"; import { GENERAL_SETTINGS_APP_ICON_LABEL, + GENERAL_SETTINGS_APP_LANGUAGE_LABEL, + GENERAL_SETTINGS_APP_LANGUAGE_TOOLTIP, GENERAL_SETTINGS_APP_NAME_LABEL, GENERAL_SETTINGS_NAME_EMPTY_MESSAGE, GENERAL_SETTINGS_APP_URL_LABEL, @@ -38,8 +40,7 @@ import { Tooltip, } from "@appsmith/ads"; import { IconSelector } from "@appsmith/ads-old"; -import React, { useCallback, useMemo, useState } from "react"; -import { useEffect } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import StaticURLConfirmationModal from "./StaticURLConfirmationModal"; import { debounce } from "lodash"; import { useDispatch, useSelector } from "react-redux"; @@ -116,6 +117,9 @@ function GeneralSettings() { const [applicationIcon, setApplicationIcon] = useState( application?.icon as AppIconName, ); + const [htmlLang, setHtmlLang] = useState( + application?.applicationDetail?.htmlLang || "", + ); const [applicationSlug, setApplicationSlug] = useState( application?.staticUrlSettings?.uniqueSlug || "", ); @@ -138,6 +142,30 @@ function GeneralSettings() { [application, application?.name, isSavingAppName], ); + useEffect( + function syncHtmlLang() { + setHtmlLang(application?.applicationDetail?.htmlLang || ""); + }, + [application?.applicationDetail?.htmlLang], + ); + + const saveHtmlLang = useCallback( + (value: string) => { + const trimmed = value.trim().toLowerCase(); + const current = application?.applicationDetail?.htmlLang || ""; + + if (trimmed === current) return; + + dispatch( + updateApplication(applicationId, { + currentApp: true, + applicationDetail: { htmlLang: trimmed }, + }), + ); + }, + [applicationId, application?.applicationDetail?.htmlLang, dispatch], + ); + useEffect( function updateApplicationSlug() { setApplicationSlug(application?.staticUrlSettings?.uniqueSlug || ""); @@ -467,6 +495,27 @@ function GeneralSettings() { /> +
+ saveHtmlLang(htmlLang)} + onChange={(value: string) => setHtmlLang(value)} + onKeyPress={(ev: React.KeyboardEvent) => { + if (ev.key === "Enter") { + saveHtmlLang(htmlLang); + } + }} + placeholder="en" + size="md" + type="text" + value={htmlLang} + /> + + {createMessage(GENERAL_SETTINGS_APP_LANGUAGE_TOOLTIP)} + +
+ {isStaticUrlFeatureEnabled && (
+ {name} {description && } diff --git a/app/client/src/pages/AppViewer/index.tsx b/app/client/src/pages/AppViewer/index.tsx index c0adb9f4d711..f270820f2d12 100644 --- a/app/client/src/pages/AppViewer/index.tsx +++ b/app/client/src/pages/AppViewer/index.tsx @@ -263,6 +263,7 @@ function AppViewer(props: Props) { )} + {props.contentComponent && Content} {props.styleComponent && Style} diff --git a/app/client/src/widgets/CustomWidget/component/index.tsx b/app/client/src/widgets/CustomWidget/component/index.tsx index 61eabe1a96b3..c32c16bfb00a 100644 --- a/app/client/src/widgets/CustomWidget/component/index.tsx +++ b/app/client/src/widgets/CustomWidget/component/index.tsx @@ -213,7 +213,7 @@ function CustomComponent(props: CustomComponentProps) { ]); const srcDoc = ` - + diff --git a/app/client/src/widgets/ExternalWidget/component/index.tsx b/app/client/src/widgets/ExternalWidget/component/index.tsx index 31541bca9a88..36040a160100 100644 --- a/app/client/src/widgets/ExternalWidget/component/index.tsx +++ b/app/client/src/widgets/ExternalWidget/component/index.tsx @@ -86,7 +86,7 @@ function ExternalComponent(props: any) { }, [props.width, props.height]); const srcDoc = ` - + diff --git a/app/client/src/widgets/wds/WDSCustomWidget/component/createHtmlTemplate.ts b/app/client/src/widgets/wds/WDSCustomWidget/component/createHtmlTemplate.ts index 3cd164c54040..47afcbd519aa 100644 --- a/app/client/src/widgets/wds/WDSCustomWidget/component/createHtmlTemplate.ts +++ b/app/client/src/widgets/wds/WDSCustomWidget/component/createHtmlTemplate.ts @@ -16,7 +16,7 @@ interface CreateHtmlTemplateProps { export const createHtmlTemplate = (props: CreateHtmlTemplateProps) => { const { cssTokens, onConsole, srcDoc } = props; - return ` + return ` diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/ApplicationDetailCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/ApplicationDetailCE.java index a76fc91ceedb..56c6d28b59be 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/ApplicationDetailCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/ApplicationDetailCE.java @@ -23,6 +23,9 @@ public class ApplicationDetailCE { @JsonView({Views.Public.class, Git.class}) Application.ThemeSetting themeSetting; + @JsonView({Views.Public.class, Git.class}) + String htmlLang; + public ApplicationDetailCE() { this.appPositioning = null; this.navigationSetting = null; diff --git a/deploy/docker/fs/opt/appsmith/caddy-reconfigure.mjs b/deploy/docker/fs/opt/appsmith/caddy-reconfigure.mjs index 4f5c94134adc..a227c44db5ae 100644 --- a/deploy/docker/fs/opt/appsmith/caddy-reconfigure.mjs +++ b/deploy/docker/fs/opt/appsmith/caddy-reconfigure.mjs @@ -229,7 +229,8 @@ function finalizeHtmlFiles() { APPSMITH_VERSION_ID: info?.version ?? "", APPSMITH_VERSION_SHA: info?.commitSha ?? "", APPSMITH_VERSION_RELEASE_DATE: info?.imageBuiltAt ?? "", - APPSMITH_HOSTNAME: process.env.HOSTNAME ?? "appsmith-0" + APPSMITH_HOSTNAME: process.env.HOSTNAME ?? "appsmith-0", + APPSMITH_DEFAULT_HTML_LANG: process.env.APPSMITH_DEFAULT_HTML_LANG || "en", } for (const file of ["index.html", "404.html"]) { diff --git a/deploy/docker/fs/opt/appsmith/templates/loading.html b/deploy/docker/fs/opt/appsmith/templates/loading.html index 1bac4bb7f390..a39e59d2cd9b 100644 --- a/deploy/docker/fs/opt/appsmith/templates/loading.html +++ b/deploy/docker/fs/opt/appsmith/templates/loading.html @@ -1,7 +1,8 @@ - + + Date: Sun, 8 Mar 2026 13:04:38 -0400 Subject: [PATCH 2/2] fix: use server-side template for HTML lang attribute in index.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 --- app/client/public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/public/index.html b/app/client/public/index.html index a822fdea1fcb..2cc6dc4f278b 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -1,5 +1,5 @@ - +