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

Architecture Trade-offs in a Go Multilingual Translation Project When Terminology Consistency Meets Performance Bottlenecks

作者:

在

While maintaining the Go Learning & Documentation Translations multilingual project, I have always held a basic principle:

Translations that have already undergone independent review, and whose content has not been genuinely affected, should not be mechanically retranslated and re-reviewed due to subsequent project expansion.

There is no problem with this principle itself.

The problem arises here: as the project gradually expanded from the initial A Tour of Go to the Site shell, Learn, Docs, and more future content packages, the evidence chain that the machine must maintain to prove “an existing translation can still be safely reused” has become increasingly complex.

Recently, while handling zh-CN, this issue finally shifted from “architectural complexity” to a very real performance problem.

Why should a task that only requires reviewing 1 page take 6 minutes to run?

After the glossary for this zh-CN was uniformly refreshed and independently reviewed, most of the original Tour translations remained reliable.

The project currently uses a glossary compatibility mechanism.

Simply put, when the glossary changes, it does not directly invalidate all old review results. Instead, it calculates:

Plaintext
Old glossary
        ↓
Semantic delta
        ↓
All current translation contexts
        ↓
Which contexts are genuinely affected
        ↓
Only re-review the affected scope

The goal of this design is very clear:

A minor glossary change should not trigger a re-review of the entire language pack.

In the current zh-CN evidence, this mechanism has indeed proven effective.

The entire compatibility inventory contains 470 contexts, of which only 32 are judged to be affected; looking solely at TranslationUnits, there are only 3 TUs affected, 119 TUs compatible.

From a language review cost perspective, this is ideal.

But then a completely unacceptable problem emerged:

Plaintext
quality-check scope
quality-check reviewer-bundle
quality-check reviewer-bundle-check
quality-check finalize
retranslation export

These commands, which should inherently be deterministic mechanical operations, began frequently requiring around 6 minutes to complete.

A real quality-check scope test took:

Plaintext
wall time: 5:58.68
user CPU: 400.28s

And the final output was merely the QC scope for 122 TranslationUnits.

This is no longer just a matter of the “CLI being a bit slow.”

If every revision, Reviewer Bundle, and finalization requires waiting several minutes, then as the 65 locales, Learn/Docs, and future content packages continue to expand, this workflow itself will gradually become unmaintainable.

Thus, two options emerged.

Option 1: Preserve glossary compatibility, but thoroughly optimize the implementation

The first option is to retain the current semantic-delta / compatibility architecture.

That is, still adhering to:

Plaintext
Glossary changed
        ↓
Machine calculates the genuinely affected scope
        ↓
Trusted unchanged A continues to carry
        ↓
Only re-review affected TranslationUnits

Initially, I was worried that the 6-minute delay might be an unavoidable cost of this precise compatibility model itself.

If that were the case, continuing to optimize would not make much sense.

However, after further inspecting the code, the situation was more optimistic than anticipated.

Currently, when BuildQualityCheckScope iterates over TranslationUnits, it makes a call per Unit:

Go
ResolveGlossaryCompatibility(
    root,
    locale,
    previousGlossarySHA,
    currentGlossarySHA,
    "tu:"+unitID,
    catalog,
)

The problem is that this API appears to be querying:

Is tu:concurrency/6 compatible?

But what it actually does internally is closer to:

Rebuilding the entire glossary compatibility world, and finally querying concurrency/6.

Every call across glossary SHAs re-verifies the current Glossary Review, rebuilds the full 470-context inventory, rescans TranslationUnit source/candidate, re-reads compatibility evidence, reloads archived glossaries, and recalculates semantic delta and classification, only to finally return the result for a single scope.

So what should originally have been similar to:

Plaintext
Build compatibility state once
+
122 map lookups

turned into approximately:

Plaintext
122 × full compatibility reconstruction

The current complexity is evaluated as approximately:

Plaintext
O(N × M)

By introducing an invocation-local prepared state, it could be reduced to approximately:

Plaintext
O(M + N)

This means the full compatibility world is built only once per command, and all subsequent Units share it.

This implies that a large portion of the current minute-level latency is not an inherent cost of the compatibility concept itself, but rather the same expensive computation being repeatedly executed over a hundred times.

Therefore, Option 1 is still worth trying.

Moreover, this optimization must be shared across all languages.

zh-CN only exposed the problem first because its current evidence and lineage are sufficiently complex. The underlying implementation itself is a generic shared code path and does not require separate optimizations for Chinese, Spanish, or German.

The correct design should be:

Plaintext
locale
old glossary SHA
new glossary SHA
current inventory identity
        ↓
Prepare once
        ↓
Shared by all TranslationUnits

Instead of:

Go
if locale == "zh-CN" {
    fastPath()
}

This point can be clarified now: if compatibility is ultimately retained, its optimization will be a one-time repository-level optimization, and all locales will automatically benefit.

Option 2: Fall back to the simplest glossary SHA gate

The second option is much more aggressive.

Directly abandon the precise affected-scope carry after a glossary change.

The rules become:

Plaintext
Glossary SHA unchanged
→ Unchanged A can carry normally

Glossary SHA changed
→ All old QC A no longer automatically carry
→ Full QC
→ Reviewer re-reviews all current TranslationUnits
→ Only Units that are genuinely B/C/D enter revision

There is an important distinction here:

Full QC does not equal full regeneration.

Assume there are 122 Tour TranslationUnits after a glossary change.

It does not mean retranslating all 122.

It means re-reviewing all 122.

If 119 of them are still correct, the Reviewer continues to give them an A; only the 3 genuinely affected by the new terminology policy enter targeted revision.

The biggest advantage of this option is its simplicity.

The machine no longer needs to work hard to prove:

Why can this A from three months ago still be trusted after two glossary changes?

It only needs to determine:

Plaintext
SHA same?

If not, re-review.

From a fail-closed and recovery perspective, this design is very easy to understand and unlikely to produce false carries.

However, further evaluation revealed that Option 2 is not as cheap as imagined.

Current compatibility does not solely serve TranslationUnit QC carry.

Processes like Course SEO, Surface Review, sitecontent package, historical closure, and protected-input recovery also rely on similar compatibility / provenance mechanisms.

Therefore, switching to hash-only does not mean we can simply delete the entire compatibility subsystem.

A more realistic outcome would likely be:

Plaintext
New QC workflow
→ Uses simple SHA gate

Historical evidence / Course SEO / Surface / recovery
→ Continues to retain compatibility legacy path

This ultimately forms a dual-track system where “the new rules are simple, but the old rules remain complex.”

This is why the current assessment considers Option 2’s migration complexity to still be very high.

The crucial common prerequisite: stabilize the glossary first

While discussing both options, I became increasingly certain of one thing:

Whether we ultimately retain compatibility or fall back to the glossary SHA gate, we should first stabilize the glossary as much as possible.

The project is no longer just Tour.

A locale permanently has only one:

Plaintext
locales/<locale>/glossary.yaml

It serves the entire Go learning and documentation system, not just a single version.

If the glossary is still built like this:

Plaintext
Working on Tour
→ Add Tour terms

Working on Learn
→ Add Learn terms

Working on Docs
→ Add Docs terms

Adding future package
→ Add another round of terms

Then the glossary SHA will inevitably change frequently.

This is bad for both options.

For Option 1, it means compatibility assessment and lineage will constantly grow.

For Option 2, it means continuously triggering full QC.

A more reasonable direction is to expand the terminology discovery scope of glossary generation in advance.

This does not mean turning the glossary into a massive English-Chinese technical dictionary, but rather:

Plaintext
Complete Go / go.dev terminology discovery corpus
        ↓
Discover terms across the entire ecosystem that may require long-term governance
        ↓
Filter
        ↓
Retain only genuinely high-value project-level terminology decisions

That is to say:

Expand the corpus, not the definition of the glossary.

The project’s existing principles remain unchanged:

The glossary should be “as small as possible, but must be valuable.”

What truly deserves to enter the glossary should still be words like these:

  • Concepts unique to Go that are prone to technical misunderstanding;
  • Words with multiple reasonable translations that require a unified project choice;
  • Concepts that appear repeatedly across Pages / surfaces and are prone to terminology drift;
  • Technical names that must retain their original identity;
  • High-impact expressions like UI, navigation, and project naming.

Ordinary, natural, and clearly unambiguous words should not mechanically enter the glossary just because they appear in the corpus.

The current frozen corpus already covers Tour, Site shell, Learn YAML, Learn/Docs Pages, Course SEO, and other current official content, comprising 403 contributors and 3,004 contexts; however, it is still not a complete Go/go.dev terminology discovery corpus.

In the future, a more ideal discovery scope could continue to cover the language specification, memory model, modules, workspace, toolchain, testing, benchmarking, fuzzing, profiling, tracing, PGO, security and supply chain, as well as high-value cross-ecosystem concepts in the standard library.

These discovery-only sources do not necessarily mean the corresponding pages have already been localized.

They simply help the glossary make long-term terminology decisions in advance.

This way, when a future content package is actually added, the following situation will not occur:

“A new page was added today, so 5 more words need to be added to the glossary.”

Adding a new page itself should not be a reason to modify the glossary.

The glossary should only be modified when a new technical area emerges, a genuine terminology conflict occurs, or it is discovered that a concept truly warrants long-term unified governance.

Current decision: give Option 1 a chance first

Based on this evaluation, I have temporarily chosen Option 1.

This is not because semantic compatibility is necessarily more correct than a simple SHA gate, but because there is now fairly clear evidence that:

The current 6-minute performance issue is primarily an implementation problem, not something the architecture itself inherently requires 6 minutes for.

If, within a single invocation, we can turn:

Plaintext
122 full compatibility reconstructions

into:

Plaintext
1 prepared compatibility state
+
122 lightweight lookups

then the Reviewer cost savings brought by compatibility remain highly valuable.

Especially in the current real-world case:

Plaintext
122 TranslationUnits
3 affected
119 compatible

Abandoning 119 trusted carries merely because the implementation was written inefficiently would be too premature.

However, this decision comes with a clear stop-loss condition.

I will not continue to introduce the following just to preserve compatibility:

Plaintext
persistent cache
cache invalidation protocol
new receipt
new schema
locale-specific tuning

This time, I only accept:

A locale-agnostic, invocation-local prepared-state refactor with no new persistent state.

If, after completing this clean refactor:

Plaintext
quality-check scope
reviewer-bundle
reviewer-bundle-check
finalize
retranslation export

still remain noticeably at the minute level, then Option 2 will be reopened.

At that point, the issue will no longer be “a certain computation was repeated a hundred times,” but rather that the long-term cost of this architecture itself has exceeded the Reviewer costs it saves.

Long-term maintainability is more important than performance

This issue made me realize one thing again.

The biggest scaling challenge for this project is not that a single locale has 122 TranslationUnits.

The true scale is:

Plaintext
65 locales
×
Tour
×
Site shell
×
Learn
×
Docs
×
future content packages
×
long-term upstream evolution

At this scale, a design must not only answer:

“Can it be precisely reused in theory?”

It must also answer:

“Can it still be understood, recovered, verified, and maintained three years from now?”

Therefore, the principles ultimately retained this time are actually more important than Option 1 or Option 2 themselves:

First, stabilize the glossary as much as possible.

Second, machine optimizations should serve all locales, rather than patching a specific language.

Third, do not infinitely increase the complexity of the evidence state machine just to save a small amount of Reviewer cost.

Fourth, having already invested a lot of development effort is not a reason to continue retaining a complex design.

If compatibility can be cleanly optimized to the second level, then continue using it.

If not, fall back to the simpler, more conservative, and more easily recoverable glossary SHA gate.

An architecture is not better just because it is more precise.

In a project that requires long-term maintenance across dozens of languages, explainability, recoverability, and verifiability are themselves part of quality.

系列导航

关于作者 · 持续学习与技术实践

我是一名拥有 15 年以上开发经验的技术从业者,自 2013 年起持续运营个人技术博客,记录工作与个人项目中遇到的真实问题,以及解决问题过程中的探索、实践和思考。

除了技术写作,我也在持续开发和维护自己的独立项目,探索新技术和新工具在实际场景中的应用。我始终相信,没有不值得去解决的问题,也没有不值得去学习的技术。

如果这篇文章对你有所帮助,欢迎浏览博客中的其他文章,也欢迎交流技术经验与想法。

关于我  ·  GitHub  ·  邮件联系