Files
upstream-twenty/packages/twenty-utils/crowdin-normalizer/rules/corrupted-model-output.rule.ts
upstream-mirror e7da97194a
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05:00

132 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { type NormalizationRule } from '../types/normalization-rule.type';
// Trailing characters the machine translator leaves behind once its response
// envelope bleeds into the string. Only stripped when the source does not end
// the same way, so legitimate quotes and brackets survive.
const DEBRIS_CHARACTERS = new Set([
' ',
'\t',
'\n',
'\r',
' ',
'`',
']',
"'",
'"',
]);
// Fragments of the translator's own JSON/markdown envelope. A salvaged prefix
// carrying one of these was cut inside the envelope rather than after it.
const ENVELOPE_REGEX = /pluralForm|```|\\",\\"|\[/;
const ICU_ARGUMENT_REGEX = /\{\s*([A-Za-z0-9_]+)\s*[,}]/g;
const ICU_TAG_REGEX = /<\/?\d+>/g;
function hasBalancedBraces(text: string): boolean {
let depth = 0;
for (const character of text) {
if (character === '{') {
depth += 1;
continue;
}
if (character === '}') {
depth -= 1;
if (depth < 0) {
return false;
}
}
}
return depth === 0;
}
// The first unmatched '}' is where the translation ends and the envelope begins.
function cutAtUnmatchedBrace(text: string): string | undefined {
let depth = 0;
for (let index = 0; index < text.length; index += 1) {
const character = text[index];
if (character === '{') {
depth += 1;
continue;
}
if (character === '}') {
if (depth === 0) {
return text.slice(0, index);
}
depth -= 1;
}
}
return undefined;
}
function trimDebris(text: string, sourceText: string): string {
let end = text.length;
while (
end > 0 &&
DEBRIS_CHARACTERS.has(text[end - 1]) &&
!sourceText.endsWith(text[end - 1])
) {
end -= 1;
}
return text.slice(0, end);
}
function icuShape(text: string): string {
const argumentNames = [...text.matchAll(ICU_ARGUMENT_REGEX)]
.map(([, name]) => name)
.sort();
const tags = [...text.matchAll(ICU_TAG_REGEX)].map(([tag]) => tag).sort();
return JSON.stringify([argumentNames, tags]);
}
function isCorrupted(text: string, sourceText?: string): boolean {
return (
sourceText !== undefined &&
hasBalancedBraces(sourceText) &&
!hasBalancedBraces(text)
);
}
function salvage(text: string, sourceText?: string): string {
const source = sourceText ?? '';
const cutText = cutAtUnmatchedBrace(text);
// Balanced text has nothing to cut. Otherwise an unclosed '{' is all that is
// left, with no clean tail to cut after, so the translation is dropped rather
// than guessed at.
if (cutText === undefined) {
return hasBalancedBraces(text) ? text : '';
}
const salvagedText = trimDebris(cutText, source);
const isSalvageable =
salvagedText !== '' &&
hasBalancedBraces(salvagedText) &&
(!ENVELOPE_REGEX.test(salvagedText) || ENVELOPE_REGEX.test(source)) &&
icuShape(salvagedText) === icuShape(source);
// An empty repair tells the runner to delete the translation rather than
// replace it, so the locale falls back to English until Crowdin retranslates.
return isSalvageable ? salvagedText : '';
}
export const CORRUPTED_MODEL_OUTPUT_RULE: NormalizationRule = {
name: 'corrupted-model-output',
formats: ['po'],
needsSourceText: true,
detect: isCorrupted,
fix: salvage,
};