├── .npmrc ├── .prettierignore ├── commitlint.config.js ├── .releaserc.json ├── .eslintignore ├── src ├── index.ts ├── utils │ ├── createValueSchemaTypeName.ts │ ├── getLanguageDisplay.ts │ ├── createAddAllTitle.ts │ ├── checkAllLanguagesArePresent.ts │ ├── getLanguagesFieldOption.ts │ ├── flattenSchemaType.ts │ ├── getDocumentsToTranslate.ts │ └── createAddLanguagePatches.ts ├── constants.ts ├── components │ ├── getToneFromValidation.ts │ ├── createFieldName.ts │ ├── Feedback.tsx │ ├── getSelectedValue.ts │ ├── Preload.tsx │ ├── AddButtons.tsx │ ├── InternationalizedField.tsx │ ├── InternationalizedArrayContext.tsx │ ├── DocumentAddButtons.tsx │ ├── InternationalizedInput.tsx │ └── InternationalizedArray.tsx ├── schema │ ├── object.ts │ └── array.ts ├── plugin.tsx ├── types.ts ├── fieldActions │ └── index.ts └── cache.ts ├── lint-staged.config.js ├── img └── internationalized-array.png ├── .npmignore ├── sanity.json ├── tsconfig.json ├── .prettierrc.json ├── .github ├── renovate.json └── workflows │ └── main.yml ├── .editorconfig ├── v2-incompatible.js ├── tsconfig.lib.json ├── tsconfig.settings.json ├── .eslintrc ├── package.config.ts ├── .gitignore ├── LICENSE ├── package.json ├── migrations └── transformObjectToArray.ts ├── README.md └── CHANGELOG.md /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib 2 | pnpm-lock.yaml 3 | yarn.lock 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/semantic-release-preset", 3 | "branches": ["main"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | .eslintrc.js 3 | commitlint.config.js 4 | lib 5 | lint-staged.config.js 6 | package.config.ts 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {clear} from './cache' 2 | export {internationalizedArray} from './plugin' 3 | export * from './types' 4 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '**/*.{js,jsx}': ['eslint'], 3 | '**/*.{ts,tsx}': ['eslint', () => 'tsc --build'], 4 | } 5 | -------------------------------------------------------------------------------- /img/internationalized-array.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/sanity-plugin-internationalized-array/HEAD/img/internationalized-array.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /test 2 | /coverage 3 | /demo 4 | .editorconfig 5 | .eslintrc 6 | .gitignore 7 | .github 8 | .prettierrc 9 | .travis.yml 10 | .nyc_output 11 | -------------------------------------------------------------------------------- /sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "implements": "part:@sanity/base/sanity-root", 5 | "path": "./v2-incompatible.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.settings", 3 | "include": ["./src", "./package.config.ts"], 4 | "compilerOptions": { 5 | "rootDir": ".", 6 | "noEmit": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/createValueSchemaTypeName.ts: -------------------------------------------------------------------------------- 1 | import {SchemaType} from 'sanity' 2 | 3 | export function createValueSchemaTypeName(schemaType: SchemaType): string { 4 | return `${schemaType.name}Value` 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": false 9 | } 10 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>sanity-io/renovate-config", 5 | "github>sanity-io/renovate-config:studio-v3" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | charset= utf8 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /v2-incompatible.js: -------------------------------------------------------------------------------- 1 | const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin') 2 | const {name, version, sanityExchangeUrl} = require('./package.json') 3 | 4 | export default showIncompatiblePluginDialog({ 5 | name: name, 6 | versions: { 7 | v3: version, 8 | v2: undefined, 9 | }, 10 | sanityExchangeUrl, 11 | }) 12 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.settings", 3 | "include": ["./src"], 4 | "exclude": [ 5 | "./src/**/__fixtures__", 6 | "./src/**/__mocks__", 7 | "./src/**/*.test.ts", 8 | "./src/**/*.test.tsx" 9 | ], 10 | "compilerOptions": { 11 | "rootDir": ".", 12 | "outDir": "./lib", 13 | "emitDeclarationOnly": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "jsx": "preserve", 5 | "module": "preserve", 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "strict": true, 8 | "downlevelIteration": true, 9 | "declaration": true, 10 | "allowSyntheticDefaultImports": true, 11 | "skipLibCheck": true, 12 | "isolatedModules": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import {PluginConfig} from './types' 2 | 3 | export const MAX_COLUMNS = { 4 | codeOnly: 5, 5 | titleOnly: 4, 6 | titleAndCode: 3, 7 | } 8 | 9 | export const CONFIG_DEFAULT: Required = { 10 | languages: [], 11 | select: {}, 12 | defaultLanguages: [], 13 | fieldTypes: [], 14 | apiVersion: '2025-10-15', 15 | buttonLocations: ['field'], 16 | buttonAddAll: true, 17 | languageDisplay: 'codeOnly', 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/getLanguageDisplay.ts: -------------------------------------------------------------------------------- 1 | import {LanguageDisplay} from '../types' 2 | 3 | export function getLanguageDisplay( 4 | languageDisplay: LanguageDisplay, 5 | title: string, 6 | code: string 7 | ): string { 8 | if (languageDisplay === 'codeOnly') return code.toUpperCase() 9 | if (languageDisplay === 'titleOnly') return title 10 | if (languageDisplay === 'titleAndCode') 11 | return `${title} (${code.toUpperCase()})` 12 | return title 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/createAddAllTitle.ts: -------------------------------------------------------------------------------- 1 | import {Language, Value} from '../types' 2 | 3 | export function createAddAllTitle( 4 | value: Value[] | undefined, 5 | languages: Language[] 6 | ): string { 7 | if (value?.length) { 8 | return `Add missing ${ 9 | languages.length - value.length === 1 ? `language` : `languages` 10 | }` 11 | } 12 | 13 | return languages.length === 1 14 | ? `Add ${languages[0].title} Field` 15 | : `Add all languages` 16 | } 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "extends": [ 8 | "sanity/typescript", 9 | "sanity/react", 10 | "plugin:prettier/recommended", 11 | "prettier" 12 | ], 13 | "plugins": ["simple-import-sort"], 14 | "rules": { 15 | "react/prop-types": "off", 16 | "react/require-default-props": "off", 17 | "simple-import-sort/imports": "error", 18 | "simple-import-sort/exports": "error" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from '@sanity/pkg-utils' 2 | 3 | export default defineConfig({ 4 | legacyExports: true, 5 | dist: 'lib', 6 | tsconfig: 'tsconfig.lib.json', 7 | 8 | // Remove this block to enable strict export validation 9 | extract: { 10 | rules: { 11 | 'ae-forgotten-export': 'off', 12 | 'ae-incompatible-release-tags': 'off', 13 | 'ae-internal-missing-underscore': 'off', 14 | 'ae-missing-release-tag': 'off', 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /src/utils/checkAllLanguagesArePresent.ts: -------------------------------------------------------------------------------- 1 | import {Language, Value} from '../types' 2 | 3 | export function checkAllLanguagesArePresent( 4 | languages: Language[], 5 | value: Value[] | undefined 6 | ): boolean { 7 | const filteredLanguageIds = languages.map((l) => l.id) 8 | const languagesInUseIds = value ? value.map((v) => v._key) : [] 9 | 10 | return ( 11 | languagesInUseIds.length === filteredLanguageIds.length && 12 | languagesInUseIds.every((l) => filteredLanguageIds.includes(l)) 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/getLanguagesFieldOption.ts: -------------------------------------------------------------------------------- 1 | import {SchemaType} from 'sanity' 2 | 3 | import {ArrayFieldOptions} from '../schema/array' 4 | 5 | export function getLanguagesFieldOption( 6 | schemaType: SchemaType | undefined 7 | ): ArrayFieldOptions['languages'] | undefined { 8 | if (!schemaType) { 9 | return undefined 10 | } 11 | const languagesOption = (schemaType.options as ArrayFieldOptions)?.languages 12 | if (languagesOption) { 13 | return languagesOption 14 | } 15 | return getLanguagesFieldOption(schemaType.type) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/getToneFromValidation.ts: -------------------------------------------------------------------------------- 1 | import type {CardTone} from '@sanity/ui' 2 | import type {FormNodeValidation} from 'sanity' 3 | 4 | export function getToneFromValidation( 5 | validations: FormNodeValidation[] 6 | ): CardTone | undefined { 7 | if (!validations?.length) { 8 | return undefined 9 | } 10 | 11 | const validationLevels = validations.map((v) => v.level) 12 | 13 | if (validationLevels.includes('error')) { 14 | return `critical` 15 | } else if (validationLevels.includes('warning')) { 16 | return `caution` 17 | } 18 | 19 | return undefined 20 | } 21 | -------------------------------------------------------------------------------- /src/components/createFieldName.ts: -------------------------------------------------------------------------------- 1 | export function camelCase(string: string): string { 2 | return string.replace(/-([a-z])/g, (g) => g[1].toUpperCase()) 3 | } 4 | 5 | export function titleCase(string: string): string { 6 | return string 7 | .split(` `) 8 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 9 | .join(` `) 10 | } 11 | 12 | export function pascalCase(string: string): string { 13 | return titleCase(camelCase(string)) 14 | } 15 | 16 | export function createFieldName(name: string, addValue = false): string { 17 | return addValue 18 | ? [`internationalizedArray`, pascalCase(name), `Value`].join(``) 19 | : [`internationalizedArray`, pascalCase(name)].join(``) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Feedback.tsx: -------------------------------------------------------------------------------- 1 | import {Card, Code, Stack, Text} from '@sanity/ui' 2 | import type React from 'react' 3 | 4 | const schemaExample = { 5 | languages: [ 6 | {id: 'en', title: 'English'}, 7 | {id: 'no', title: 'Norsk'}, 8 | ], 9 | } 10 | 11 | export default function Feedback(): React.ReactElement { 12 | return ( 13 | 14 | 15 | 16 | An array of language objects must be passed into the{' '} 17 | internationalizedArray helper function, each with an{' '} 18 | id and title field. Example: 19 | 20 | 21 | 22 | {JSON.stringify(schemaExample, null, 2)} 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/getSelectedValue.ts: -------------------------------------------------------------------------------- 1 | import {get} from 'lodash' 2 | 3 | export const getSelectedValue = ( 4 | select: Record | undefined, 5 | document: 6 | | { 7 | [x: string]: unknown 8 | } 9 | | undefined 10 | ): Record => { 11 | if (!select || !document) { 12 | return {} 13 | } 14 | 15 | const selection: Record = select || {} 16 | const selectedValue: Record = {} 17 | for (const [key, path] of Object.entries(selection)) { 18 | let value = get(document, path) 19 | if (Array.isArray(value)) { 20 | // If there are references in the array, ensure they have `_ref` set, otherwise they are considered empty and can safely be ignored 21 | value = value.filter((item) => 22 | typeof item === 'object' 23 | ? item?._type === 'reference' && '_ref' in item 24 | : true 25 | ) 26 | } 27 | selectedValue[key] = value 28 | } 29 | 30 | return selectedValue 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # macOS finder cache file 40 | .DS_Store 41 | 42 | # VS Code settings 43 | .vscode 44 | 45 | # IntelliJ 46 | .idea 47 | *.iml 48 | 49 | # Cache 50 | .cache 51 | 52 | # Yalc 53 | .yalc 54 | yalc.lock 55 | 56 | # npm package zips 57 | *.tgz 58 | 59 | # Compiled plugin 60 | lib 61 | -------------------------------------------------------------------------------- /src/schema/object.ts: -------------------------------------------------------------------------------- 1 | import {defineField, FieldDefinition} from 'sanity' 2 | 3 | import {createFieldName} from '../components/createFieldName' 4 | import InternationalizedInput from '../components/InternationalizedInput' 5 | 6 | type ObjectFactoryConfig = { 7 | type: string | FieldDefinition 8 | } 9 | 10 | export default (config: ObjectFactoryConfig): FieldDefinition<'object'> => { 11 | const {type} = config 12 | const typeName = typeof type === `string` ? type : type.name 13 | const objectName = createFieldName(typeName, true) 14 | 15 | return defineField({ 16 | name: objectName, 17 | title: `Internationalized array ${type}`, 18 | type: 'object', 19 | components: { 20 | // @ts-expect-error - fix typings 21 | item: InternationalizedInput, 22 | }, 23 | fields: [ 24 | defineField({ 25 | ...(typeof type === 'string' ? {type} : type), 26 | name: 'value', 27 | }), 28 | ], 29 | preview: { 30 | select: { 31 | title: 'value', 32 | subtitle: '_key', 33 | }, 34 | }, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Sanity.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/Preload.tsx: -------------------------------------------------------------------------------- 1 | import {memo} from 'react' 2 | import {useClient} from 'sanity' 3 | 4 | import {createCacheKey, peek, preloadWithKey, setFunctionCache} from '../cache' 5 | import type {PluginConfig} from '../types' 6 | 7 | export default memo(function Preload( 8 | props: Required> 9 | ) { 10 | const client = useClient({apiVersion: props.apiVersion}) 11 | 12 | // Use the same cache key structure as the main component 13 | // This should match the main component when selectedValue is empty 14 | const cacheKey = createCacheKey({}) 15 | 16 | if (!Array.isArray(peek({}))) { 17 | // eslint-disable-next-line require-await 18 | preloadWithKey(async () => { 19 | if (Array.isArray(props.languages)) { 20 | return props.languages 21 | } 22 | const result = await props.languages(client, {}) 23 | // Populate function cache for sharing with other components 24 | // Use the same key structure as the main component 25 | setFunctionCache(props.languages, {}, result) 26 | return result 27 | }, cacheKey) 28 | } 29 | 30 | return null 31 | }) 32 | -------------------------------------------------------------------------------- /src/utils/flattenSchemaType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isDocumentSchemaType, 3 | type ObjectField, 4 | type Path, 5 | type SchemaType, 6 | } from 'sanity' 7 | 8 | type ObjectFieldWithPath = ObjectField & {path: Path} 9 | 10 | /** 11 | * Flattens a document's schema type into a flat array of fields and includes their path 12 | */ 13 | export function flattenSchemaType( 14 | schemaType: SchemaType 15 | ): ObjectFieldWithPath[] { 16 | if (!isDocumentSchemaType(schemaType)) { 17 | console.error(`Schema type is not a document`) 18 | return [] 19 | } 20 | 21 | return extractInnerFields(schemaType.fields, [], 3) 22 | } 23 | 24 | function extractInnerFields( 25 | fields: ObjectField[], 26 | path: Path, 27 | maxDepth: number 28 | ): ObjectFieldWithPath[] { 29 | if (path.length >= maxDepth) { 30 | return [] 31 | } 32 | 33 | return fields.reduce((acc, field) => { 34 | const thisFieldWithPath = {path: [...path, field.name], ...field} 35 | 36 | if (field.type.jsonType === 'object') { 37 | const innerFields = extractInnerFields( 38 | field.type.fields, 39 | [...path, field.name], 40 | maxDepth 41 | ) 42 | 43 | return [...acc, thisFieldWithPath, ...innerFields] 44 | } else if ( 45 | field.type.jsonType === 'array' && 46 | field.type.of.length && 47 | field.type.of.some((item) => 'fields' in item) 48 | ) { 49 | const innerFields = field.type.of.flatMap((innerField) => 50 | extractInnerFields( 51 | // @ts-expect-error - Fix TS assertion for array fields 52 | innerField.fields, 53 | [...path, field.name], 54 | maxDepth 55 | ) 56 | ) 57 | 58 | return [...acc, thisFieldWithPath, ...innerFields] 59 | } 60 | 61 | return [...acc, thisFieldWithPath] 62 | }, []) 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/getDocumentsToTranslate.ts: -------------------------------------------------------------------------------- 1 | import {SanityDocument} from 'sanity' 2 | 3 | export interface DocumentsToTranslate { 4 | path: (string | number)[] 5 | pathString: string 6 | _key: string 7 | _type: string 8 | [key: string]: unknown 9 | } 10 | 11 | export const getDocumentsToTranslate = ( 12 | value: SanityDocument | unknown, 13 | rootPath: (string | number)[] = [] 14 | ): DocumentsToTranslate[] => { 15 | if (Array.isArray(value)) { 16 | const arrayRootPath = [...rootPath] 17 | 18 | // if item contains internationalized return array 19 | const internationalizedValues = value.filter((item) => { 20 | if (Array.isArray(item)) return false 21 | 22 | if (typeof item === 'object') { 23 | const type = item?._type as string | undefined 24 | return ( 25 | type?.startsWith('internationalizedArray') && type?.endsWith('Value') 26 | ) 27 | } 28 | return false 29 | }) 30 | 31 | if (internationalizedValues.length > 0) { 32 | return internationalizedValues.map((internationalizedValue) => { 33 | return { 34 | ...internationalizedValue, 35 | path: arrayRootPath, 36 | pathString: arrayRootPath.join('.'), 37 | } 38 | }) 39 | } 40 | 41 | if (value.length > 0) { 42 | return value 43 | .map((item, index) => 44 | getDocumentsToTranslate(item, [...arrayRootPath, index]) 45 | ) 46 | .flat() 47 | } 48 | 49 | return [] 50 | } 51 | if (typeof value === 'object' && value) { 52 | const startsWithUnderscoreRegex = /^_/ 53 | const itemKeys = Object.keys(value).filter( 54 | (key) => !key.match(startsWithUnderscoreRegex) 55 | ) as (keyof typeof value)[] 56 | 57 | return itemKeys 58 | .map((item) => { 59 | const selectedValue = value[item] as unknown 60 | const path = [...rootPath, item] 61 | return getDocumentsToTranslate(selectedValue, path) 62 | }) 63 | .flat() 64 | } 65 | return [] 66 | } 67 | -------------------------------------------------------------------------------- /src/components/AddButtons.tsx: -------------------------------------------------------------------------------- 1 | import {AddIcon} from '@sanity/icons' 2 | import {Button, Grid} from '@sanity/ui' 3 | import type React from 'react' 4 | import {memo} from 'react' 5 | 6 | import {MAX_COLUMNS} from '../constants' 7 | import type {Language, Value} from '../types' 8 | import {getLanguageDisplay} from '../utils/getLanguageDisplay' 9 | import {useInternationalizedArrayContext} from './InternationalizedArrayContext' 10 | 11 | type AddButtonsProps = { 12 | languages: Language[] 13 | readOnly: boolean 14 | value: Value[] | undefined 15 | onClick: (event: React.MouseEvent) => void 16 | } 17 | 18 | function AddButtons(props: AddButtonsProps) { 19 | const {languages, readOnly, value, onClick} = props 20 | const {languageDisplay} = useInternationalizedArrayContext() 21 | 22 | return languages.length > 0 ? ( 23 | 27 | {languages.map((language) => { 28 | const languageTitle: string = getLanguageDisplay( 29 | languageDisplay, 30 | language.title, 31 | language.id 32 | ) 33 | return ( 34 |