sanity-plugin-internationalized-array
Version:
Store localized fields in an array to save on attributes
1,013 lines • 60.4 kB
JavaScript
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,