In a multilingual translation workflow, I originally thought the “candidate snapshot” was a relatively simple stage.
The preceding translation generation was already complete, and the candidate translations had passed machine validation. The next step was simply to freeze the current candidate state and hand it over to an independent review session for quality review.
But while recently processing the site-v2-shell homepage, I ran into a very typical problem:
I only meant to create a candidate snapshot for one page on the homepage, but the system generated 122 translation units.
The real trouble wasn’t just that “the count was wrong.”
It was that this candidate snapshot had already been generated as an official workflow artifact. According to the project’s consistent principle of historical immutability, it could not simply be deleted, overwritten, or pretended to have never existed just because it was generated incorrectly.
Ultimately, I had to keep this incorrect snapshot, fix the actual problem, and then create a new successor snapshot.
This incident reinforced one thing for me:
In an official workflow, the scope of operations cannot rely on contextual guessing. Whenever a command might affect multiple content packages, it should explicitly bind the content package it operates on.
Background: The homepage is no longer the original translation unit
This problem occurred on the Simplified Chinese Site v2 homepage.
The original source file for the homepage is _content/index.md.
Its raw bytes had not changed.
What actually changed was:
How the machine splits this source file into “translatable content” and “protected content.”
Previously, the homepage contained a piece of natural language used for code editor instruction:
You can edit this code!The old machine representation incorrectly placed it in a non-editable area.
In other words, from a language quality perspective, we knew it should be translated.
But according to the machine rules, the translation generation phase had no permission to modify it at all.
Later, I specifically built a new homepage representation inheritance mechanism for this type of problem.
This mechanism does not modify the original source file, rewrite old candidate translations, or directly carry over old language review conclusions to the new translation unit.
Once the new representation officially took effect, the homepage received a new page translation unit:
f4365d05fcff295676c0964f81058e6485bb0361f8440cbf243220a6acc1e97dThis new unit needs to go through the complete language workflow again:
Translation generation
→ Machine validation
→ Candidate snapshot
→ Independent quality reviewThis was supposed to be a very narrow processing pipeline.
The current scope was only:
- Content package:
site-v2-shell - Route:
/ - Pages: 1
- Examples: 0
- Translation units: 1
But right when creating the candidate snapshot, the problem appeared.
One page, why did it suddenly become 122 units?
The first candidate snapshot generated at that time was site-v2-shell-qc-003.
The expectation was very simple:
Freeze only the single site-v2-shell homepage page.
But after generation, I discovered:
122 translation units appeared in the snapshot.
The number 122 was actually very familiar.
It exactly matched the total number of translation units in the Tour at that time:
- 103 pages
- 19 examples
In other words, this was not random data corruption.
The machine had actually “diligently” executed a complete scope.
It just executed the wrong scope.
The real problem lay in the candidate snapshot command’s selection of the “content package.”
Although the command was executed within the Site shell workflow context, the candidate snapshot itself was not constrained by a sufficiently explicit content package parameter.
As a result, the underlying translation process still used the default scope.
And this default scope happened to be Tour.
From the caller’s perspective, I was working on site-v2-shell.
But from the underlying workflow’s perspective, what it understood was closer to:
Create a candidate snapshot for the current default set of translation units.
Neither side threw an obvious error.
Yet the results were completely different things.
The most dangerous problems don’t necessarily throw errors
This type of problem is actually more dangerous than a direct crash or validation failure.
Because it satisfies many superficial “normal conditions”:
- The JSON structure is valid;
- The candidate snapshot can be written successfully;
- The translation units inside are all real;
- The count is not 0;
- The file hash can be calculated normally;
- The identity identifier can be generated normally;
- The file format is not corrupted;
- The command might even return success.
If you only look at the exit status:
exit status = 0It is easy to assume the entire step is complete.
But the real problem is:
The machine only proved that this artifact is internally self-consistent, but it did not prove that it belongs to the content package I actually intended to operate on.
This is also a crucial distinction between an official workflow and a regular script.
Regular scripts often only care about:
Is the result valid?
But an official workflow must also continue to ask:
Which scope does this result actually belong to?
Why can’t the incorrect snapshot just be deleted?
After discovering that site-v2-shell-qc-003 was wrong, the most intuitive course of action was, of course:
Delete it, then regenerate it.
But I did not do that.
Because once an official workflow artifact has been produced, it itself constitutes a historical fact.
Even if it is the result of an incorrect operation, it should be retained so that we can still answer later:
- What input was used at the time;
- Why it contained 122 units;
- Which command contract failed to constrain the scope;
- How it was subsequently fixed.
If it is directly deleted, a more troublesome problem arises when later looking at logs, hashes, references, or diagnostic records:
The historical records point to an official state that “now appears to have never existed.”
So this time, we still followed the principle the project has always adopted:
History can fail, but it cannot be rewritten.
site-v2-shell-qc-003 was kept.
It is no longer used as the correct Site shell candidate snapshot, but it was also not overwritten.
The real fix isn’t “switching to a different default value”
At first, it was easy to think of a simple fix:
Since we were working on site-v2-shell this time, just have the candidate snapshot default to using site-v2-shell.
But that would only shift the problem from one content package to another.
Today, defaulting to Tour causes an error.
Tomorrow, defaulting to Site shell could just as easily cause errors for Learn / Docs.
What really needs to be fixed is:
When creating a candidate snapshot, the command must explicitly know which content package the caller has chosen.
Therefore, “content package selection” was later elevated to part of the official command contract.
The caller can no longer rely on:
- The current directory;
- What command was executed in the previous step;
- The most recent planning result;
- Some default translation directory;
- The most historically used content package;
- “Everyone knows we’re working on Site shell right now.”
Instead, it must be explicitly stated:
--content-package site-v2-shellThis way, the scope of operations goes from an implicit assumption to a formal input that the machine can validate.
After the fix, the candidate snapshot finally returned to 1 page
After fixing the command contract, I did not modify the original site-v2-shell-qc-003.
Instead, I created a new candidate snapshot:
site-v2-shell-qc-004This time, the result finally matched the actual workflow scope:
- Pages: 1
- Examples: 0
- Translation units: 1
- Current unit: the new translation unit for the homepage
- Content package:
site-v2-shell
In other words, from the incorrect:
122 translation unitsIt returned to what actually needed review:
1 translation unitThe most important thing here is not actually “121 fewer units.”
What really matters is:
The candidate snapshot’s identity, member list, and content package scope are finally realigned.
From this moment on, the subsequent independent quality review once again has a trustworthy input boundary.
A content package is not a convenient classification label
After this incident, my understanding of “content packages” also became clearer.
It is not an extra layer of classification added just to make directories look tidier.
It actually determines many key issues:
- Which translation units belong to the current lifecycle;
- Which historical results can continue to be inherited;
- Which candidate translations can enter the current snapshot;
- Which review conclusions match the current object;
- Which final confirmation results can enter the content package archive;
- Which site review results belong to the current content;
- Which state changes belong to the current task;
- Which content is merely surrounding context.
In other words:
The content package itself is an authoritative boundary.
Whenever a command might cross this boundary, the content package should become an explicit input, rather than just an implicit environment.
We already have a translation unit ID, why isn’t that enough?
A very natural question arises here:
Since we already know the new translation unit ID for the homepage, for example:
f4365d05fcff295676c0964f81058e6485bb0361f8440cbf243220a6acc1e97dIsn’t it enough to just pass this ID?
Not quite enough.
The translation unit ID can answer:
Which translation unit do I want to operate on?
But the content package also needs to answer another question:
Which official lifecycle does this translation unit currently belong to?
The same underlying translation system can simultaneously serve:
- Tour
- Learn / Docs
- Site shell
And the subsequent workflow includes:
Candidate snapshot
→ Independent quality review
→ Final confirmation
→ Content package archive
→ Integrated site review
→ ActivationThese stages do not just look at an isolated translation unit ID.
They also need to know:
Which content package this unit belongs to, which processing pipeline, and which set of official histories.
Therefore, the translation unit ID and the content package are not an either-or choice.
They solve problems at two different levels.
One determines the specific object.
The other determines the official scope in which this object resides.
Why is this type of problem especially common in large workflows?
When a project initially has only a single translation workflow, many default values are actually fine.
For example, if the system only has Tour.
Then:
No content package specified
And:
Use the only existing content package
The actual effect is exactly the same.
But later, the system gradually expanded into:
Tour
+ Learn / Docs
+ Site shell
+ Structured contentAnd the problem appeared.
The old default value that was once “the only possible answer” slowly became:
“One of several possible answers.”
Yet that old default value remained hidden in the code.
The superficial commands did not change.
But the underlying semantics had already changed.
This type of problem is particularly likely to occur in:
- Candidate snapshots;
- Translation exports;
- Review package exports;
- Final confirmations;
- Content package archives;
- Work planning;
- Failure recovery.
Because these commands were likely originally designed during the era when “there was only one type of object in the system.”
After the supported scope expanded later, if the command contracts were not re-examined, this kind of implicit scope leakage could easily occur.
I now actually prefer to make official commands “a bit more troublesome”
In the past, when designing command-line tools, I used to think:
If a parameter can be automatically inferred, don’t make people enter it manually.
Now my view has shifted somewhat.
For regular tools, reducing parameters is of course great.
But for official state-change workflows:
Some seemingly repetitive parameters are actually part of the security design.
For example, an official command might need to explicitly specify simultaneously:
localecontent package- Translation unit ID
- Previous candidate snapshot
- Pre-state SHA
- Expected identity identifier
These parameters answer respectively:
- Which language is being processed;
- Which content package is being processed;
- Which specific translation unit is being processed;
- Which historical state this operation inherits from;
- What state the repository should be in before execution;
- Who the expected generated official result is.
In terms of user experience, this is of course more cumbersome than a command that “automatically guesses for you.”
But when things do go wrong, these parameters can form a very clear provenance chain.
Conversely, if a command relies heavily on:
It’s probably this one right now.
It looks like a time-saver on normal days.
But once an anomaly occurs, the diagnostic cost will actually be higher.
A few rules left behind by this incident
After this candidate snapshot scope incident, I established a few clearer rules for similar commands.
First:
Official commands that cross content packages must explicitly bind the content package.
Second:
A successful command execution does not mean the operation scope is correct.
Third:
Generated incorrect official artifacts must not be deleted or overwritten; they should be corrected through a successor state.
Fourth:
Once a default value changes from “the only possibility” to “one of several possibilities,” it should be re-evaluated to see if it is still suitable as a default value.
Fifth:
The operation scope itself is part of the authoritative state, not just a filter condition.
The last one is also the one I find most worth recording:
A workflow artifact can be entirely correct in format, hash, identity identifier, and content structure, but as long as it belongs to the wrong operation scope, it is still an incorrect official result.
This is also why my multilingual workflows increasingly tend to:
Treat it as a failure when it cannot be confirmed.
Not because I want the system to fail more easily.
But because when the system can no longer prove:
Is what I am operating on right now actually the correct object?
At that point, continuing to “succeed” is often more dangerous than stopping explicitly.

