UNPKG

sanity-plugin-internationalized-array

Version:
1,013 lines 60.4 kB
import { languageFilter, useLanguageFilterStudioContext } from "@sanity/language-filter"; import { ArrayOfObjectsItem, MemberItemError, PatchEvent, defineDocumentFieldAction, defineField, definePlugin, insert, isDocumentSchemaType, isObjectInputProps, isSanityDocument, set, setIfMissing, unset, useClient, useFormValue, useGetFormValue, useSchema, useWorkspace } from "sanity"; import { c } from "react/compiler-runtime"; import { Box, Button, Card, Code, Flex, Grid, Label, Menu, MenuButton, MenuItem, Spinner, Stack, Text, Tooltip, useToast } from "@sanity/ui"; import { randomKey } from "@sanity/util/content"; import { createContext, createElement, use, useCallback, useContext, useDeferredValue, useEffect } from "react"; import { useDocumentPane } from "sanity/structure"; import { AddIcon } from "@sanity/icons/Add"; import get from "lodash-es/get.js"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { TranslateIcon } from "@sanity/icons/Translate"; import camelCase from "lodash-es/camelCase.js"; import upperFirst from "lodash-es/upperFirst.js"; import { WarningOutlineIcon } from "@sanity/icons/WarningOutline"; import { RemoveCircleIcon } from "@sanity/icons/RemoveCircle"; const namespace = "sanity-plugin-internationalized-array", functionKeyCache = /* @__PURE__ */ new WeakMap(), promiseCache = /* @__PURE__ */ new Map(), functionCache = /* @__PURE__ */ new Map(); function stringifyCacheKey(key) { return JSON.stringify(key); } const preloadWithKey = (fn, key) => { let keyStr = stringifyCacheKey(key); promiseCache.has(keyStr) || promiseCache.set(keyStr, fn()); }, clear = () => { promiseCache.clear(); }, peek = (selectedValue) => { let key = stringifyCacheKey([ "v1", namespace, selectedValue ]), promise = promiseCache.get(key); if (promise && promise._status === "fulfilled") return promise._value; }, createCacheKey = (selectedValue, workspaceId) => { let selectedValueHash = JSON.stringify(selectedValue); return workspaceId ? [ "v1", namespace, selectedValueHash, workspaceId ] : [ "v1", namespace, selectedValueHash ]; }, createOrGetPromise = (fn, key) => { let keyStr = stringifyCacheKey(key); if (promiseCache.has(keyStr)) return promiseCache.get(keyStr); let promise = fn(); return promiseCache.set(keyStr, promise), promise; }, getFunctionKey = (fn) => { let cachedKey = functionKeyCache.get(fn); if (cachedKey) return cachedKey; let fnStr = fn.toString(), hash = 0, maxLength = Math.min(fnStr.length, 100); for (let i = 0; i < maxLength; i++) { let char = fnStr.charCodeAt(i); hash = (hash << 5) - hash + char, hash &= hash; } let key = `anonymous_${Math.abs(hash)}`; return functionKeyCache.set(fn, key), key; }, createFunctionCacheKey = (fn, selectedValue, workspaceId) => { let functionKey = getFunctionKey(fn), selectedValueHash = JSON.stringify(selectedValue); return workspaceId ? `${functionKey}:${selectedValueHash}:${workspaceId}` : `${functionKey}:${selectedValueHash}`; }, getFunctionCache = (fn, selectedValue, workspaceId) => { let key = createFunctionCacheKey(fn, selectedValue, workspaceId); return functionCache.get(key); }, setFunctionCache = (fn, selectedValue, languages, workspaceId) => { let key = createFunctionCacheKey(fn, selectedValue, workspaceId); functionCache.set(key, languages); }, LANGUAGE_FIELD_NAME = "language", MAX_COLUMNS = { codeOnly: 5, titleOnly: 4, titleAndCode: 3 }, CONFIG_DEFAULT = { languages: [], select: {}, defaultLanguages: [], fieldTypes: [], apiVersion: "2025-10-15", buttonLocations: ["field"], buttonAddAll: !0, languageDisplay: "codeOnly", includeForDocumentType: (documentType) => documentType !== "translation.metadata", languageFilter: { documentTypes: [] } }, getDocumentsToTranslate = (value, rootPath = []) => { if (Array.isArray(value)) { let arrayRootPath = [...rootPath], internationalizedValues = value.filter((item) => { if (Array.isArray(item)) return !1; if (typeof item == "object") { let type = item?._type; return type?.startsWith("internationalizedArray") && type?.endsWith("Value"); } return !1; }); return internationalizedValues.length > 0 ? internationalizedValues.map((internationalizedValue) => Object.assign({}, internationalizedValue, { path: arrayRootPath, pathString: arrayRootPath.join(".") })) : value.length > 0 ? value.flatMap((item, index) => getDocumentsToTranslate(item, [...arrayRootPath, index])) : []; } if (typeof value == "object" && value) { let startsWithUnderscoreRegex = /^_/; return Object.keys(value).filter((key) => !key.match(startsWithUnderscoreRegex)).flatMap((item) => { let selectedValue = value[item], path = [...rootPath, item]; return getDocumentsToTranslate(selectedValue, path); }); } return []; }; /** * Formats a language label for display in buttons and field headers * based on the configured `languageDisplay` mode: * * - `'codeOnly'` -> uppercase code, e.g. `"EN"` * - `'titleOnly'` -> title as-is, e.g. `"English"` * - `'titleAndCode'` -> title with uppercase code, e.g. `"English (EN)"` * * Falls back to `title` for any unrecognized display mode. */ function getLanguageDisplay(languageDisplay, title, code) { return languageDisplay === "codeOnly" ? code.toUpperCase() : languageDisplay === "titleOnly" ? title : languageDisplay === "titleAndCode" ? `${title} (${code.toUpperCase()})` : title; } /** * Extracts a subset of values from a Sanity document based on a `select` * mapping (as configured in the plugin's `select` option). * * Each key in `select` becomes a key in the returned object, with its value * resolved from the document using the corresponding dot-path (via lodash `get`). * * Array values are filtered to remove incomplete references (objects with * `_type: 'reference'` but no `_ref`), since those represent empty reference * fields that should be ignored. * * Returns an empty object when either `select` or `document` is undefined. */ const getSelectedValue = (select, document) => { if (!select || !document) return {}; let selection = select || {}, selectedValue = {}; for (let [key, path] of Object.entries(selection)) { let value = get(document, path); Array.isArray(value) && (value = value.filter((item) => typeof item != "object" || item?._type === "reference" && "_ref" in item)), selectedValue[key] = value; } return selectedValue; }, InternationalizedArrayContext = createContext({ ...CONFIG_DEFAULT, languages: [], filteredLanguages: [] }); function useInternationalizedArrayContext() { return useContext(InternationalizedArrayContext); } function InternationalizedArrayProvider(props) { let $ = c(33), { internationalizedArray, documentType } = props, t0; $[0] === internationalizedArray.apiVersion ? t0 = $[1] : (t0 = { apiVersion: internationalizedArray.apiVersion }, $[0] = internationalizedArray.apiVersion, $[1] = t0); let client = useClient(t0), workspace = useWorkspace(), { formState } = useDocumentPane(), deferredDocument = useDeferredValue(formState?.value), t1; $[2] !== deferredDocument || $[3] !== internationalizedArray.select ? (t1 = getSelectedValue(internationalizedArray.select, deferredDocument), $[2] = deferredDocument, $[3] = internationalizedArray.select, $[4] = t1) : t1 = $[4]; let selectedValue = t1, t2; bb0: { if (workspace?.name) { t2 = workspace.name; break bb0; } let t3 = workspace?.name, t4 = workspace?.title, t5; $[5] !== t3 || $[6] !== t4 ? (t5 = JSON.stringify({ name: t3, title: t4 }), $[5] = t3, $[6] = t4, $[7] = t5) : t5 = $[7], t2 = t5; } let workspaceId = t2, t3; $[8] !== selectedValue || $[9] !== workspaceId ? (t3 = createCacheKey(selectedValue, workspaceId), $[8] = selectedValue, $[9] = workspaceId, $[10] = t3) : t3 = $[10]; let cacheKey = t3, t4; bb1: { if (Array.isArray(internationalizedArray.languages)) { t4 = null; break bb1; } let t5; $[11] !== client || $[12] !== internationalizedArray || $[13] !== selectedValue || $[14] !== workspaceId ? (t5 = async () => { if (typeof internationalizedArray.languages == "function") { let result = await internationalizedArray.languages(client, selectedValue); return setFunctionCache(internationalizedArray.languages, selectedValue, result, workspaceId), result; } return internationalizedArray.languages; }, $[11] = client, $[12] = internationalizedArray, $[13] = selectedValue, $[14] = workspaceId, $[15] = t5) : t5 = $[15]; let t6; $[16] !== cacheKey || $[17] !== t5 ? (t6 = createOrGetPromise(t5, cacheKey), $[16] = cacheKey, $[17] = t5, $[18] = t6) : t6 = $[18], t4 = t6; } let languagesPromise = t4, languages = languagesPromise ? use(languagesPromise) : internationalizedArray.languages, { selectedLanguageIds, options: languageFilterOptions } = useLanguageFilterStudioContext(), t5; $[19] !== documentType || $[20] !== languageFilterOptions.documentTypes ? (t5 = languageFilterOptions.documentTypes.includes(documentType), $[19] = documentType, $[20] = languageFilterOptions.documentTypes, $[21] = t5) : t5 = $[21]; let languageFilterEnabled = t5, t6; $[22] !== languageFilterEnabled || $[23] !== languages || $[24] !== selectedLanguageIds ? (t6 = languageFilterEnabled ? languages.filter((language) => selectedLanguageIds.includes(language.id)) : languages, $[22] = languageFilterEnabled, $[23] = languages, $[24] = selectedLanguageIds, $[25] = t6) : t6 = $[25]; let filteredLanguages = t6, t7; $[26] !== filteredLanguages || $[27] !== internationalizedArray || $[28] !== languages ? (t7 = { ...internationalizedArray, languages, filteredLanguages }, $[26] = filteredLanguages, $[27] = internationalizedArray, $[28] = languages, $[29] = t7) : t7 = $[29]; let context = t7, t8; return $[30] !== context || $[31] !== props.children ? (t8 = /* @__PURE__ */ jsx(InternationalizedArrayContext.Provider, { value: context, children: props.children }), $[30] = context, $[31] = props.children, $[32] = t8) : t8 = $[32], t8; } /** * Renders a grid of "add language" buttons — one per configured language. * * Returns `null` when the `languages` array is empty. * * Each button is disabled when: * - `readOnly` is `true`, or * - the language already exists in the current `value` array * (matched via `LANGUAGE_FIELD_NAME`). * * The button label is formatted according to the `languageDisplay` setting * from the plugin context (e.g. code-only, title-only, or both). * An `AddIcon` is shown unless there are more languages than fit in one row * and the display mode is `'codeOnly'`. */ function AddButtons(props) { let $ = c(15), { readOnly, languagesInUse, handleClick } = props, { languageDisplay, filteredLanguages: languages } = useInternationalizedArrayContext(); if (!languages.length) return null; let t0 = Math.min(languages.length, MAX_COLUMNS[languageDisplay]), t1; if ($[0] !== handleClick || $[1] !== languageDisplay || $[2] !== languages || $[3] !== languagesInUse || $[4] !== readOnly) { let t2; $[6] !== handleClick || $[7] !== languageDisplay || $[8] !== languages.length || $[9] !== languagesInUse || $[10] !== readOnly ? (t2 = (language) => { let languageTitle = getLanguageDisplay(languageDisplay, language.title, language.id); return /* @__PURE__ */ jsx(Button, { tone: "primary", mode: "ghost", fontSize: 1, "data-testid": `add-${language.id}`, disabled: readOnly || languagesInUse.includes(language.id), text: languageTitle, icon: languages.length > MAX_COLUMNS[languageDisplay] && languageDisplay === "codeOnly" ? void 0 : AddIcon, value: language.id, onClick: () => handleClick(language.id) }, language.id); }, $[6] = handleClick, $[7] = languageDisplay, $[8] = languages.length, $[9] = languagesInUse, $[10] = readOnly, $[11] = t2) : t2 = $[11], t1 = languages.map(t2), $[0] = handleClick, $[1] = languageDisplay, $[2] = languages, $[3] = languagesInUse, $[4] = readOnly, $[5] = t1; } else t1 = $[5]; let t2; return $[12] !== t0 || $[13] !== t1 ? (t2 = /* @__PURE__ */ jsx(Grid, { gridTemplateColumns: t0, gap: 2, "data-testid": "add-buttons-grid", children: t1 }), $[12] = t0, $[13] = t1, $[14] = t2) : t2 = $[14], t2; } /** * Document-level "add translation" panel that appears outside individual * internationalized array fields (when `buttonLocations` includes `'document'`). * * Renders a heading and a row of per-language buttons. When a language button * is clicked the component: * * 1. Scans the current document for all internationalized array fields * using `getDocumentsToTranslate`. * 2. Filters out fields that already contain a translation for the selected * language, and deduplicates by field path. * 3. Shows an error toast if no eligible fields remain. * 4. Creates `setIfMissing` + `insert` patches to add the new language * entry to each eligible field, dispatching them via `onChange`. * * For Portable Text and other array-based value fields, the initial value * is set to an empty array (`[]`) rather than `undefined`. */ function DocumentAddButtons() { let $ = c(13), getFormValue = useGetFormValue(), { filteredLanguages } = useInternationalizedArrayContext(), toast = useToast(), { onChange, formState } = useDocumentPane(), schema = useSchema(), t0; $[0] === schema ? t0 = $[1] : (t0 = (typeName) => { if (!typeName) return; let match = typeName.match(/^internationalizedArray(.+)Value$/); if (!match || !match[1]) return; let baseTypeName = match[1].charAt(0).toLowerCase() + match[1].slice(1), arrayBasedTypes = /* @__PURE__ */ new Set([ "body", "htmlContent", "blockContent", "portableText" ]); if (arrayBasedTypes.has(baseTypeName)) return []; let schemaType = schema.get(typeName); if (schemaType && "fields" in schemaType) { let valueField = schemaType.fields.find(_temp$2); if (valueField) { let fieldType = valueField.type; if (fieldType?.jsonType === "array" || fieldType?.name === "array" || fieldType?.type === "array" || fieldType?.of !== void 0 || fieldType?.name && arrayBasedTypes.has(fieldType.name)) return []; } } }, $[0] = schema, $[1] = t0); let getInitialValueForType = t0, t1; $[2] !== filteredLanguages || $[3] !== getFormValue || $[4] !== getInitialValueForType || $[5] !== onChange || $[6] !== toast ? (t1 = async (languageId) => { let value = getFormValue([]); if (!isSanityDocument(value)) { toast.push({ status: "error", title: "No document value found" }); return; } let documentsToTranslation = getDocumentsToTranslate(value, []), alreadyTranslated = documentsToTranslation.filter((translation) => translation?.[LANGUAGE_FIELD_NAME] === languageId), removeDuplicates = documentsToTranslation.reduce((filteredTranslations, translation_0) => (alreadyTranslated.filter((alreadyTranslation) => alreadyTranslation.pathString === translation_0.pathString).length > 0 || filteredTranslations.filter((filteredTranslation) => filteredTranslation.path === translation_0.path).length > 0 || filteredTranslations.push(translation_0), filteredTranslations), []); if (removeDuplicates.length === 0) { let language = filteredLanguages.find((l) => l.id === languageId); toast.push({ status: "warning", title: `No missing translations for ${language?.title || languageId} found.` }); return; } let patches = []; for (let toTranslate of removeDuplicates) { let path = toTranslate.path, initialValue = getInitialValueForType(toTranslate._type), ifMissing = setIfMissing([], path), insertValue = insert([{ _key: randomKey(), [LANGUAGE_FIELD_NAME]: languageId, _type: toTranslate._type, value: initialValue }], "after", [...path, -1]); patches.push(ifMissing), patches.push(insertValue); } onChange(PatchEvent.from(patches.flat())); }, $[2] = filteredLanguages, $[3] = getFormValue, $[4] = getInitialValueForType, $[5] = onChange, $[6] = toast, $[7] = t1) : t1 = $[7]; let handleDocumentButtonClick = t1, t2; $[8] === Symbol.for("react.memo_cache_sentinel") ? (t2 = /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "Add translation to internationalized fields" }) }), $[8] = t2) : t2 = $[8]; let t3 = !!formState?.readOnly, t4; $[9] === Symbol.for("react.memo_cache_sentinel") ? (t4 = [], $[9] = t4) : t4 = $[9]; let t5; return $[10] !== handleDocumentButtonClick || $[11] !== t3 ? (t5 = /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [t2, /* @__PURE__ */ jsx(AddButtons, { readOnly: t3, handleClick: handleDocumentButtonClick, languagesInUse: t4 })] }), $[10] = handleDocumentButtonClick, $[11] = t3, $[12] = t5) : t5 = $[12], t5; } function _temp$2(f) { return f.name === "value"; } /** * An input component for the root object of an internationalized array. * It renders the document add buttons if the buttonLocations include 'document'. */ function InternationalizedArrayFormInput(props) { let $ = c(7); if (props.pluginConfig?.buttonLocations?.includes("document")) { let t0; $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = /* @__PURE__ */ jsx(DocumentAddButtons, {}), $[0] = t0) : t0 = $[0]; let t1; $[1] === props ? t1 = $[2] : (t1 = props.renderDefault(props), $[1] = props, $[2] = t1); let t2; return $[3] === t1 ? t2 = $[4] : (t2 = /* @__PURE__ */ jsxs(Stack, { gap: 5, children: [t0, t1] }), $[3] = t1, $[4] = t2), t2; } let t0; return $[5] === props ? t0 = $[6] : (t0 = props.renderDefault(props), $[5] = props, $[6] = t0), t0; } /** * Returns true when a document schema contains any field (including nested * object/array item fields) whose type name starts with `internationalizedArray`. * * Traversal short-circuits on first match to avoid unnecessary work. */ function hasInternationalizedArrayField(schemaType) { return isDocumentSchemaType(schemaType) ? hasInternationalizedArrayInFields(schemaType.fields) : !1; } function hasInternationalizedArrayInFields(fields, visited = /* @__PURE__ */ new Set()) { for (let field of fields) if (!visited.has(field.type)) { if (visited.add(field.type), field.type.name.startsWith("internationalizedArray") || field.type.jsonType === "object" && hasInternationalizedArrayInFields(field.type.fields, visited)) return !0; if (field.type.jsonType === "array" && field.type.of.length > 0) for (let item of field.type.of) { if ("name" in item && typeof item.name == "string" && item.name.startsWith("internationalizedArray")) return !0; if ("fields" in item && Array.isArray(item.fields)) { if (visited.has(item)) continue; if (visited.add(item), hasInternationalizedArrayInFields(item.fields, visited)) return !0; } } } return !1; } function InternationalizedArrayLayout(props) { let $ = c(10), schemaType = useSchema().get(props.documentType); if (!schemaType) { console.error(`Schema type not found: ${props.documentType}`); let t0; return $[0] === props ? t0 = $[1] : (t0 = props.renderDefault(props), $[0] = props, $[1] = t0), t0; } if (hasInternationalizedArrayField(schemaType) && props.pluginConfig.includeForDocumentType(props.documentType)) { let t0 = props.pluginConfig, t1 = props.documentType, t2; $[2] === props ? t2 = $[3] : (t2 = props.renderDefault(props), $[2] = props, $[3] = t2); let t3; return $[4] !== props.documentType || $[5] !== props.pluginConfig || $[6] !== t2 ? (t3 = /* @__PURE__ */ jsx(InternationalizedArrayProvider, { internationalizedArray: t0, documentType: t1, children: t2 }), $[4] = props.documentType, $[5] = props.pluginConfig, $[6] = t2, $[7] = t3) : t3 = $[7], t3; } let t0; return $[8] === props ? t0 = $[9] : (t0 = props.renderDefault(props), $[8] = props, $[9] = t0), t0; } function Preload(props) { let $ = c(2), t0; $[0] === props.apiVersion ? t0 = $[1] : (t0 = { apiVersion: props.apiVersion }, $[0] = props.apiVersion, $[1] = t0); let client = useClient(t0), cacheKey = createCacheKey({}); return Array.isArray(peek({})) || preloadWithKey(async () => { if (Array.isArray(props.languages)) return props.languages; let result = await props.languages(client, {}); return setFunctionCache(props.languages, {}, result), result; }, cacheKey), null; } /** * Checks whether every language in the provided list has a corresponding entry * in the value array. It extracts language IDs from value items using * `LANGUAGE_FIELD_NAME` and compares them against the expected language IDs. * * Returns `true` only when the value array length matches the number of unique * languages and every value language ID is found in the languages list * (order does not matter). The length check ensures duplicates in value are * correctly rejected. */ function checkAllLanguagesArePresent(languages, value) { let filteredLanguageIds = new Set(languages.map((l) => l.id)), languagesInUseIds = value ? value.map((v) => v[LANGUAGE_FIELD_NAME]) : []; return languagesInUseIds.length !== filteredLanguageIds.size || new Set(languagesInUseIds).size !== languagesInUseIds.length ? !1 : languagesInUseIds.every((key) => filteredLanguageIds.has(key)); } /** * Generates the label text for the "add all / add missing languages" button. * * - When there is no existing value: returns `"Add {title} Field"` for a * single language, or `"Add all languages"` for multiple. * - When some values already exist: returns `"Add missing language"` (singular) * or `"Add missing languages"` (plural) depending on how many are left. */ function createAddAllTitle(value, languages) { return value?.length ? `Add missing ${languages.length - value.length === 1 ? "language" : "languages"}` : languages.length === 1 && languages[0] ? `Add ${languages[0].title} Field` : "Add all languages"; } function isInternationalizedArrayItemType(type) { return type.startsWith("internationalizedArray") && type.endsWith("Value"); } /** * Creates an array of Sanity `FormInsertPatch` objects that add new language * entries to an internationalized array field. * * If `addLanguageKeys` is provided, patches are created for those specific * language IDs. Otherwise, patches are created for all filtered languages * that are not already present in the current `value` array, for example when adding all missing languages. * * Each new item is assigned the correct `_type` (derived from the schema) * and the language identifier via `LANGUAGE_FIELD_NAME`. * * Insertions are ordered to maintain the same sequence as the master * `languages` list: each new item is inserted before the next existing * language in the value array, or appended at the end if no subsequent * language exists. */ function createAddLanguagePatches(config) { let { addLanguageKeys, schemaTypeName, languages, filteredLanguages, value, path = [] } = config, type = `${schemaTypeName}Value`; if (!isInternationalizedArrayItemType(type)) throw Error(`Invalid internationalized array type: ${type}`); let itemBase = { _type: type }, newItems = Array.isArray(addLanguageKeys) && addLanguageKeys.length > 0 ? addLanguageKeys.filter((id) => !value?.length || !value.find((v) => v[LANGUAGE_FIELD_NAME] === id)).map((id) => Object.assign({}, itemBase, { _key: randomKey(), [LANGUAGE_FIELD_NAME]: id })) : filteredLanguages.filter((language) => !value?.length || !value.find((v) => v[LANGUAGE_FIELD_NAME] === language.id)).map((language) => Object.assign({}, itemBase, { _key: randomKey(), [LANGUAGE_FIELD_NAME]: language.id })), languagesInUse = value?.length ? value.map((v) => v) : []; return newItems.map((item) => { let itemLanguage = item[LANGUAGE_FIELD_NAME], languageIndex = languages.findIndex((l) => itemLanguage === l.id), remainingLanguages = languages.slice(languageIndex + 1), nextLanguageIndex = languagesInUse.findIndex((l) => remainingLanguages.find((r) => r.id === l[LANGUAGE_FIELD_NAME])); return nextLanguageIndex < 0 ? languagesInUse.push(item) : languagesInUse.splice(nextLanguageIndex, 0, item), nextLanguageIndex < 0 ? insert([item], "after", [...path, nextLanguageIndex]) : insert([item], "before", [...path, nextLanguageIndex]); }); } const createTranslateFieldActions = (fieldActionProps, { languages, filteredLanguages }) => languages.map((language) => { let value = useFormValue(fieldActionProps.path), { onChange, formState } = useDocumentPane(), disabled = !!formState?.readOnly || (value && Array.isArray(value) ? !!value?.find((item) => item.language === language.id) : !1), hidden = !filteredLanguages.some((f) => f.id === language.id); return { type: "action", icon: AddIcon, onAction: useCallback(() => { let { schemaType, path } = fieldActionProps, patches = createAddLanguagePatches({ addLanguageKeys: [language.id], schemaTypeName: schemaType.name, languages, filteredLanguages, value, path }); onChange(PatchEvent.from([setIfMissing([], path), ...patches])); }, [ language.id, value, onChange ]), title: language.title, hidden, disabled }; }), AddMissingTranslationsFieldAction = (fieldActionProps, { languages, filteredLanguages }) => { let value = useFormValue(fieldActionProps.path), { onChange, formState } = useDocumentPane(), disabled = !!formState?.readOnly || value && value.length === filteredLanguages.length, hidden = checkAllLanguagesArePresent(filteredLanguages, value); return { type: "action", icon: AddIcon, onAction: useCallback(() => { let { schemaType, path } = fieldActionProps, patches = createAddLanguagePatches({ addLanguageKeys: [], schemaTypeName: schemaType.name, languages, filteredLanguages, value, path }); onChange(PatchEvent.from([setIfMissing([], path), ...patches])); }, [ fieldActionProps, filteredLanguages, languages, onChange, value ]), title: createAddAllTitle(value, filteredLanguages), disabled, hidden }; }, internationalizedArrayFieldAction = defineDocumentFieldAction({ name: "internationalizedArray", useAction(fieldActionProps) { let isInternationalizedArrayField = fieldActionProps?.schemaType?.type?.name.startsWith("internationalizedArray"), { languages, filteredLanguages } = useInternationalizedArrayContext(), translateFieldActions = createTranslateFieldActions(fieldActionProps, { languages, filteredLanguages }); return { type: "group", icon: TranslateIcon, title: "Add Translation", renderAsButton: !0, children: isInternationalizedArrayField ? [...translateFieldActions, AddMissingTranslationsFieldAction(fieldActionProps, { languages, filteredLanguages })] : [], hidden: !isInternationalizedArrayField }; } }); /** * Converts a string to PascalCase (e.g. `"my-field"` -> `"MyField"`). */ function pascalCase(string) { return upperFirst(camelCase(string)); } /** * Generates the schema type name for an internationalized array field. * * - Without the value suffix: `"internationalizedArray{PascalName}"` * (used for the outer array type) * - With the value suffix: `"internationalizedArray{PascalName}Value"` * (used for the inner object type that wraps each language entry) * * For example, `createFieldName('block-content', true)` returns * `"internationalizedArrayBlockContentValue"`. */ function createFieldName(name, addValue = !1) { return addValue ? [ "internationalizedArray", pascalCase(name), "Value" ].join("") : ["internationalizedArray", pascalCase(name)].join(""); } /** * Default filter function for the internationalized array field. * It filter the field base on the `language` value of the object. */ const internationalizedArrayLanguageFilter = (enclosingType, _member, selectedLanguageIds, parentValue, languages) => { if (isInternationalizedArrayItemType(enclosingType.name)) { let language = typeof parentValue?.language == "string" ? parentValue?.[LANGUAGE_FIELD_NAME] : typeof parentValue?._key == "string" ? parentValue?._key : null; return !language || languages.find((l) => l.id === language) ? language ? selectedLanguageIds.includes(language) : !1 : !0; } return !0; }, schemaExample = { languages: [{ id: "en", title: "English" }, { id: "no", title: "Norsk" }] }; function Feedback() { let $ = c(4), t0; $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = /* @__PURE__ */ jsx("code", { children: "internationalizedArray" }), $[0] = t0) : t0 = $[0]; let t1; $[1] === Symbol.for("react.memo_cache_sentinel") ? (t1 = /* @__PURE__ */ jsx("code", { children: "id" }), $[1] = t1) : t1 = $[1]; let t2; $[2] === Symbol.for("react.memo_cache_sentinel") ? (t2 = /* @__PURE__ */ jsxs(Text, { children: [ "An array of language objects must be passed into the ", t0, " ", "helper function, each with an ", t1, " and ", /* @__PURE__ */ jsx("code", { children: "title" }), " field. Example:" ] }), $[2] = t2) : t2 = $[2]; let t3; return $[3] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsx(Card, { tone: "caution", border: !0, radius: 2, padding: 3, children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [t2, /* @__PURE__ */ jsx(Card, { padding: 2, border: !0, radius: 2, children: /* @__PURE__ */ jsx(Code, { size: 1, language: "javascript", children: JSON.stringify(schemaExample, null, 2) }) })] }) }), $[3] = t3) : t3 = $[3], t3; } /** * Migration banner component that displays a warning if the items need to be migrated to the v5 format. */ function MigrationBanner(t0) { let $ = c(11), { itemsNeedingMigration } = t0; if (!itemsNeedingMigration.length) return null; let t1; $[0] === Symbol.for("react.memo_cache_sentinel") ? (t1 = /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { size: 1, children: /* @__PURE__ */ jsx(WarningOutlineIcon, {}) }) }), $[0] = t1) : t1 = $[0]; let t2; $[1] === Symbol.for("react.memo_cache_sentinel") ? (t2 = /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "Data migration required" }), $[1] = t2) : t2 = $[1]; let t3 = itemsNeedingMigration.length === 1 ? "" : "s", t4 = itemsNeedingMigration.length === 1 ? "needs" : "need", t5; $[2] !== itemsNeedingMigration.length || $[3] !== t3 || $[4] !== t4 ? (t5 = /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [ itemsNeedingMigration.length, " item", t3, " ", t4, " to be migrated to the v5 format." ] }), $[2] = itemsNeedingMigration.length, $[3] = t3, $[4] = t4, $[5] = t5) : t5 = $[5]; let t6; $[6] === Symbol.for("react.memo_cache_sentinel") ? (t6 = /* @__PURE__ */ jsx("code", { children: "_key" }), $[6] = t6) : t6 = $[6]; let t7; $[7] === Symbol.for("react.memo_cache_sentinel") ? (t7 = /* @__PURE__ */ jsx("code", { children: "language" }), $[7] = t7) : t7 = $[7]; let t8; $[8] === Symbol.for("react.memo_cache_sentinel") ? (t8 = /* @__PURE__ */ jsx(Box, { marginTop: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [ "This field still uses the v4 format where language is stored in ", t6, ". Migrate to the v5 format where language is stored in ", t7, ".", " ", /* @__PURE__ */ jsx("a", { rel: "noopener noreferrer", target: "_blank", href: "https://github.com/sanity-io/plugins/blob/main/plugins/sanity-plugin-internationalized-array/README.md#migrate-from-v4-to-v5", children: "Learn more" }), "." ] }) }), $[8] = t8) : t8 = $[8]; let t9; return $[9] === t5 ? t9 = $[10] : (t9 = /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 3, radius: 2, border: !0, children: /* @__PURE__ */ jsxs(Flex, { gap: 3, align: "center", children: [t1, /* @__PURE__ */ jsxs(Stack, { gap: 2, flex: 1, children: [ t2, t5, t8 ] })] }) }), $[9] = t5, $[10] = t9), t9; } /** * Main array input component for internationalized array fields. * * Replaces the default Sanity array input and manages the full lifecycle of * language entries: * * - **Language filter integration**: When `@sanity/language-filter` is active * for the current document type, array members are filtered to only show * languages matching the user's selection. * - **Adding languages**: Exposes per-language buttons and an "Add all / Add * missing languages" button (controlled by `buttonAddAll` and * `buttonLocations` config). Dispatches `setIfMissing` + `insert` patches. * - **Default languages**: Automatically adds entries for languages listed in * `defaultLanguages` when those entries are missing. Only runs once the * document exists in the dataset (it has a `_rev`), so the effect never * recreates a just-deleted document or creates a draft before the user's * first edit. * - **Ordering**: Detects when value items are out of order relative to the * master `languages` list and automatically re-sorts them. * - **Validation**: Shows a `<Feedback>` component if the languages * configuration is invalid (e.g. missing `id` or `title`). * - **Empty state**: Displays a "no translations" message when the field has * no entries and the add buttons are not visible. */ function InternationalizedArray(props) { let $ = c(105), { members, value: _value, schemaType, onChange, readOnly: documentReadOnly } = props, value = _value, t0; $[0] === value ? t0 = $[1] : (t0 = value?.filter(_temp$1) ?? [], $[0] = value, $[1] = t0); let itemsNeedingMigration = t0, shouldMigrateArray = itemsNeedingMigration.length > 0, readOnly = !!documentReadOnly || schemaType.readOnly === !0, toast = useToast(), getFormValue = useGetFormValue(), { languages, filteredLanguages, defaultLanguages, buttonAddAll, buttonLocations, languageFilter: builtInLanguageFilter } = useInternationalizedArrayContext(), { selectedLanguageIds, options: languageFilterOptions } = useLanguageFilterStudioContext(), t1; $[2] === Symbol.for("react.memo_cache_sentinel") ? (t1 = ["_type"], $[2] = t1) : t1 = $[2]; let documentType = useFormValue(t1), t2; $[3] !== documentType || $[4] !== languageFilterOptions ? (t2 = typeof documentType == "string" && languageFilterOptions.documentTypes.includes(documentType), $[3] = documentType, $[4] = languageFilterOptions, $[5] = t2) : t2 = $[5]; let usingLanguageFilterPlugin = t2, t3; $[6] !== builtInLanguageFilter || $[7] !== documentType ? (t3 = typeof documentType == "string" && builtInLanguageFilter.documentTypes.includes(documentType), $[6] = builtInLanguageFilter, $[7] = documentType, $[8] = t3) : t3 = $[8]; let usingBuiltInLanguageFilter = t3, t4; $[9] !== languageFilterOptions || $[10] !== languages || $[11] !== members || $[12] !== selectedLanguageIds || $[13] !== usingBuiltInLanguageFilter || $[14] !== usingLanguageFilterPlugin ? (t4 = usingLanguageFilterPlugin || usingBuiltInLanguageFilter ? members.filter((member) => { if (member.kind !== "item") return !1; let valueMember = member.item.members[0]; return !valueMember || valueMember.kind !== "field" ? !1 : usingBuiltInLanguageFilter ? internationalizedArrayLanguageFilter(member.item.schemaType, valueMember, selectedLanguageIds, member.item.value, languages) : languageFilterOptions.filterField(member.item.schemaType, valueMember, selectedLanguageIds, member.item.value); }) : members, $[9] = languageFilterOptions, $[10] = languages, $[11] = members, $[12] = selectedLanguageIds, $[13] = usingBuiltInLanguageFilter, $[14] = usingLanguageFilterPlugin, $[15] = t4) : t4 = $[15]; let filteredMembers = t4, t5; $[16] !== filteredLanguages || $[17] !== getFormValue || $[18] !== languages || $[19] !== onChange || $[20] !== props.path || $[21] !== schemaType ? (t5 = (addLanguageKeys) => { let formValue = getFormValue(props.path); if (!filteredLanguages?.length) return; let patches = createAddLanguagePatches({ addLanguageKeys: Array.isArray(addLanguageKeys) ? addLanguageKeys : [addLanguageKeys], schemaTypeName: schemaType.name, languages, filteredLanguages, value: formValue }); onChange([setIfMissing([]), ...patches]); }, $[16] = filteredLanguages, $[17] = getFormValue, $[18] = languages, $[19] = onChange, $[20] = props.path, $[21] = schemaType, $[22] = t5) : t5 = $[22]; let handleAddLanguages = t5, { isDeleted, isDeleting } = useDocumentPane(), t6; $[23] === Symbol.for("react.memo_cache_sentinel") ? (t6 = ["_rev"], $[23] = t6) : t6 = $[23]; let documentExists = !!useFormValue(t6), t7; $[24] === value ? t7 = $[25] : (t7 = value?.map(_temp2$1).filter(Boolean).join(","), $[24] = value, $[25] = t7); let languageKeysFromValue = t7, t8; if ($[26] !== languageKeysFromValue || $[27] !== languages) { bb0: { let languageKeys = languageKeysFromValue?.split(",") || []; if (!languageKeys?.length) { let t9; $[29] === Symbol.for("react.memo_cache_sentinel") ? (t9 = [], $[29] = t9) : t9 = $[29], t8 = t9; break bb0; } if (!languages?.length) { let t9; $[30] === Symbol.for("react.memo_cache_sentinel") ? (t9 = [], $[30] = t9) : t9 = $[30], t8 = t9; break bb0; } t8 = languages.filter((l) => languageKeys?.find((key) => key === l.id)).map(_temp3$1); } $[26] = languageKeysFromValue, $[27] = languages, $[28] = t8; } else t8 = $[28]; let addedLanguages = t8, t10, t9; $[31] !== addedLanguages || $[32] !== defaultLanguages || $[33] !== documentExists || $[34] !== handleAddLanguages || $[35] !== isDeleted || $[36] !== isDeleting || $[37] !== languages || $[38] !== readOnly || $[39] !== shouldMigrateArray ? (t9 = () => { let hasAddedDefaultLanguages = defaultLanguages.filter((language) => languages.find((l_1) => l_1.id === language)).every((language_0) => addedLanguages.includes(language_0)); if (documentExists && !isDeleting && !isDeleted && !hasAddedDefaultLanguages && !shouldMigrateArray) { let languagesToAdd = defaultLanguages.filter((language_1) => !addedLanguages.includes(language_1)).filter((language_2) => languages.find((l_2) => l_2.id === language_2)), timeout = setTimeout(() => { readOnly || handleAddLanguages(languagesToAdd); }); return () => clearTimeout(timeout); } }, t10 = [ documentExists, isDeleted, isDeleting, handleAddLanguages, defaultLanguages, addedLanguages, languages, readOnly, shouldMigrateArray ], $[31] = addedLanguages, $[32] = defaultLanguages, $[33] = documentExists, $[34] = handleAddLanguages, $[35] = isDeleted, $[36] = isDeleting, $[37] = languages, $[38] = readOnly, $[39] = shouldMigrateArray, $[40] = t10, $[41] = t9) : (t10 = $[40], t9 = $[41]), useEffect(t9, t10); let t11; $[42] !== languages || $[43] !== onChange || $[44] !== toast || $[45] !== value ? (t11 = () => { if (!value?.length || !languages?.length) return; let updatedValue = value.reduce((acc, v_1) => { let newIndex = languages.findIndex((l_3) => l_3.id === v_1?.[LANGUAGE_FIELD_NAME]); return newIndex > -1 && (acc[newIndex] = v_1), acc; }, []).filter(Boolean); value?.length !== updatedValue.length && toast.push({ title: "There was an error reordering languages", status: "warning" }), onChange(set(updatedValue)); }, $[42] = languages, $[43] = onChange, $[44] = toast, $[45] = value, $[46] = t11) : t11 = $[46]; let handleRestoreOrder = t11, t12; bb1: { if (!value?.length || !languages?.length) { t12 = !0; break bb1; } let t13; $[47] !== languages || $[48] !== value ? (t13 = value?.every((v_2) => languages.find((l_4) => l_4?.id === v_2?.[LANGUAGE_FIELD_NAME])), $[47] = languages, $[48] = value, $[49] = t13) : t13 = $[49], t12 = t13; } let allKeysAreLanguages = t12, t13; bb2: { if (!value?.length || !addedLanguages.length) { let t14; $[50] === Symbol.for("react.memo_cache_sentinel") ? (t14 = [], $[50] = t14) : t14 = $[50], t13 = t14; break bb2; } let t14; if ($[51] !== addedLanguages || $[52] !== value) { let t15; $[54] === addedLanguages ? t15 = $[55] : (t15 = (v_3, vIndex) => vIndex === addedLanguages.findIndex((language_3) => language_3 === v_3.language) ? null : v_3, $[54] = addedLanguages, $[55] = t15), t14 = value.map(t15).filter(Boolean), $[51] = addedLanguages, $[52] = value, $[53] = t14; } else t14 = $[53]; t13 = t14; } let languagesOutOfOrder = t13, languagesAreValid = !languages?.length || languages?.length && languages.every(_temp4$1), t14; $[56] !== allKeysAreLanguages || $[57] !== handleRestoreOrder || $[58] !== languagesOutOfOrder.length || $[59] !== readOnly ? (t14 = () => { languagesOutOfOrder.length > 0 && allKeysAreLanguages && !readOnly && handleRestoreOrder(); }, $[56] = allKeysAreLanguages, $[57] = handleRestoreOrder, $[58] = languagesOutOfOrder.length, $[59] = readOnly, $[60] = t14) : t14 = $[60]; let t15; $[61] !== allKeysAreLanguages || $[62] !== handleRestoreOrder || $[63] !== languagesOutOfOrder || $[64] !== readOnly ? (t15 = [ languagesOutOfOrder, allKeysAreLanguages, handleRestoreOrder, readOnly ], $[61] = allKeysAreLanguages, $[62] = handleRestoreOrder, $[63] = languagesOutOfOrder, $[64] = readOnly, $[65] = t15) : t15 = $[65], useEffect(t14, t15); let t16; $[66] !== filteredLanguages || $[67] !== value ? (t16 = checkAllLanguagesArePresent(filteredLanguages, value), $[66] = filteredLanguages, $[67] = value, $[68] = t16) : t16 = $[68]; let allLanguagesArePresent = t16, t17; $[69] !== filteredLanguages || $[70] !== handleAddLanguages ? (t17 = () => { handleAddLanguages(filteredLanguages.map(_temp5)); }, $[69] = filteredLanguages, $[70] = handleAddLanguages, $[71] = t17) : t17 = $[71]; let addAllMissingLanguages = t17; if (!languagesAreValid) { let t18; return $[72] === Symbol.for("react.memo_cache_sentinel") ? (t18 = /* @__PURE__ */ jsx(Feedback, {}), $[72] = t18) : t18 = $[72], t18; } let t18; $[73] !== allLanguagesArePresent || $[74] !== buttonLocations || $[75] !== filteredLanguages.length || $[76] !== shouldMigrateArray ? (t18 = !shouldMigrateArray && buttonLocations.includes("field") && filteredLanguages?.length > 0 && !allLanguagesArePresent, $[73] = allLanguagesArePresent, $[74] = buttonLocations, $[75] = filteredLanguages.length, $[76] = shouldMigrateArray, $[77] = t18) : t18 = $[77]; let addButtonsAreVisible = t18, fieldHasMembers = members?.length > 0, t19; $[78] !== filteredLanguages || $[79] !== value ? (t19 = createAddAllTitle(value, filteredLanguages), $[78] = filteredLanguages, $[79] = value, $[80] = t19) : t19 = $[80]; let addAllTitle = t19, t20; if ($[81] !== filteredMembers || $[82] !== props) { let t21; $[84] === props ? t21 = $[85] : (t21 = (member_0) => member_0.kind === "item" ? /* @__PURE__ */ createElement(ArrayOfObjectsItem, { ...props, key: member_0.key, member: member_0 }) : /* @__PURE__ */ jsx(MemberItemError, { member: member_0 }, member_0.key), $[84] = props, $[85] = t21), t20 = filteredMembers.map(t21), $[81] = filteredMembers, $[82] = props, $[83] = t20; } else t20 = $[83]; let t21; $[86] === itemsNeedingMigration ? t21 = $[87] : (t21 = /* @__PURE__ */ jsx(MigrationBanner, { itemsNeedingMigration }), $[86] = itemsNeedingMigration, $[87] = t21); let t22; $[88] !== addButtonsAreVisible || $[89] !== fieldHasMembers ? (t22 = !addButtonsAreVisible && !fieldHasMembers ? /* @__PURE__ */ jsx(Card, { border: !0, tone: "transparent", padding: 3, radius: 2, children: /* @__PURE__ */ jsx(Text, { size: 1, children: "This internationalized field currently has no translations." }) }) : null, $[88] = addButtonsAreVisible, $[89] = fieldHasMembers, $[90] = t22) : t22 = $[90]; let t23; $[91] !== addAllMissingLanguages || $[92] !== addAllTitle || $[93] !== addButtonsAreVisible || $[94] !== addedLanguages || $[95] !== allLanguagesArePresent || $[96] !== buttonAddAll || $[97] !== handleAddLanguages || $[98] !== readOnly ? (t23 = addButtonsAreVisible ? /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [/* @__PURE__ */ jsx(AddButtons, { languagesInUse: addedLanguages, readOnly, handleClick: handleAddLanguages }), buttonAddAll ? /* @__PURE__ */ jsx(Button, { tone: "primary", mode: "ghost", "data-testid": "add-all-languages", disabled: readOnly || allLanguagesArePresent, icon: AddIcon, text: addAllTitle, onClick: addAllMissingLanguages }) : null] }) : null, $[91] = addAllMissingLanguages, $[92] = addAllTitle, $[93] = addButtonsAreVisible, $[94] = addedLanguages, $[95] = allLanguagesArePresent, $[96] = buttonAddAll, $[97] = handleAddLanguages, $[98] = readOnly, $[99] = t23) : t23 = $[99]; let t24; return $[100] !== t20 || $[101] !== t21 || $[102] !== t22 || $[103] !== t23 ? (t24 = /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [ t20, t21, t22, t23 ] }), $[100] = t20, $[101] = t21, $[102] = t22, $[103] = t23, $[104] = t24) : t24 = $[104], t24; } function _temp5(language_4) { return language_4.id; } function _temp4$1(item) { return item.id && item.title; } function _temp3$1(l_0) { return l_0.id; } function _temp2$1(v_0) { return v_0.language ?? v_0._key; } function _temp$1(v) { return !v[LANGUAGE_FIELD_NAME]; } function getLanguagesFieldOption(schemaType) { return schemaType ? schemaType.options?.languages || getLanguagesFieldOption(schemaType.type) : void 0; } var array_default = (config) => { let { apiVersion, select, languages, type } = config, typeName = typeof type == "string" ? type : type.name, arrayName = createFieldName(typeName), objectName = createFieldName(typeName, !0); return defineField({ name: arrayName, title: "Internationalized array", type: "array", components: { field: (props) => props.renderDefault({ ...props, level: 0 }), input: InternationalizedArray }, options: { apiVersion, select, languages }, of: [defineField({ ...typeof type == "string" ? {} : type, name: objectName, type: objectName })], validation: (rule) => rule.custom(async (value, context) => { if (!value || value.length === 0) return !0; let itemsMissingLanguage = value.filter((item) => item && !item.language && item._key); if (itemsMissingLanguage.length > 0) return { message: "Language is required for each array item. Run the migration to update your data from the old format.", paths: itemsMissingLanguage.flatMap((item) => [[{ _key: item._key }], [{ _key: item._key }, "value"]]) }; if (value.length === 1 && !value[0]?.language) return !0; let selectedValue = getSelectedValue(select, context.document), client = context.getClient({ apiVersion }), contextLanguages = [], languagesFieldOption = getLanguagesFieldOption(context?.type); if (Array.isArray(languagesFieldOption)) contextLanguages = languagesFieldOption; else if (Array.isArray(peek(selectedValue))) contextLanguages = peek(selectedValue) || []; else if (typeof languagesFieldOption == "function") { let cachedLanguages = getFunctionCache(languagesFieldOption, selectedValue); if (Array.isArray(cachedLanguages)) contextLanguages = cachedLanguages; else { let suspendCachedLanguages = peek(selectedValue); Array.isArray(suspendCachedLanguages) ? contextLanguages = suspendCachedLanguages : (contextLanguages = await languagesFieldOption(client, selectedValue), setFunctionCache(languagesFieldOption, selectedValue, contextLanguages)); } } if (value && value.length > contextLanguages.length) return `Cannot be more than ${contextLanguages.length === 1 ? "1 item" : `${contextLanguages.length} items`}`; let languageIds = new Set(contextLanguages.map((lang) => lang.id)), nonLanguageKeys = value.filter((item) => item?.language && !languageIds.has(item.language)); if (nonLanguageKeys.length) return { message: "Array item keys must be valid languages registered to the field type", paths: nonLanguageKeys.map((item) => [{ _key: item._key }]) }; let seenLanguages = /* @__PURE__ */ new Set(), duplicateValues = []; for (let item of value) item?.language && (seenLanguages.has(item.language) ? duplicateValues.push(item) : seenLanguages.add(item[LANGUAGE_FIELD_NAME])); return !duplicateValues.length || { message: "There can only be one field per language", paths: duplicateValues.map((item) => [{ _key: item._key }]) }; }) }); }; /** * Maps an array of Sanity form validation entries to a `@sanity/ui` `CardTone` * for visual feedback on internationalized array items. * * - Returns `'critical'` if any validation has `level: 'error'` * - Returns `'caution'` if any validation has `level: 'warning'` (and no errors) * - Returns `undefined` otherwise (no tone applied) * * Error level always takes precedence over warning. */ function getToneFromValidation(validations) { if (!validations?.length) return; let validationLevels = new Set(validations.map((v) => v.level)); if (validationLevels.has("error")) return "critical"; if (validationLevels.has("warning")) return "caution"; } function ChangeLanguageButton(props) { let $ = c(24), { value, onChange, path } = props, t0; $[0] === path ? t0 = $[1] : (t0 = path.slice(0, -1), $[0] = path, $[1] = t0); let parentValue = useFormValue(t0), { languages } = useInternationalizedArrayContext(), t1; $[2] === parentValue ? t1 = $[3] : (t1 = parentValue?.map(_temp) ?? [], $[2] = parentValue, $[3] = t1); let languageKeysInUse = t1, t2; $[4] !== languages || $[5] !== onChange || $[6] !== value ? (t2 = (event) => { let languageId = event?.currentTarget?.value; !value || !languages?.length || !languages.find((l) => l.id === languageId) || onChange([set(languageId, [LANGUAGE_FIELD_NAME])]); }, $[4] = languages, $[5] = onChange, $[6] = value, $[7] = t2) : t2 = $[7]; let handleKeyChange = t2, t3 = `Change "${value[LANGUAGE_FIELD_NAME]}"`, t4; $[8] === t3 ? t4 = $[9] : (t4 = /* @__PURE__ */ jsx(Button, { fontSize: 1, text: t3 }), $[8] = t3, $[9] = t4); let t5 = `${value[LANGUAGE_FIELD_NAME]}-change-key`, t6; if ($[10] !== handleKeyChange || $[11] !== languageKeysInUse || $[12] !== languages) { let t7; $[14] !== handleKeyChange || $[15] !== languageKeysInUse ? (t7 = (lang) => /* @__PURE__ */ jsx(MenuItem, { disabled: languageKeysInUse.includes(lang.id), fontSize: 1, text: lang.id.toLocaleUpperCase(), value: lang.id, onClick: handleKeyChange }, lang.id), $[14] = handleKeyChange, $[15] = languageKeysInUse,