@sanity/document-internationalization 1.0.0 → 1.0.1

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 (114) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +89 -47
  3. package/lib/index.esm.js +2 -0
  4. package/lib/index.esm.js.map +1 -0
  5. package/lib/index.js +2 -0
  6. package/lib/index.js.map +1 -0
  7. package/lib/src/index.d.ts +200 -0
  8. package/package.json +100 -43
  9. package/src/actions/DeleteWithi18nAction.tsx +118 -0
  10. package/src/actions/DuplicateWithi18nAction.tsx +77 -0
  11. package/src/actions/PublishWithi18nAction.ts +118 -0
  12. package/src/actions/index.ts +42 -0
  13. package/src/badges/LanguageBadge.tsx +22 -0
  14. package/src/constants/I18nDelimiter.ts +1 -0
  15. package/src/constants/I18nPrefix.ts +1 -0
  16. package/src/constants/IdStructure.ts +4 -0
  17. package/src/constants/LanguageCultures.ts +902 -0
  18. package/src/constants/ReferenceBehavior.ts +5 -0
  19. package/src/constants/SupportedEmojiFlagCodes.ts +251 -0
  20. package/src/constants/UiMessages.ts +56 -0
  21. package/src/constants/index.ts +7 -0
  22. package/src/documentI18n.tsx +33 -0
  23. package/src/hooks/index.ts +2 -0
  24. package/src/hooks/useConfig.ts +22 -0
  25. package/src/hooks/useDelayedFlag.ts +31 -0
  26. package/src/index.ts +6 -1
  27. package/src/language-select/components/LanguageSelect/LanguageConfigContext.tsx +4 -0
  28. package/src/language-select/components/LanguageSelect/LanguageSelect.tsx +188 -0
  29. package/src/language-select/components/LanguageSelect/LanguageSelectContext.ts +16 -0
  30. package/src/language-select/components/LanguageSelect/LanguageSelectLabel.tsx +12 -0
  31. package/src/language-select/components/LanguageSelect/LanguageSelectList.tsx +84 -0
  32. package/src/language-select/components/LanguageSelect/LanguageSelectListItem.tsx +251 -0
  33. package/src/language-select/components/LanguageSelect/index.tsx +25 -0
  34. package/src/language-select/components/SingleFlag/SingleFlag.tsx +44 -0
  35. package/src/language-select/components/SingleFlag/allEmojiFlagCodes.ts +251 -0
  36. package/src/language-select/components/SingleFlag/index.ts +1 -0
  37. package/src/language-select/components/SplitPaneIcon/SplitPaneIcon.tsx +25 -0
  38. package/src/language-select/components/SplitPaneIcon/index.ts +1 -0
  39. package/src/language-select/components/index.ts +2 -0
  40. package/src/language-select/hooks/index.ts +1 -0
  41. package/src/language-select/hooks/useLanguages.ts +37 -0
  42. package/src/language-select/hooks/useListeningQuery.ts +66 -0
  43. package/src/structure/IDefaultDocumentNodeStructureProps.ts +4 -0
  44. package/src/structure/components/MaintenanceTab/MaintenanceTab.tsx +13 -0
  45. package/src/structure/components/MaintenanceTab/MaintenanceTabContent.tsx +259 -0
  46. package/src/structure/components/MaintenanceTab/index.ts +1 -0
  47. package/src/structure/components/MaintenanceTabResult/MaintenanceTabResult.tsx +44 -0
  48. package/src/structure/components/MaintenanceTabResult/index.ts +1 -0
  49. package/src/structure/components/MaintenanceTabTypeSelector/MaintenanceTabTypeSelector.tsx +56 -0
  50. package/src/structure/components/MaintenanceTabTypeSelector/index.ts +1 -0
  51. package/src/structure/hooks/index.ts +1 -0
  52. package/src/structure/hooks/useDocumentsInformation.ts +110 -0
  53. package/src/structure/index.tsx +120 -0
  54. package/src/structure/utils/fixBaseDocumentRefs.ts +30 -0
  55. package/src/structure/utils/fixBaseLanguageMismatch.ts +21 -0
  56. package/src/structure/utils/fixIdStructureMismatchDocuments.ts +72 -0
  57. package/src/structure/utils/fixLanguageFields.ts +32 -0
  58. package/src/structure/utils/fixOrphanedDocuments.ts +22 -0
  59. package/src/structure/utils/fixTranslationRefs.ts +50 -0
  60. package/src/structure/utils/index.ts +6 -0
  61. package/src/types/IEditState.ts +6 -0
  62. package/src/types/IExtendedLanguageObject.ts +6 -0
  63. package/src/types/ILanguageObject.ts +4 -0
  64. package/src/types/ILanguageQuery.ts +13 -0
  65. package/src/types/IResolverProps.ts +10 -0
  66. package/src/types/ITranslationRef.ts +6 -0
  67. package/src/types/IType.ts +40 -0
  68. package/src/types/IUseDocumentOperationResult.ts +11 -0
  69. package/src/types/TFieldNamesConfig.ts +5 -0
  70. package/src/types/TLanguage.ts +3 -0
  71. package/src/types/TLanguagesOption.ts +4 -0
  72. package/src/types/TSchema.ts +6 -0
  73. package/src/types/Ti18nConfig.ts +33 -0
  74. package/src/types/Ti18nDocument.ts +14 -0
  75. package/src/types/Ti18nSchema.ts +13 -0
  76. package/src/types/index.ts +15 -0
  77. package/src/utils/applyConfig.ts +43 -0
  78. package/src/utils/baseToTop.ts +5 -0
  79. package/src/utils/buildDocId.ts +8 -0
  80. package/src/utils/createSanityReference.ts +11 -0
  81. package/src/utils/getBaseIdFromId.ts +20 -0
  82. package/src/utils/getBaseLanguage.ts +6 -0
  83. package/src/utils/getFlag.ts +38 -0
  84. package/src/utils/getLanguageFromDocument.ts +9 -0
  85. package/src/utils/getLanguageFromId.ts +13 -0
  86. package/src/utils/getLanguagesFromOption.ts +40 -0
  87. package/src/utils/getTranslationsForId.ts +39 -0
  88. package/src/utils/index.ts +14 -0
  89. package/src/utils/makeObjectKey.ts +3 -0
  90. package/src/utils/normalizeLanguageList.ts +26 -0
  91. package/src/utils/serializePath.ts +11 -0
  92. package/src/utils/updateIntlFieldsForDocument.ts +79 -0
  93. package/src/utils/useSanityClient.ts +6 -0
  94. package/src/validators/index.ts +1 -0
  95. package/src/validators/isSlugUnique.ts +56 -0
  96. package/src/withDocumentI18nPlugin.ts +24 -0
  97. package/v2-incompatible.js +1 -1
  98. package/lib/cjs/index.js +0 -539
  99. package/lib/cjs/index.js.map +0 -1
  100. package/lib/esm/index.js +0 -532
  101. package/lib/esm/index.js.map +0 -1
  102. package/lib/types/index.d.ts +0 -12
  103. package/lib/types/index.d.ts.map +0 -1
  104. package/src/LanguageManage.tsx +0 -25
  105. package/src/LanguageOption.tsx +0 -158
  106. package/src/LanguagePatch.tsx +0 -61
  107. package/src/MenuButton.tsx +0 -101
  108. package/src/badges/index.tsx +0 -23
  109. package/src/constants.ts +0 -1
  110. package/src/hooks/useLanguageMetadata.tsx +0 -21
  111. package/src/hooks/useOpenInNewPane.tsx +0 -27
  112. package/src/plugin.tsx +0 -104
  113. package/src/schema/translation/metadata.ts +0 -42
  114. package/src/types.ts +0 -24
@@ -0,0 +1,6 @@
1
+ import {ILanguageObject} from '../types'
2
+
3
+ export const getBaseLanguage = (langs: ILanguageObject[], base?: string) => {
4
+ if (base) return langs.find((l) => l.id === base) || null
5
+ return langs.length > 0 ? langs[0] : null
6
+ }
@@ -0,0 +1,38 @@
1
+ import {getEmojiFlag} from '@cprecioso/country-flag-emoji'
2
+ import {SupportedEmojiFlagCodes} from '../constants'
3
+
4
+ // Get flag from a valid country code
5
+ export const getFlag = (code = ``): string => {
6
+ if (!code) {
7
+ return ``
8
+ }
9
+
10
+ const flagCode = getFlagCode(code)
11
+
12
+ if (!SupportedEmojiFlagCodes.includes(flagCode.toUpperCase())) {
13
+ return ``
14
+ }
15
+
16
+ const emoji = getEmojiFlag(flagCode)
17
+
18
+ return emoji
19
+ }
20
+
21
+ // Convert some language codes to country codes
22
+ export const getFlagCode = (code = ``): string => {
23
+ if (!code) {
24
+ return ``
25
+ }
26
+
27
+ switch (code.toLocaleLowerCase()) {
28
+ // Return :flag-gb: for English
29
+ case `en`:
30
+ return `gb`
31
+ // Return :flag-al: (Albania) for `sq` language code
32
+ case `sq`:
33
+ return `al`
34
+
35
+ default:
36
+ return code
37
+ }
38
+ }
@@ -0,0 +1,9 @@
1
+ import type {SanityDocument} from '@sanity/client'
2
+ import type {ApplyConfigResult} from './applyConfig'
3
+
4
+ export function getLanguageFromDocument(
5
+ doc: SanityDocument,
6
+ config: ApplyConfigResult
7
+ ): string | null {
8
+ return doc?.[config.fieldNames.lang] || config.base || null
9
+ }
@@ -0,0 +1,13 @@
1
+ import {I18nPrefix, I18nDelimiter} from '../constants'
2
+
3
+ export const getLanguageFromId = (id: string): string | undefined => {
4
+ // subpath
5
+ const rx = new RegExp(`${I18nPrefix}\\.[^.]+\\.([^.]+)`)
6
+ const match = id.match(rx)
7
+ if (match && match.length === 2) return match[1]
8
+
9
+ // delimiter
10
+ const split = id.split(I18nDelimiter)
11
+ if (split.length > 1) return split[1]
12
+ return undefined
13
+ }
@@ -0,0 +1,40 @@
1
+ import get from 'just-safe-get'
2
+ import type {SanityClient, SanityDocument} from '@sanity/client'
3
+ import {ILanguageObject, ILanguageQuery, TLanguagesOption} from '../types'
4
+ import type {ApplyConfigResult} from '../utils'
5
+ import {normalizeLanguageList} from './normalizeLanguageList'
6
+
7
+ export const getLanguagesFromOption = async <D extends SanityDocument>(
8
+ client: SanityClient,
9
+ config: ApplyConfigResult,
10
+ langs: TLanguagesOption,
11
+ document?: D | null
12
+ ): Promise<ILanguageObject[]> => {
13
+ const languages = normalizeLanguageList(
14
+ await (async () => {
15
+ if (Array.isArray(langs)) return langs
16
+ const r = await client.fetch<ILanguageQuery['value'][]>(langs.query)
17
+ const value = langs.value
18
+
19
+ if (typeof value === 'string') return r.map((l) => get(l, value))
20
+ return r.map((l) => {
21
+ // @deprecated
22
+ if ('name' in value) {
23
+ return {
24
+ name: get(l, value.name),
25
+ title: get(l, value.title),
26
+ }
27
+ }
28
+
29
+ return {
30
+ id: get(l, value.id),
31
+ title: get(l, value.title),
32
+ }
33
+ })
34
+ })()
35
+ )
36
+ if (config.languagesLoader) {
37
+ return config.languagesLoader(languages, document ?? undefined)
38
+ }
39
+ return languages
40
+ }
@@ -0,0 +1,39 @@
1
+ import {SanityClient} from '@sanity/client'
2
+ import type {ApplyConfigResult} from '../utils'
3
+ import {I18nDelimiter, IdStructure} from '../constants'
4
+ import {Ti18nDocument} from '../types'
5
+ import {buildDocId} from './buildDocId'
6
+
7
+ export const getTranslationsFor = async (
8
+ client: SanityClient,
9
+ config: ApplyConfigResult,
10
+ baseDocumentId: string,
11
+ includeDrafts = false
12
+ ): Promise<Ti18nDocument[]> => {
13
+ if (config.idStructure === IdStructure.DELIMITER) {
14
+ const segments = baseDocumentId.split('-')
15
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${I18nDelimiter}*`
16
+ const documents = await client.fetch<Ti18nDocument[]>(
17
+ includeDrafts
18
+ ? '*[_id match $segments]'
19
+ : `*[_id match $segments && !(_id in path('drafts.**'))]`,
20
+ {segments}
21
+ )
22
+
23
+ return documents
24
+ ? documents.filter(
25
+ (d) =>
26
+ d._id.startsWith(baseDocumentId) ||
27
+ (includeDrafts && d._id.startsWith(`drafts.${baseDocumentId}`))
28
+ )
29
+ : []
30
+ }
31
+ const documents = await client.fetch<Ti18nDocument[]>(
32
+ includeDrafts ? '*[_id in path($path) || _id in path($draftPath)]' : '*[_id in path($path)]',
33
+ {
34
+ path: buildDocId(config, baseDocumentId, '*'),
35
+ draftPath: `drafts.${buildDocId(config, baseDocumentId, '*')}`,
36
+ }
37
+ )
38
+ return documents ?? []
39
+ }
@@ -0,0 +1,14 @@
1
+ export * from './buildDocId'
2
+ export * from './normalizeLanguageList'
3
+ export * from './getBaseLanguage'
4
+ export * from './useSanityClient'
5
+ export * from './getLanguagesFromOption'
6
+ export * from './getLanguageFromId'
7
+ export * from './getBaseIdFromId'
8
+ export * from './getTranslationsForId'
9
+ export * from './makeObjectKey'
10
+ export * from './createSanityReference'
11
+ export * from './updateIntlFieldsForDocument'
12
+ export * from './serializePath'
13
+ export * from './applyConfig'
14
+ export * from './getLanguageFromDocument'
@@ -0,0 +1,3 @@
1
+ export function makeObjectKey(input: string) {
2
+ return input.replace(/[^a-zA-Z0-9_]/g, '_')
3
+ }
@@ -0,0 +1,26 @@
1
+ import {ILanguageObject} from '../types'
2
+ import {LanguageCultures} from '../constants'
3
+
4
+ type LanguageConfigObject =
5
+ | ILanguageObject
6
+ | (Omit<ILanguageObject, 'id'> & {
7
+ name: string
8
+ })
9
+
10
+ export const normalizeLanguageList = (languages: (string | LanguageConfigObject)[]) =>
11
+ languages.map<ILanguageObject>((l) => {
12
+ if (typeof l === 'string') {
13
+ const langCult = LanguageCultures.find((x) => x.value === l)
14
+ if (langCult) return {title: langCult.title, id: l}
15
+ return {title: l, id: l}
16
+ }
17
+
18
+ if ('name' in l) {
19
+ console.warn(
20
+ `The "name" field in your language configuration is deprecated, please replace it with "id".`
21
+ )
22
+ return {title: l.title, id: l.name}
23
+ }
24
+
25
+ return {title: l.title, id: l.id}
26
+ })
@@ -0,0 +1,11 @@
1
+ import {Path, isKeyedObject} from 'sanity'
2
+
3
+ export function serializePath(path: Path): string {
4
+ return path.reduce<string>((target, part, i) => {
5
+ const isIndex = typeof part === 'number'
6
+ const isKey = isKeyedObject(part)
7
+ const separator = i === 0 ? '' : '.'
8
+ const add = isIndex || isKey ? '[]' : `${separator}${part}`
9
+ return `${target}${add}`
10
+ }, '')
11
+ }
@@ -0,0 +1,79 @@
1
+ import compact from 'lodash/compact'
2
+ import {SanityClient, SanityDocument} from '@sanity/client'
3
+ import {ITranslationRef} from '../types'
4
+ import {ReferenceBehavior} from '../constants'
5
+ import {ApplyConfigResult} from '../utils'
6
+ import {getLanguagesFromOption} from './getLanguagesFromOption'
7
+ import {getBaseLanguage} from './getBaseLanguage'
8
+ import {getTranslationsFor} from './getTranslationsForId'
9
+ import {getBaseIdFromId} from './getBaseIdFromId'
10
+ import {createSanityReference} from './createSanityReference'
11
+ import {getLanguageFromDocument} from './getLanguageFromDocument'
12
+
13
+ // @TODO make this into a hook so the hook
14
+ // can look up the existance of a base document on its own
15
+ export async function updateIntlFieldsForDocument(
16
+ client: SanityClient,
17
+ config: ApplyConfigResult,
18
+ document: SanityDocument,
19
+ baseDocument?: SanityDocument
20
+ ): Promise<void> {
21
+ const {_type: type, _id: id} = document
22
+ const baseDocumentId = getBaseIdFromId(id)
23
+ const isTranslation = id !== baseDocumentId
24
+ const fieldName = config.fieldNames.lang
25
+ const refsFieldName = config.fieldNames.references
26
+ const baseRefFieldName = config.fieldNames.baseReference
27
+ const langs = await getLanguagesFromOption(client, config, config.languages, document)
28
+ const languageId =
29
+ getLanguageFromDocument(document, config) || getBaseLanguage(langs, config.base)?.id
30
+
31
+ // Update I18n field for current document
32
+ const currentDocumentTransaction = client.transaction()
33
+ currentDocumentTransaction.createIfNotExists({_id: id, _type: type})
34
+ currentDocumentTransaction.patch(id, {
35
+ set: {
36
+ [fieldName]: languageId,
37
+ ...(isTranslation && config.referenceBehavior !== ReferenceBehavior.DISABLED
38
+ ? {
39
+ [baseRefFieldName]: createSanityReference(
40
+ baseDocumentId,
41
+ config.referenceBehavior === ReferenceBehavior.WEAK
42
+ ),
43
+ }
44
+ : {}),
45
+ },
46
+ })
47
+ await currentDocumentTransaction.commit()
48
+
49
+ // update base document reference if required
50
+ if (baseDocument) {
51
+ const translatedDocuments = await getTranslationsFor(client, config, baseDocumentId)
52
+ if (translatedDocuments.length > 0) {
53
+ const baseDocumentTransaction = client.transaction()
54
+ let translatedRefs: ITranslationRef[] = []
55
+ if (config.referenceBehavior !== ReferenceBehavior.DISABLED) {
56
+ translatedRefs = compact(
57
+ translatedDocuments.map((doc) => {
58
+ const lang = getLanguageFromDocument(doc, config)
59
+ if (!lang) return null
60
+ return {
61
+ _key: lang,
62
+ ...createSanityReference(
63
+ doc._id,
64
+ config.referenceBehavior === ReferenceBehavior.WEAK
65
+ ),
66
+ }
67
+ }, {})
68
+ )
69
+ }
70
+ // baseDocumentTransaction.createIfNotExists({_id: baseDocumentId, _type: type})
71
+ baseDocumentTransaction.patch(baseDocumentId, {
72
+ set: {
73
+ [refsFieldName]: translatedRefs,
74
+ },
75
+ })
76
+ await baseDocumentTransaction.commit()
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,6 @@
1
+ import {SanityClient} from '@sanity/client'
2
+ import {useClient} from 'sanity'
3
+
4
+ export function useSanityClient(): SanityClient {
5
+ return useClient({apiVersion: '2021-10-01'})
6
+ }
@@ -0,0 +1 @@
1
+ export * from './isSlugUnique'
@@ -0,0 +1,56 @@
1
+ import type {SlugIsUniqueValidator} from 'sanity'
2
+ import type {SanityClient} from '@sanity/client'
3
+ import {I18nDelimiter} from '../constants'
4
+ import {getBaseIdFromId, serializePath} from '../utils'
5
+
6
+ /*
7
+ * most of this is taken from the deafultUnique function in @sanity/validation
8
+ */
9
+ export function createIsSlugUnique(client: SanityClient): SlugIsUniqueValidator {
10
+ return (slug, context) => {
11
+ const {document, path, type} = context
12
+ const schemaOptions = type?.options as {disableArrayWarning?: boolean} | undefined
13
+
14
+ if (!document) {
15
+ throw new Error(`\`document\` was not provided in validation context.`)
16
+ }
17
+ if (!path) {
18
+ throw new Error(`\`path\` was not provided in validation context.`)
19
+ }
20
+
21
+ const disableArrayWarning = schemaOptions?.disableArrayWarning || false
22
+ const baseId = getBaseIdFromId(document._id)
23
+ const docType = document._type
24
+ const atPath = serializePath(path.concat('current'))
25
+
26
+ if (!disableArrayWarning && atPath.includes('[]')) {
27
+ const serializedPath = serializePath(path)
28
+ console.warn(
29
+ [
30
+ `Slug field at path ${serializedPath} is within an array and cannot be automatically checked for uniqueness`,
31
+ `If you need to check for uniqueness, provide your own "isUnique" method`,
32
+ `To disable this message, set \`disableArrayWarning: true\` on the slug \`options\` field`,
33
+ ].join('\n')
34
+ )
35
+ }
36
+
37
+ const constraints = [
38
+ '_type == $docType',
39
+ '!(_id in path("drafts.**"))', // exclude drafts
40
+ '_id != $baseId', // exclude own base document
41
+ '!(_id in path("i18n." + $baseId + ".*"))', // exclude any subpath translations
42
+ `!(_id match $baseId + "${I18nDelimiter}*")`, // exclude any delimiter based translations
43
+ `${atPath} == $slug`,
44
+ ].join(' && ')
45
+
46
+ return client.fetch<boolean>(
47
+ `!defined(*[${constraints}][0]._id)`,
48
+ {
49
+ docType,
50
+ baseId,
51
+ slug,
52
+ },
53
+ {tag: 'validation.slug-is-unique'}
54
+ )
55
+ }
56
+ }
@@ -0,0 +1,24 @@
1
+ import compact from 'lodash/compact'
2
+ import type {PluginOptions} from 'sanity'
3
+ import {deskTool} from 'sanity/desk'
4
+ import type {Ti18nConfig} from './types'
5
+ import {documentI18n} from './documentI18n'
6
+ import {getDocumentList} from './structure'
7
+
8
+ export function withDocumentI18nPlugin(
9
+ plugins: PluginOptions[] | ((config: Ti18nConfig) => PluginOptions[]),
10
+ config: Ti18nConfig & {
11
+ includeDeskTool?: boolean
12
+ }
13
+ ): PluginOptions[] {
14
+ const i18nplugin = documentI18n(config)
15
+ return compact([
16
+ ...(Array.isArray(plugins) ? plugins : plugins(config)),
17
+ config.includeDeskTool ?? true
18
+ ? deskTool({
19
+ structure: (S, {schema}) => getDocumentList({S, schema, config}),
20
+ })
21
+ : null,
22
+ i18nplugin,
23
+ ])
24
+ }
@@ -5,7 +5,7 @@ export default showIncompatiblePluginDialog({
5
5
  name: name,
6
6
  versions: {
7
7
  v3: version,
8
- v2: undefined,
8
+ v2: '^0.3.2',
9
9
  },
10
10
  sanityExchangeUrl,
11
11
  })