From d7c52aa5a82263f53ed85d93ba80479008a98ea2 Mon Sep 17 00:00:00 2001 From: Vitor Wild Date: Thu, 30 Jul 2026 19:12:53 -0300 Subject: [PATCH] feat: add Brazilian Portuguese locale --- .../plans/2026-07-30-pt-br-i18n.md | 195 +++++ frontend/src/i18n/index.js | 4 +- locales/languages.json | 4 + locales/pt-BR.json | 667 ++++++++++++++++++ package.json | 3 +- scripts/validate-locale.mjs | 73 ++ 6 files changed, 943 insertions(+), 3 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-30-pt-br-i18n.md create mode 100644 locales/pt-BR.json create mode 100644 scripts/validate-locale.mjs diff --git a/docs/superpowers/plans/2026-07-30-pt-br-i18n.md b/docs/superpowers/plans/2026-07-30-pt-br-i18n.md new file mode 100644 index 00000000..b6216401 --- /dev/null +++ b/docs/superpowers/plans/2026-07-30-pt-br-i18n.md @@ -0,0 +1,195 @@ +# pt-BR Localization Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add full Brazilian Portuguese localization to the MiroFish frontend. + +**Architecture:** The frontend already discovers locale files from `locales/*.json` and exposes only entries registered in `locales/languages.json`. This plan adds a complete `pt-BR` locale file, registers it, and makes it the default for new sessions while preserving stored user preferences. + +**Tech Stack:** Vue 3, Vue I18n, Vite, Node.js validation script. + +## Global Constraints + +- Add `pt-BR` to the language registry with a Brazilian Portuguese label and LLM response instruction. +- Add a complete `locales/pt-BR.json` file covering every translation key present in the existing baseline locale files. +- Keep existing `zh` and `en` translations unchanged. +- Set `pt-BR` as the default locale only when no locale is stored in `localStorage`. +- Preserve the current language switcher UI and storage behavior. +- Do not translate backend source logs or API internals in this change. +- Do not redesign the language switcher. +- Do not remove existing locales or change existing persisted user locale behavior. + +--- + +### Task 1: Locale Coverage Validation + +**Files:** +- Create: `scripts/validate-locale.mjs` +- Modify: `package.json` +- Test: `npm run test:i18n` + +**Interfaces:** +- Consumes: `locales/en.json`, `locales/languages.json`, and `locales/pt-BR.json`. +- Produces: `npm run test:i18n`, a command that exits with status 0 only when `pt-BR` is registered and structurally complete. + +- [ ] **Step 1: Write the failing validation script** + +```js +import { readFileSync, existsSync } from 'node:fs' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const localePath = (name) => resolve(root, 'locales', `${name}.json`) +const readJson = (path) => JSON.parse(readFileSync(path, 'utf8')) + +const languages = readJson(resolve(root, 'locales', 'languages.json')) +const en = readJson(localePath('en')) +const ptPath = localePath('pt-BR') + +const errors = [] + +if (!languages['pt-BR']) { + errors.push('languages.json must register pt-BR') +} + +if (!existsSync(ptPath)) { + errors.push('locales/pt-BR.json must exist') +} + +const flattenKeys = (value, prefix = '') => { + if (Array.isArray(value)) { + return [prefix] + } + if (value && typeof value === 'object') { + return Object.keys(value).flatMap((key) => flattenKeys(value[key], prefix ? `${prefix}.${key}` : key)) + } + return [prefix] +} + +if (existsSync(ptPath)) { + const pt = readJson(ptPath) + const enKeys = flattenKeys(en).sort() + const ptKeys = flattenKeys(pt).sort() + const missing = enKeys.filter((key) => !ptKeys.includes(key)) + const extra = ptKeys.filter((key) => !enKeys.includes(key)) + + if (missing.length) { + errors.push(`pt-BR is missing keys: ${missing.slice(0, 20).join(', ')}`) + } + if (extra.length) { + errors.push(`pt-BR has extra keys: ${extra.slice(0, 20).join(', ')}`) + } + + const firstScreenKeys = [ + 'home.dragToUpload', + 'home.startEngine', + 'home.tagline', + 'home.promptPlaceholder', + 'home.workflowSequence', + 'main.layoutWorkbench' + ] + + const getValue = (obj, path) => path.split('.').reduce((current, key) => current?.[key], obj) + for (const key of firstScreenKeys) { + const value = getValue(pt, key) + if (typeof value !== 'string') { + errors.push(`pt-BR key ${key} must be a string`) + continue + } + if (typeof value === 'string' && /\b(Upload|Start|Workflow|Workbench|Describe)\b/.test(value)) { + errors.push(`pt-BR key ${key} still appears to be English: ${value}`) + } + } +} + +if (errors.length) { + console.error(errors.join('\n')) + process.exit(1) +} + +console.log('pt-BR locale validation passed') +``` + +- [ ] **Step 2: Add the npm script** + +```json +"test:i18n": "node scripts/validate-locale.mjs" +``` + +- [ ] **Step 3: Run test to verify it fails** + +Run: `npm run test:i18n` + +Expected: FAIL with `languages.json must register pt-BR` and `locales/pt-BR.json must exist`. + +### Task 2: pt-BR Locale Implementation + +**Files:** +- Create: `locales/pt-BR.json` +- Modify: `locales/languages.json` +- Modify: `frontend/src/i18n/index.js` +- Test: `npm run test:i18n`, `npm run build` + +**Interfaces:** +- Consumes: `npm run test:i18n` from Task 1. +- Produces: `pt-BR` as a complete selectable frontend locale and default locale for new sessions. + +- [ ] **Step 1: Register `pt-BR`** + +Add this entry to `locales/languages.json`: + +```json +"pt-BR": { + "label": "Português (Brasil)", + "llmInstruction": "Por favor, responda em português do Brasil." +} +``` + +- [ ] **Step 2: Change the default locale** + +In `frontend/src/i18n/index.js`, change: + +```js +const savedLocale = localStorage.getItem('locale') || 'zh' +``` + +to: + +```js +const savedLocale = localStorage.getItem('locale') || 'pt-BR' +``` + +and change: + +```js +fallbackLocale: 'zh', +``` + +to: + +```js +fallbackLocale: 'pt-BR', +``` + +- [ ] **Step 3: Add the full translation file** + +Create `locales/pt-BR.json` with the same structure as `locales/en.json` and Brazilian Portuguese strings for every key. + +- [ ] **Step 4: Run locale validation** + +Run: `npm run test:i18n` + +Expected: PASS with `pt-BR locale validation passed`. + +- [ ] **Step 5: Run frontend build** + +Run: `npm run build` + +Expected: PASS and Vite produces the production bundle. + +- [ ] **Step 6: Commit implementation** + +```bash +git add package.json scripts/validate-locale.mjs locales/languages.json locales/pt-BR.json frontend/src/i18n/index.js docs/superpowers/plans/2026-07-30-pt-br-i18n.md +git commit -m "feat: add Brazilian Portuguese locale" +``` diff --git a/frontend/src/i18n/index.js b/frontend/src/i18n/index.js index aa265535..a675b8b5 100644 --- a/frontend/src/i18n/index.js +++ b/frontend/src/i18n/index.js @@ -14,12 +14,12 @@ for (const path in localeFiles) { } } -const savedLocale = localStorage.getItem('locale') || 'zh' +const savedLocale = localStorage.getItem('locale') || 'pt-BR' const i18n = createI18n({ legacy: false, locale: savedLocale, - fallbackLocale: 'zh', + fallbackLocale: 'pt-BR', messages }) diff --git a/locales/languages.json b/locales/languages.json index 81732cec..c9faebb9 100644 --- a/locales/languages.json +++ b/locales/languages.json @@ -19,6 +19,10 @@ "label": "Português", "llmInstruction": "Por favor, responda em português." }, + "pt-BR": { + "label": "Português (Brasil)", + "llmInstruction": "Por favor, responda em português do Brasil." + }, "ru": { "label": "Русский", "llmInstruction": "Пожалуйста, отвечайте на русском языке." diff --git a/locales/pt-BR.json b/locales/pt-BR.json new file mode 100644 index 00000000..94c2d3f3 --- /dev/null +++ b/locales/pt-BR.json @@ -0,0 +1,667 @@ +{ + "common": { + "confirm": "Confirmar", + "cancel": "Cancelar", + "loading": "Carregando...", + "error": "Erro", + "success": "Sucesso", + "completed": "Concluído", + "processing": "Gerando", + "pending": "Pendente", + "ready": "Pronto", + "running": "Em execução", + "failed": "Falhou", + "unknown": "Desconhecido", + "unknownError": "Erro desconhecido", + "none": "Nenhum", + "close": "Fechar", + "back": "Voltar", + "next": "Próximo", + "retry": "Tentar novamente", + "noData": "Nenhum dado disponível", + "hours": "horas", + "minutes": "minutos", + "rounds": "rodadas", + "items": "itens", + "files": "arquivos" + }, + "meta": { + "title": "MiroFish - Preveja tudo", + "description": "MiroFish - Sistema de simulação de opinião em redes sociais" + }, + "nav": { + "visitGithub": "Visite nossa página no GitHub" + }, + "home": { + "tagline": "Motor de inteligência coletiva conciso e universal", + "version": "/ v0.1-Preview", + "heroTitle1": "Envie relatórios,", + "heroTitle2": "Preveja o futuro", + "heroDesc": "A partir de um único documento, {brand} extrai sementes da realidade para gerar automaticamente um mundo paralelo com até {agentScale}. Injete variáveis a partir de uma visão global para encontrar o {optimalSolution} em dinâmicas coletivas complexas.", + "heroDescBrand": "MiroFish", + "heroDescAgentScale": "agentes em escala de milhões", + "heroDescOptimalSolution": "\"ótimo local\"", + "slogan": "Deixe os agentes ensaiarem o futuro; deixe as decisões prevalecerem", + "systemStatus": "Status do sistema", + "systemReady": "Pronto", + "systemReadyDesc": "Motor de previsão em espera. Envie dados não estruturados para iniciar uma sequência de simulação.", + "metricLowCost": "Baixo custo", + "metricLowCostDesc": "Média de US$ 5/sim.", + "metricHighAvail": "Escalável", + "metricHighAvailDesc": "Milhões de agentes", + "workflowSequence": "Fluxo de trabalho", + "step01Title": "Construção do grafo", + "step01Desc": "Extração de sementes, injeção de memória e construção GraphRAG", + "step02Title": "Configuração do ambiente", + "step02Desc": "Extração de entidades, geração de personas e injeção de configuração dos agentes", + "step03Title": "Simulação", + "step03Desc": "Simulação paralela em duas plataformas, interpretação automática dos requisitos e memória temporal", + "step04Title": "Relatório", + "step04Desc": "O ReportAgent interage com o ambiente pós-simulação usando ferramentas avançadas", + "step05Title": "Interação", + "step05Desc": "Converse com qualquer indivíduo simulado e dialogue com o ReportAgent", + "realitySeed": "01 / Semente da realidade", + "supportedFormats": "Formatos: PDF, MD, TXT", + "dragToUpload": "Arraste arquivos para enviar", + "orBrowse": "ou clique para procurar arquivos", + "inputParams": "Parâmetros de entrada", + "simulationPrompt": ">_ 02 / Prompt da simulação", + "promptPlaceholder": "// Descreva sua necessidade de simulação ou previsão em linguagem natural", + "engineBadge": "Motor: MiroFish-V1.0", + "startEngine": "Iniciar motor", + "initializing": "Inicializando..." + }, + "main": { + "layoutGraph": "Grafo", + "layoutSplit": "Dividido", + "layoutWorkbench": "Área de trabalho", + "stepNames": ["Construção do grafo", "Configuração do ambiente", "Executar simulação", "Geração de relatório", "Interação profunda"] + }, + "step1": { + "ontologyGeneration": "Geração de ontologia", + "ontologyCompleted": "Concluída", + "ontologyGenerating": "Gerando", + "ontologyPending": "Pendente", + "ontologyDesc": "A LLM analisa o conteúdo dos documentos e os requisitos da simulação, extrai sementes da realidade e gera automaticamente uma estrutura de ontologia adequada", + "analyzingDocs": "Analisando documentos...", + "graphRagBuild": "Construção GraphRAG", + "graphRagDesc": "Com base na ontologia gerada, os documentos são divididos automaticamente em trechos e enviados ao Zep para construir um grafo de conhecimento, extraindo entidades e relações e formando memória temporal e resumos de comunidades", + "entityNodes": "Nós de entidade", + "relationEdges": "Arestas de relação", + "schemaTypes": "Tipos de esquema", + "buildComplete": "Construção concluída", + "buildCompleteDesc": "A construção do grafo foi concluída. Avance para a próxima etapa para configurar o ambiente de simulação.", + "inProgress": "Em andamento", + "creating": "Criando...", + "enterEnvSetup": "Entrar na configuração do ambiente", + "createSimulationFailed": "Falha ao criar simulação: {error}", + "createSimulationException": "Erro ao criar simulação: {error}" + }, + "step2": { + "simInstanceInit": "Inicialização da instância de simulação", + "simInstanceDesc": "Cria uma nova instância de simulação e carrega modelos de parâmetros do mundo", + "asyncTaskDone": "Tarefa assíncrona concluída", + "generateAgentPersona": "Gerar personas dos agentes", + "generateAgentPersonaDesc": "Combina o contexto para extrair automaticamente entidades e relações do grafo de conhecimento, inicializar indivíduos simulados e atribuir comportamentos e memórias únicos com base nas sementes da realidade", + "currentAgentCount": "Agentes atuais", + "expectedAgentTotal": "Total esperado de agentes", + "relatedTopicsCount": "Tópicos relacionados às sementes da realidade", + "generatedAgentPersonas": "Personas de agentes geradas", + "unknownProfession": "Profissão desconhecida", + "noBio": "Nenhuma biografia disponível", + "dualPlatformConfig": "Gerar configuração de duas plataformas", + "dualPlatformConfigDesc": "A LLM define de forma inteligente o fluxo de tempo do mundo, algoritmos de recomendação, horários ativos de cada indivíduo, frequência de publicações, gatilhos de eventos e muito mais, com base nos requisitos e nas sementes da realidade", + "simulationDuration": "Duração da simulação", + "roundDuration": "Duração da rodada", + "totalRounds": "Total de rodadas", + "activePerHour": "Ativos por hora", + "peakHours": "Horários de pico", + "workHours": "Horário comercial", + "morningHours": "Horário da manhã", + "offPeakHours": "Horários fora de pico", + "agentConfig": "Configuração do agente", + "activeTimePeriod": "Horários ativos", + "postsPerHour": "Posts/h", + "commentsPerHour": "Comentários/h", + "responseDelay": "Atraso de resposta", + "activityLevel": "Nível de atividade", + "sentimentBias": "Viés de sentimento", + "influenceWeight": "Influência", + "recommendAlgoConfig": "Configuração do algoritmo de recomendação", + "platform1Name": "Plataforma 1: Praça / Feed", + "platform2Name": "Plataforma 2: Tópico / Comunidade", + "recencyWeight": "Peso de recência", + "popularityWeight": "Peso de popularidade", + "relevanceWeight": "Peso de relevância", + "viralThreshold": "Limiar de viralização", + "echoChamberStrength": "Força da bolha informacional", + "llmConfigReasoning": "Raciocínio da configuração pela LLM", + "initialActivation": "Orquestração da ativação inicial", + "initialActivationDesc": "Gera automaticamente eventos de ativação inicial e tópicos quentes com base na direção narrativa para guiar o estado inicial do mundo simulado", + "orchestrating": "Orquestrando", + "narrativeDirection": "Direção narrativa", + "initialHotTopics": "Tópicos quentes iniciais", + "initialActivationSeq": "Sequência de ativação inicial ({count})", + "setupComplete": "Configuração concluída", + "setupCompleteDesc": "O ambiente de simulação está pronto. Agora você pode iniciar a simulação.", + "roundsConfig": "Configuração de rodadas da simulação", + "roundsConfigDesc": "O MiroFish planeja automaticamente simular {hours} horas do mundo real, com cada rodada representando {minutesPerRound} minutos decorridos", + "customToggle": "Personalizado", + "roundsUnit": "rodadas", + "estimatedDuration": "Para 100 agentes: estimativa de ~{minutes} minutos", + "estimatedDurationFull": "Para 100 agentes: estimativa de {minutes} minutos", + "recommendedRounds": "{rounds} (recomendado)", + "customTip": "Para primeiras execuções, recomendamos fortemente alternar para o modo personalizado para reduzir as rodadas, obter uma prévia rápida e diminuir o risco de erro", + "backToGraphBuild": "Voltar para a construção do grafo", + "startDualWorldSim": "Iniciar simulação paralela de dois mundos", + "profileModalAge": "Idade aparente", + "profileModalGender": "Gênero aparente", + "profileModalCountry": "País/região", + "profileModalMbti": "MBTI aparente", + "profileModalBio": "Biografia da persona", + "profileModalTopics": "Tópicos relacionados às sementes da realidade", + "profileModalPersona": "Contexto detalhado da persona", + "personaDimExperience": "Experiência completa no evento", + "personaDimExperienceDesc": "Trajetória comportamental completa neste evento", + "personaDimBehavior": "Perfil comportamental", + "personaDimBehaviorDesc": "Resumo da experiência e preferências comportamentais", + "personaDimMemory": "Marca de memória única", + "personaDimMemoryDesc": "Memórias formadas a partir das sementes da realidade", + "personaDimSocial": "Rede social", + "personaDimSocialDesc": "Conexões individuais e grafo de interação", + "genderMale": "Masculino", + "genderFemale": "Feminino", + "genderOther": "Outro", + "yearsOld": "anos", + "initializing": "Inicializando", + "generating": "Gerando" + }, + "step3": { + "startGenerateReport": "Gerar relatório", + "generatingReport": "Iniciando...", + "waitingForActions": "Aguardando ações dos agentes...", + "errorMissingSimId": "Erro: simulationId ausente", + "startingDualSim": "Iniciando simulação paralela em duas plataformas...", + "graphMemoryUpdateEnabled": "Atualização dinâmica da memória do grafo ativada", + "setMaxRounds": "Máximo de rodadas da simulação definido como: {rounds}", + "oldSimCleared": "Logs antigos da simulação limpos; reiniciando simulação", + "engineStarted": "Motor de simulação iniciado com sucesso", + "startFailed": "Falha ao iniciar: {error}", + "startException": "Erro de inicialização: {error}", + "stoppingSim": "Parando simulação...", + "simStopped": "Simulação parada", + "stopFailed": "Falha ao parar: {error}", + "stopException": "Erro ao parar: {error}", + "allPlatformsCompleted": "Todas as simulações das plataformas terminaram", + "simCompleted": "Simulação concluída", + "graphRealtimeRefresh": "Atualização em tempo real do grafo ativada (30s)", + "graphRefreshStopped": "Atualização em tempo real do grafo interrompida", + "preparingGoBack": "Preparando retorno à Etapa 2; fechando simulação...", + "closingSimEnv": "Fechando ambiente de simulação...", + "simEnvClosed": "Ambiente de simulação fechado", + "closeFailed": "Falha ao fechar o ambiente de simulação; tentando parada forçada...", + "stoppingProcess": "Parando processo de simulação...", + "checkStatusFailed": "Falha ao verificar status da simulação: {error}", + "forceStopSuccess": "Simulação parada à força", + "forceStopFailed": "Falha na parada forçada: {error}", + "startGenerateReportBtn": "Gerar relatório", + "generatingReportBtn": "Iniciando..." + }, + "step4": { + "generatingSection": "Gerando {title}...", + "goToInteraction": "Entrar na interação profunda", + "waitingForReportAgent": "Aguardando o Report Agent...", + "collapse": "Recolher ▲", + "expandAll": "Mostrar todos ({count}) ▼", + "expandAllEntities": "Mostrar todos ({count}) ▼", + "scenarioLabel": "Cenário: ", + "tabKeyFacts": "Fatos principais ({count})", + "tabCoreEntities": "Entidades centrais ({count})", + "tabRelationChains": "Cadeias de relação ({count})", + "tabSubQueries": "Subconsultas ({count})", + "panelKeyFacts": "Fatos principais mais recentes da memória temporal", + "totalCount": "{count} no total", + "totalEntityCount": "{count} no total", + "panelCoreEntities": "Entidades centrais", + "factCount": "{count} fatos", + "panelRelationChains": "Cadeias de relação", + "panelSubQueries": "Subperguntas da análise de deriva da consulta", + "emptyKeyFacts": "Nenhum fato principal disponível", + "emptyCoreEntities": "Nenhuma entidade central disponível", + "emptyRelationChains": "Nenhuma cadeia de relação disponível", + "tabActiveFacts": "Fatos ativos ({count})", + "tabHistoricalFacts": "Fatos históricos ({count})", + "tabEntities": "Entidades ({count})", + "panelActiveFacts": "Fatos ativos", + "emptyActiveFacts": "Nenhum fato ativo disponível", + "panelHistoricalFacts": "Fatos históricos", + "emptyHistoricalFacts": "Nenhum fato histórico disponível", + "panelEntities": "Entidades", + "emptyEntities": "Nenhuma entidade disponível", + "searchLabel": "Busca: ", + "tabFacts": "Fatos ({count})", + "tabEdges": "Arestas ({count})", + "tabNodes": "Nós ({count})", + "panelSearchResults": "Resultados da busca", + "emptySearchResults": "Nenhum resultado encontrado", + "panelRelatedEdges": "Arestas relacionadas", + "panelRelatedNodes": "Nós relacionados", + "world1": "Mundo 1", + "world2": "Mundo 2" + }, + "step5": { + "interactiveTools": "Ferramentas interativas", + "agentsAvailable": "{count} agentes disponíveis", + "chatWithReportAgent": "Conversar com o Report Agent", + "chatWithAgent": "Conversar com qualquer indivíduo do mundo", + "selectChatTarget": "Selecione o alvo da conversa", + "sendSurvey": "Enviar pesquisa para o mundo", + "reportAgentChat": "Report Agent - Chat", + "reportAgentDesc": "Uma versão conversacional do agente de geração de relatórios, com acesso a 4 ferramentas profissionais e à memória completa do MiroFish", + "toolInsightForge": "Atribuição profunda InsightForge", + "toolInsightForgeDesc": "Alinha dados-semente do mundo real ao estado da simulação, combinando memória global/local para análise profunda de atribuição entre tempos", + "toolPanoramaSearch": "Rastreamento completo PanoramaSearch", + "toolPanoramaSearchDesc": "Algoritmo BFS baseado em grafo que reconstrói caminhos de propagação de eventos, capturando toda a topologia do fluxo de informação", + "toolQuickSearch": "Recuperação rápida QuickSearch", + "toolQuickSearchDesc": "Interface de consulta instantânea baseada em GraphRAG, com indexação otimizada para extração rápida de atributos de nós e fatos discretos", + "toolInterviewSubAgent": "Entrevista virtual InterviewSubAgent", + "toolInterviewSubAgentDesc": "Entrevistas autônomas que conduzem diálogos paralelos em múltiplas rodadas com indivíduos simulados, coletando dados opinativos não estruturados e estados psicológicos", + "profileBio": "Biografia", + "chatEmptyReportAgent": "Converse com o Report Agent para explorar o conteúdo do relatório em profundidade", + "chatEmptyAgent": "Converse com indivíduos simulados para entender suas perspectivas", + "chatInputPlaceholder": "Digite sua pergunta...", + "selectSurveyTarget": "Selecione os alvos da pesquisa", + "selectedCount": "Selecionados {selected} / {total}", + "surveyQuestions": "Perguntas da pesquisa", + "surveyInputPlaceholder": "Digite a pergunta que deseja fazer a todos os alvos selecionados...", + "submitSurvey": "Enviar pesquisa", + "surveyResults": "Resultados da pesquisa", + "surveyResultsCount": "{count} respostas", + "selectAll": "Selecionar todos", + "clearSelection": "Limpar", + "errorOccurred": "Desculpe, ocorreu um erro: {error}", + "noResponse": "Sem resposta", + "requestFailed": "Falha na solicitação", + "selectAgentFirst": "Selecione primeiro um indivíduo simulado" + }, + "graph": { + "panelTitle": "Visualização do grafo de relacionamentos", + "refreshGraph": "Atualizar grafo", + "graphMemoryRealtime": "Memória de curto/longo prazo do GraphRAG atualizada em tempo real", + "realtimeUpdating": "Atualizando em tempo real...", + "pendingContentHint": "Alguns conteúdos ainda estão em processamento. Considere atualizar o grafo manualmente mais tarde.", + "nodeDetails": "Detalhes do nó", + "relationship": "Relacionamento", + "graphDataLoading": "Carregando dados do grafo...", + "waitingOntology": "Aguardando geração da ontologia...", + "toggleMaximize": "Maximizar/restaurar", + "closeHint": "Fechar dica" + }, + "history": { + "title": "Histórico de simulações", + "graphBuild": "Construção do grafo", + "envSetup": "Configuração do ambiente", + "analysisReport": "Relatório de análise", + "moreFiles": "+{count} arquivos", + "noFiles": "Nenhum arquivo", + "loadingText": "Carregando...", + "simRequirement": "Requisito da simulação", + "relatedFiles": "Arquivos relacionados", + "noRelatedFiles": "Nenhum arquivo relacionado", + "replayTitle": "Reprodução da simulação", + "step1Button": "Construção do grafo", + "step2Button": "Configuração do ambiente", + "step4Button": "Relatório de análise", + "replayHint": "A Etapa 3, 'Executar simulação', e a Etapa 5, 'Interação profunda', devem ser iniciadas durante a execução e não oferecem suporte à reprodução pelo histórico", + "notStarted": "Não iniciado", + "roundsProgress": "{current}/{total} rodadas", + "untitledSimulation": "Simulação sem título", + "unknownFile": "Arquivo desconhecido" + }, + "api": { + "projectNotFound": "Projeto não encontrado: {id}", + "projectDeleteFailed": "Projeto não encontrado ou falha ao excluir: {id}", + "projectDeleted": "Projeto excluído: {id}", + "projectReset": "Projeto redefinido: {id}", + "requireSimulationRequirement": "Informe um requisito de simulação (simulation_requirement)", + "requireFileUpload": "Envie pelo menos um arquivo de documento", + "noDocProcessed": "Nenhum documento foi processado com sucesso. Verifique os formatos dos arquivos.", + "requireProjectId": "Informe project_id", + "configError": "Erro de configuração: {details}", + "zepApiKeyMissing": "ZEP_API_KEY não configurada", + "ontologyNotGenerated": "A ontologia ainda não foi gerada. Chame /ontology/generate primeiro.", + "graphBuilding": "Construção do grafo em andamento. Não reenvie. Para forçar a reconstrução, adicione force: true.", + "textNotFound": "Conteúdo de texto extraído não encontrado", + "ontologyNotFound": "Definição da ontologia não encontrada", + "graphBuildStarted": "Tarefa de construção do grafo iniciada. Consulte o progresso em /task/{taskId}.", + "graphBuildComplete": "Construção do grafo concluída", + "buildFailed": "Falha na construção: {error}", + "taskNotFound": "Tarefa não encontrada: {id}", + "graphDeleted": "Grafo excluído: {id}", + "entityNotFound": "Entidade não encontrada: {id}", + "graphNotBuilt": "O grafo ainda não foi construído. Chame /api/graph/build primeiro.", + "requireSimulationId": "Informe simulation_id", + "simulationNotFound": "Simulação não encontrada: {id}", + "projectMissingRequirement": "O projeto não possui requisito de simulação (simulation_requirement)", + "prepareStarted": "Tarefa de preparação iniciada. Consulte o progresso em /api/simulation/prepare/status.", + "alreadyPrepared": "A preparação já está concluída. Não é necessário gerar novamente.", + "notStartedPrepare": "A preparação não foi iniciada. Chame /api/simulation/prepare.", + "taskCompletedPrepared": "Tarefa concluída (preparação já existente)", + "requireTaskOrSimId": "Informe task_id ou simulation_id", + "configNotFound": "Configuração da simulação não encontrada. Chame /prepare primeiro.", + "configFileNotFound": "Arquivo de configuração não encontrado. Chame /prepare primeiro.", + "unknownScript": "Script desconhecido: {name}. Disponíveis: {allowed}", + "scriptFileNotFound": "Arquivo de script não encontrado: {name}", + "requireGraphId": "Informe graph_id", + "noMatchingEntities": "Nenhuma entidade correspondente encontrada", + "maxRoundsPositive": "max_rounds deve ser um inteiro positivo", + "maxRoundsInvalid": "max_rounds deve ser um inteiro válido", + "invalidPlatform": "Tipo de plataforma inválido: {platform}. Opções: twitter/reddit/parallel", + "simRunningForceHint": "A simulação está em execução. Pare-a primeiro via /stop ou use force=true para reiniciar.", + "simNotReady": "A simulação não está pronta. Status atual: {status}. Chame /prepare primeiro.", + "graphIdRequiredForMemory": "A atualização da memória do grafo exige um graph_id válido. Garanta que o grafo foi construído.", + "dbNotExist": "O banco de dados não existe. A simulação pode ainda não ter sido executada.", + "requireMessage": "Informe uma mensagem", + "missingGraphId": "ID do grafo ausente", + "missingGraphIdEnsure": "ID do grafo ausente. Garanta que o grafo foi construído.", + "missingSimRequirement": "Descrição do requisito de simulação ausente", + "reportAlreadyExists": "O relatório já existe", + "reportGenerateStarted": "Tarefa de geração de relatório iniciada. Consulte o progresso em /api/report/generate/status.", + "reportGenerated": "Relatório gerado", + "reportNotFound": "Relatório não encontrado: {id}", + "noReportForSim": "Nenhum relatório encontrado para esta simulação: {id}", + "reportDeleted": "Relatório excluído: {id}", + "reportGenerateFailed": "Falha na geração do relatório", + "sectionNotFound": "Seção não encontrada: section_{index}.md", + "reportProgressNotAvail": "Relatório não encontrado ou progresso indisponível: {id}", + "requireAgentId": "Informe agent_id", + "requirePrompt": "Informe um prompt (pergunta da entrevista)", + "invalidInterviewPlatform": "A plataforma deve ser 'twitter' ou 'reddit'", + "envNotRunning": "O ambiente de simulação não está em execução ou foi fechado. Garanta que a simulação foi concluída e está em modo de espera por comandos.", + "interviewTimeout": "Tempo limite da resposta da entrevista excedido: {error}", + "requireInterviews": "Informe interviews (lista de entrevistas)", + "interviewListMissingAgentId": "O item {index} da lista de entrevistas não possui agent_id", + "interviewListMissingPrompt": "O item {index} da lista de entrevistas não possui prompt", + "interviewListInvalidPlatform": "A plataforma do item {index} da lista de entrevistas deve ser 'twitter' ou 'reddit'", + "batchInterviewTimeout": "Tempo limite da resposta da entrevista em lote excedido: {error}", + "globalInterviewTimeout": "Tempo limite da resposta da entrevista global excedido: {error}", + "envRunning": "O ambiente está em execução e pronto para comandos de entrevista", + "envNotRunningShort": "Ambiente não está em execução ou foi fechado", + "requireGraphIdAndQuery": "Informe graph_id e query", + "initReportAgent": "Inicializando Report Agent..." + }, + "progress": { + "initGraphService": "Inicializando serviço de construção do grafo...", + "textChunking": "Dividindo texto em trechos...", + "creatingZepGraph": "Criando grafo no Zep...", + "settingOntology": "Definindo ontologia...", + "addingChunks": "Adicionando {count} trechos de texto...", + "waitingZepProcess": "Aguardando o Zep processar os dados...", + "fetchingGraphData": "Buscando dados do grafo...", + "graphBuildComplete": "Construção do grafo concluída", + "buildFailed": "Falha na construção: {error}", + "startBuildingGraph": "Iniciando construção do grafo...", + "graphCreated": "Grafo criado: {graphId}", + "ontologySet": "Ontologia definida", + "textSplit": "Texto dividido em {count} trechos", + "fetchingGraphInfo": "Buscando informações do grafo...", + "sendingBatch": "Enviando lote {current}/{total} ({chunks} trechos)...", + "batchFailed": "Lote {batch} falhou: {error}", + "noEpisodesWait": "Nenhum episódio para aguardar", + "waitingEpisodes": "Aguardando processamento de {count} trechos de texto...", + "episodesTimeout": "Alguns trechos excederam o tempo limite; {completed}/{total} concluídos", + "zepProcessing": "Zep processando... {completed}/{total} concluídos, {pending} pendentes ({elapsed}s)", + "processingComplete": "Processamento concluído: {completed}/{total}", + "taskComplete": "Tarefa concluída", + "taskFailed": "Tarefa falhou", + "startPreparingEnv": "Preparando ambiente de simulação...", + "connectingZepGraph": "Conectando ao grafo do Zep...", + "readingNodeData": "Lendo dados dos nós...", + "readingComplete": "Concluído, {count} entidades encontradas", + "startGenerating": "Iniciando geração...", + "analyzingRequirements": "Analisando requisitos da simulação...", + "generatingOutline": "Gerando estrutura do relatório...", + "parsingOutline": "Interpretando estrutura do roteiro...", + "outlinePlanComplete": "Planejamento da estrutura concluído", + "deepSearchAndWrite": "Busca profunda e escrita ({current}/{max})", + "initReport": "Inicializando relatório...", + "startPlanningOutline": "Planejando estrutura do relatório...", + "outlineDone": "Estrutura concluída, {count} seções", + "generatingSection": "Gerando seção: {title} ({current}/{total})", + "sectionDone": "Seção {title} concluída", + "assemblingReport": "Montando relatório completo...", + "reportComplete": "Geração do relatório concluída", + "reportFailed": "Falha na geração do relatório: {error}", + "savingProfiles": "Salvando arquivos de perfil...", + "profilesComplete": "Concluído, {count} perfis gerados", + "callingLLMConfig": "Chamando LLM para gerar configuração...", + "savingConfigFiles": "Salvando arquivos de configuração...", + "configComplete": "Geração da configuração concluída", + "generatingTimeConfig": "Gerando configuração de tempo...", + "generatingEventConfig": "Gerando configuração de eventos e tópicos quentes...", + "generatingAgentConfig": "Gerando configuração dos agentes ({start}-{end}/{total})...", + "generatingPlatformConfig": "Gerando configuração da plataforma...", + "zepSearchQuery": "Todas as informações, atividades, eventos, relações e contexto sobre {name}", + "timeConfigLabel": "Configuração de tempo", + "eventConfigLabel": "Configuração de eventos", + "agentConfigResult": "Configuração dos agentes: {count} gerados", + "postAssignResult": "Atribuição de posts: {count} posts atribuídos", + "profileGenerated": "[Gerado] {name} ({type})", + "readingGraphEntities": "Lendo entidades do grafo", + "generatingProfiles": "Gerando perfis dos agentes", + "generatingSimConfig": "Gerando configuração da simulação", + "preparingScripts": "Preparando scripts" + }, + "log": { + "preparingGoBack": "Preparando retorno à Etapa 2; fechando simulação...", + "closingSimEnv": "Fechando ambiente de simulação...", + "simEnvClosed": "✓ Ambiente de simulação fechado", + "closeSimEnvFailed": "Falha ao fechar o ambiente de simulação; tentando parada forçada...", + "simForceStopSuccess": "✓ Simulação parada à força", + "forceStopFailed": "Falha na parada forçada: {error}", + "stoppingSimProcess": "Parando processo de simulação...", + "simStopped": "✓ Simulação parada", + "stopSimFailed": "Falha ao parar simulação: {error}", + "checkStatusFailed": "Falha ao verificar status da simulação: {error}", + "enterStep4": "Entrando na Etapa 4: Geração de relatório", + "loadingSimData": "Carregando dados da simulação: {id}", + "timeConfig": "Configuração de tempo: {minutes} minutos por rodada", + "timeConfigFetchFailed": "Falha ao buscar configuração de tempo; usando padrão: {minutes} min/rodada", + "projectLoadSuccess": "Projeto carregado: {id}", + "loadSimDataFailed": "Falha ao carregar dados da simulação: {error}", + "loadException": "Erro de carregamento: {error}", + "graphDataLoadSuccess": "Dados do grafo carregados com sucesso", + "graphLoadFailed": "Falha ao carregar grafo: {error}", + "graphRealtimeRefreshStart": "Atualização em tempo real do grafo ativada (30s)", + "graphRealtimeRefreshStop": "Atualização em tempo real do grafo interrompida", + "simRunViewInit": "SimulationRunView inicializada", + "customRounds": "Rodadas personalizadas da simulação: {rounds}", + "enterStep3": "Entrando na Etapa 3: Executar simulação", + "customRoundsConfig": "Rodadas personalizadas da simulação: {rounds} rodadas", + "useAutoRounds": "Usando rodadas de simulação configuradas automaticamente", + "detectedSimEnvRunning": "Ambiente de simulação em execução detectado; fechando...", + "closeSimEnvFailedWithError": "Falha ao fechar ambiente de simulação: {error}", + "closeSimEnvException": "Erro ao fechar ambiente de simulação: {error}", + "detectedSimRunning": "Simulação em execução detectada; parando...", + "forceStopSimFailed": "Falha ao forçar parada da simulação: {error}", + "forceStopSimException": "Erro ao forçar parada da simulação: {error}", + "simViewInit": "SimulationView inicializada", + "errorMissingSimId": "Erro: simulationId ausente", + "simInstanceCreated": "Instância de simulação criada: {id}", + "preparingSimEnv": "Preparando ambiente de simulação...", + "detectedExistingPrep": "Preparação existente detectada; usando diretamente", + "prepareTaskStarted": "Tarefa de preparação iniciada", + "prepareTaskId": " └─ ID da tarefa: {taskId}", + "zepEntitiesFound": "{count} entidades encontradas no grafo do Zep", + "entityTypes": " └─ Tipos de entidade: {types}", + "startPollingProgress": "Consultando progresso da preparação...", + "prepareFailed": "Preparação falhou: {error}", + "prepareException": "Erro de preparação: {error}", + "prepareComplete": "✓ Preparação concluída", + "prepareFailedWithError": "✗ Preparação falhou: {error}", + "startGeneratingConfig": "Gerando configuração de simulação em duas plataformas...", + "generatingAgentProfileConfig": "Gerando configuração de personas dos agentes...", + "generatingLLMConfig": "Chamando LLM para gerar parâmetros de configuração da simulação...", + "configComplete": "✓ Configuração da simulação gerada", + "configSummaryAgents": " ├─ Agentes: {count}", + "configSummaryHours": " ├─ Duração: {hours} horas", + "configSummaryPosts": " ├─ Posts iniciais: {count}", + "configSummaryTopics": " ├─ Tópicos quentes: {count}", + "configSummaryPlatforms": " └─ Plataformas: Twitter {twitter}, Reddit {reddit}", + "timeConfigDetail": "Configuração de tempo: {minutes} min/rodada, {rounds} rodadas no total", + "narrativeDirection": "Direção narrativa: {direction}", + "envSetupComplete": "✓ Configuração do ambiente concluída, pronto para simular", + "startSimCustomRounds": "Iniciando simulação, rodadas personalizadas: {rounds}", + "startSimAutoRounds": "Iniciando simulação, rodadas configuradas automaticamente: {rounds}", + "startGeneratingAgentProfiles": "Gerando personas dos agentes...", + "agentProfile": "→ Persona do agente {current}/{total}: {name} ({profession})", + "allProfilesComplete": "✓ Todas as {count} personas de agentes foram geradas", + "loadingExistingConfig": "Carregando dados de configuração existentes...", + "loadedAgentProfiles": "{count} personas de agentes carregadas", + "configLoadSuccess": "✓ Configuração da simulação carregada", + "configSummaryPostsAlt": " └─ Posts iniciais: {count}", + "configGenerating": "Configuração em geração, consultando...", + "configNotGenerating": "A configuração da simulação está ausente e o backend não está mais gerando", + "loadConfigFailed": "Falha ao carregar configuração: {error}", + "step2Init": "Configuração de ambiente da Etapa 2 inicializada", + "step3Init": "Execução de simulação da Etapa 3 inicializada", + "startingDualSim": "Iniciando simulação paralela em duas plataformas...", + "setMaxRounds": "Máximo de rodadas da simulação definido como: {rounds}", + "graphMemoryUpdateEnabled": "Atualização dinâmica da memória do grafo ativada", + "oldSimCleared": "✓ Logs antigos da simulação limpos; reiniciando simulação", + "engineStarted": "✓ Motor de simulação iniciado com sucesso", + "startFailed": "✗ Falha ao iniciar: {error}", + "startException": "✗ Erro de inicialização: {error}", + "stoppingSim": "Parando simulação...", + "simStoppedSuccess": "✓ Simulação parada", + "stopFailed": "Falha ao parar: {error}", + "stopException": "Erro ao parar: {error}", + "allPlatformsCompleted": "✓ Todas as simulações das plataformas terminaram", + "simCompleted": "✓ Simulação concluída", + "simFailed": "✗ Simulação falhou", + "reportRequestSent": "Solicitação de geração de relatório enviada, aguarde...", + "startingReportGen": "Iniciando geração de relatório...", + "reportGenTaskStarted": "✓ Tarefa de geração de relatório iniciada: {reportId}", + "reportGenFailed": "✗ Falha ao iniciar geração de relatório: {error}", + "reportGenException": "✗ Erro na geração de relatório: {error}", + "step5Init": "Interação profunda da Etapa 5 inicializada", + "selectChatTarget": "Alvo da conversa selecionado: {name}", + "sendFailed": "Falha no envio: {error}", + "sendToReportAgent": "Enviado ao Report Agent: {message}...", + "reportAgentReplied": "Report Agent respondeu", + "sendToAgent": "Enviado para {name}: {message}...", + "agentReplied": "{name} respondeu", + "sendSurvey": "Enviando pesquisa para {count} alvos...", + "receivedReplies": "{count} respostas recebidas", + "surveySendFailed": "Falha ao enviar pesquisa: {error}", + "loadReportData": "Carregando dados do relatório: {id}", + "loadReportFailed": "Falha ao carregar relatório: {error}", + "reportDataLoaded": "Dados do relatório carregados", + "loadReportLogFailed": "Falha ao carregar logs do relatório: {error}", + "loadedProfiles": "{count} indivíduos simulados carregados", + "loadProfilesFailed": "Falha ao carregar indivíduos simulados: {error}", + "interactionViewInit": "InteractionView inicializada", + "reportViewInit": "ReportView inicializada", + "getReportInfoFailed": "Falha ao obter informações do relatório: {error}", + "enterStep": "Entrando na Etapa {step}: {name}", + "returnToStep": "Retornando à Etapa {step}: {name}", + "customSimRounds": "Rodadas personalizadas da simulação: {rounds} rodadas" + }, + "report": { + "taskStarted": "Tarefa de geração de relatório iniciada", + "planningStart": "Iniciando planejamento da estrutura do relatório", + "fetchSimContext": "Buscando contexto da simulação", + "planningComplete": "Planejamento da estrutura concluído", + "sectionStart": "Iniciando geração da seção: {title}", + "reactThought": "Rodada ReACT {iteration} em reflexão", + "toolCall": "Chamando ferramenta: {toolName}", + "toolResult": "Ferramenta {toolName} retornou resultado", + "llmResponse": "Resposta da LLM (chamadas de ferramenta: {hasToolCalls}, resposta final: {hasFinalAnswer})", + "sectionContentDone": "Geração do conteúdo da seção {title} concluída", + "sectionComplete": "Geração da seção {title} concluída", + "reportComplete": "Geração do relatório concluída", + "errorOccurred": "Ocorreu um erro: {error}", + "agentInitDone": "ReportAgent inicializado: graph_id={graphId}, simulation_id={simulationId}", + "executingTool": "Executando ferramenta: {toolName}, parâmetros: {params}", + "toolExecFailed": "Falha ao executar ferramenta: {toolName}, erro: {error}", + "startPlanningOutline": "Iniciando planejamento da estrutura do relatório...", + "outlinePlanDone": "Planejamento da estrutura concluído: {count} seções", + "outlinePlanFailed": "Falha no planejamento da estrutura: {error}", + "reactGenerateSection": "ReACT gerando seção: {title}", + "sectionIterNone": "Seção {title}, iteração {iteration}: LLM retornou None", + "sectionConflict": "Seção {title}, rodada {iteration}: saída da LLM contém chamada de ferramenta e Final Answer (conflito #{conflictCount})", + "sectionConflictDowngrade": "Seção {title}: {conflictCount} conflitos consecutivos; rebaixando para truncar e executar a primeira chamada de ferramenta", + "sectionGenDone": "Geração da seção {title} concluída (chamadas de ferramenta: {count})", + "multiToolOnlyFirst": "A LLM tentou {total} chamadas de ferramenta; executando apenas a primeira: {toolName}", + "sectionNoPrefix": "Seção {title} sem prefixo 'Final Answer:'; adotando saída da LLM como conteúdo final (chamadas de ferramenta: {count})", + "sectionMaxIter": "Seção {title} atingiu o máximo de iterações; forçando geração", + "sectionForceFailed": "Finalização forçada da seção {title}: LLM retornou None; usando mensagem de erro padrão", + "sectionGenFailedContent": "(Esta seção falhou ao gerar: a LLM retornou resposta vazia. Tente novamente mais tarde)", + "outlineSavedToFile": "Estrutura salva em arquivo: {reportId}/outline.json", + "sectionSaved": "Seção salva: {reportId}/section_{sectionNum}.md", + "reportGenDone": "Geração do relatório concluída: {reportId}", + "reportGenFailed": "Falha na geração do relatório: {error}", + "agentChat": "Chat do Report Agent: {message}...", + "fetchReportFailed": "Falha ao buscar conteúdo do relatório: {error}", + "outlineSaved": "Estrutura salva: {reportId}", + "sectionFileSaved": "Seção salva: {reportId}/{fileSuffix}", + "fullReportAssembled": "Relatório completo montado: {reportId}", + "reportSaved": "Relatório salvo: {reportId}", + "reportFolderDeleted": "Pasta do relatório excluída: {reportId}", + "redirectToQuickSearch": "search_graph redirecionado para quick_search", + "redirectToInsightForge": "get_simulation_context redirecionado para insight_forge" + }, + "console": { + "zepToolsInitialized": "ZepToolsService inicializado", + "zepRetryAttempt": "Tentativa {attempt} da operação Zep {operation} falhou: {error}; tentando novamente em {delay}s...", + "zepAllRetriesFailed": "Operação Zep {operation} falhou após {retries} tentativas: {error}", + "graphSearch": "Busca no grafo: graph_id={graphId}, query={query}...", + "graphSearchOp": "Busca no grafo (graph={graphId})", + "searchComplete": "Busca concluída: {count} fatos relevantes encontrados", + "zepSearchApiFallback": "API de busca do Zep falhou; usando busca local como fallback: {error}", + "usingLocalSearch": "Usando busca local: query={query}...", + "localSearchComplete": "Busca local concluída: {count} fatos relevantes encontrados", + "localSearchFailed": "Busca local falhou: {error}", + "fetchingAllNodes": "Buscando todos os nós do grafo {graphId}...", + "fetchedNodes": "{count} nós obtidos", + "fetchingAllEdges": "Buscando todas as arestas do grafo {graphId}...", + "fetchedEdges": "{count} arestas obtidas", + "fetchingNodeDetail": "Buscando detalhe do nó: {uuid}...", + "fetchNodeDetailOp": "Buscar detalhe do nó (uuid={uuid}...)", + "fetchNodeDetailFailed": "Falha ao buscar detalhe do nó: {error}", + "fetchingNodeEdges": "Buscando arestas do nó {uuid}...", + "foundNodeEdges": "{count} arestas relacionadas ao nó encontradas", + "fetchNodeEdgesFailed": "Falha ao buscar arestas do nó: {error}", + "fetchingEntitiesByType": "Buscando entidades do tipo {type}...", + "foundEntitiesByType": "{count} entidades do tipo {type} encontradas", + "fetchingEntitySummary": "Buscando resumo de relacionamentos da entidade {name}...", + "fetchingGraphStats": "Buscando estatísticas do grafo {graphId}...", + "fetchingSimContext": "Buscando contexto da simulação: {requirement}...", + "insightForgeStart": "Recuperação de insight profundo InsightForge: {query}...", + "generatedSubQueries": "{count} subconsultas geradas", + "insightForgeComplete": "InsightForge concluído: {facts} fatos, {entities} entidades, {relationships} relacionamentos", + "generateSubQueriesFailed": "Falha ao gerar subconsultas: {error}; usando padrões", + "panoramaSearchStart": "Busca ampla PanoramaSearch: {query}...", + "panoramaSearchComplete": "PanoramaSearch concluído: {active} ativos, {historical} históricos", + "quickSearchStart": "Busca simples QuickSearch: {query}...", + "quickSearchComplete": "QuickSearch concluído: {count} resultados", + "interviewAgentsStart": "Entrevista profunda InterviewAgents (API real): {requirement}...", + "profilesNotFound": "Perfis não encontrados para a simulação {simId}", + "loadedProfiles": "{count} perfis de agentes carregados", + "selectedAgentsForInterview": "{count} agentes selecionados para entrevista: {indices}", + "generatedInterviewQuestions": "{count} perguntas de entrevista geradas", + "callingBatchInterviewApi": "Chamando API de entrevista em lote (duas plataformas): {count} agentes", + "interviewApiReturned": "API de entrevista retornou: {count} resultados, success={success}", + "interviewApiReturnedFailure": "API de entrevista retornou falha: {error}", + "interviewApiCallFailed": "Falha na chamada da API de entrevista (ambiente não está em execução?): {error}", + "interviewApiCallException": "Exceção na chamada da API de entrevista: {error}", + "interviewAgentsComplete": "InterviewAgents concluído: {count} agentes entrevistados (duas plataformas)", + "loadedRedditProfiles": "{count} perfis carregados de reddit_profiles.json", + "readRedditProfilesFailed": "Falha ao ler reddit_profiles.json: {error}", + "loadedTwitterProfiles": "{count} perfis carregados de twitter_profiles.csv", + "readTwitterProfilesFailed": "Falha ao ler twitter_profiles.csv: {error}", + "llmSelectAgentFailed": "Seleção de agente pela LLM falhou; usando seleção padrão: {error}", + "generateInterviewQuestionsFailed": "Falha ao gerar perguntas de entrevista: {error}", + "generateInterviewSummaryFailed": "Falha ao gerar resumo da entrevista: {error}" + } +} diff --git a/package.json b/package.json index 3baf424a..ca254d48 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "dev": "concurrently --kill-others -n \"backend,frontend\" -c \"green,cyan\" \"npm run backend\" \"npm run frontend\"", "backend": "cd backend && uv run python run.py", "frontend": "cd frontend && npm run dev", - "build": "cd frontend && npm run build" + "build": "cd frontend && npm run build", + "test:i18n": "node scripts/validate-locale.mjs" }, "devDependencies": { "concurrently": "^9.2.4" diff --git a/scripts/validate-locale.mjs b/scripts/validate-locale.mjs new file mode 100644 index 00000000..92db8a9e --- /dev/null +++ b/scripts/validate-locale.mjs @@ -0,0 +1,73 @@ +import { readFileSync, existsSync } from 'node:fs' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const localePath = (name) => resolve(root, 'locales', `${name}.json`) +const readJson = (path) => JSON.parse(readFileSync(path, 'utf8')) + +const languages = readJson(resolve(root, 'locales', 'languages.json')) +const en = readJson(localePath('en')) +const ptPath = localePath('pt-BR') + +const errors = [] + +if (!languages['pt-BR']) { + errors.push('languages.json must register pt-BR') +} + +if (!existsSync(ptPath)) { + errors.push('locales/pt-BR.json must exist') +} + +const flattenKeys = (value, prefix = '') => { + if (Array.isArray(value)) { + return [prefix] + } + if (value && typeof value === 'object') { + return Object.keys(value).flatMap((key) => flattenKeys(value[key], prefix ? `${prefix}.${key}` : key)) + } + return [prefix] +} + +if (existsSync(ptPath)) { + const pt = readJson(ptPath) + const enKeys = flattenKeys(en).sort() + const ptKeys = flattenKeys(pt).sort() + const missing = enKeys.filter((key) => !ptKeys.includes(key)) + const extra = ptKeys.filter((key) => !enKeys.includes(key)) + + if (missing.length) { + errors.push(`pt-BR is missing keys: ${missing.slice(0, 20).join(', ')}`) + } + if (extra.length) { + errors.push(`pt-BR has extra keys: ${extra.slice(0, 20).join(', ')}`) + } + + const firstScreenKeys = [ + 'home.dragToUpload', + 'home.startEngine', + 'home.tagline', + 'home.promptPlaceholder', + 'home.workflowSequence', + 'main.layoutWorkbench' + ] + + const getValue = (obj, path) => path.split('.').reduce((current, key) => current?.[key], obj) + for (const key of firstScreenKeys) { + const value = getValue(pt, key) + if (typeof value !== 'string') { + errors.push(`pt-BR key ${key} must be a string`) + continue + } + if (typeof value === 'string' && /\b(Upload|Start|Workflow|Workbench|Describe)\b/.test(value)) { + errors.push(`pt-BR key ${key} still appears to be English: ${value}`) + } + } +} + +if (errors.length) { + console.error(errors.join('\n')) + process.exit(1) +} + +console.log('pt-BR locale validation passed')