Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: i18n array serialization #62

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sanity-naive-html-serializer",
"version": "3.1.7",
"version": "3.2.0-beta.2",
"description": "This is the source for tooling for naively turning documents and rich text fields into HTML, deserializing them, combining them with source documents, and patching them back. Ideally, this should take in objects that are in portable text, text arrays, or objects with text fields without knowing their specific names or types, and be able to patch them back without additional work on the part of the developer.",
"keywords": [
"sanity",
Expand Down
1 change: 0 additions & 1 deletion src/BaseDocumentDeserializer/BaseDocumentDeserializer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// import {htmlToBlocks} from '@sanity/block-tools'
import {htmlToBlocks} from '@sanity/block-tools'
import {customDeserializers, customBlockDeserializers} from '../BaseSerializationConfig'
import {Deserializer} from '../types'
Expand Down
76 changes: 74 additions & 2 deletions src/BaseDocumentMerger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import {Merger} from './types'
import {SanityDocument} from 'sanity'
import {extractWithPath, arrayToJSONMatchPath} from '@sanity/mutator'
import {InsertPatch, SanityDocument} from 'sanity'
import {extractWithPath, arrayToJSONMatchPath, extract} from '@sanity/mutator'

interface I18nArrayItem {
_key: string
_type: string
value: Record<string, any> | string | Array<any>
}

const reconcileArray = (origArray: any[], translatedArray: any[]): any[] => {
//arrays of strings don't have keys, so just replace the array and return
Expand Down Expand Up @@ -106,6 +112,71 @@ const fieldLevelMerge = (
return merged
}

const internationalizedArrayMerge = (
translatedItems: Record<string, any>,
//should be fetched according to the revision and id of the translated obj above
baseDoc: SanityDocument,
localeId: string,
baseLang: string = 'en'
): Record<string, any> => {
const patches: InsertPatch[] = []

//get all keys that match the base language from the translated doc,
//since those are the strings that have been translated
const extractionKey = `..[_key == "${baseLang}"]`
const originPaths = extractWithPath(extractionKey, translatedItems)

//slice off the index to get the arrays at which all the translated fields live
//then transform to string so we can extract
const i18nArrayPaths = originPaths
.map((match) => match.path.slice(0, match.path.length - 1))
.map((path) => arrayToJSONMatchPath(path))

//extract produces duplicates. Likely we need to replace
//the function we're using. For now, just dedupe
Array.from(new Set(i18nArrayPaths)).forEach((path) => {
//we need to merge the translated values with those things
//that were not set off for translation. Get the original first
const origArray = extract(path, baseDoc)[0] as Array<I18nArrayItem>
const origVal = origArray.find((item: I18nArrayItem) => item._key === baseLang)?.value

const translatedVal = (extract(path, translatedItems)[0] as Array<I18nArrayItem>).find(
(item: I18nArrayItem) => item._key === baseLang
)?.value

//then, combine the translated values with the original recursively
let valToPatch
if (typeof translatedVal === 'string') {
valToPatch = translatedVal
} else if (Array.isArray(translatedVal) && translatedVal.length) {
valToPatch = reconcileArray((origVal as Array<any>) ?? [], translatedVal)
} else if (
typeof translatedVal === 'object' &&
Object.keys(translatedVal as Record<string, any>).length
) {
valToPatch = reconcileObject(
(origVal as Record<string, any>) ?? {},
translatedVal as Record<string, any>
)
}
const items = [{_key: localeId, _type: origArray[0]._type, value: valToPatch}]

//check the original array to see what operation we should run
//(we don't want duplicates of locale keys)
const existingLocaleKey = origArray.find((item) => item._key === localeId)
const patch: InsertPatch = existingLocaleKey
? {
replace: `${path}[_key == "${localeId}"]`,
items,
}
: {after: `${path}[-1]`, items}

patches.push(patch)
})

return patches
}

const documentLevelMerge = (
translatedFields: Record<string, any>,
//should be fetched according to the revision and id of the translated obj above
Expand All @@ -117,6 +188,7 @@ const documentLevelMerge = (
export const BaseDocumentMerger: Merger = {
fieldLevelMerge,
documentLevelMerge,
internationalizedArrayMerge,
reconcileArray,
reconcileObject,
}
118 changes: 0 additions & 118 deletions src/BaseDocumentSerializer/fieldFilters.ts

This file was deleted.

39 changes: 39 additions & 0 deletions src/BaseDocumentSerializer/fieldFilters/baseFieldFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {ObjectField, TypedObject} from 'sanity'

const META_FIELDS = ['_key', '_type', '_id']

/*
* Eliminates stop-types and non-localizable fields
* for document-level translation.
*/
export const fieldFilter = (
obj: Record<string, any>,
objFields: ObjectField[],
stopTypes: string[]
): TypedObject => {
const filteredObj: TypedObject = {_type: obj._type}

const fieldFilterFunc = (field: Record<string, any>) => {
if (field.localize === false) {
return false
} else if (field.type === 'string' || field.type === 'text') {
return true
} else if (Array.isArray(obj[field.name])) {
return true
} else if (!stopTypes.includes(field.type)) {
return true
}
return false
}

const validFields = [
...META_FIELDS,
...objFields?.filter(fieldFilterFunc)?.map((field) => field.name),
]
validFields.forEach((field) => {
if (obj[field]) {
filteredObj[field] = obj[field]
}
})
return filteredObj
}
3 changes: 3 additions & 0 deletions src/BaseDocumentSerializer/fieldFilters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export {fieldFilter} from './baseFieldFilter'
export {languageObjectFieldFilter} from './languageObjectFieldFilter'
export {internationalizedArrayFilter} from './internationalizedArrayFilter'
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {SanityDocument, TypedObject} from 'sanity'

const META_FIELDS = ['_key', '_type', '_id']

const isValidInternationalizedArray = (arr: any[], baseLang: string): boolean => {
const internationalizedRegex = /^internationalizedArray/
return (
arr.length > 0 &&
typeof arr[0] === 'object' &&
internationalizedRegex.test(arr[0]._type) &&
arr.filter((obj) => obj._key === baseLang).length > 0
)
}

const filterToBaseLang = (arr: TypedObject[], baseLang: string) => {
return arr.filter((obj) => obj._key === baseLang)
}

/*
* Reduces an array like [
* {_key: 'en', _type: 'internationalizedArrayStringValue', value: 'eng text'},
* {_key: 'es', _type: 'internationalizedArrayStringValue', value: 'spanish text'}
* ]
* to [{value: 'eng text', _key, _type}]
* (for any base language, not just english)
* Works recursively, in case there are nested arrays.
*/
const findArraysWithBaseLang = (
childObj: Record<string, any>,
baseLang: string
): Record<string, any> => {
const filteredObj: Record<string, any> = {}
META_FIELDS.forEach((field) => {
if (childObj[field]) {
filteredObj[field] = childObj[field]
}
})

for (const key in childObj) {
if (childObj[key]) {
const value: any = childObj[key]
if (Array.isArray(value) && isValidInternationalizedArray(value, baseLang)) {
//we've reached an internationalized array, add it to
//what we want to send to translation
filteredObj[key] = filterToBaseLang(value, baseLang)
}
//we have an array that may have language arrays in its objects
else if (Array.isArray(value) && value.length && typeof value[0] === 'object') {
//recursively find and filter for any objects that have an internationalized array
const validArr: Record<string, any>[] = []
value.forEach((objInArray) => {
//we recurse down for each object. if there's a value
//that's not default system value it passed the filter
const filtered = findArraysWithBaseLang(objInArray, baseLang)
const nonMetaFields = Object.keys(filtered).filter(
(objInArrayKey) => !META_FIELDS.includes(objInArrayKey)
)
if (nonMetaFields.length) {
validArr.push(filtered)
}
})
if (validArr.length) {
filteredObj[key] = validArr
}
}
//we have an object nested in an object
//recurse down the tree
else if (typeof value === 'object') {
const nestedLangObj = findArraysWithBaseLang(value, baseLang)
const nonMetaFields = Object.keys(nestedLangObj).filter(
(nestedObjKey) => !META_FIELDS.includes(nestedObjKey)
)
if (nonMetaFields.length) {
filteredObj[key] = nestedLangObj
}
}
}
}
return filteredObj
}

/*
* Helper. If field-level translation pattern used, only sends over
* content from the base language. Works recursively, so if users
* use this pattern several layers deep, base language fields will still be found.
*/
export const internationalizedArrayFilter = (
document: SanityDocument,
baseLang: string
): Record<string, any> => {
//send top level object into recursive function
return findArraysWithBaseLang(document, baseLang)
}
Loading