feat: add Brazilian Portuguese locale

This commit is contained in:
Vitor Wild 2026-07-30 19:12:53 -03:00
parent 72aa216bf2
commit d7c52aa5a8
6 changed files with 943 additions and 3 deletions

View File

@ -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"
```

View File

@ -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
})

View File

@ -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": "Пожалуйста, отвечайте на русском языке."

667
locales/pt-BR.json Normal file
View File

@ -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}"
}
}

View File

@ -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"

View File

@ -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')