Over the past few days, I have continued adding new language versions to the A Tour of Go multilingual translation project.
Following Italian, I have successively completed:
- Dutch — Nederlands
- Brazilian Portuguese — Português (Brasil)
- Turkish — Türkçe
So far, the project has 10 officially launched community language versions:
Brazilian Portuguese, Dutch, French, German, Italian, Japanese, Korean, Simplified Chinese, Spanish, and Turkish.
English still points directly to the official Go A Tour of Go.
![[Figure 1: The current list of language versions on the A Tour of Go project homepage]](https://media.shuijingwanwq.com/2026/09/1-19.png)
Looking only at the final results, the launch process for these languages has become increasingly similar.
Initialize locale, build glossary, translate TranslationUnit, Automatic Validation, Quality Check, Locale Surface Review, Preview, Production, search engine closeout…
The entire workflow is basically fixed now.
This is why I later became less inclined to continue the approach of “writing a separate blog post for every new language added.”
What is truly worth recording is no longer:
How was Dutch launched?
Or:
What commands were executed for Turkish?
But rather, after repeatedly running several rounds, what boundary issues that were originally hard to discover have been exposed by real Production usage.
These three recent languages gave me another opportunity to tighten up several details.
1. The workflow has stabilized, but Git commits are still numerous
Looking at the Git commits from the past few days together is quite interesting.
![[Figure 2: Git commit history during the continuous launch of nl-NL, pt-BR, and tr-TR]](https://media.shuijingwanwq.com/2026/09/2-18.png)
Of course, there are many normal locale lifecycle records:
Completed nl-NL TranslationUnit workflow
Completed nl-NL Locale Surface Review A
Completed nl-NL preview acceptance
Completed nl-NL first production closeout
Completed pt-BR TranslationUnit workflow
Completed pt-BR Locale Surface Review A
Completed pt-BR preview acceptance
Completed pt-BR first production closeout
Completed tr-TR TranslationUnit promotion
Completed tr-TR Locale Surface Review A
Completed tr-TR preview acceptance
Completed tr-TR first production closeoutBut interspersed among these routine steps are also quite a few fix:
Made first-production browser acceptance reuse zgocloud SOCKS
Fixed first-production SOCKS tunnel lifecycle
Strengthened first-production review evidence verification
Added Locale Surface Review audit package export
Solidified Quality Check revision lineage
Fixed production prerendered directory verification
Fixed multilingual page visual alignment and output line wrappingThis perfectly illustrates the current stage of this project.
The macro-architecture no longer needs to be redesigned for every new language added, but only by continuously running the real workflow do we gradually encounter minor issues that were hard to expose when the sample size was small.
2. Production browser acceptance must also consider the stability of the public network link itself
I had previously handled an issue with Production public network acceptance.
My development machine is located in mainland China, and most of the new language sites use Cloudflare. In actual use, I found that the public network connection from mainland China to Cloudflare is not always stable, especially during evening peak hours, where transient issues like connection failures and timeouts are more likely.
This causes a problem:
The page and Production itself might be perfectly fine, but if a public network acceptance happens to encounter network fluctuation, it will be judged as a failure.
So later, I moved a large amount of public network machine acceptance directly to zgocloud for execution, ensuring that requests actually accessing Cloudflare take the overseas public network path as much as possible.
But continuing with the launch of nl-NL exposed another detail:
Headless Chrome browser acceptance should also use the same stable public network path.
If machine acceptance has already been verified via zgocloud, and then Headless Chrome directly uses the maintainer’s computer’s current public network exit to access Cloudflare, the same first-production process actually mixes two different network paths.
Passing local direct access verification is, of course, valid Production verification in itself.
The real problem is that, in my actual network environment, this mainland China → Cloudflare link is more susceptible to transient network fluctuations, resulting in false failures not caused by site failures themselves.
Therefore, the subsequent first-production process was further unified:
Browser acceptance explicitly reuses the zgocloud SOCKS endpoint already established by the current first-production invocation.
Thus:
public-machine
↓
zgocloud
↓
Cloudflare
browser
↓
zgocloud
↓
CloudflarePublic network machine acceptance and Headless Chrome browser acceptance should use a consistent overseas public network path as much as possible.
This is not about bypassing the maintainer’s computer.
The entire process is still initiated by the local machine, and the local machine must be able to connect to zgocloud normally. If the local connection to zgocloud fails, Production acceptance will also fail.
What is truly reduced is another type of uncertainty:
Avoiding a normal Production being misjudged as a failure due to fluctuations on the public network link from mainland China to Cloudflare at a given moment.
3. A tunnel can be established, but it must also survive the entire Production workflow
The launch of pt-BR continued to expose a very practical issue:
Successful SOCKS establishment does not mean it will stay alive until browser acceptance.
first-production does not just run for a few seconds.
In between, there are:
preflight
→ infrastructure
→ Playground Origin
→ deploy
→ direct-origin acceptance
→ DNS / CDN
→ public-machine
→ browserSome stages may last quite a while.
If the SSH ControlMaster or SOCKS tunnel itself has a shorter idle deadline, the following situation might occur:
All preceding stages are normal, but by the time it is actually time for browser acceptance, the tunnel is already gone.
So later, the ControlMaster lifecycle was adjusted.
It no longer relies on shorter idle timeouts for automatic maintenance, but is explicitly managed by the current first-production invocation:
Create → use → success, failure, or signal exit → cleanup.
This type of issue is actually quite typical.
Testing SOCKS in isolation:
Can connectIs completely different from:
Being reliable throughout the entire Production lifecycle.Only by actually running several consecutive rounds of new locales does it become easier to discover such lifecycle issues.
4. Quality Check revision: Not re-reviewing all 122 TranslationUnits
An official revision also occurred during this Turkish round.
The final Quality Check scope was:
![[Figure 3: Quality Check revision scope for tr-TR qc-002]](https://media.shuijingwanwq.com/2026/09/3-20-1024x112.png)
locale=tr-TR
snapshot=qc-002
total=122
current=13
carry_forward=109
pending=0
A/B/C/D=122/0/0/0
ready_for_finalization=trueI think this clearly reflects the current changes in the Quality Check workflow.
The 109 Units that were already A in the first round and whose TranslationUnit identity has not changed at all do not need to be re-reviewed for the revision.
They can be carried forward.
Only the 13 Units that were actually re-translated and had their candidates regenerated, resulting in changed identities, enter the new Quality Check.
Ultimately, it must still satisfy:
A = 122
B/C/D = 0
pending = 0before being allowed to enter machine finalization.
This neither lowered the A-only quality bar, nor did it mechanically re-review over a hundred completely unchanged Units every time a dozen or so Units are modified.
5. But carry-forward introduced a new issue: revision lineage must be fixed
With carry-forward, another issue that must be strictly defined emerged:
What exactly is qc-002 doing a revision on top of?
For example:
qc-001
↓
revision
↓
qc-002If qc-002 has already started writing the Quality Check result, allowing previous_snapshot_id to be swapped for another Snapshot after the fact would make the entire carry-forward evidence chain unreliable.
So this was later formally solidified:
When the revision Snapshot successfully writes the Quality Check result for the first time, the lineage identity is fixed.
Subsequently, the predecessor can be omitted, letting the system continue using the already persisted lineage.
However, if provided explicitly, it must be an exact match.
You cannot:
Originally qc-002 → qc-001And later change it to:
qc-002 → qc-000Nor can you manually modify JSON to patch the evidence chain.
Even cyclic lineages must fail closed.
For example:
qc-001 → qc-002
qc-002 → qc-001This situation cannot be accepted by machine finalization.
I increasingly feel that the focus of this type of workflow is not “generating a few more JSON files,” but rather ensuring:
How the final A was obtained can be re-interpreted by the machine along a deterministic lineage.
6. Locale Surface Review no longer relies on ad-hoc assembly of review materials
TranslationUnit Quality Check addresses the linguistic quality of Pages and eligible Examples.
But a complete locale has many elements that are not part of TranslationUnit:
- Public UI;
- Homepage;
/tour/;/tour/list;- Navigation;
- Runtime messages;
- Article metadata;
- Course SEO metadata;
- Language selector;
- Other combined page surfaces.
These have always been the responsibility of the independent Locale Surface Review.
Recently, a fairly significant improvement was made to this part:
A formal deterministic audit package export entry point was added.
For example, for Turkish:
![[Figure 4: Exporting the tr-TR Locale Surface Review audit package]](https://media.shuijingwanwq.com/2026/09/4-15-1024x93.png)
go run -mod=readonly ./cmd/tour-i18n surface-review export \
--locale tr-TR \
--output /tmp/tr-TR-surface-review.jsonThe actual coverage exported this time is:
pages=103
ui=92
articles=7
translation_units=122
other_surfaces=21This exporter itself does not judge:
Whether the Turkish translation is good or bad.
It also does not automatically grant Surface Review A.
It is only responsible for one thing:
Completely and deterministically extracting the materials needed for an official review from the current working tree.
This includes the glossary, UI source ↔ target, article metadata, Page source/canonical target, course description, and the first-party runtime/template source context actually involved in the execution.
This way, when ChatGPT performs the Locale Surface Review, it no longer needs to rely on ad-hoc material assembly, and it is less prone to incomplete review scopes due to missing a source file.
One thing I particularly like here is:
Automation is responsible for ensuring “complete review input,” but it does not pretend that automation equates to “linguistic quality judgment.”
The responsibilities of the two remain separate.
7. Turkish exposed another interesting Validator boundary
During the launch of tr-TR, a very typical HTML serialization issue was also encountered.
The Turkish course catalog contains content like this:
![[Figure 5: struct'lar in the Turkish course catalog]](https://media.shuijingwanwq.com/2026/09/5-12.png)
struct'lar in the Turkish course catalog]For example:
Diğer türler: struct'lar, dilimler ve eşlemelerThe browser displays it perfectly fine.
However, when HTML is serialized, a standard single quote ' might be represented as:
'Thus, something that is logically identical to:
struct'larmight become the following in raw HTML bytes:
struct'larIf the validator’s approach is:
Directly searching for the official lesson title string within the raw HTML bytes.
Then a page that is actually correct might be judged as:
missing localized lesson contentThis is actually very similar to the issue I previously encountered with the translation validator:
A Validator should not just be as strict as possible, but must be strict at the correct semantic level.
After this fix, the course catalog validation no longer relies simply on raw bytes.
Instead, it:
Parses HTML
→ Gets the DOM
→ Reads the decoded text
→ Compares with the official title / descriptionAt the same time, things like:
href
canonical
render markerwhich truly belong to the HTML structure, continue to be validated structurally.
This way, neither will it produce false failures due to equivalent HTML entities like ', nor does it lower the requirements for official content integrity.
8. Starting to focus on visual details that previously wouldn’t block a launch
After reaching ten languages, another change has become increasingly apparent:
Previously, my primary goals were:
Is the page broken?
Is there horizontal overflow?
Can Run / Format / Reset work?
Does Production pass?
Now that these are basically stable, some minor issues that previously wouldn’t block a launch have started to become worth addressing.
For example, longer language titles make the header’s visual alignment issues more obvious.
Later, the header’s flex alignment was uniformly adjusted, rather than writing a separate CSS special case for Turkish.
Another rather intuitive issue is the Playground output.
I specifically constructed a very long, continuous output with almost no natural line break opportunities:
![[Figure 6: Testing the Playground output area using a long string]](https://media.shuijingwanwq.com/2026/09/6-7-1024x552.png)
If this type of output <pre> maintains the traditional non-wrapping behavior, it can easily stretch the output area or even the entire page horizontally.
Later, the output area was supplemented with something like:
white-space: pre-wrap;
overflow-wrap: anywhere;While preserving the original space and newline semantics.
This change itself is very small.
But it also demonstrates a change:
Now that the core workflow has stabilized, real multilingual pages are in turn helping me discover finer boundary issues in the shared UI.
9. Even the GitHub README no longer wants manually maintained language lists
Upon reaching 10 languages, I recently noticed another very minor issue that will become increasingly annoying over time:
The GitHub README originally only had:
A Tour of Go 简体中文
A Tour of Go 日本語If this continued, every time a locale was added, I would have to remember to manually add a link to the README.
But Production itself already has the official:
production/identity.jsonused to record whether a locale is currently:
production_state=liveThe homepage language registry also already has:
EnglishName
Autonym
URLSo there is no need to let the README become a third independent manual language list.
Therefore, the README has now also been changed to an automatic projection:
![[Figure 7: The currently auto-generated official live language entry points in the GitHub README]](https://media.shuijingwanwq.com/2026/09/7-3.png)
The final rule is:
production_state=live in production/identity.json
+
EnglishName / Autonym in the language registry
↓
GitHub README live locale listAnd this projection is directly integrated into the first Production finalization.
That is, only after a new locale has truly completed:
first-production
→ HUMAN visual gate
→ finalize
→ production_state=livewill the README automatically show this language.
The first-production status will not be prematurely displayed as officially launched.
If the README marker is corrupted, the URL drifts, the registry is missing, or the identity cannot be uniquely matched, it will directly fail closed.
This optimization for the current 10 languages might just mean maintaining a few less lines of Markdown.
But if it actually scales to 20 or 30 languages in the future, the significance will be completely different.
10. I am also starting to change how I write this blog series
Over the past few days, I have had another rather obvious feeling.
When I first started doing multilingual work, a new language itself could bring many new issues.
So my thought at the time was:
For every new language added, as long as there are new workflow optimizations, write a blog post.
But continuing to write this way becomes increasingly repetitive.
Because a normal new locale is now basically:
Initialization
→ TranslationUnit
→ Quality Check
→ Locale Surface Review
→ Preview
→ Production
→ Search engine closeoutIf there are no new engineering issues, writing separate posts for:
Dutch launch record
Brazilian Portuguese launch record
Turkish launch record
has little practical value.
So going forward, I am more inclined to switch to a different rhythm:
After continuously launching several languages,集中整理 the actual workflow changes that occurred during this period.
If a sufficiently independent issue is encountered in between, such as the previous Headless Chrome Production false failure or IndexNow, I will still write a separate feature post.
This way, I won’t generate a large number of similar articles just to “leave a record for every language,” and it will be easier to clearly explain the technical issues truly worth retaining long-term.
Summary
From nl-NL, pt-BR to tr-TR, the launch process for these three languages themselves has had no major architectural changes.
What really changed are some increasingly fine boundaries:
Which network path browser acceptance actually takes
Whether the SOCKS tunnel can survive the entire Production invocation
How Quality Check revision fixes lineage
How A carry-forward ensures reliable evidence
How Locale Surface Review obtains complete review input
At what semantic level HTML entities should be validated
How multilingual long text continues to expose shared UI issues
What manual synchronization tasks can be eliminated after adding a new localeIndividually, these issues do not seem as “big” as when initially designing the entire multilingual architecture.
But I actually feel that this is the normal state for a workflow gradually entering the long-term maintenance phase.
What was solved at the very beginning was:
Whether a new language can be correctly launched.
Now, more and more issues have become:
How to ensure that the 10th, 20th, and 30th languages continue to use the same workflow, with as little reliance as possible on human memory, ad-hoc operations, and coincidental environments.
To date, the A Tour of Go multilingual project has officially launched 10 community language versions.
I will continue to add new locales next.
But the update method for this blog series will also gradually shift from “writing a post for every new language added” to:
Centrally recording when new real issues arise, new engineering improvements are formed, or a new stage milestone is reached.
This should be more suitable for the project’s continued long-term expansion.
需要长期技术维护或远程问题排查?
我是拥有 15+ 年经验的 PHP / Go 后端工程师,长期关注已有系统维护、Bug 修复、性能优化、服务器排查、WordPress 网站维护和小功能迭代。
如果你的项目遇到以下情况,可以先从一次小问题排查开始合作:
- ✅ PHP / Laravel / Yii2 老项目无人维护
- ✅ Go / Gin 后端接口需要排查或优化
- ✅ WordPress 网站访问慢、报错或插件冲突
- ✅ Nginx / MySQL / Redis / Linux 服务器异常
- ✅ CDN / Cloudflare / DNS / HTTPS 配置问题
- ✅ 需要长期远程技术支持或兼职维护
更多介绍请查看:关于我 & 合作
微信:13980074657
邮箱:shuijingwanwq@gmail.com
Telegram:@shuijingwan
GitHub:https://github.com/shuijingwan
