sanity-plugin-internationalized-array 3.2.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +3 -34
  3. package/{lib → dist}/index.d.ts +41 -61
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +882 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +37 -75
  8. package/lib/index.d.mts +0 -149
  9. package/lib/index.esm.js +0 -933
  10. package/lib/index.esm.js.map +0 -1
  11. package/lib/index.js +0 -942
  12. package/lib/index.js.map +0 -1
  13. package/lib/index.mjs +0 -933
  14. package/lib/index.mjs.map +0 -1
  15. package/sanity.json +0 -8
  16. package/src/cache.ts +0 -148
  17. package/src/components/AddButtons.tsx +0 -60
  18. package/src/components/DocumentAddButtons.tsx +0 -183
  19. package/src/components/Feedback.tsx +0 -28
  20. package/src/components/InternationalizedArray.tsx +0 -286
  21. package/src/components/InternationalizedArrayContext.tsx +0 -136
  22. package/src/components/InternationalizedField.tsx +0 -57
  23. package/src/components/InternationalizedInput.tsx +0 -257
  24. package/src/components/Preload.tsx +0 -31
  25. package/src/components/createFieldName.ts +0 -20
  26. package/src/components/getSelectedValue.ts +0 -31
  27. package/src/components/getToneFromValidation.ts +0 -20
  28. package/src/constants.ts +0 -18
  29. package/src/fieldActions/index.ts +0 -138
  30. package/src/index.ts +0 -3
  31. package/src/plugin.tsx +0 -87
  32. package/src/schema/array.ts +0 -148
  33. package/src/schema/object.ts +0 -36
  34. package/src/types.ts +0 -135
  35. package/src/utils/checkAllLanguagesArePresent.ts +0 -14
  36. package/src/utils/createAddAllTitle.ts +0 -16
  37. package/src/utils/createAddLanguagePatches.ts +0 -84
  38. package/src/utils/createValueSchemaTypeName.ts +0 -5
  39. package/src/utils/flattenSchemaType.ts +0 -63
  40. package/src/utils/getDocumentsToTranslate.ts +0 -66
  41. package/src/utils/getLanguageDisplay.ts +0 -13
  42. package/src/utils/getLanguagesFieldOption.ts +0 -16
  43. package/v2-incompatible.js +0 -11
@@ -1,148 +0,0 @@
1
- /* eslint-disable no-nested-ternary */
2
- import {defineField, type FieldDefinition, type Rule} from 'sanity'
3
-
4
- import {getFunctionCache, peek, setFunctionCache} from '../cache'
5
- import {createFieldName} from '../components/createFieldName'
6
- import {getSelectedValue} from '../components/getSelectedValue'
7
- import InternationalizedArray from '../components/InternationalizedArray'
8
- import type {Language, LanguageCallback, Value} from '../types'
9
- import {getLanguagesFieldOption} from '../utils/getLanguagesFieldOption'
10
-
11
- type ArrayFactoryConfig = {
12
- apiVersion: string
13
- select?: Record<string, string>
14
- languages: Language[] | LanguageCallback
15
- defaultLanguages?: string[]
16
- type: string | FieldDefinition
17
- }
18
-
19
- export type ArrayFieldOptions = Pick<
20
- ArrayFactoryConfig,
21
- 'apiVersion' | 'select' | 'languages'
22
- >
23
-
24
- export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
25
- const {apiVersion, select, languages, type} = config
26
- const typeName = typeof type === `string` ? type : type.name
27
- const arrayName = createFieldName(typeName)
28
- const objectName = createFieldName(typeName, true)
29
-
30
- return defineField({
31
- name: arrayName,
32
- title: 'Internationalized array',
33
- type: 'array',
34
- components: {
35
- input: InternationalizedArray,
36
- },
37
- options: {
38
- // @ts-expect-error - these options are required for validation rules – not the custom input component
39
- apiVersion,
40
- select,
41
- languages,
42
- },
43
- of: [
44
- defineField({
45
- ...(typeof type === 'string' ? {} : type),
46
- name: objectName,
47
- type: objectName,
48
- }),
49
- ],
50
- // @ts-expect-error - fix typings
51
- validation: (rule: Rule) =>
52
- rule.custom<Value[]>(async (value, context) => {
53
- if (!value || value.length === 0) {
54
- return true
55
- }
56
-
57
- // Early return for simple cases to avoid expensive operations
58
- if (value.length === 1 && !value[0]?._key) {
59
- return true
60
- }
61
-
62
- const selectedValue = getSelectedValue(select, context.document)
63
- const client = context.getClient({apiVersion})
64
-
65
- let contextLanguages: Language[] = []
66
- const languagesFieldOption = getLanguagesFieldOption(context?.type)
67
-
68
- if (Array.isArray(languagesFieldOption)) {
69
- contextLanguages = languagesFieldOption
70
- } else if (Array.isArray(peek(selectedValue))) {
71
- contextLanguages = peek(selectedValue) || []
72
- } else if (typeof languagesFieldOption === 'function') {
73
- // Try to get from function cache first (if it's the same function as the component)
74
- const cachedLanguages = getFunctionCache(
75
- languagesFieldOption,
76
- selectedValue
77
- )
78
-
79
- if (Array.isArray(cachedLanguages)) {
80
- contextLanguages = cachedLanguages
81
- } else {
82
- // Try suspend cache as fallback
83
- const suspendCachedLanguages = peek(selectedValue)
84
- if (Array.isArray(suspendCachedLanguages)) {
85
- contextLanguages = suspendCachedLanguages
86
- } else {
87
- // Only make the async call if we don't have cached data
88
- contextLanguages = await languagesFieldOption(
89
- client,
90
- selectedValue
91
- )
92
- // Cache the result for future validation calls
93
- setFunctionCache(
94
- languagesFieldOption,
95
- selectedValue,
96
- contextLanguages
97
- )
98
- }
99
- }
100
- }
101
-
102
- if (value && value.length > contextLanguages.length) {
103
- return `Cannot be more than ${
104
- contextLanguages.length === 1
105
- ? `1 item`
106
- : `${contextLanguages.length} items`
107
- }`
108
- }
109
-
110
- // Create a Set for faster language ID lookups
111
- const languageIds = new Set(contextLanguages.map((lang) => lang.id))
112
-
113
- // Check for invalid language keys
114
- const nonLanguageKeys = value.filter(
115
- (item) => item?._key && !languageIds.has(item._key)
116
- )
117
- if (nonLanguageKeys.length) {
118
- return {
119
- message: `Array item keys must be valid languages registered to the field type`,
120
- paths: nonLanguageKeys.map((item) => [{_key: item._key}]),
121
- }
122
- }
123
-
124
- // Check for duplicate language keys (more efficient)
125
- const seenKeys = new Set<string>()
126
- const duplicateValues: Value[] = []
127
-
128
- for (const item of value) {
129
- if (item?._key) {
130
- if (seenKeys.has(item._key)) {
131
- duplicateValues.push(item)
132
- } else {
133
- seenKeys.add(item._key)
134
- }
135
- }
136
- }
137
-
138
- if (duplicateValues.length) {
139
- return {
140
- message: 'There can only be one field per language',
141
- paths: duplicateValues.map((item) => [{_key: item._key}]),
142
- }
143
- }
144
-
145
- return true
146
- }),
147
- })
148
- }
@@ -1,36 +0,0 @@
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
- }
package/src/types.ts DELETED
@@ -1,135 +0,0 @@
1
- import type {
2
- FieldDefinition,
3
- Rule,
4
- RuleTypeConstraint,
5
- SanityClient,
6
- } from 'sanity'
7
-
8
- export type Language = {
9
- id: Intl.UnicodeBCP47LocaleIdentifier
10
- title: string
11
- }
12
-
13
- export type AllowedType = 'string' | 'number' | 'boolean' | 'text' | 'reference'
14
-
15
- export type ArrayConfig = {
16
- name: string
17
- type: AllowedType
18
- languages: Language[]
19
- title?: string
20
- group?: string
21
- hidden?: boolean | (() => boolean)
22
- readOnly?: boolean | (() => boolean)
23
- validation?: Rule | Rule[]
24
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
- field?: {[key: string]: any; options: {[key: string]: any}}
26
- }
27
-
28
- export type Value = {
29
- _key: string
30
- value?: unknown
31
- }
32
-
33
- export type LanguageCallback = (
34
- client: SanityClient,
35
- selectedValue: Record<string, unknown>
36
- ) => Promise<Language[]>
37
-
38
- export type LanguageDisplay = 'titleOnly' | 'codeOnly' | 'titleAndCode'
39
-
40
- export type PluginConfig = {
41
- /**
42
- * https://www.sanity.io/docs/api-versioning
43
- * @defaultValue '2025-10-15'
44
- */
45
- apiVersion?: string
46
- /**
47
- * Specify fields that should be available in the language callback:
48
- * ```tsx
49
- * {
50
- * select: {
51
- * markets: 'markets'
52
- * },
53
- * languages: (client, {markets}) =>
54
- * query.fetch(groq`*[_type == "language" && market in $markets]{id,title}`, {markets})
55
- * }
56
- * ```
57
- */
58
- select?: Record<string, string>
59
- /**
60
- * You can give it an array of language definitions:
61
- * ```tsx
62
- * {
63
- * languages: [
64
- * {id: 'en', title: 'English'},
65
- * {id: 'fr', title: 'French'}
66
- * ]
67
- * }
68
- * ```
69
- * You can load them async by passing a function that returns a promise:
70
- * ```tsx
71
- * {
72
- * languages: async () => {
73
- * const response = await fetch('https://example.com/languages')
74
- * return response.json()
75
- * }
76
- * }
77
- * ```
78
- * You can query your dataset for languages::
79
- * ```tsx
80
- * {
81
- * languages: (client) =>
82
- * query.fetch(groq`*[_type == "language"]{id,title}`)
83
- * }
84
- * ```
85
- */
86
- languages: Language[] | LanguageCallback
87
- /**
88
- * You can specify a list of language IDs that should be pre-filled when creating a new document
89
- * ```tsx
90
- * {
91
- * defaultLanguages: ['en']
92
- * }
93
- * ```
94
- */
95
- defaultLanguages?: string[]
96
- /**
97
- * Can be a string matching core field types, as well as custom ones:
98
- * ```tsx
99
- * {
100
- * fieldTypes: [
101
- * "date", "datetime", "file", "image", "number", "string", "text", "url"
102
- * ]
103
- * }
104
- * ```
105
- * You can also define a type directly:
106
- * ```tsx
107
- * {
108
- * fieldTypes: [
109
- * defineField({
110
- * name: 'featuredProduct',
111
- * type: 'reference',
112
- * to: [{type: 'product'}]
113
- * hidden: (({document}) => !document?.title)
114
- * })
115
- * ]
116
- * }
117
- * ```
118
- */
119
- fieldTypes: (string | RuleTypeConstraint | FieldDefinition)[]
120
- /**
121
- * Locations where the "+ EN" add language buttons are visible
122
- * @defaultValue ['field']
123
- * */
124
- buttonLocations?: ('field' | 'unstable__fieldAction' | 'document')[]
125
- /**
126
- * Show or hide the "Add missing languages" button
127
- * @defaultValue true
128
- * */
129
- buttonAddAll?: boolean
130
- /**
131
- * How to display the languages on buttons and fields
132
- * @defaultValue 'code'
133
- * */
134
- languageDisplay?: LanguageDisplay
135
- }
@@ -1,14 +0,0 @@
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
- }
@@ -1,16 +0,0 @@
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
- }
@@ -1,84 +0,0 @@
1
- import {FormInsertPatch, insert, Path, SchemaType} from 'sanity'
2
-
3
- import {Language, Value} from '../types'
4
- import {createValueSchemaTypeName} from './createValueSchemaTypeName'
5
-
6
- type AddConfig = {
7
- // New keys to add to the field
8
- addLanguageKeys: string[]
9
- // Schema of the current field
10
- schemaType: SchemaType
11
- // All languages registered in the plugin
12
- languages: Language[]
13
- // Languages that are currently visible
14
- filteredLanguages: Language[]
15
- // Current value of the internationalizedArray field
16
- value?: Value[]
17
- // Path to this item
18
- path?: Path
19
- }
20
-
21
- export function createAddLanguagePatches(config: AddConfig): FormInsertPatch[] {
22
- const {
23
- addLanguageKeys,
24
- schemaType,
25
- languages,
26
- filteredLanguages,
27
- value,
28
- path = [],
29
- } = config
30
-
31
- const itemBase = {_type: createValueSchemaTypeName(schemaType)}
32
-
33
- // Create new items
34
- const getNewItems = () => {
35
- if (Array.isArray(addLanguageKeys) && addLanguageKeys.length > 0) {
36
- return addLanguageKeys.map((id) => ({
37
- ...itemBase,
38
- _key: id,
39
- }))
40
- }
41
-
42
- return filteredLanguages
43
- .filter((language) =>
44
- value?.length ? !value.find((v) => v._key === language.id) : true
45
- )
46
- .map((language) => ({
47
- ...itemBase,
48
- _key: language.id,
49
- }))
50
- }
51
- const newItems = getNewItems()
52
-
53
- // Insert new items in the correct order
54
- const languagesInUse = value?.length ? value.map((v) => v) : []
55
-
56
- const insertions = newItems.map((item) => {
57
- // What's the original index of this language?
58
- const languageIndex = languages.findIndex((l) => item._key === l.id)
59
-
60
- // What languages are there beyond that index?
61
- const remainingLanguages = languages.slice(languageIndex + 1)
62
-
63
- // So what is the index in the current value array of the next language in the language array?
64
- const nextLanguageIndex = languagesInUse.findIndex((l) =>
65
- // eslint-disable-next-line max-nested-callbacks
66
- remainingLanguages.find((r) => r.id === l._key)
67
- )
68
-
69
- // Keep local state up to date incase multiple insertions are being made
70
- if (nextLanguageIndex < 0) {
71
- languagesInUse.push(item)
72
- } else {
73
- languagesInUse.splice(nextLanguageIndex, 0, item)
74
- }
75
-
76
- return nextLanguageIndex < 0
77
- ? // No next language (-1), add to end of array
78
- insert([item], 'after', [...path, nextLanguageIndex])
79
- : // Next language found, insert before that
80
- insert([item], 'before', [...path, nextLanguageIndex])
81
- })
82
-
83
- return insertions
84
- }
@@ -1,5 +0,0 @@
1
- import {SchemaType} from 'sanity'
2
-
3
- export function createValueSchemaTypeName(schemaType: SchemaType): string {
4
- return `${schemaType.name}Value`
5
- }
@@ -1,63 +0,0 @@
1
- import {
2
- isDocumentSchemaType,
3
- type ObjectField,
4
- type Path,
5
- type SchemaType,
6
- } from 'sanity'
7
-
8
- type ObjectFieldWithPath = ObjectField<SchemaType> & {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<SchemaType>[],
26
- path: Path,
27
- maxDepth: number
28
- ): ObjectFieldWithPath[] {
29
- if (path.length >= maxDepth) {
30
- return []
31
- }
32
-
33
- return fields.reduce<ObjectFieldWithPath[]>((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
- }
@@ -1,66 +0,0 @@
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
- }
@@ -1,13 +0,0 @@
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
- }
@@ -1,16 +0,0 @@
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
- }
@@ -1,11 +0,0 @@
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
- })