x
This commit is contained in:
1 parent
1c0313495f
commit
ac387185e0
15 files changed
+1855
-12
No files matched your search
@@ -0,0 +1,171 @@
|
||||
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko'] as const;
|
||||
|
||||
export const SUPPORTED_LANGS = LANGS;
|
||||
|
||||
export type AttributeType = 'TEXT' | 'NUMBER' | 'BOOLEAN' | 'SELECT' | 'JSON';
|
||||
|
||||
const ATTRIBUTE_TYPES: AttributeType[] = ['TEXT', 'NUMBER', 'BOOLEAN', 'SELECT', 'JSON'];
|
||||
|
||||
export function normalizeAttributeType(type: unknown): AttributeType {
|
||||
const normalized = String(type || '').toUpperCase();
|
||||
if (!ATTRIBUTE_TYPES.includes(normalized as AttributeType)) {
|
||||
throw new Error(`Unsupported attribute type '${type}'. Allowed types: ${ATTRIBUTE_TYPES.join(', ')}`);
|
||||
}
|
||||
return normalized as AttributeType;
|
||||
}
|
||||
|
||||
export function resolveLocalizedText(
|
||||
baseValue: string | null | undefined,
|
||||
translations: Array<{ langCode: string; valueText?: string | null }> | undefined,
|
||||
lang: string
|
||||
) {
|
||||
if (lang === 'all') return baseValue ?? '';
|
||||
const exact = translations?.find((item) => item.langCode === lang)?.valueText;
|
||||
if (exact) return exact;
|
||||
const thai = translations?.find((item) => item.langCode === 'th')?.valueText;
|
||||
if (thai) return thai;
|
||||
return baseValue ?? '';
|
||||
}
|
||||
|
||||
export function resolveDefinitionLocalizedField<T extends { langCode: string; label: string; description?: string | null }>(
|
||||
baseValue: string | null | undefined,
|
||||
translations: T[] | undefined,
|
||||
lang: string,
|
||||
field: 'label' | 'description'
|
||||
) {
|
||||
if (lang === 'all') return baseValue ?? null;
|
||||
const exact = translations?.find((item) => item.langCode === lang)?.[field];
|
||||
if (exact !== undefined && exact !== null && exact !== '') return exact;
|
||||
const thai = translations?.find((item) => item.langCode === 'th')?.[field];
|
||||
if (thai !== undefined && thai !== null && thai !== '') return thai;
|
||||
return baseValue ?? null;
|
||||
}
|
||||
|
||||
export function getPrimaryOptionValue(value: any): string {
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return value.th || value.en || Object.values(value)[0] || '';
|
||||
}
|
||||
return String(value ?? '');
|
||||
}
|
||||
|
||||
export function getLocalizedOptionLabel(value: any, lang: string): string {
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return value[lang] || value[lang.slice(0, 2)] || value.th || value.en || Object.values(value)[0] || '';
|
||||
}
|
||||
return String(value ?? '');
|
||||
}
|
||||
|
||||
export function getSelectOptionDisplay(options: any, storedValue: string, lang: string): string {
|
||||
if (!Array.isArray(options)) return storedValue;
|
||||
for (const option of options) {
|
||||
const primary = getPrimaryOptionValue(option);
|
||||
if (primary === storedValue) {
|
||||
return getLocalizedOptionLabel(option, lang);
|
||||
}
|
||||
}
|
||||
return storedValue;
|
||||
}
|
||||
|
||||
export function validateAttributeValue(type: AttributeType, payload: Record<string, any>) {
|
||||
const populated = [
|
||||
payload.valueText !== undefined && payload.valueText !== null && payload.valueText !== '',
|
||||
payload.valueNumber !== undefined && payload.valueNumber !== null,
|
||||
payload.valueBoolean !== undefined && payload.valueBoolean !== null,
|
||||
payload.valueJson !== undefined && payload.valueJson !== null,
|
||||
].filter(Boolean).length;
|
||||
|
||||
if (populated !== 1) {
|
||||
throw new Error('Exactly one of valueText, valueNumber, valueBoolean, or valueJson must be provided.');
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'TEXT':
|
||||
if (typeof payload.valueText !== 'string') throw new Error('TEXT attributes require valueText as a string.');
|
||||
return;
|
||||
case 'NUMBER':
|
||||
if (typeof payload.valueNumber !== 'number' || Number.isNaN(payload.valueNumber)) {
|
||||
throw new Error('NUMBER attributes require valueNumber as a valid number.');
|
||||
}
|
||||
return;
|
||||
case 'BOOLEAN':
|
||||
if (typeof payload.valueBoolean !== 'boolean') {
|
||||
throw new Error('BOOLEAN attributes require valueBoolean as a boolean.');
|
||||
}
|
||||
return;
|
||||
case 'JSON':
|
||||
if (payload.valueJson === undefined || payload.valueJson === null) {
|
||||
throw new Error('JSON attributes require valueJson.');
|
||||
}
|
||||
return;
|
||||
case 'SELECT':
|
||||
if (typeof payload.valueText !== 'string') throw new Error('SELECT attributes require valueText as the stored option key.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function validateSelectOption(options: any, storedValue: string) {
|
||||
if (!Array.isArray(options)) {
|
||||
throw new Error('SELECT attribute definition must provide options.');
|
||||
}
|
||||
const allowed = options.map(getPrimaryOptionValue);
|
||||
if (!allowed.includes(storedValue)) {
|
||||
throw new Error(`Invalid SELECT value '${storedValue}'. Allowed values: [${allowed.join(', ')}]`);
|
||||
}
|
||||
}
|
||||
|
||||
export function localizeAttributeDefinition(definition: any, lang: string) {
|
||||
if (lang === 'all') {
|
||||
const label: Record<string, string> = {};
|
||||
const description: Record<string, string> = {};
|
||||
definition.translations?.forEach((translation: any) => {
|
||||
label[translation.langCode] = translation.label;
|
||||
if (translation.description) description[translation.langCode] = translation.description;
|
||||
});
|
||||
|
||||
return {
|
||||
...definition,
|
||||
label,
|
||||
description,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...definition,
|
||||
label: resolveDefinitionLocalizedField(definition.label, definition.translations, lang, 'label') || definition.label,
|
||||
description: resolveDefinitionLocalizedField(definition.description, definition.translations, lang, 'description'),
|
||||
};
|
||||
}
|
||||
|
||||
export function localizeProductAttribute(attribute: any, lang: string) {
|
||||
const localizedDefinition = localizeAttributeDefinition(attribute.attributeDefinition, lang);
|
||||
const type = normalizeAttributeType(attribute.attributeDefinition.type);
|
||||
|
||||
const value =
|
||||
type === 'TEXT'
|
||||
? resolveLocalizedText(attribute.valueText, attribute.translations, lang)
|
||||
: type === 'SELECT'
|
||||
? getSelectOptionDisplay(attribute.attributeDefinition.options, attribute.valueText || '', lang)
|
||||
: attribute.valueBoolean ?? attribute.valueNumber ?? attribute.valueJson ?? attribute.valueText ?? null;
|
||||
|
||||
if (lang === 'all') {
|
||||
const valueTranslations: Record<string, string> = {};
|
||||
attribute.translations?.forEach((translation: any) => {
|
||||
valueTranslations[translation.langCode] = translation.valueText;
|
||||
});
|
||||
|
||||
return {
|
||||
...attribute,
|
||||
attributeDefinition: localizedDefinition,
|
||||
value,
|
||||
valueTranslations,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...attribute,
|
||||
attributeDefinition: localizedDefinition,
|
||||
value,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user