import type { AllFields, AllPresets, References, TStrings } from './types.def.ts'; export function isReference(string: string) { return string.startsWith('{') && string.endsWith('}'); } /** * This is only used to expand references to _untranslated content_. * For example, `fields` can reference the list of field IDs from another * preset. */ export function dereferenceUntranslatedContent(presets: AllPresets, fields: AllFields, references: References) { for (const presetID in presets) { const preset = presets[presetID]; // fields and moreFields can reference other presets for (const prop of ['fields', 'moreFields'] as const) { if (!preset[prop]) continue; for (let i = 0; i < preset[prop].length || 0; i++) { const otherPresetID = preset[prop][i]; if (isReference(otherPresetID)) { const referencedPreset = presets[otherPresetID.slice(1, -1)]; if (!referencedPreset) { throw new Error( `Preset “${presetID}” references “${otherPresetID}” in ${prop}.${i}, but there is no such preset.`, ); } // preset (A) references the fields of preset (B), but (B) has no // fields. We silently and intentionally skip this, as it allows presets // to specify fields and moreFields inheritance even while parent has no such field yet. // Otherwise defining for example new moreFields would require checking all children presets // whether inheritance should be added there. if (!referencedPreset[prop]) { preset[prop].splice(i--, 1); continue; } // Skip `fields` for the keys which define the preset. // These are usually `typeCombo` fields like `shop=*` function shouldInherit(fieldId: string) { // shouldInherit is called recursively as references are expanded. // if this field is reference, skip it for now. It will be // processed in the next loop iteration. if (isReference(fieldId)) return true; const field = fields[fieldId]; if (!field.key) return true; if ( preset.tags[field.key] && // inherit anyway if multiple values are allowed or just a checkbox field.type !== 'multiCombo' && field.type !== 'semiCombo' && field.type !== 'manyCombo' && field.type !== 'check' ) { return false; } if ( preset.fields?.some(originalField => field.key === fields[originalField]?.key) || preset.moreFields?.some(originalField => field.key === fields[originalField]?.key) ) { // current preset already has a field for this field return false; } return true; } // replace the reference with every field. decrement i to reprocess this array index. // this is necessary as it can also be a reference preset[prop].splice(i--, 1, ...referencedPreset[prop].filter(shouldInherit)); } } } // presets can reference the relation schema from other presets if (preset.relationCrossReference) { const referencedPreset = presets[preset.relationCrossReference.slice(1, -1)]; if (!referencedPreset) { throw new Error( `Preset “${presetID}” references “${preset.relationCrossReference}” in relationCrossReference, but there is no such preset.`, ); } preset.relation = referencedPreset.relation; delete preset.relationCrossReference; } // presets can reference the locationSet from other fields or presets if (preset.locationSetCrossReference) { const [type, ...foreignId] = preset.locationSetCrossReference .slice(1, -1) .split('/'); const referenced = ( type === 'presets' ? presets : type === 'fields' ? fields : {} )[foreignId.join('/')]; if (!referenced) { throw new Error( `Preset “${presetID}” references “${foreignId}” in locationSetCrossReference, but there is no such ${type}.`, ); } preset.locationSet = referenced.locationSet; delete preset.locationSetCrossReference; } } for (const fieldID in fields) { const field = fields[fieldID]; // fields can reference icons from other presets if (field.iconsCrossReference) { const referencedField = fields[field.iconsCrossReference.slice(1, -1)]; if (!referencedField) { throw new Error( `Field “${fieldID}” references “${field.iconsCrossReference}” in iconsCrossReference, but there is no such field.`, ); } field.icons = referencedField.icons; delete field.iconsCrossReference; } // if a field uses `stringsCrossReference`, we need to copy `options` if it // doesn't already exist. const stringsCrossReference = references.fields[fieldID]?.stringsCrossReference; if (stringsCrossReference) { const referencedField = fields[stringsCrossReference.slice(1, -1)]; if (!referencedField) { throw new Error( `Field “${fieldID}” references “${stringsCrossReference}” in stringsCrossReference, but there is no such field.`, ); } if (referencedField.options) { fields[fieldID].options ||= referencedField.options; } } // fields can reference the locationSet from other fields or presets if (field.locationSetCrossReference) { const [type, ...foreignId] = field.locationSetCrossReference .slice(1, -1) .split('/'); const referenced = ( type === 'presets' ? presets : type === 'fields' ? fields : {} )[foreignId.join('/')]; if (!referenced) { throw new Error( `Field “${fieldID}” references “${foreignId}” in locationSetCrossReference, but there is no such ${type}.`, ); } field.locationSet = referenced.locationSet; delete field.locationSetCrossReference; } } } /** * Copies translated strings to the presets that reference these strings. */ export function dereferencedTranslatableContent(tstrings: TStrings, references: References, strict: boolean) { for (const presetID in references.presets) { if (!tstrings.presets) continue; const p = references.presets[presetID]; // presets can reference the name + terms + aliases from other presets if (p.nameTermsAliases) { const referencedPreset = tstrings.presets[p.nameTermsAliases.slice(1, -1)]; if (referencedPreset) { if (!tstrings.presets[presetID]) tstrings.presets[presetID] = {}; tstrings.presets[presetID].name = referencedPreset.name; tstrings.presets[presetID].aliases = referencedPreset.aliases; tstrings.presets[presetID].terms = referencedPreset.terms; } else if (strict) { throw new Error( `Preset “${presetID}” references “${p.nameTermsAliases}” in the name, but there is no such preset.`, ); } } // presets can reference the relation schema from other presets // (including relation.role_labels which is translatable content) if (p.relation) { const referencedPreset = tstrings.presets[p.relation.slice(1, -1)]; if (referencedPreset) { if (!tstrings.presets[presetID]) tstrings.presets[presetID] = {}; tstrings.presets[presetID].relation = referencedPreset.relation; } else if (strict) { throw new Error( `Preset “${presetID}” references “${p.relation}” in relationCrossReference, but there is no such preset.`, ); } } } for (const fieldID in references.fields) { if (!tstrings.fields) continue; const f = references.fields[fieldID]; // fields can reference the label + terms from other fields if (f.labelAndTerms) { const referencedField = tstrings.fields[f.labelAndTerms.slice(1, -1)]; if (referencedField) { if (!tstrings.fields[fieldID]) tstrings.fields[fieldID] = {}; tstrings.fields[fieldID].label = referencedField.label; tstrings.fields[fieldID].terms = referencedField.terms; } else if (strict) { throw new Error( `Field “${fieldID}” references “${f.labelAndTerms}” in the label, but there is no such field.`, ); } } // fields can reference the placeholder from other fields if (f.placeholder) { const referencedField = tstrings.fields[f.placeholder.slice(1, -1)]; if (referencedField) { if (!tstrings.fields[fieldID]) tstrings.fields[fieldID] = {}; tstrings.fields[fieldID].placeholder = referencedField.placeholder; } else if (strict) { throw new Error( `Field “${fieldID}” references “${f.placeholder}” in the placeholder, but there is no such field.`, ); } } // fields can reference the entire strings.options object from other fields if (f.stringsCrossReference) { const referencedField = tstrings.fields[f.stringsCrossReference.slice(1, -1)]; if (referencedField) { if (!tstrings.fields[fieldID]) tstrings.fields[fieldID] = {}; for (const prop in referencedField) { if (typeof referencedField[prop] === 'object') { tstrings.fields[fieldID][prop] = referencedField[prop]; } } } else if (strict) { throw new Error( `Field “${fieldID}” references “${f.stringsCrossReference}” in stringsCrossReference, but there is no such field.`, ); } } // specific field options can reference the label from other fields *and from other presets* if (f.options) { for (const prop in f.options) { for (const key in f.options[prop]) { const [type, ...foreignId] = f.options[prop][key] .slice(1, -1) .split('/'); const referenced = type === 'presets' ? tstrings.presets?.[foreignId.join('/')]?.name : type === 'fields' ? tstrings.fields?.[foreignId.join('/')]?.label : undefined; if (referenced) { if (!tstrings.fields[fieldID]) tstrings.fields[fieldID] = {}; tstrings.fields[fieldID][prop] ||= {}; tstrings.fields[fieldID][prop][key] = referenced; } else if (strict) { throw new Error( `Field “${fieldID}” references “${foreignId}” in options.${prop}.${key}, but there is no such ${type}.`, ); } } } } } }