Skip to content

Commit 590ed05

Browse files
melgarafaelclaude
andcommitted
docs(marca): white-label em EN e ES, com selo que reprova traducao defasada
O guia comercial do revendedor existia so em portugues. Guia errado em ingles e PIOR que ausente, entao a traducao vem com o gate junto: um selo com o hash do original no topo de cada copia, e um caso que reprova quando o original anda sem as traducoes andarem. O FORMATO NAO FOI INVENTADO — o repo ja tinha os tres precedentes, e eles foram copiados em vez de reinventados: scripts/inventario-de-telas.leitura.ts modulo puro irmao do script, porque o + lint-channels.pattern.ts script varre disco e chama process.exit manifest-x-migrations.test.ts catraca sobre texto que compila errado lib/channels/meta/contract-hash.ts hash como ancora de obsolescencia, com os DOIS modos de errar declarados E por causa do terceiro precedente o hash normaliza CRLF, espaco a direita e linha em branco no fim: hash instavel produz alarme falso, alarme falso produz alguem desligando o gate. O CORTE DO PLANO FOI RESPEITADO E AGORA ESTA ESCRITO NOS TRES ARQUIVOS: os READMEs NAO entram no selo. Selar 30 KB x 3 no documento mais editado do repo transformaria cada um dos 9 commits do ciclo normal num PR bloqueado ate 490 linhas serem re-traduzidas duas vezes — e o desfecho realista nao seria "traducoes em dia", seria alguem rodar --todas sem traduzir. O gate desenharia o proprio mecanismo de derrota. Escrito no rodape para o proximo nao "completar" o gate por simetria. AUDITORIA ANTES DE PROPAGAR, porque traduzir mentira e multiplica-la: varridas as afirmacoes do original contra o codigo — install.sh pergunta e grava as chaves, os dois scripts chamam marca-emails.sh, a rampa tem mesmo 11 stops, o rls-isolation existe, o proxy e NOOP por host declarado, o agent.sh reverte a imagem e nao o banco, os 6 scripts da tabela Operacao existem, e o alarme de orcamento DE FATO nao tem linha no scheduler. Nenhuma falsa. Uma aresta reportada sem mexer: desde 7934e0d o logo renderizado e o RESOLVIDO, e precisaSemear so semeia linha inteiramente vazia — entao numa instalacao cujo logo_url ja foi semeado com valor, trocar APP_LOGO_URL no .env depois nao troca o logo. Nao e frase falsa (com logo_url nulo, o caso comum, a camada do .env vence) e a regra geral ja esta escrita dois paragrafos abaixo. A onda do upload reescreve a secao inteira. FIDELIDADE MEDIDA POR FERRAMENTA, nao por leitura: 15 headings nos mesmos niveis e mesma ordem nos tres arquivos, 197 linhas cada, e cada chave e contrato (APP_*, RESEND_*, SUPPORT_EMAIL, X-Deskcomm-Signature, token_hash, as duas rotas de marca, rls-isolation, agent.sh, Atkinson Hyperlegible, CD/ANPD 19/2024) aparece o MESMO numero de vezes nos tres. So valores de exemplo foram localizados. O aviso contra vender "servidor no Brasil = conformidade com a LGPD" esta nos tres idiomas. Sabotagem, previsto vs medido: 2/2, 1/1, 1/1. A segunda e a que importa — trocar o CAMINHO dentro do selo mantendo o hash certo reprova so o caso 3, provando que ele nao e decorativo. A terceira mede o que separa "nada defasado" de "parei de medir". O par +13 reconciliado: 11 do teste novo + 2 porque seed-nao-le-env-local monta suas sondas com readdirSync("scripts") e cada script novo vira um caso. Sem essa conta, "+13 para um arquivo de 11" seria numero pegando carona. LIMITE DECLARADO: o selo prova que alguem OLHOU o original naquele estado, nunca que traduziu bem. Contra `--todas` sem traduzir so existem duas defesas, ambas fracas por natureza: o script tem CONFERIR como default (escrever precisa ser pedido) e o aviso no cabecalho. typecheck 0 · lint 0 · lint:channels 0 · test:unit 395 files / 4481 passed · test:shell 272 ✓ / 0 ✗. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TCiffR3ugjQbxsfceQE2GB
1 parent 25910ac commit 590ed05

9 files changed

Lines changed: 729 additions & 4 deletions

File tree

README.en.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ Among the invariants is the **RLS isolation test**: it creates 2 organizations,
329329
| [`VISION.md`](VISION.md) | **Vision & positioning** — what the project is, what it believes, where it's going |
330330
| [`CHANGELOG.md`](CHANGELOG.md) | What changed in each version — **read your target version's section before updating** |
331331
| [`docs/SETUP.md`](docs/SETUP.md) | Development setup, step by step, for every integration |
332-
| [`docs/white-label.md`](docs/white-label.md) | **Installing for clients** — rebranding, one-install-per-client vs shared, reseller operations |
332+
| [`docs/white-label.en.md`](docs/white-label.en.md) | **Installing for clients** — rebranding, one-install-per-client vs shared, reseller operations |
333333
| [`docs/runbooks/waha-hostgator.md`](docs/runbooks/waha-hostgator.md) | Production runbook for WAHA (sizing, recovery) |
334334
| [`CLAUDE.md`](CLAUDE.md) | Non-negotiable conventions (required reading to contribute) |
335335
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | One-page architecture overview |

README.es.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ Entre los invariantes está el **test de aislamiento RLS**: crea 2 organizacione
330330
| [`VISION.md`](VISION.md) | **Visión y posicionamiento** — qué es el proyecto, en qué cree y hacia dónde va |
331331
| [`CHANGELOG.md`](CHANGELOG.md) | Qué cambió en cada versión — **lee la sección de tu versión antes de actualizar** |
332332
| [`docs/SETUP.md`](docs/SETUP.md) | Setup de desarrollo, paso a paso, de todas las integraciones |
333-
| [`docs/white-label.md`](docs/white-label.md) | **Instalar para clientes** — cambiar la marca, una instalación por cliente vs compartida, reventa |
333+
| [`docs/white-label.es.md`](docs/white-label.es.md) | **Instalar para clientes** — cambiar la marca, una instalación por cliente vs compartida, reventa |
334334
| [`docs/runbooks/waha-hostgator.md`](docs/runbooks/waha-hostgator.md) | Runbook de WAHA en producción (dimensionamiento, recuperación) |
335335
| [`CLAUDE.md`](CLAUDE.md) | Convenciones no negociables (lectura obligatoria para contribuir) |
336336
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | Visión de 1 página de la arquitectura |

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ Documentação de *processo*. Alta rotatividade; trate como estado, não como co
136136
- [`handoffs/`](handoffs/) — arquivados: casos humanos, inbox multimodal, CRM vivo, LGPD, wave1-devvivo, contrato wave5, briefing CRM vivo
137137
- [`stories/`](stories/) — épicos e stories (`epics/MASTER.md` = plano por epic/wave)
138138
- [`superpowers/`](superpowers/)`plans/` e `specs/` datados por onda, mais `handoffs/`
139-
- [`growth/`](growth/) — material de crescimento · [`brand/`](brand/) — marca · [`white-label.md`](white-label.md) — instalação com marca própria
139+
- [`growth/`](growth/) — material de crescimento · [`brand/`](brand/) — marca · [`white-label.md`](white-label.md) — instalação com marca própria, também em [en](white-label.en.md) e [es](white-label.es.md) (traduções seladas pelo hash do original; ver `scripts/selar-traducao.ts`)
140140
- [`../plan/`](../plan/) — backlog do gov-loop (`features.json` 31/31, `phases.md`, `progress.md`)
141141
- [`../loop/`](../loop/) — máquina do gov-loop (`LOOP.md`, `CHECKPOINT.md`, `checkpoints/G1..G6-report.md` + `.approved`)
142142
- [`../tasks/todo.md`](../tasks/todo.md) — workflow de construção original (Fase 0 → PRD → specs)

docs/white-label.en.md

Lines changed: 199 additions & 0 deletions
Large diffs are not rendered by default.

docs/white-label.es.md

Lines changed: 199 additions & 0 deletions
Large diffs are not rendered by default.

docs/white-label.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
🇧🇷 Português · [🇺🇸 English](white-label.en.md) · [🇪🇸 Español](white-label.es.md)
2+
13
# Instalar para clientes (agências e revendedores)
24

35
Guia para quem instala o DeskcommCRM **para outras empresas** — agência, consultoria, revendedor — e cobra por isso.
@@ -190,4 +192,6 @@ Guia completo de instalação: [`hostgator-setup-kit/README.md`](../hostgator-se
190192

191193
*Última atualização: 14 de agosto de 2026 — revisão completa depois do épico de marca própria. As duas frases mais citadas deste documento ("cores, fontes e tema não são configuráveis" e "a marca é por instalação, não por organização") deixaram de ser verdadeiras e foram reescritas com o limite honesto de hoje.*
192194

193-
*Este documento existe em português apenas. Os três READMEs (pt/en/es) apontam todos para ele. Tradução é dívida declarada, com pré-requisito nomeado: sem um gate que reprove tradução defasada, três cópias divergem no primeiro conserto seguinte — e um guia comercial errado em inglês é pior que um guia inexistente.*
195+
*Este documento existe em três idiomas, e o pré-requisito que a versão anterior deste rodapé nomeava foi pago: as traduções carregam na primeira linha um selo com o hash do original, e editar `docs/white-label.md` sem re-selar reprova `pnpm test:unit`. Depois de traduzir, re-sele com `pnpm exec tsx scripts/selar-traducao.ts --todas`.*
196+
197+
*Os três READMEs ficaram **fora** do selo de propósito. São o arquivo mais editado do repositório: com selo, cada conserto viraria um PR bloqueado até ~490 linhas serem re-traduzidas duas vezes — e o desfecho realista disso não é tradução em dia, é alguém re-selar sem traduzir, que é o único jeito de o selo morrer. Eles entram quando alguém quiser pagar esse custo de olhos abertos.*

scripts/selar-traducao.selo.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* O SELO DE TRADUÇÃO — manifesto, hash e leitura, sem I/O e sem `process.exit`.
3+
*
4+
* ## O problema que o selo resolve
5+
*
6+
* Documento traduzido não tem catraca nenhuma. O original é corrigido, as
7+
* cópias ficam para trás, e ninguém percebe: markdown errado compila, e uma
8+
* tradução defasada parece exatamente igual a uma tradução em dia. O rodapé de
9+
* `docs/white-label.md` chegou a declarar isso como dívida — *"sem um gate que
10+
* reprove tradução defasada, três cópias divergem no primeiro conserto
11+
* seguinte"* — e declarar não conserta.
12+
*
13+
* O selo é o hash do original gravado na primeira linha da tradução. Editar o
14+
* original muda o hash; a tradução ainda carrega o antigo; `pnpm test:unit`
15+
* reprova nomeando o comando de re-selar. É a mesma disciplina de
16+
* `tests/unit/manifest-x-migrations.test.ts` (texto que precisa de catraca) e de
17+
* `lib/channels/meta/contract-hash.ts` (hash como âncora de obsolescência).
18+
*
19+
* ## Por que é um módulo separado do script
20+
*
21+
* Precedente medido no repo: `scripts/inventario-de-telas.leitura.ts` e
22+
* `scripts/lint-channels.pattern.ts` existem porque os scripts deles varrem o
23+
* disco e chamam `process.exit` no topo do módulo — importá-los de um teste
24+
* rodaria o script. Mesma restrição aqui: `tests/unit/traducao-nao-defasa.test.ts`
25+
* precisa do MESMO manifesto e do MESMO hash que `scripts/selar-traducao.ts`
26+
* usa. Duas listas em sincronia seriam o defeito que o selo existe para pegar.
27+
*
28+
* ## Por que só o par `white-label`, e não os READMEs
29+
*
30+
* Decisão declarada, com razão medida. Os três READMEs são o arquivo mais
31+
* editado do repositório (~30 KB cada, ~490 linhas): selá-los transformaria
32+
* cada conserto de README num PR bloqueado até duas re-traduções completas. O
33+
* desfecho realista disso não é "traduções em dia" — é alguém rodar
34+
* `selar-traducao.ts --todas` sem traduzir, que é o único jeito de o selo
35+
* morrer. Um gate que desenha o próprio mecanismo de derrota é pior que gate
36+
* nenhum, porque ainda parece proteção. `docs/white-label.md` tem 197 linhas e
37+
* baixa rotatividade: aqui o custo cabe.
38+
*
39+
* Acrescentar par a esta lista é decisão de quem vai pagar o custo, não zelo de
40+
* completude.
41+
*/
42+
43+
import { createHash } from "node:crypto";
44+
45+
/** Um par original → tradução, sob selo. */
46+
export type ParTraduzido = {
47+
/** Caminho do original, a partir da raiz do repo. É o que vai DENTRO do selo. */
48+
readonly original: string;
49+
/** Caminho da tradução, a partir da raiz do repo. */
50+
readonly traducao: string;
51+
/** Só para a mensagem de falha ficar legível. */
52+
readonly idioma: string;
53+
};
54+
55+
export const TRADUCOES: readonly ParTraduzido[] = [
56+
{ original: "docs/white-label.md", traducao: "docs/white-label.en.md", idioma: "inglês" },
57+
{ original: "docs/white-label.md", traducao: "docs/white-label.es.md", idioma: "espanhol" },
58+
];
59+
60+
/** O comando que conserta — a mensagem de falha SEMPRE o nomeia. */
61+
export const COMANDO_DE_RESELO = "pnpm exec tsx scripts/selar-traducao.ts --todas";
62+
63+
/**
64+
* A primeira linha de toda tradução.
65+
*
66+
* O caminho do original entra no selo de propósito: sem ele, um selo copiado de
67+
* OUTRO arquivo passaria por todas as outras conferências (existe, casa a
68+
* regex, e o hash é de um original de verdade). É o modo de falha que a
69+
* asserção 3 do teste guarda.
70+
*
71+
* Comentário HTML porque o GitHub não renderiza nada — o leitor da tradução não
72+
* vê o selo, e quem edita o arquivo vê na primeira linha.
73+
*/
74+
export const SELO = /^<!-- traduzido-de: (\S+)@([0-9a-f]{12}) -->$/;
75+
76+
/** Monta a linha do selo. Fonte única: o script grava o que a regex lê. */
77+
export function linhaDoSelo(original: string, hash: string): string {
78+
return `<!-- traduzido-de: ${original}@${hash} -->`;
79+
}
80+
81+
/** Lê a linha 1 de uma tradução. `null` = não é um selo. */
82+
export function lerSelo(conteudo: string): { original: string; hash: string } | null {
83+
const primeira = conteudo.split("\n", 1)[0] ?? "";
84+
const m = SELO.exec(primeira.trimEnd());
85+
return m?.[1] && m[2] ? { original: m[1], hash: m[2] } : null;
86+
}
87+
88+
/**
89+
* O hash do original — 12 hex de um SHA-256 do conteúdo NORMALIZADO.
90+
*
91+
* A normalização não é enfeite: sem ela, um editor configurado com CRLF ou um
92+
* `format` que apara espaço à direita reprova as duas traduções sem que uma
93+
* palavra do texto tenha mudado. Gate que acende sem defeito é gate que se
94+
* aprende a re-selar no automático — exatamente o que este arquivo evita.
95+
*
96+
* Normaliza o que NÃO muda o texto lido: fim de linha, espaço à direita de cada
97+
* linha e linhas em branco no fim do arquivo. Qualquer outra diferença — uma
98+
* vírgula, uma palavra, um parágrafo novo — muda o hash, que é o ponto.
99+
*
100+
* 12 hex (48 bits) porque o selo é lido por humano na primeira linha do arquivo
101+
* e o adversário aqui é o esquecimento, não a colisão adversarial.
102+
*/
103+
export function hashDoOriginal(conteudo: string): string {
104+
const normalizado = conteudo
105+
.replace(/\r\n?/g, "\n")
106+
.split("\n")
107+
.map((linha) => linha.replace(/[ \t]+$/, ""))
108+
.join("\n")
109+
.replace(/\n+$/, "\n");
110+
return createHash("sha256").update(normalizado, "utf8").digest("hex").slice(0, 12);
111+
}
112+
113+
/**
114+
* Controle positivo: o manifesto tem de estar vivo antes de a ausência de
115+
* achado valer alguma coisa.
116+
*
117+
* Lista vazia devolveria "nenhuma tradução defasada" — indistinguível de "não
118+
* há tradução nenhuma sob gate". É o mesmo modo de falha de `conferirVacuidade`
119+
* em `inventario-de-telas.leitura.ts`, e por isso LANÇA em vez de devolver
120+
* `false`: quem esquecer de chamar não fica verde por omissão.
121+
*/
122+
export function conferirVacuidade(pares: readonly ParTraduzido[]): void {
123+
if (pares.length === 0) {
124+
throw new Error(
125+
"TRADUCOES está vazio — o gate de tradução não está medindo nada. " +
126+
"Se a intenção foi remover um par, remova também o teste que o cita.",
127+
);
128+
}
129+
}

scripts/selar-traducao.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Confere e regrava o selo das traduções.
3+
*
4+
* Uso:
5+
* `pnpm exec tsx scripts/selar-traducao.ts` — só confere; sai ≠0 se algo defasou
6+
* `pnpm exec tsx scripts/selar-traducao.ts --todas` — regrava o selo de todas
7+
*
8+
* ⚠️ `--todas` NÃO traduz nada: ele apenas afirma que a tradução já acompanha o
9+
* original. Rodá-lo sem ter traduzido é o único jeito conhecido de o gate
10+
* morrer — o selo passa a dizer "em dia" sobre um texto que envelheceu. Por
11+
* isso o modo de conferência é o padrão, e o de escrita precisa ser pedido.
12+
*
13+
* O manifesto e o hash moram em `selar-traducao.selo.ts`: este arquivo lê o
14+
* disco e chama `process.exit`, e importá-lo de um teste rodaria a escrita.
15+
* Mesmo motivo de `inventario-de-telas.leitura.ts` e `lint-channels.pattern.ts`.
16+
*/
17+
import { readFileSync, writeFileSync } from "node:fs";
18+
19+
import {
20+
COMANDO_DE_RESELO,
21+
TRADUCOES,
22+
conferirVacuidade,
23+
hashDoOriginal,
24+
lerSelo,
25+
linhaDoSelo,
26+
} from "./selar-traducao.selo";
27+
28+
const escrever = process.argv.includes("--todas");
29+
const desconhecidos = process.argv.slice(2).filter((a) => a !== "--todas");
30+
if (desconhecidos.length > 0) {
31+
console.error(`Argumento desconhecido: ${desconhecidos.join(", ")}. Use --todas ou nada.`);
32+
process.exit(2);
33+
}
34+
35+
try {
36+
conferirVacuidade(TRADUCOES);
37+
} catch (erro) {
38+
console.error(erro instanceof Error ? erro.message : String(erro));
39+
process.exit(1);
40+
}
41+
42+
const defasadas: string[] = [];
43+
44+
for (const par of TRADUCOES) {
45+
const esperado = hashDoOriginal(readFileSync(par.original, "utf8"));
46+
const conteudo = readFileSync(par.traducao, "utf8");
47+
const selo = lerSelo(conteudo);
48+
49+
if (selo && selo.original === par.original && selo.hash === esperado) {
50+
console.info(` em dia ${par.traducao} (${par.original}@${esperado})`);
51+
continue;
52+
}
53+
54+
if (!escrever) {
55+
defasadas.push(
56+
selo === null
57+
? `${par.traducao} — sem selo na linha 1`
58+
: `${par.traducao} — selo diz ${selo.original}@${selo.hash}, o original está em ${par.original}@${esperado}`,
59+
);
60+
continue;
61+
}
62+
63+
// Sem selo ainda: a linha nasce no topo. Com selo: a linha 1 é substituída —
64+
// nunca acumula duas, que passariam pela regex e mentiriam sobre qual vale.
65+
const corpo = selo === null ? `${linhaDoSelo(par.original, esperado)}\n\n${conteudo}` : conteudo;
66+
const linhas = corpo.split("\n");
67+
linhas[0] = linhaDoSelo(par.original, esperado);
68+
writeFileSync(par.traducao, linhas.join("\n"));
69+
console.info(` selada ${par.traducao} (${par.original}@${esperado})`);
70+
}
71+
72+
if (defasadas.length > 0) {
73+
console.error(
74+
`\nTradução defasada:\n ${defasadas.join("\n ")}\n\n` +
75+
` Traduza a mudança do original e então rode: ${COMANDO_DE_RESELO}`,
76+
);
77+
process.exit(1);
78+
}
79+
80+
console.info("\nselar-traducao: ok");
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* A TRADUÇÃO NÃO PODE FICAR PARA TRÁS DO ORIGINAL EM SILÊNCIO.
3+
*
4+
* ## O defeito que fez este arquivo existir
5+
*
6+
* `docs/white-label.md` é o guia comercial do revendedor — o texto que alguém lê
7+
* antes de vender uma instalação. Ele não tinha tradução, e o rodapé dizia por
8+
* quê: *"sem um gate que reprove tradução defasada, três cópias divergem no
9+
* primeiro conserto seguinte — e um guia comercial errado em inglês é pior que
10+
* um guia inexistente."*
11+
*
12+
* O modo de falha é o mais silencioso que existe: markdown errado compila,
13+
* `typecheck` passa, `lint` passa, e uma tradução de três meses atrás parece na
14+
* tela exatamente igual a uma tradução de hoje. O leitor de inglês não tem como
15+
* saber que a frase que ele está lendo foi corrigida no original.
16+
*
17+
* ## Os quatro casos, e por que são quatro
18+
*
19+
* Cada um cobre um modo de falha que os outros três deixam passar:
20+
*
21+
* 1. **arquivo existe** — alguém apaga a tradução e o par some do disco sem o
22+
* manifesto notar;
23+
* 2. **linha 1 é um selo** — a tradução nasce sem selo (o caso normal de quem
24+
* cria o arquivo à mão) e nunca entra sob o gate;
25+
* 3. **o caminho DENTRO do selo é o do manifesto** — selo copiado de outro
26+
* arquivo. Este é o único que os outros três não pegam: ele existe, casa a
27+
* regex e carrega um hash legítimo — só que de outro original;
28+
* 4. **o hash bate** — o caso que faz o arquivo existir: o original mudou e a
29+
* tradução não acompanhou.
30+
*
31+
* ## Por que a vacuidade vem antes de tudo
32+
*
33+
* As quatro asserções rodam sobre uma lista. Lista vazia dá quatro zeros e um
34+
* verde — indistinguível de "nenhuma tradução defasada". `conferirVacuidade`
35+
* LANÇA, e é a mesma função que o script chama: uma checagem, não duas.
36+
*
37+
* ## O que este arquivo NÃO guarda
38+
*
39+
* Não guarda os três READMEs, e a ausência é decisão declarada — a razão medida
40+
* está no cabeçalho de `scripts/selar-traducao.selo.ts` e no rodapé do próprio
41+
* `docs/white-label.md`. Não guarda a QUALIDADE da tradução: o selo prova que
42+
* alguém olhou o original naquele estado, nunca que traduziu bem. E não impede
43+
* `--todas` sem traduzir; contra isso o que existe é o padrão do script ser
44+
* conferir, e o aviso escrito nele.
45+
*/
46+
import { existsSync, readFileSync } from "node:fs";
47+
import path from "node:path";
48+
49+
import { describe, expect, it } from "vitest";
50+
51+
import {
52+
COMANDO_DE_RESELO,
53+
SELO,
54+
TRADUCOES,
55+
conferirVacuidade,
56+
hashDoOriginal,
57+
lerSelo,
58+
} from "../../scripts/selar-traducao.selo";
59+
60+
const raiz = (relativo: string) => path.join(process.cwd(), relativo);
61+
62+
describe("o manifesto de traduções está vivo", () => {
63+
it("tem pelo menos um par — senão as asserções abaixo passam sobre nada", () => {
64+
expect(() => conferirVacuidade(TRADUCOES)).not.toThrow();
65+
});
66+
});
67+
68+
describe("hashDoOriginal", () => {
69+
it("ignora fim de linha e espaço à direita — senão o gate acende sem defeito", () => {
70+
// Um editor com CRLF ou um `prettier --write` reprovariam toda tradução sem
71+
// que uma palavra do texto tivesse mudado, e o conserto aprendido seria
72+
// re-selar no automático.
73+
const lf = "# Título\n\nUm parágrafo.\n";
74+
expect(hashDoOriginal("# Título\r\n\r\nUm parágrafo.\r\n")).toBe(hashDoOriginal(lf));
75+
expect(hashDoOriginal("# Título \n\nUm parágrafo.\t\n")).toBe(hashDoOriginal(lf));
76+
expect(hashDoOriginal("# Título\n\nUm parágrafo.\n\n\n")).toBe(hashDoOriginal(lf));
77+
});
78+
79+
it("muda quando o TEXTO muda — inclusive por uma palavra", () => {
80+
expect(hashDoOriginal("A cor é derivada.\n")).not.toBe(hashDoOriginal("A cor é aplicada.\n"));
81+
});
82+
});
83+
84+
describe.each(TRADUCOES)("$traducao ($idioma)", (par) => {
85+
it("existe no disco", () => {
86+
expect(existsSync(raiz(par.traducao)), `${par.traducao} sumiu do disco`).toBe(true);
87+
});
88+
89+
it("tem um selo na linha 1", () => {
90+
const primeira = readFileSync(raiz(par.traducao), "utf8").split("\n", 1)[0] ?? "";
91+
expect(
92+
primeira,
93+
`${par.traducao} não começa com o selo. Rode: ${COMANDO_DE_RESELO}`,
94+
).toMatch(SELO);
95+
});
96+
97+
it("o selo aponta para o original deste par, e não para outro arquivo", () => {
98+
const selo = lerSelo(readFileSync(raiz(par.traducao), "utf8"));
99+
expect(
100+
selo?.original,
101+
`o selo de ${par.traducao} cita outro original — selo copiado de outro arquivo`,
102+
).toBe(par.original);
103+
});
104+
105+
it("o hash do selo é o do original de HOJE", () => {
106+
const selo = lerSelo(readFileSync(raiz(par.traducao), "utf8"));
107+
const esperado = hashDoOriginal(readFileSync(raiz(par.original), "utf8"));
108+
expect(
109+
selo?.hash,
110+
`${par.original} mudou e ${par.traducao} não acompanhou. ` +
111+
`Traduza a mudança e então rode: ${COMANDO_DE_RESELO}`,
112+
).toBe(esperado);
113+
});
114+
});

0 commit comments

Comments
 (0)