{/if}
- {#if (valvesSpec.properties[property]?.description ?? null) !== null}
+ {#if (displaySpec.properties[property]?.description ?? null) !== null}
{@html DOMPurify.sanitize(
- marked.parse(valvesSpec.properties[property].description ?? '', { async: false })
+ marked.parse(displaySpec.properties[property].description ?? '', { async: false })
)}
{/if}
diff --git a/src/lib/components/workspace/Models/ActionsSelector.svelte b/src/lib/components/workspace/Models/ActionsSelector.svelte
index e946bf01cf..c4f4aaee05 100644
--- a/src/lib/components/workspace/Models/ActionsSelector.svelte
+++ b/src/lib/components/workspace/Models/ActionsSelector.svelte
@@ -1,4 +1,5 @@
+
+{#if loadError}
+ {$i18n.t('Failed to load valves')}
+
{/if}
+
diff --git a/src/lib/components/workspace/common/ValvesModal.svelte b/src/lib/components/workspace/common/ValvesModal.svelte
index fc75607131..f91ba38abe 100644
--- a/src/lib/components/workspace/common/ValvesModal.svelte
+++ b/src/lib/components/workspace/common/ValvesModal.svelte
@@ -2,6 +2,7 @@
import { toast } from 'svelte-sonner';
import { createEventDispatcher } from 'svelte';
import { onMount, getContext } from 'svelte';
+ import { tools, functions } from '$lib/stores';
import { addUser } from '$lib/apis/auths';
import Modal from '../../common/Modal.svelte';
@@ -38,6 +39,9 @@
export let type = 'tool';
export let id = null;
export let userValves = false;
+ export let meta = null;
+ $: resourceMeta =
+ meta ?? (type === 'tool' ? $tools : $functions)?.find((item) => item.id === id)?.meta ?? {};
let saving = false;
let loading = false;
@@ -179,7 +183,7 @@
>
{#if !loading}
-
+
{:else}
{/if}
diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts
index 475ead8fd4..38acf0d178 100644
--- a/src/lib/types/index.ts
+++ b/src/lib/types/index.ts
@@ -1,4 +1,5 @@
export type Banner = {
+ i18n?: Record;
id: string;
type: string;
title?: string;
diff --git a/src/lib/utils/localizedContent.ts b/src/lib/utils/localizedContent.ts
index 3bcdb6b60e..cf59d4fd2e 100644
--- a/src/lib/utils/localizedContent.ts
+++ b/src/lib/utils/localizedContent.ts
@@ -32,6 +32,91 @@ export const resolveLocalizedString = (
return fallback ?? '';
};
+export const resolveLocalizedResource = (resource: any, locale: string, field = 'name') => {
+ const meta = resource?.meta;
+ const key = resource?.action_id ? `actions.${resource.action_id}.${field}` : field;
+ return resolveLocalizedString(
+ resource?.[field] ?? meta?.[field] ?? (field === 'name' ? resource?.id : ''),
+ meta?.i18n,
+ locale,
+ key
+ );
+};
+
+export const resolveLocalizedFunction = (
+ item: any,
+ functions: any[] | null,
+ locale: string,
+ field = 'name'
+) => {
+ const owner = (functions ?? [])
+ .filter((fn) => item?.id === fn.id || item?.id?.startsWith(`${fn.id}.`))
+ .sort((a, b) => b.id.length - a.id.length)[0];
+ const key =
+ owner && item.id !== owner.id
+ ? `actions.${item.id.slice(owner.id.length + 1)}.${field}`
+ : field;
+ return resolveLocalizedString(item?.[field], owner?.meta?.i18n ?? item?.meta?.i18n, locale, key);
+};
+
+// This is a display-only copy: identifiers, defaults and submitted values stay untouched.
+export const localizeValvesSchema = (
+ schema: any,
+ locale: string,
+ meta: any = {},
+ prefix = 'valves'
+) => {
+ if (!schema) return schema;
+ const translate = (value: string, key: string) =>
+ resolveLocalizedString(value, meta?.i18n, locale, `${prefix}.${key}`);
+ return {
+ ...schema,
+ properties: Object.fromEntries(
+ Object.entries(schema.properties ?? {}).map(([key, value]: [string, any]) => [
+ key,
+ {
+ ...value,
+ title: translate(value.title ?? key, `${key}.title`),
+ description: translate(value.description ?? '', `${key}.description`),
+ ...(Array.isArray(value.input?.options)
+ ? {
+ input: {
+ ...value.input,
+ options: value.input.options.map((option: any) =>
+ typeof option === 'object'
+ ? {
+ ...option,
+ label: translate(
+ option.label ?? String(option.value),
+ `${key}.enum.${option.value}`
+ )
+ }
+ : { value: option, label: translate(String(option), `${key}.enum.${option}`) }
+ )
+ }
+ }
+ : {})
+ }
+ ])
+ )
+ };
+};
+
+export const valveTranslationSource = (schema: any, prefix: string): Record => {
+ const strings: Record = {};
+ for (const [key, field] of Object.entries(schema?.properties ?? {}) as [string, any][]) {
+ strings[`${prefix}.${key}.title`] = field.title ?? key;
+ if (field.description) strings[`${prefix}.${key}.description`] = field.description;
+ for (const option of field.enum ?? field.input?.options ?? []) {
+ const value = typeof option === 'object' ? option.value : option;
+ strings[`${prefix}.${key}.enum.${value}`] = String(
+ typeof option === 'object' ? (option.label ?? value) : value
+ );
+ }
+ }
+ return strings;
+};
+
export const resolveLocalizedModelName = (model: any, locale?: string | null) => {
const meta = model?.info?.meta ?? model?.meta;
const info = model?.info ?? model;
diff --git a/src/lib/utils/translationDictionary.ts b/src/lib/utils/translationDictionary.ts
new file mode 100644
index 0000000000..d9d4e1a3e6
--- /dev/null
+++ b/src/lib/utils/translationDictionary.ts
@@ -0,0 +1,25 @@
+export const placeholders = (text: string) =>
+ [
+ ...new Set(
+ Array.from(text.matchAll(/\{\{\s*-?\s*([^},]+)(?:,[^}]+)?\s*\}\}/g), (match) =>
+ match[1].trim()
+ )
+ )
+ ].sort();
+
+export const validateDictionary = (
+ value: unknown,
+ source: Record = {}
+): Record => {
+ if (!value || typeof value !== 'object' || Array.isArray(value))
+ throw new Error('Expected a JSON object of translation keys and strings.');
+ for (const [key, text] of Object.entries(value)) {
+ if (typeof text !== 'string') throw new Error(`Translation must be a string: ${key}`);
+ if (
+ text.trim() &&
+ JSON.stringify(placeholders(source[key] || key)) !== JSON.stringify(placeholders(text))
+ )
+ throw new Error(`Interpolation placeholders do not match: ${key}`);
+ }
+ return value as Record;
+};