snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
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

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
import { CORRUPTED_MODEL_OUTPUT_RULE } from '../corrupted-model-output.rule';
const { detect, fix } = CORRUPTED_MODEL_OUTPUT_RULE;
describe('CORRUPTED_MODEL_OUTPUT_RULE', () => {
it('cuts a leaked response envelope off a plain translation', () => {
const sourceText = 'Group name';
const translationText =
'Nome do grupo}]}``` }}} code block? Wait. We messed up.';
expect(detect(translationText, sourceText)).toBe(true);
expect(fix(translationText, sourceText)).toBe('Nome do grupo');
});
it('cuts a trailing JSON array separator', () => {
expect(detect('レコードテーブルを編集},{', 'Edit Record Table')).toBe(true);
expect(fix('レコードテーブルを編集},{', 'Edit Record Table')).toBe(
'レコードテーブルを編集',
);
});
it('keeps a plural block intact while dropping the envelope after it', () => {
const sourceText =
'{hiddenFieldCount, plural, one {# more populated field available} other {# more populated fields available}}';
const translationText =
'{hiddenFieldCount, plural, other {還有 # 個已填入欄位可用}}}]}દાવાદ്?ฤศจassistant to=developer';
expect(detect(translationText, sourceText)).toBe(true);
expect(fix(translationText, sourceText)).toBe(
'{hiddenFieldCount, plural, other {還有 # 個已填入欄位可用}}',
);
});
it('keeps a closing quote the source also ends with', () => {
const sourceText = 'Set fields created in the future as "visible"';
const translationText =
'Aseta tulevaisuudessa luotavat kentät "näkyviksi"}]}``` }]} } } }';
expect(fix(translationText, sourceText)).toBe(
'Aseta tulevaisuudessa luotavat kentät "näkyviksi"',
);
});
it('drops a trailing apostrophe the source does not carry', () => {
const sourceText = 'An error occurred';
expect(fix("'n Fout het voorgekom'}]}```}``` }```{", sourceText)).toBe(
"'n Fout het voorgekom",
);
});
it('deletes a translation whose salvageable prefix is still envelope', () => {
const sourceText = 'Trigger';
const translationText = 'הפעל","pluralForm":null}]}]}{';
expect(detect(translationText, sourceText)).toBe(true);
expect(fix(translationText, sourceText)).toBe('');
});
it('deletes a translation with an unclosed brace and no clean cut', () => {
expect(fix('Nome do grupo {broken', 'Group name')).toBe('');
});
it('keeps markdown links when the source carries them too', () => {
const sourceText = 'See [Getting Started](https://twenty.com/docs) first.';
const translationText =
'Consulte [Introdução](https://twenty.com/docs) primeiro.},{';
expect(fix(translationText, sourceText)).toBe(
'Consulte [Introdução](https://twenty.com/docs) primeiro.',
);
});
it('does not flag a healthy translation', () => {
expect(detect('Nome do grupo', 'Group name')).toBe(false);
expect(
detect(
'{count, plural, other {# champs}}',
'{count, plural, one {# field} other {# fields}}',
),
).toBe(false);
});
it('does not flag when the source is unavailable', () => {
expect(detect('Nome do grupo}]}', undefined)).toBe(false);
});
it('does not flag when the source is itself unbalanced', () => {
expect(detect('Algo}', 'Something}')).toBe(false);
});
it('is idempotent', () => {
const sourceText = 'Group name';
const once = fix('Nome do grupo}]}``` }}}', sourceText);
expect(detect(once, sourceText)).toBe(false);
expect(fix(once, sourceText)).toBe(once);
});
});

View File

@@ -0,0 +1,42 @@
import { ESCAPED_EVERY_CHARACTER_RULE } from '../escaped-every-character.rule';
const { detect, fix } = ESCAPED_EVERY_CHARACTER_RULE;
describe('ESCAPED_EVERY_CHARACTER_RULE', () => {
it.each([
['\\ا\\ل\\إ\\ع\\د\\ا\\د\\ا\\ت', 'الإعدادات'],
['\\ダ\\ー\\ク', 'ダーク'],
['\\明\\る\\い', '明るい'],
['\\S\\e\\t\\t\\i\\n\\g\\s', 'Settings'],
])(
'unescapes a message escaped character by character: %s',
(text, expected) => {
expect(detect(text)).toBe(true);
expect(fix(text)).toBe(expected);
},
);
// A path or a regex carries a few backslashes among ordinary text, which is
// the translator writing what the source asked for.
it.each([
'C:\\Users\\name\\file',
'Escape it as \\n to break the line',
'Matches \\d followed by \\w in the pattern',
'Réglages',
'',
'\\',
])('leaves a message whose backslashes are its content: %s', (text) => {
expect(detect(text)).toBe(false);
expect(fix(text)).toBe(text);
});
it('needs more than a couple of escapes before it acts', () => {
expect(detect('\\a\\b')).toBe(false);
expect(fix('\\a\\b')).toBe('\\a\\b');
});
it('runs against both catalog formats and needs no source text', () => {
expect(ESCAPED_EVERY_CHARACTER_RULE.formats).toEqual(['po', 'mdx']);
expect(ESCAPED_EVERY_CHARACTER_RULE.needsSourceText).toBeUndefined();
});
});

View File

@@ -0,0 +1,65 @@
import { ESCAPED_INLINE_CODE_TAGS_RULE } from '../escaped-inline-code-tags.rule';
const { detect, fix, sourceFilter } = ESCAPED_INLINE_CODE_TAGS_RULE;
describe('ESCAPED_INLINE_CODE_TAGS_RULE', () => {
it('repairs an escaped tag inside an inline-code span', () => {
const translationText = 'Remplacez `<path>` par votre chemin';
expect(detect(translationText)).toBe(true);
expect(fix(translationText)).toBe('Remplacez `<path>` par votre chemin');
});
it('repairs numeric and hexadecimal entities', () => {
expect(fix('Utilisez `&#60;img&#x3E;` ici')).toBe('Utilisez `<img>` ici');
});
it('leaves escaped angle brackets outside inline code untouched', () => {
expect(fix('Entourez-le de `&lt;Trans&gt;` et gardez &lt;3 en prose')).toBe(
'Entourez-le de `<Trans>` et gardez &lt;3 en prose',
);
});
it('does not flag a translation whose inline code is already correct', () => {
expect(detect('Remplacez `<path>` par votre chemin')).toBe(false);
});
it('only selects sources that carry a tag inside inline code', () => {
expect(sourceFilter?.('Replace `<path>` with your path')).toBe(true);
expect(sourceFilter?.('Escape a literal &lt; in prose')).toBe(false);
});
it('repairs a backslash-escaped tag inside an inline-code span', () => {
const translationText =
'Ouvrez `https://\\<your-workspace-subdomain>.withtwenty.com`';
expect(detect(translationText)).toBe(true);
expect(fix(translationText)).toBe(
'Ouvrez `https://<your-workspace-subdomain>.withtwenty.com`',
);
});
it('repairs several backslash-escaped tags in one span', () => {
expect(fix('Utilisez `\\<server-url>/s/\\<path>` ici')).toBe(
'Utilisez `<server-url>/s/<path>` ici',
);
});
it('leaves backslash-escaped angle brackets outside inline code untouched', () => {
expect(fix('Gardez `\\<path>` mais laissez \\<literal> en prose')).toBe(
'Gardez `<path>` mais laissez \\<literal> en prose',
);
});
it('is idempotent for backslash escapes', () => {
const once = fix('Utilisez `\\<path>` et `\\<name>`');
expect(fix(once)).toBe(once);
});
it('is idempotent', () => {
const once = fix('Remplacez `&lt;path&gt;` par `&lt;autre&gt;`');
expect(fix(once)).toBe(once);
});
});

View File

@@ -0,0 +1,54 @@
import { ESCAPED_UNICODE_RULE } from '../escaped-unicode.rule';
const { detect, fix } = ESCAPED_UNICODE_RULE;
describe('ESCAPED_UNICODE_RULE', () => {
it('restores characters that leaked as literal \\uXXXX sequences', () => {
const translationText = '\\u62db\\u8058';
expect(detect(translationText, 'Recruiting')).toBe(true);
expect(fix(translationText, 'Recruiting')).toBe('招聘');
});
it('restores surrogate pairs', () => {
expect(fix('ok \\ud83d\\ude00', 'ok 😀')).toBe('ok 😀');
});
it('keeps an escape the source writes itself, such as a code sample', () => {
const sourceText = 'Escape the accent as \\u00e9 in the config';
const translationText =
"Echappez l'accent en \\u00e9 dans la configuration";
expect(detect(translationText, sourceText)).toBe(false);
expect(fix(translationText, sourceText)).toBe(translationText);
});
it('decodes only the escapes the source does not carry', () => {
const sourceText = 'Use \\u00e9 here';
expect(fix('Utilisez \\u00e9 ici \\u62db', sourceText)).toBe(
'Utilisez \\u00e9 ici 招',
);
});
it('stands down when the source is unknown, rather than decoding blind', () => {
expect(detect('\\u62db\\u8058', undefined)).toBe(false);
expect(fix('\\u62db\\u8058', undefined)).toBe('\\u62db\\u8058');
});
it('scans every string rather than filtering by source', () => {
expect(ESCAPED_UNICODE_RULE.sourceFilter).toBeUndefined();
expect(ESCAPED_UNICODE_RULE.needsSourceText).toBe(true);
});
it('leaves text without escaped sequences untouched', () => {
expect(detect('招聘', 'Recruiting')).toBe(false);
});
it('is idempotent', () => {
const once = fix('\\u62db', 'Recruiting');
expect(detect(once, 'Recruiting')).toBe(false);
expect(fix(once, 'Recruiting')).toBe(once);
});
});

View File

@@ -0,0 +1,68 @@
import { INVENTED_ARGUMENT_RULE } from '../invented-argument.rule';
const { detect, fix } = INVENTED_ARGUMENT_RULE;
describe('INVENTED_ARGUMENT_RULE', () => {
it('restores the source spelling of a recased argument', () => {
const sourceText =
'Multiple records found for {conflictingFieldsValues}. Cannot determine which record to update.';
const translationText =
'{ConflictingFieldsValues} のために複数のレコードが見つかりました。';
expect(detect(translationText, sourceText)).toBe(true);
expect(fix(translationText, sourceText)).toBe(
'{conflictingFieldsValues} のために複数のレコードが見つかりました。',
);
});
it('deletes a translation that pluralises an argument-less source', () => {
const sourceText = 'Record(s) selected';
const translationText =
'{count, plural, one {# registro selecionado} other {# registros selecionados}}';
expect(detect(translationText, sourceText)).toBe(true);
expect(fix(translationText, sourceText)).toBe('');
});
it('deletes a translation that adds an argument nothing will supply', () => {
const sourceText = 'We keep your data for {dataRetentionDays} days.';
const translationText = 'Чувамо податке {dataRetentionDays} {dayOrDays}.';
expect(fix(translationText, sourceText)).toBe('');
});
it('allows a translation to add plural cases its locale needs', () => {
const sourceText = '{count, plural, one {# record} other {# records}}';
const translationText =
'{count, plural, one {# запись} few {# записи} many {# записей} other {# записи}}';
expect(detect(translationText, sourceText)).toBe(false);
});
it('does not read plural case bodies as arguments', () => {
const sourceText = '{unitCount, plural, one {Day} other {Days}}';
const translationText = '{unitCount, plural, one {Dag} other {Dae}}';
expect(detect(translationText, sourceText)).toBe(false);
});
it('does not flag a matching translation or a missing source', () => {
expect(detect('Supprimer {name}', 'Delete {name}')).toBe(false);
expect(detect('Supprimer {objet}', undefined)).toBe(false);
});
it('ignores a dropped argument, which it cannot repair', () => {
expect(detect('Supprimer', 'Delete {name}')).toBe(false);
});
it('is idempotent', () => {
const sourceText = 'Found for {conflictingFieldsValues}.';
const once = fix(
'{ConflictingFieldsValues} で見つかりました。',
sourceText,
);
expect(detect(once, sourceText)).toBe(false);
expect(fix(once, sourceText)).toBe(once);
});
});

View File

@@ -0,0 +1,54 @@
import { INVENTED_MARKUP_RULE } from '../invented-markup.rule';
const { detect, fix } = INVENTED_MARKUP_RULE;
describe('INVENTED_MARKUP_RULE', () => {
it('strips a highlight span the source never had', () => {
const sourceText = 'is not a valid calling code';
const translationText =
'n\'est pas un <span class="highlight">code d\'appel valide</span>';
expect(detect(translationText, sourceText)).toBe(true);
expect(fix(translationText, sourceText)).toBe(
"n'est pas un code d'appel valide",
);
});
it('strips nested direction spans', () => {
const translationText =
'<span dir="rtl"><span dir="ltr">כשל זמני</span></span>';
expect(fix(translationText, 'Temporary Failure')).toBe('כשל זמני');
});
it('strips bold added around numbers and punctuation', () => {
expect(fix('過去<b>12</b>時間', 'Last 12 hours')).toBe('過去12時間');
expect(fix('API名<b>(</b>単数形<b>)</b>', 'API Name (Singular)')).toBe(
'API名(単数形)',
);
});
it('leaves a translation alone when the source carries markup of its own', () => {
const sourceText = 'Your workspace <0>{name}</0> was deleted.';
const translationText = 'Ваш простор <0>{name}</0> је обрисан.';
expect(detect(translationText, sourceText)).toBe(false);
});
it('leaves a Lingui-tagged source alone even when the translation uses html', () => {
expect(detect('Votre <b>espace</b>', 'Your <0>workspace</0>')).toBe(false);
});
it('does not flag a plain translation, a comparison, or a missing source', () => {
expect(detect('Rechercher', 'Search')).toBe(false);
expect(detect('moins de < 5 minutes', 'less than < 5 minutes')).toBe(false);
expect(detect('<b>Recherche</b>', undefined)).toBe(false);
});
it('is idempotent', () => {
const once = fix('過去<b>4</b>時間', 'Last 4 hours');
expect(detect(once, 'Last 4 hours')).toBe(false);
expect(fix(once, 'Last 4 hours')).toBe(once);
});
});

View File

@@ -0,0 +1,126 @@
import { TRANSLATED_IDENTIFIER_RULE } from '../translated-identifier.rule';
const { detect, fix } = TRANSLATED_IDENTIFIER_RULE;
describe('TRANSLATED_IDENTIFIER_RULE', () => {
it('restores a code identifier translated inside backticks', () => {
const sourceText =
'Re-renders are often caused by unnecessary `useEffect`.';
const translationText =
'وكثيرا ما تكون عمليات إعادة التسليم ناجمة عن `فعالية الاستخدام` غير الضرورية.';
expect(detect(translationText, sourceText)).toBe(true);
expect(fix(translationText, sourceText)).toBe(
'وكثيرا ما تكون عمليات إعادة التسليم ناجمة عن `useEffect` غير الضرورية.',
);
});
// A translation reorders spans to suit its own grammar, so position says
// nothing about which source span belongs in which slot.
it('stands down on more than one span, where order cannot be trusted', () => {
const sourceText =
'The `twenty-app` keyword in your `package.json` `keywords` array';
const translationText =
'Cuvântul cheie `twenty-app` din array-ul `keywords` al fișierului `package.json`';
expect(detect(translationText, sourceText)).toBe(false);
expect(fix(translationText, sourceText)).toBe(translationText);
});
// Backticks in the docs mark UI labels as often as symbols, and the reader
// sees those labels translated in their own app, so restoring them would
// un-translate correct work.
it.each([
[
'Go to `Settings` in the left sidebar.',
'Accesați `Setări` din bara laterală stângă.',
],
[
'Go to `Data Model`, then select the object.',
'Accesați `Model de date`, apoi selectați obiectul.',
],
[
'Select `Deactivate` from the dropdown.',
'Selectați `Dezactivează` din lista derulantă.',
],
])('keeps a translated UI label: %s', (sourceText, translationText) => {
expect(detect(translationText, sourceText)).toBe(false);
expect(fix(translationText, sourceText)).toBe(translationText);
});
it.each([
[
'Caused by `useEffect`.',
'Cauzat de `efectul de utilizare`.',
'Cauzat de `useEffect`.',
],
[
'Fields of type `SELECT`.',
'Câmpuri de tip `SELECTARE`.',
'Câmpuri de tip `SELECT`.',
],
[
'Set `is_auth_required`.',
'Setați `este_necesară_autentificarea`.',
'Setați `is_auth_required`.',
],
[
'Call `client.run()`.',
'Apelați `client.rulează()`.',
'Apelați `client.run()`.',
],
[
'Build the `twenty-emails` package first.',
'Vous devez construire le paquet `vingt-emails`.',
'Vous devez construire le paquet `twenty-emails`.',
],
[
'Send the `x-slack-signature` header.',
'Trimiteți antetul `x-semnătură-slack`.',
'Trimiteți antetul `x-slack-signature`.',
],
])(
'restores an identifier-shaped span: %s',
(sourceText, translationText, expected) => {
expect(detect(translationText, sourceText)).toBe(true);
expect(fix(translationText, sourceText)).toBe(expected);
},
);
// Lining them up positionally is only sound when both sides hold the same
// number; a translation that dropped or added one cannot be matched safely.
it('stands down when the counts do not line up', () => {
const sourceText = 'Use `a` and `b`.';
expect(detect('Utilisez `x`.', sourceText)).toBe(false);
expect(fix('Utilisez `x`.', sourceText)).toBe('Utilisez `x`.');
});
it('leaves a translation whose identifiers already match', () => {
const sourceText = 'Call `useEffect` from [here](/docs/a).';
const translationText = 'Appelez `useEffect` depuis [ici](/docs/a).';
expect(detect(translationText, sourceText)).toBe(false);
});
// Translated pages live under /l/<lang>/ and link to each other there, so a
// locale-prefixed target is the translation being right, not wrong. Paths and
// link targets are left to a human.
it('leaves link targets alone, including locale-prefixed ones', () => {
const sourceText = 'See [the guide](/user-guide/billing).';
const translationText = 'Vezi [ghidul](/l/ro/user-guide/billing).';
expect(detect(translationText, sourceText)).toBe(false);
expect(fix(translationText, sourceText)).toBe(translationText);
});
it('stands down when the source is unknown', () => {
expect(detect('`useEffect`', undefined)).toBe(false);
expect(fix('`useEffect`', undefined)).toBe('`useEffect`');
});
it('runs against MDX catalogs only', () => {
expect(TRANSLATED_IDENTIFIER_RULE.formats).toEqual(['mdx']);
expect(TRANSLATED_IDENTIFIER_RULE.needsSourceText).toBe(true);
});
});

View File

@@ -0,0 +1,72 @@
import { WORDLESS_TRANSLATION_RULE } from '../wordless-translation.rule';
const { detect, fix } = WORDLESS_TRANSLATION_RULE;
describe('WORDLESS_TRANSLATION_RULE', () => {
it('drops a translation that is a lone backslash', () => {
expect(detect('\\', 'The values of this field must be unique')).toBe(true);
expect(fix('\\', 'The values of this field must be unique')).toBe('');
});
it('drops a translation that is an escaped quote pair', () => {
expect(detect("''", 'Count false')).toBe(true);
expect(fix("''", 'Count false')).toBe('');
});
it('drops punctuation and whitespace debris', () => {
expect(detect(' .,; ', 'Regenerate key')).toBe(true);
});
// \w is ASCII-only in JavaScript, so a naive check would delete every
// translation in a non-Latin script.
it.each([
['他們的值必須是唯一的', 'Chinese'],
['הערכים חייבים להיות ייחודיים', 'Hebrew'],
['القيم يجب أن تكون فريدة', 'Arabic'],
['Значения должны быть уникальными', 'Cyrillic'],
['Արժեքներըպետք է եզակի լինեն', 'Armenian'],
['値は一意である必要があります', 'Japanese'],
])('keeps %s (%s)', (translationText) => {
expect(detect(translationText, 'The values must be unique')).toBe(false);
});
it('keeps a translation that is only digits, which are words enough', () => {
expect(detect('2026', 'Year')).toBe(false);
});
it('keeps a wordless translation when the source has no words either', () => {
expect(detect('—', '—')).toBe(false);
expect(detect(':', ':')).toBe(false);
});
it('keeps a translation that is only a placeholder, as its source is', () => {
expect(detect('{count}', '{count}')).toBe(false);
});
// Ignoring placeholder names would catch a translation left as a bare
// {count}, but a dry run over ~200k strings found none of those and two
// Finnish translations that legitimately render a connector word as a
// symbol. Deleting a good translation is worse than keeping a thin one, so
// the rule reads the whole string.
it('keeps a translation that renders a connector word as a symbol', () => {
expect(detect('{0} / {1}', '{0} of {1}')).toBe(false);
expect(detect(' /{intervalLabel}', ' per {intervalLabel}')).toBe(false);
});
it('stands down when the source is unknown, rather than deleting blind', () => {
expect(detect('\\', undefined)).toBe(false);
});
it('leaves a translation already dropped by an earlier rule alone', () => {
expect(detect('', 'Regenerate key')).toBe(false);
});
it('scans every string rather than filtering by source', () => {
expect(WORDLESS_TRANSLATION_RULE.sourceFilter).toBeUndefined();
expect(WORDLESS_TRANSLATION_RULE.needsSourceText).toBe(true);
});
it('runs against PO catalogs only', () => {
expect(WORDLESS_TRANSLATION_RULE.formats).toEqual(['po']);
});
});