From a1073a5e0c5e2435fc5e71bbb6ec07ab83d81aae Mon Sep 17 00:00:00 2001 From: RamGopalSrikar Date: Wed, 29 Apr 2026 10:49:09 -0400 Subject: [PATCH] feat(i18n): translate default node display_name/description on language change, preserve user-customized names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds a `component_display_names` map (all locale translations per component type) in `/api/v1/all`. `syncNodeTranslations` checks whether a node's current display_name/description appears in that set before overwriting — values in the set are defaults (safe to translate), values not in the set are user-customized (left alone). - Backend: add `build_component_display_names()` to i18n.py; append result to `/api/v1/all` response - Frontend: store map in typesStore; extract from API response before setTypes; apply set-membership check in syncNodeTranslations for display_name and description --- src/backend/base/langflow/api/v1/endpoints.py | 11 +++-- src/backend/base/langflow/utils/i18n.py | 46 +++++++++++++++++++ .../API/queries/flows/use-get-types.ts | 14 +++++- src/frontend/src/stores/flowStore.ts | 18 +++++++- src/frontend/src/stores/typesStore.ts | 6 ++- src/frontend/src/types/api/index.ts | 5 ++ src/frontend/src/types/zustand/types/index.ts | 4 +- 7 files changed, 96 insertions(+), 8 deletions(-) diff --git a/src/backend/base/langflow/api/v1/endpoints.py b/src/backend/base/langflow/api/v1/endpoints.py index 586c910a16..78e466a99f 100644 --- a/src/backend/base/langflow/api/v1/endpoints.py +++ b/src/backend/base/langflow/api/v1/endpoints.py @@ -118,16 +118,19 @@ async def get_all(request: Request): with display_names translated to the locale indicated by Accept-Language. """ from langflow.interface.components import get_and_cache_all_types_dict - from langflow.utils.i18n import translate_component_dict + from langflow.utils.i18n import build_component_display_names, translate_component_dict try: - all_types = await get_and_cache_all_types_dict(settings_service=get_settings_service()) + all_types_en = await get_and_cache_all_types_dict(settings_service=get_settings_service()) locale = getattr(request.state, "locale", "en") if locale != "en": - all_types = translate_component_dict(all_types, locale) + all_types = translate_component_dict(all_types_en, locale) + else: + all_types = all_types_en - return compress_response(all_types) + component_display_names = build_component_display_names(all_types_en) + return compress_response({**all_types, "component_display_names": component_display_names}) except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) from exc diff --git a/src/backend/base/langflow/utils/i18n.py b/src/backend/base/langflow/utils/i18n.py index 09c2c0889a..825031eb89 100644 --- a/src/backend/base/langflow/utils/i18n.py +++ b/src/backend/base/langflow/utils/i18n.py @@ -140,6 +140,52 @@ def translate_flow_notes(nodes: list[dict], locale: str) -> list[dict]: return result +def build_component_display_names(all_types_en: dict[str, Any]) -> dict[str, dict[str, list[str]]]: + """Build the set of all known translations for display_name and description per component. + + Iterates every loaded locale and looks up each component's display_name / description key + directly in the locale dicts (one key lookup per locale, not a full translate_component_dict + pass). The English value is always included as the baseline. + + Returns a dict keyed by normalized component key (e.g. "textinput") where each value is: + {"display_name": [...all locale values...], "description": [...all locale values...]} + """ + if not _translations: + _load_translations() + + result: dict[str, dict[str, set[str]]] = {} + + for category, components in all_types_en.items(): + for name, data in components.items(): + norm = normalize_component_key(name) + if norm not in result: + result[norm] = {"display_name": set(), "description": set()} + + dn_en = data.get("display_name", "") + desc_en = data.get("description", "") + + if dn_en: + key = component_field_key(norm, "display_name", dn_en) + result[norm]["display_name"].add(dn_en) + for locale_dict in _translations.values(): + val = locale_dict.get(key) + if val: + result[norm]["display_name"].add(val) + + if desc_en: + key = component_field_key(norm, "description", desc_en) + result[norm]["description"].add(desc_en) + for locale_dict in _translations.values(): + val = locale_dict.get(key) + if val: + result[norm]["description"].add(val) + + return { + k: {"display_name": list(v["display_name"]), "description": list(v["description"])} + for k, v in result.items() + } + + def translate_component_dict(all_types: dict[str, Any], locale: str) -> dict[str, Any]: """Return a copy of all_types with display_names substituted for locale. diff --git a/src/frontend/src/controllers/API/queries/flows/use-get-types.ts b/src/frontend/src/controllers/API/queries/flows/use-get-types.ts index 189f13d74f..4622968a39 100644 --- a/src/frontend/src/controllers/API/queries/flows/use-get-types.ts +++ b/src/frontend/src/controllers/API/queries/flows/use-get-types.ts @@ -7,6 +7,7 @@ import useFlowsManagerStore from "@/stores/flowsManagerStore"; import { useTypesStore } from "@/stores/typesStore"; import type { APIObjectType, + ComponentDisplayNamesType, useQueryFunctionType, } from "../../../../types/api"; import { api } from "../../api"; @@ -21,6 +22,9 @@ export const useGetTypes: useQueryFunctionType< const { query } = UseRequestProcessor(); const setLoading = useFlowsManagerStore((state) => state.setIsLoading); const setTypes = useTypesStore((state) => state.setTypes); + const setComponentDisplayNames = useTypesStore( + (state) => state.setComponentDisplayNames, + ); const getTypesFn = async (checkCache = false) => { try { @@ -34,12 +38,20 @@ export const useGetTypes: useQueryFunctionType< const response = await api.get( `${getURL("ALL")}?force_refresh=true`, ); - const data = response?.data; + const raw = response?.data as Record; + + const componentDisplayNames = + raw?.component_display_names as ComponentDisplayNamesType | undefined; + delete raw.component_display_names; + const data = raw as APIObjectType; if (!ENABLE_KNOWLEDGE_BASES) { delete data.knowledge_bases; } + if (componentDisplayNames) { + setComponentDisplayNames(componentDisplayNames); + } setTypes(data); syncNodeTranslations(); recomputeComponentsToUpdateIfNeeded(); diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index aa83679e7c..cf58597453 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -1352,7 +1352,12 @@ export function syncNodeTranslations(): void { const { nodes } = useFlowStore.getState(); if (nodes.length === 0) return; - const { data: typesData, types, templates } = useTypesStore.getState(); + const { + data: typesData, + types, + templates, + componentDisplayNames, + } = useTypesStore.getState(); // Build normalized lookup: normalize(registryKey) → registryKey // This lets us find "Prompt Template" in the registry when nodeType is "PromptTemplate". @@ -1397,6 +1402,15 @@ export function syncNodeTranslations(): void { if (!freshDef) return node; + // Determine whether display_name / description are default (safe to translate) + // or user-customized (leave alone). A value is "default" if it appears in the + // known-translations set for this component type across any supported locale. + const normKey = normalizeComponentKey(nodeType); + const knownNames = componentDisplayNames[normKey]?.display_name ?? []; + const knownDescs = componentDisplayNames[normKey]?.description ?? []; + const shouldTranslateName = knownNames.includes(node.data.node!.display_name); + const shouldTranslateDesc = knownDescs.includes(node.data.node!.description); + // Update input field display_names, info (tooltips), and placeholders const updatedTemplate = { ...node.data.node!.template }; for (const fieldName of Object.keys(updatedTemplate)) { @@ -1433,6 +1447,8 @@ export function syncNodeTranslations(): void { ...node.data, node: { ...node.data.node!, + ...(shouldTranslateName && { display_name: freshDef.display_name }), + ...(shouldTranslateDesc && { description: freshDef.description }), template: updatedTemplate, ...(updatedOutputs && { outputs: updatedOutputs }), }, diff --git a/src/frontend/src/stores/typesStore.ts b/src/frontend/src/stores/typesStore.ts index e2181855d5..3436a1be21 100644 --- a/src/frontend/src/stores/typesStore.ts +++ b/src/frontend/src/stores/typesStore.ts @@ -1,5 +1,5 @@ import { create } from "zustand"; -import type { APIDataType } from "../types/api"; +import type { APIDataType, ComponentDisplayNamesType } from "../types/api"; import type { TypesStoreType } from "../types/zustand/types"; import { extractSecretFieldsFromComponents, @@ -38,4 +38,8 @@ export const useTypesStore = create((set, get) => ({ set({ data: newChange }); get().setComponentFields(extractSecretFieldsFromComponents(newChange)); }, + componentDisplayNames: {} as ComponentDisplayNamesType, + setComponentDisplayNames: (data: ComponentDisplayNamesType) => { + set({ componentDisplayNames: data }); + }, })); diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index 8665640c75..ab974a76fe 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -14,6 +14,11 @@ export type APITemplateType = { [key: string]: InputFieldType; }; +export type ComponentDisplayNamesType = Record< + string, + { display_name: string[]; description: string[] } +>; + export type APICodeValidateType = { imports: { errors: Array }; function: { errors: Array }; diff --git a/src/frontend/src/types/zustand/types/index.ts b/src/frontend/src/types/zustand/types/index.ts index f82dc8c28c..664cee99f3 100644 --- a/src/frontend/src/types/zustand/types/index.ts +++ b/src/frontend/src/types/zustand/types/index.ts @@ -1,4 +1,4 @@ -import type { APIClassType, APIDataType } from "../../api"; +import type { APIClassType, APIDataType, ComponentDisplayNamesType } from "../../api"; export type TypesStoreType = { types: { [char: string]: string }; @@ -10,4 +10,6 @@ export type TypesStoreType = { ComponentFields: Set; setComponentFields: (fields: Set) => void; addComponentField: (field: string) => void; + componentDisplayNames: ComponentDisplayNamesType; + setComponentDisplayNames: (data: ComponentDisplayNamesType) => void; };