没有不值得去解决的问题,也没有不值得去学习的技术!

After Launching Three Languages for A Tour of Go: How I Improved My Workflow

[Figure 1: The current list of language versions on the A Tour of Go project homepage]

作者:

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]
[Figure 1: The current list of language versions on the A Tour of Go project homepage]

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]
[Figure 2: Git commit history during the continuous launch of nl-NL, pt-BR, and tr-TR]

Of course, there are many normal locale lifecycle records:

Plaintext
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 closeout

But interspersed among these routine steps are also quite a few fix:

Plaintext
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 wrapping

This 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:

Plaintext
public-machine

    zgocloud

   Cloudflare

browser

    zgocloud

   Cloudflare

Public 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:

Plaintext
preflight
→ infrastructure
→ Playground Origin
→ deploy
→ direct-origin acceptance
→ DNS / CDN
→ public-machine
→ browser

Some 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:

Plaintext
Can connect

Is completely different from:

Plaintext
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]
[Figure 3: Quality Check revision scope for tr-TR qc-002]
Plaintext
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=true

I 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:

Plaintext
A = 122
B/C/D = 0
pending = 0

before 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:

Plaintext
qc-001

revision

qc-002

If 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:

Plaintext
Originally qc-002 → qc-001

And later change it to:

Plaintext
qc-002 → qc-000

Nor can you manually modify JSON to patch the evidence chain.

Even cyclic lineages must fail closed.

For example:

Plaintext
qc-001 → qc-002
qc-002 → qc-001

This 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]
[Figure 4: Exporting the tr-TR Locale Surface Review audit package]
Bash
go run -mod=readonly ./cmd/tour-i18n surface-review export \
  --locale tr-TR \
  --output /tmp/tr-TR-surface-review.json

The actual coverage exported this time is:

Plaintext
pages=103
ui=92
articles=7
translation_units=122
other_surfaces=21

This 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]
[Figure 5: struct'lar in the Turkish course catalog]

For example:

Plaintext
Diğer türler: struct'lar, dilimler ve eşlemeler

The browser displays it perfectly fine.

However, when HTML is serialized, a standard single quote ' might be represented as:

HTML
'

Thus, something that is logically identical to:

Plaintext
struct'lar

might become the following in raw HTML bytes:

HTML
struct'lar

If 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:

Plaintext
missing localized lesson content

This 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:

Plaintext
Parses HTML
→ Gets the DOM
→ Reads the decoded text
→ Compares with the official title / description

At the same time, things like:

Plaintext
href
canonical
render marker

which 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]
[Figure 6: Testing the Playground output area using a long string]

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:

CSS
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:

Plaintext
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:

Plaintext
production/identity.json

used to record whether a locale is currently:

Plaintext
production_state=live

The homepage language registry also already has:

Plaintext
EnglishName
Autonym
URL

So 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]
[Figure 7: The currently auto-generated official live language entry points in the GitHub README]

The final rule is:

Plaintext
production_state=live in production/identity.json
                +
EnglishName / Autonym in the language registry

GitHub README live locale list

And this projection is directly integrated into the first Production finalization.

That is, only after a new locale has truly completed:

Plaintext
first-production
→ HUMAN visual gate
→ finalize
→ production_state=live

will 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:

Plaintext
Initialization
→ TranslationUnit
→ Quality Check
→ Locale Surface Review
→ Preview
→ Production
→ Search engine closeout

If 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:

Plaintext
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 locale

Individually, 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