From e6f0572b16684198bbfc21be51d95f61e3d9773f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Be=C3=B1at=20Espi=C3=B1a?=
Date: Mon, 9 Nov 2026 20:05:00 +0100
Subject: [PATCH 1/2] feat: label the Japanese and Portuguese posts as machine
translations
The note under a translated post now says the ja and pt-BR versions are
machine-translated and links to a new issue to report a mistake; Spanish,
which the author reviews, keeps its note. A back-translation of both
posts found and fixed one change of meaning in Japanese (what crypto-
shredding misses read as what leaks), a claim about Brazil's LGPD the
original never makes, and a handful of calques in Portuguese.
---
src/components/blog/post-page.astro | 10 +++++++++
.../right-to-be-forgotten-event-sourcing.md | 2 +-
.../translations/pt-br/a-command-is-a-file.md | 8 +++----
.../right-to-be-forgotten-event-sourcing.md | 21 +++++++++----------
src/i18n.ts | 10 +++++++--
5 files changed, 33 insertions(+), 18 deletions(-)
diff --git a/src/components/blog/post-page.astro b/src/components/blog/post-page.astro
index a0485d2..11bf551 100644
--- a/src/components/blog/post-page.astro
+++ b/src/components/blog/post-page.astro
@@ -97,6 +97,16 @@ const structuredData = {
locale !== "en" && (
)
}
diff --git a/src/content/translations/ja/right-to-be-forgotten-event-sourcing.md b/src/content/translations/ja/right-to-be-forgotten-event-sourcing.md
index 5dac503..df467cb 100644
--- a/src/content/translations/ja/right-to-be-forgotten-event-sourcing.md
+++ b/src/content/translations/ja/right-to-be-forgotten-event-sourcing.md
@@ -1,6 +1,6 @@
---
title: 決して削除しないログで「忘れられる権利」を実現する
-description: イベントソーシングはすべての事実を永遠に残し、GDPR は本人に消去を求める権利を与えます。クリプトシュレッディングがその両方をどう両立させるか、何が漏れるか、そしてイベントストアが取る三つのアプローチ。
+description: イベントソーシングはすべての事実を永遠に残し、GDPR は本人に消去を求める権利を与えます。クリプトシュレッディングがその両方をどう両立させるか、何を取りこぼすか、そしてイベントストアが取る三つのアプローチ。
tryIt:
label: デモで購入者を忘れる
---
diff --git a/src/content/translations/pt-br/a-command-is-a-file.md b/src/content/translations/pt-br/a-command-is-a-file.md
index 7f2a017..de975a3 100644
--- a/src/content/translations/pt-br/a-command-is-a-file.md
+++ b/src/content/translations/pt-br/a-command-is-a-file.md
@@ -1,8 +1,8 @@
---
title: Um comando é um arquivo
-description: Uma feature inteira com event sourcing em dois arquivos pequenos, e as cinco coisas que você deixa de escrever para chegar lá. De onde vem esse formato, e quanto ele custa.
+description: Uma feature inteira com event sourcing em dois arquivos pequenos, e as cinco coisas que você deixa de escrever para chegar lá. De onde vem esse formato, e o que ele custa.
tryIt:
- label: Bata no limite de lugares na demo ao vivo
+ label: Chegue ao limite de lugares na demo ao vivo
---
Event sourcing tem fama de cerimônia. Antes de guardar o primeiro fato, você já escreveu uma classe
@@ -168,10 +168,10 @@ projeção trata.
Os frameworks de event sourcing já consolidados modelam o agregado como uma classe: uma classe
anotada no
[Axon](https://docs.axoniq.io/axon-framework-reference/5.4/commands/entities/event-sourced-entity/),
-uma classe simples dobrada por métodos `Apply` no
+uma classe simples que faz o fold com métodos `Apply` no
[Marten](https://martendb.io/events/projections/aggregate-projections.html). É um design sólido,
comprovado ao longo de anos, e é o que permite ao Axon rodar o mesmo handler numa frota de
-serviços. Para um app TypeScript, porém, funções em arquivos dão três coisas que uma classe não dá:
+serviços. Para um único app TypeScript, porém, funções em arquivos dão três coisas que uma classe não dá:
- **Um estado que não pode se desviar dos eventos.** Ele é o que os eventos devolvem, então não há
uma segunda declaração para manter em sincronia à mão.
diff --git a/src/content/translations/pt-br/right-to-be-forgotten-event-sourcing.md b/src/content/translations/pt-br/right-to-be-forgotten-event-sourcing.md
index a3bd863..9b0d12e 100644
--- a/src/content/translations/pt-br/right-to-be-forgotten-event-sourcing.md
+++ b/src/content/translations/pt-br/right-to-be-forgotten-event-sourcing.md
@@ -1,6 +1,6 @@
---
title: O direito ao esquecimento, num log que nunca apaga
-description: Event sourcing guarda cada fato para sempre; a GDPR (e a LGPD) deixam uma pessoa pedir que seus dados sejam apagados. Como o crypto-shredding torna as duas coisas verdade, o que escapa e os três jeitos como os event stores o abordam.
+description: Event sourcing guarda cada fato para sempre; a GDPR deixa uma pessoa pedir que seus dados sejam apagados. Como o crypto-shredding torna as duas coisas verdade, o que escapa e os três jeitos como os event stores o abordam.
tryIt:
label: Esqueça um comprador na demo
---
@@ -9,8 +9,8 @@ Cedo ou tarde chega um e-mail: *por favor, apaguem todos os dados que vocês tê
seu sistema guarda o estado em tabelas, é uma tarde de comandos `DELETE` e uma conferida nos
backups. Se ele usa event sourcing, é uma crise de identidade. A graça do event store é que nada
do que entra nele é alterado ou removido. É isso que torna uma reconstrução fiel, uma trilha de
-auditoria completa e um bug reproduzível. E agora a lei, o artigo 17 da GDPR (no Brasil, o artigo 18 da
-LGPD prevê o mesmo direito), afirma que a parte de uma pessoa precisa sumir.
+auditoria completa e um bug reproduzível. E agora a lei, o artigo 17 da GDPR, afirma que a parte
+de uma pessoa precisa sumir.
As equipes costumam recorrer a uma de duas respostas, e as duas estão erradas. A primeira é
reescrever a história: encontrar os eventos da pessoa e editá-los no lugar. Isso quebra todas as
@@ -25,13 +25,13 @@ Existe uma terceira resposta, e ela começa por uma pergunta que a maioria pula.
Um evento registra uma decisão. `OrderPlaced` é o fato de que o cliente `c-42` pediu dois itens.
O nome, o endereço e o telefone dele não fazem parte desse fato: são detalhes de que a
transportadora precisa e que a nota fiscal imprime. Um command handler que os copia para o evento
-porque «estavam no comando» fez do evento o dono de dados que pertencem a outra parte do sistema.
+porque “estavam no comando” fez do evento o dono de dados que pertencem a outra parte do sistema.
Então a primeira ferramenta não é criptografia, é uma fronteira. O evento leva o id, os detalhes
ficam atrás de uma porta cuja implementação sabe apagá-los, e apagar a pessoa é um `DELETE` nesse
armazenamento. Mathias Verraes chama isso de
[forgettable payloads](https://verraes.net/2019/05/eventsourcing-patterns-forgettable-payloads).
-Basta sempre que o dado não é necessário para explicar o passado, e ainda o mantém fora de todas
+Basta sempre que o dado não for necessário para explicar o passado, e ainda o mantém fora de todas
as cópias do log.
Mas às vezes o dado *é* a decisão. `AddressChanged` registra o endereço para onde alguém se mudou.
@@ -55,8 +55,8 @@ Este é o stream de um cliente, antes e depois:
| `001 204` | `PersonalDataForgotten` | | |
Nada acima da última linha foi editado. As posições são as mesmas, os tipos de evento são os
-mesmos, o pedido continua sendo dobrado e os read models continuam sendo reconstruídos. Só a chave
-sumiu.
+mesmos, o fold do pedido continua funcionando e os read models continuam sendo reconstruídos. Só
+a chave sumiu.
No [Bounda](https://docs.bounda.dev/getting-started/), marcar um campo é uma palavra no schema do
evento:
@@ -81,7 +81,7 @@ seja um fold, uma projeção ou uma policy, recebe `string | null`.
Esse `null` não é questão de estilo. É o que faz o compilador levar você a cada lugar que supunha
que o e-mail sempre estaria lá: a projeção que o copiava para uma coluna `NOT NULL`, o fold que o
passava para minúsculas, a policy que envia o e-mail de boas-vindas. Os eventos de uma pessoa
-esquecida continuam sendo dobrados, projetados e reproduzidos. Eles carregam um buraco onde o dado
+esquecida continuam passando pelo fold, pela projeção e pelo replay. Eles carregam um buraco onde o dado
estava, e o seu código é obrigado a ter decidido o que esse buraco significa antes de chegar o
primeiro pedido de exclusão, não depois.
@@ -95,7 +95,7 @@ Apagar a chave torna os eventos ilegíveis. Não faz nada com tudo o que foi con
deles, e num sistema com event sourcing muita coisa é construída a partir deles:
- **Read models.** O diretório de clientes projetou o e-mail numa linha.
-- **Snapshots.** O estado dobrado do agregado, guardado em claro para que um stream longo carregue
+- **Snapshots.** O estado do agregado após o fold, guardado sem criptografia para que um stream longo carregue
rápido.
- **Respostas lembradas.** Um comando enviado com uma chave de idempotência guarda o que respondeu,
e a mensagem de uma rejeição pode citar o estado em que se baseou.
@@ -157,8 +157,7 @@ dados.
- **Os backups guardam as chaves.** As chaves ficam ao lado dos eventos, então um backup do banco
é uma cópia dos dados enquanto for mantido.
-- **Os logs guardam o que você imprimiu.** Se um handler registrou um payload, o log o tem em
- claro.
+- **Os logs guardam o que você imprimiu.** Se um handler registrou um payload, o log o tem em texto puro.
- **O que saiu do sistema continua lá fora,** a menos que uma policy vá apagá-lo.
- **Campos não marcados são texto puro.** A marca é por campo e por evento: esqueça de marcar um e
ele fica legível para sempre.
diff --git a/src/i18n.ts b/src/i18n.ts
index 3b24141..3ed48d2 100644
--- a/src/i18n.ts
+++ b/src/i18n.ts
@@ -26,6 +26,8 @@ export interface Language {
readonly languages: string;
readonly translated: string;
readonly original: string;
+ /** Only for machine translations, which a native reader is asked to correct. */
+ readonly report: string;
readonly newsletter: Newsletter;
}
@@ -55,6 +57,7 @@ export const LANGUAGES: Record = {
languages: "Read in",
translated: "",
original: "",
+ report: "",
newsletter: {
heading: "Newsletter",
lead: "Each new post by email. Nothing else.",
@@ -79,6 +82,7 @@ export const LANGUAGES: Record = {
languages: "Leer en",
translated: "Traducido del original en inglés.",
original: "Leer el original",
+ report: "",
newsletter: {
heading: "Newsletter",
lead: "Cada post nuevo por email. Nada más.",
@@ -101,8 +105,9 @@ export const LANGUAGES: Record = {
more: "ほかの記事",
seeItRun: "ライブデモで動きを見る",
languages: "言語",
- translated: "英語の原文からの翻訳です。",
+ translated: "英語の原文からの機械翻訳です。",
original: "原文を読む",
+ report: "誤りを報告する",
newsletter: {
heading: "ニュースレター",
lead: "新しい記事をメールで。それ以外は送りません。",
@@ -125,8 +130,9 @@ export const LANGUAGES: Record = {
more: "Mais posts",
seeItRun: "Veja funcionando na demo",
languages: "Ler em",
- translated: "Traduzido do original em inglês.",
+ translated: "Tradução automática do original em inglês.",
original: "Ler o original",
+ report: "Avisar de um erro",
newsletter: {
heading: "Newsletter",
lead: "Cada post novo por email. Nada mais.",
From 57e81847beef6713f48e22086410115107c5167d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Be=C3=B1at=20Espi=C3=B1a?=
Date: Mon, 9 Nov 2026 20:15:37 +0100
Subject: [PATCH 2/2] feat: list a post's translations under it, not above, and
fix the glossary
The language row leaves the head of the post: the original lists its
translations in one line under the text, and a translation keeps only
the note on where it comes from. No page redirects by language; search
engines pick the version through hreflang. GLOSSARY.md fixes how each
language renders Bounda's vocabulary and how a machine translation is
checked, and the README says how the blog's languages work.
---
README.md | 11 ++++-
src/components/blog/post-page.astro | 68 +++++++++-------------------
src/content/translations/GLOSSARY.md | 43 ++++++++++++++++++
src/i18n.ts | 6 +++
4 files changed, 80 insertions(+), 48 deletions(-)
create mode 100644 src/content/translations/GLOSSARY.md
diff --git a/README.md b/README.md
index 7e3612d..f032927 100644
--- a/README.md
+++ b/README.md
@@ -39,10 +39,17 @@ published. The blog also feeds `/blog/rss.xml` and `/llms.txt`.
Each post is translated into Spanish, Japanese and Brazilian Portuguese when it is published:
`src/content/translations//.md` holds the title, the description and the body in
that language, and takes the date, the kind and the draft flag from the English post, which is
-the original. It is served at `//blog//`, with its own card, `hreflang` links to
-every version and a note linking the original. The interface strings and the languages live in
+the original. It is served at `//blog//`, with its own card and `hreflang` links to
+every version, `x-default` being the original (set in `src/layouts/base.astro`). The interface strings and the languages live in
`src/i18n.ts`. A translation whose original does not exist fails the build.
+No page redirects by language: search engines pick the version through `hreflang`, and each link
+that is shared points at its language. The original lists its translations in one line under the
+post; a translation says where it comes from above it. Spanish is reviewed by the author and says
+*translated*; Japanese and Portuguese are machine translations, say so, and link to a new issue to
+report a mistake. [`GLOSSARY.md`](src/content/translations/GLOSSARY.md) fixes how each language
+renders Bounda's vocabulary, and how a machine translation is checked before it is published.
+
## Keeping the two sites coherent
[`DESIGN.md`](DESIGN.md) is the design system both sites follow: palette, type, logo, the ledger
diff --git a/src/components/blog/post-page.astro b/src/components/blog/post-page.astro
index 11bf551..c175c6d 100644
--- a/src/components/blog/post-page.astro
+++ b/src/components/blog/post-page.astro
@@ -72,27 +72,6 @@ const structuredData = {
{text.minutes(minutes)}
+ )
+ }
@@ -225,40 +221,20 @@ const structuredData = {
text-decoration: underline;
}
- .languages {
- display: flex;
- flex-wrap: wrap;
- gap: 8px 18px;
- align-items: baseline;
- margin-top: 14px;
- }
-
- .languages ul {
- display: flex;
- flex-wrap: wrap;
- gap: 6px 16px;
- margin: 0;
- padding: 0;
- list-style: none;
+ .also-in {
+ margin: 40px 0 0;
font-size: 0.875rem;
+ color: var(--muted);
}
- .languages a {
+ .also-in a {
color: var(--muted);
- text-decoration: none;
}
- .languages a:hover,
- .languages a[aria-current="page"] {
+ .also-in a:hover {
color: var(--text);
}
- .languages a[aria-current="page"] {
- text-decoration: underline;
- text-decoration-color: var(--accent);
- text-underline-offset: 4px;
- }
-
.translated {
margin: 14px 0 0;
font-size: 0.875rem;
diff --git a/src/content/translations/GLOSSARY.md b/src/content/translations/GLOSSARY.md
new file mode 100644
index 0000000..3eb6ba4
--- /dev/null
+++ b/src/content/translations/GLOSSARY.md
@@ -0,0 +1,43 @@
+# Translation glossary
+
+How the blog's translations render Bounda's vocabulary, so every post says the same thing the same
+way. Taken from the posts already published; a new term is added here the first time a post uses
+it. Code, identifiers, file names and error codes are never translated.
+
+Spanish is reviewed by the author. Japanese and Brazilian Portuguese are machine translations,
+checked before publishing by translating them back to English and comparing them with the
+original, and labelled as such on the page, with a link to report a mistake.
+
+| English | es | ja | pt-BR |
+|---|---|---|---|
+| event sourcing | event sourcing | イベントソーシング | event sourcing |
+| event store | event store | イベントストア | event store |
+| event | evento | イベント | evento |
+| command | comando | コマンド | comando |
+| query | query | クエリ | query |
+| aggregate | agregado | 集約 | agregado |
+| state | estado | 状態 | estado |
+| stream | stream | ストリーム | stream |
+| fold (noun / verb) | fold / plegar | 畳み込み / 畳み込む | fold / fazer o fold |
+| read model | read model | リードモデル | read model |
+| projection | proyección | プロジェクション | projeção |
+| policy | policy | ポリシー | policy |
+| process | process | プロセス | process |
+| handler | handler | ハンドラー | handler |
+| payload | payload | ペイロード | payload |
+| rejection | rechazo | 拒否 | rejeição |
+| snapshot | snapshot | スナップショット | snapshot |
+| idempotency key | clave de idempotencia | 冪等性キー | chave de idempotência |
+| dead letter | dead letter | デッドレター | dead letter |
+| port | puerto | ポート | porta |
+| crypto-shredding | crypto-shredding | クリプトシュレッディング | crypto-shredding |
+| in the clear | en claro | 平文で | sem criptografia |
+
+Notes:
+
+- **Do not add** what the original does not say: no local laws, examples or asides. A translation
+ that needs context for its readers gets it from the author, in the original.
+- Link texts that point to English pages stay in English, followed by "(in English)" in the
+ language of the page when that helps: (en inglés), (英語), (em inglês).
+- pt-BR uses Brazilian conventions: “ ” quotes, "você", the subjunctive after *sempre que*.
+- A scene of the demo keeps its English name, with a short gloss in parentheses.
diff --git a/src/i18n.ts b/src/i18n.ts
index 3ed48d2..2ea934d 100644
--- a/src/i18n.ts
+++ b/src/i18n.ts
@@ -28,6 +28,8 @@ export interface Language {
readonly original: string;
/** Only for machine translations, which a native reader is asked to correct. */
readonly report: string;
+ /** Under the original, before the translations it has. Only English uses it. */
+ readonly alsoIn: string;
readonly newsletter: Newsletter;
}
@@ -58,6 +60,7 @@ export const LANGUAGES: Record = {
translated: "",
original: "",
report: "",
+ alsoIn: "Also in",
newsletter: {
heading: "Newsletter",
lead: "Each new post by email. Nothing else.",
@@ -83,6 +86,7 @@ export const LANGUAGES: Record = {
translated: "Traducido del original en inglés.",
original: "Leer el original",
report: "",
+ alsoIn: "",
newsletter: {
heading: "Newsletter",
lead: "Cada post nuevo por email. Nada más.",
@@ -108,6 +112,7 @@ export const LANGUAGES: Record = {
translated: "英語の原文からの機械翻訳です。",
original: "原文を読む",
report: "誤りを報告する",
+ alsoIn: "",
newsletter: {
heading: "ニュースレター",
lead: "新しい記事をメールで。それ以外は送りません。",
@@ -133,6 +138,7 @@ export const LANGUAGES: Record = {
translated: "Tradução automática do original em inglês.",
original: "Ler o original",
report: "Avisar de um erro",
+ alsoIn: "",
newsletter: {
heading: "Newsletter",
lead: "Cada post novo por email. Nada mais.",