mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-27 10:18:52 +08:00
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:
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 }),
|
||||
},
|
||||
|
||||
@ -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 });
|
||||
},
|
||||
}));
|
||||
|
||||
@ -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> };
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user