fix: prevent unknown character corruption in translations - #68
Merged
Merged
Conversation
## Changed - Set contentType explicitly per parser so Lara no longer auto-detects TextBlock[] as HTML and replaces non-ASCII chars with literal `?` - Engine splits each batch by detected content type so values containing inline HTML are sent as text/html and plain values as text/plain - Retry U+FFFD-corrupted translations up to 3 times as solo calls before failing with a neutral "please retry" message - Bump version to 1.3.4 ## New - contentType utility (hasHtmlMarkup / resolveContentType) and per-parser getContentType() exposed through ParserFactory - Parameterized integration tests covering content-type routing for all 10 supported file formats - Deterministic test reproducing the upstream UTF-8 streaming bug in @translated/lara, plus integration coverage for the retry guard
There was a problem hiding this comment.
Pull request overview
This PR addresses translation output corruption by explicitly controlling Lara contentType per parsed format/value and adding a mitigation for an upstream UTF-8 chunk-decoding bug that can introduce U+FFFD (�) in streaming responses.
Changes:
- Add parser-level
getContentType()defaults (plain-text formats →text/plain, Android XML →text/html) and route values via a per-value HTML-markup heuristic. - Update
TranslationEngineto split batch requests by resolvedcontentTypeand retry any�-corrupted results as solo calls (up to 3 attempts), failing with a neutral message if still corrupted. - Add/extend unit + integration coverage for content-type routing and UTF-8 corruption reproduction/mitigation; bump version to 1.3.4.
Reviewed changes
Copilot reviewed 34 out of 34 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/utils/contentType.ts | Adds HTML-markup detection + per-value contentType resolution helper. |
| src/interface/parser.ts | Extends Parser interface with getContentType(). |
| src/parsers/parser.factory.ts | Exposes getContentType() through the factory facade. |
| src/parsers/json.parser.ts | Declares default contentType as text/plain. |
| src/parsers/po.parser.ts | Declares default contentType as text/plain. |
| src/parsers/ts.parser.ts | Declares default contentType as text/plain. |
| src/parsers/vue.parser.ts | Declares default contentType as text/plain. |
| src/parsers/markdown.parser.ts | Declares default contentType as text/plain. |
| src/parsers/txt.parser.ts | Declares default contentType as text/plain. |
| src/parsers/xcode-strings.parser.ts | Declares default contentType as text/plain. |
| src/parsers/xcode-stringsdict.parser.ts | Declares default contentType as text/plain. |
| src/parsers/xcode-xcstrings.parser.ts | Declares default contentType as text/plain. |
| src/parsers/android-xml.parser.ts | Declares default contentType as text/html for Android resources. |
| src/modules/translation/translation.engine.ts | Implements content-type routing/splitting and U+FFFD retry guard; passes contentType into Lara options. |
| src/messages/messages.ts | Adds neutral failure message for exhausted corruption retries. |
| src/tests/utils/contentType.test.ts | Unit tests for markup detection and content-type resolution. |
| src/tests/sdk-utf8-streaming.repro.test.ts | Deterministically reproduces upstream UTF-8 streaming bug and demonstrates StringDecoder fix. |
| src/tests/parsers/json.parser.test.ts | Asserts getContentType() returns text/plain. |
| src/tests/parsers/po.parser.test.ts | Asserts getContentType() returns text/plain. |
| src/tests/parsers/ts.parser.test.ts | Asserts getContentType() returns text/plain. |
| src/tests/parsers/vue.parser.test.ts | Asserts getContentType() returns text/plain. |
| src/tests/parsers/markdown.parser.test.ts | Asserts getContentType() returns text/plain. |
| src/tests/parsers/txt.parser.test.ts | Asserts getContentType() returns text/plain. |
| src/tests/parsers/xcode-strings.parser.test.ts | Asserts getContentType() returns text/plain. |
| src/tests/parsers/xcode-stringsdict.parser.test.ts | Asserts getContentType() returns text/plain. |
| src/tests/parsers/xcode-xcstrings.parser.test.ts | Asserts getContentType() returns text/plain. |
| src/tests/parsers/android-xml.parser.test.ts | Asserts getContentType() returns text/html. |
| src/tests/parsers/parser.factory.test.ts | Validates ParserFactory.getContentType() per supported extension. |
| src/tests/integration/content-type-routing.integration.test.ts | Verifies per-format routing of plain vs HTML-bearing values to correct contentType. |
| src/tests/integration/utf8-retry.integration.test.ts | End-to-end tests for U+FFFD detection, solo retry recovery, and neutral failure after retries. |
| src/tests/integration/batch-translation.integration.test.ts | Regression tests for explicit contentType and mixed plain+HTML batch splitting. |
| src/tests/integration/android-xml.integration.test.ts | Asserts Android XML translation uses contentType=text/html. |
| package.json | Bumps package version to 1.3.4. |
| README.md | Updates version badge to 1.3.4. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
## Changed - Replace relative `../../utils/contentType.js` import with `#utils/contentType.js` to match the convention used by sibling utility tests (e.g. entities.test.ts).
Starllordz
added a commit
that referenced
this pull request
May 26, 2026
* fix: prevent unknown character corruption in translations (#68) * fix: prevent unknown character corruption in translations ## Changed - Set contentType explicitly per parser so Lara no longer auto-detects TextBlock[] as HTML and replaces non-ASCII chars with literal `?` - Engine splits each batch by detected content type so values containing inline HTML are sent as text/html and plain values as text/plain - Retry U+FFFD-corrupted translations up to 3 times as solo calls before failing with a neutral "please retry" message - Bump version to 1.3.4 ## New - contentType utility (hasHtmlMarkup / resolveContentType) and per-parser getContentType() exposed through ParserFactory - Parameterized integration tests covering content-type routing for all 10 supported file formats - Deterministic test reproducing the upstream UTF-8 streaming bug in @translated/lara, plus integration coverage for the retry guard * chore: use #utils path alias in contentType test imports ## Changed - Replace relative `../../utils/contentType.js` import with `#utils/contentType.js` to match the convention used by sibling utility tests (e.g. entities.test.ts). * chore: upgrade glob and patch brace-expansion CVE (#69) ## Changed - Bump `glob` from `^11.1.0` to `^13.0.6` to remove deprecation warning at install - Override transitive `brace-expansion` in vulnerable range `>=5.0.0 <5.0.6` to `^5.0.6` (CVE-2026-45149, GHSA-jxxr-4gwj-5jf2)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the long-standing unknown character corruption observed across translations — both the visible
?characters in plain text (e.g.з??дуinstead ofзгодуin Ukrainian) and the rarerU+FFFDcorruption in Cyrillic / Greek / CJK / Arabic targets.Two distinct root causes:
TextBlock[]input with nocontentTypewas auto-detected by the API as HTML-flavored, which mangled plain text containing curly apostrophes, dashes, and multi-byte characters into?.@translated/lara. The SDK callschunk.toString()per TCPdataevent, so multi-byte UTF-8 characters that straddle a chunk boundary lose bytes and decode toU+FFFD. Becomes visible at batch sizes ≥ a few keys because the response is large enough to span multiple TCP chunks. Reproduced deterministically insdk-utf8-streaming.repro.test.ts.What changed
contentTypedeclaration. NewgetContentType()on the Parser interface, exposed throughParserFactory. Default istext/plainfor value-based formats (JSON, PO, TS, Vue, Markdown, Xcode.strings/.stringsdict/.xcstrings, TXT);text/htmlfor Android XML where inline markup is part of the format.hasHtmlMarkupheuristic upgrades any individual value containing<a>,<b>,<br/>, etc. totext/html. Batches are grouped by detected content type across the whole file, so a JSON mixing"Hello"and"Click <a>here</a>"is sent as two API calls (one per content type) instead of one corrupting call.�. Any affected task is retried up to 3 times as a solo call (tiny response, almost never spans a chunk). If all 3 retries still come back corrupted, the run exits 1 with a short"Translation failed for some keys. Please retry."message — no SDK internals leak into user-facing output.package.jsonand the README badge.Tests
content-type-routing.integration.test.ts) verifies that each of the 10 supported file formats routes plain-text values totext/plainand HTML-bearing values totext/html.utf8-retry.integration.test.tsdrives the retry guard end-to-end: one test shows a corrupted batch entry is recovered by the solo retry; the other shows the neutral failure message after three exhausted retries.sdk-utf8-streaming.repro.test.tsreproduces the upstream SDK bug deterministically (no network) by driving the realTranslatoragainst a local HTTP server that splits the response between the two UTF-8 bytes ofи. Includes aStringDecoder-based test showing the one-line upstream fix.getContentType()assertions.Test plan
pnpm install && pnpm test— all 742 tests passpnpm run build && pnpm run lint && pnpm run format:check— cleanukandbg. Confirm no?appears mid-word and noтез??/тез��in the output.Click <a href="/x">here</a>and verify the tags survive the round trip.Follow-ups
The
U+FFFDcorruption is fundamentally an upstream issue in@translated/lara'snode-client.js:172(buffer += chunk.toString()per data event — should beStringDecoder('utf8').write(chunk)). The retry guard mitigates user impact for now; a one-line upstream PR would let us delete the guard and the repro test.sdk-utf8-streaming.repro.test.tsis suitable to attach to that upstream report.