feat(i18n): translate default node display_name/description on language change, preserve user-customized names

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
This commit is contained in:
RamGopalSrikar
2026-04-29 10:49:09 -04:00
parent a42daef661
commit a1073a5e0c
7 changed files with 96 additions and 8 deletions

View File

@ -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

View File

@ -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.

View File

@ -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<APIObjectType>(
`${getURL("ALL")}?force_refresh=true`,
);
const data = response?.data;
const raw = response?.data as Record<string, unknown>;
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();

View File

@ -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 }),
},

View File

@ -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<TypesStoreType>((set, get) => ({
set({ data: newChange });
get().setComponentFields(extractSecretFieldsFromComponents(newChange));
},
componentDisplayNames: {} as ComponentDisplayNamesType,
setComponentDisplayNames: (data: ComponentDisplayNamesType) => {
set({ componentDisplayNames: data });
},
}));

View File

@ -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<string> };
function: { errors: Array<string> };

View File

@ -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<string>;
setComponentFields: (fields: Set<string>) => void;
addComponentField: (field: string) => void;
componentDisplayNames: ComponentDisplayNamesType;
setComponentDisplayNames: (data: ComponentDisplayNamesType) => void;
};