export async function fetchJsonWithFallback(requests, fallbackMessage) { let lastResponse = null let lastError = null for (const request of requests) { let response try { response = await fetch(request.url, request.options) lastResponse = response } catch (error) { lastError = error continue } if (response.ok) { return parseJsonResponse(response) } if (!shouldFallback(response)) { throw new Error(await getResponseError(response, fallbackMessage)) } } if (lastError && !lastResponse) { throw new Error(translateErrorMessage(lastError.message || fallbackMessage)) } throw new Error(await getResponseError(lastResponse, fallbackMessage)) } export function normalizeCollection(data, keys = []) { if (Array.isArray(data)) return data for (const key of keys) { if (Array.isArray(data?.[key])) return data[key] } return [] } export function normalizeItem(data, keys = []) { if (Array.isArray(data)) return data[0] || null for (const key of keys) { if (data?.[key]) return data[key] } return data || null } export async function getResponseError(response, fallbackMessage) { if (!response) return fallbackMessage const text = await response.text().catch(() => '') const error = parseErrorBody(text) const message = translateErrorMessage( error.error_description || error.msg || error.message || error.error || error.details || error.hint || text || fallbackMessage, ) return response.status ? `${fallbackMessage} (${response.status}): ${message}` : message } export function translateErrorMessage(message) { const rawMessage = String(message || '').trim() const normalized = rawMessage.toLowerCase() if (!rawMessage) return 'Erro inesperado.' if (isPortugueseMessage(rawMessage)) return rawMessage const translations = [ [/failed to fetch|networkerror|load failed|network request failed/, 'Não foi possível conectar ao servidor. Verifique sua conexão e tente novamente.'], [/invalid login credentials|invalid credentials/, 'E-mail ou senha inválidos.'], [/email not confirmed/, 'E-mail ainda não confirmado. Verifique sua caixa de entrada.'], [/user already registered|already registered/, 'Este e-mail já está cadastrado.'], [/user not found/, 'Usuário não encontrado.'], [/jwt expired|invalid jwt|jwt malformed|invalid token|token is expired/, 'Sessão expirada. Faça login novamente.'], [/missing required parameters?/, 'Parâmetros obrigatórios não foram enviados.'], [/duplicate key value violates unique constraint/, 'Já existe um registro com essas informações.'], [/new row violates row-level security policy|row-level security policy|permission denied/, 'Você não tem permissão para realizar esta ação.'], [/violates foreign key constraint/, 'Não foi possível salvar porque há um vínculo obrigatório ausente ou inválido.'], [/null value in column "([^"]+)".*violates not-null constraint/, 'Campo obrigatório não preenchido.'], [/invalid input value for enum ([^:]+): "([^"]+)"/, 'Valor inválido para uma opção do sistema.'], [/invalid input syntax for type uuid/, 'Identificador inválido enviado para a API.'], [/relation .* does not exist/, 'Recurso da API não encontrado.'], [/function .* does not exist/, 'Endpoint da API não encontrado.'], [/cors|preflight/, 'A API bloqueou a requisição por configuração de CORS.'], ] for (const [pattern, translation] of translations) { if (pattern.test(normalized)) return translation } return rawMessage } function isPortugueseMessage(message) { return /[ãõáéíóúâêôç]/i.test(message) || /\b(erro|falha|não|nao|usuário|usuario|senha|campo|obrigatório|obrigatorio|sessão|sessao)\b/i.test(message) } function shouldFallback(response) { return [404, 405].includes(response.status) } async function parseJsonResponse(response) { if (response.status === 204) return null const text = await response.text() if (!text) return null try { return JSON.parse(text) } catch { return { message: text } } } function parseErrorBody(text) { if (!text) return {} try { return JSON.parse(text) } catch { return { message: text } } }