// Shared by GNOME preferences and the daemon; no platform-specific imports. export function normalizeSettings(source, fields, { strict = false } = {}) { const config = source && typeof source === 'object' && !Array.isArray(source) ? source : {}; const result = {}; for (const field of fields) { let value = config[field.key]; if (value == null) { for (const alias of field.aliases || []) if (config[alias] != null) { value = config[alias]; break; } } if (value == null) value = field.default; if (field.type === 'boolean' && typeof value === 'string') { const flag = value.trim().toLowerCase(); if (['false', '0', 'off', 'no'].includes(flag)) value = false; else if (['true', '1', 'on', 'yes'].includes(flag)) value = true; } if (field.type === 'number' && typeof value === 'string' && value.trim()) value = Number(value); if (field.type === 'list' && typeof value === 'string') value = value.split(',').map(v => v.trim()).filter(Boolean); if (field.key === 'asrLanguage' && typeof value === 'string') value = value.toLowerCase().split(/[-_]/)[0]; if (['string', 'file'].includes(field.type) && typeof value === 'string') value = value.trim(); const valid = field.type === 'boolean' ? typeof value === 'boolean' : field.type === 'number' ? Number.isFinite(value) && value >= field.min && value <= field.max && (field.step < 1 || Number.isInteger(value)) : field.type === 'choice' ? field.options.some(option => option.value === value) : field.type === 'list' ? Array.isArray(value) && value.every(item => typeof item === 'string') : typeof value === 'string' && value.length <= 4096; if (!valid && strict) throw new Error(`Invalid value for ${field.title}`); result[field.key] = valid ? value : field.default; } if (result.vadMinSpeechMs > result.vadMaxSpeechSeconds * 1000) throw new Error('Minimum speech must be shorter than the maximum recording'); return result; } export function mergeSettings(source, changes, fields) { const merged = { ...source, ...changes }; // Remove obsolete aliases only for settings actually changed by this window. for (const field of fields) if (Object.hasOwn(changes, field.key)) { for (const alias of field.aliases || []) delete merged[alias]; } return merged; } export function settingVisible(field, values) { return !field.when || Object.entries(field.when).every(([key, choices]) => choices.includes(values[key])); }