Files
upstream-twenty/packages/twenty-oxlint-rules/rules/no-miscased-acronym-in-message.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

194 lines
5.3 KiB
TypeScript

import { defineRule } from '@oxlint/plugins';
export const RULE_NAME = 'no-miscased-acronym-in-message';
const MESSAGE_TAG_NAMES = ['t', 'msg', 'defineMessage'];
const CORRECT_CASING: Record<string, string> = {
Api: 'API',
Apis: 'APIs',
Cdn: 'CDN',
Cname: 'CNAME',
Cors: 'CORS',
Crm: 'CRM',
Csv: 'CSV',
Dkim: 'DKIM',
Dmarc: 'DMARC',
Dns: 'DNS',
Html: 'HTML',
Http: 'HTTP',
Https: 'HTTPS',
Id: 'ID',
Ids: 'IDs',
Imap: 'IMAP',
Json: 'JSON',
Jwt: 'JWT',
Mfa: 'MFA',
Oauth: 'OAuth',
Otp: 'OTP',
Pdf: 'PDF',
Saml: 'SAML',
Sdk: 'SDK',
Smtp: 'SMTP',
Sql: 'SQL',
Sso: 'SSO',
Ssl: 'SSL',
Tls: 'TLS',
Totp: 'TOTP',
Ttl: 'TTL',
Uid: 'UID',
Url: 'URL',
Urls: 'URLs',
Uuid: 'UUID',
Xml: 'XML',
Yaml: 'YAML',
};
// `}` joins the boundary characters so that `${fieldName}Id` is left alone: the
// acronym there is the tail of a code identifier the sentence is naming, not an
// English word. The trailing guard rejects `Identity` and `Urls` alike - `Urls`
// only matches because the map lists it in its own right.
const MISCASED_ACRONYM_REGEX = new RegExp(
`(?<![A-Za-z}])(${Object.keys(CORRECT_CASING).join('|')})(?![a-z])`,
'g',
);
const getTagName = (tag: any): string | undefined => {
if (tag.type === 'Identifier') {
return tag.name;
}
// useLingui() hands the macro back on an object, so `i18n.t` and `_.t` reach
// the extractor exactly as a bare tag does.
if (tag.type === 'MemberExpression' && tag.property.type === 'Identifier') {
return tag.property.name;
}
return undefined;
};
const findMiscasedAcronyms = (text: string): string[] => [
...new Set([...text.matchAll(MISCASED_ACRONYM_REGEX)].map(([word]) => word)),
];
// A template literal reaches the rule already split at its placeholders, so the
// `Id` of `${fieldName}Id` opens its own chunk with nothing in front of it and
// the boundary guard would wave it through. Putting the brace back restores the
// context the split removed.
const withPrecedingPlaceholder = (
text: string,
isPrecededByPlaceholder: boolean,
): string => (isPrecededByPlaceholder ? `}${text}` : text);
export const rule = defineRule({
meta: {
type: 'problem',
docs: {
description:
'Disallow title-cased acronyms in translatable source strings, which translators render correctly and Crowdin then reports as a mismatch.',
},
messages: {
miscasedAcronym:
'Write {{corrected}} rather than {{found}} in a translatable string. Translators render the acronym correctly in their own language, so the miscased English is what Crowdin reports as a QA failure - and it ships to users as a wrong label.',
},
schema: [],
},
create: (context) => {
const reportMiscasedAcronyms = (
node: any,
text: string,
isPrecededByPlaceholder = false,
) => {
for (const found of findMiscasedAcronyms(
withPrecedingPlaceholder(text, isPrecededByPlaceholder),
)) {
context.report({
node,
messageId: 'miscasedAcronym',
data: { found, corrected: CORRECT_CASING[found] },
});
}
};
return {
TaggedTemplateExpression: (node: any) => {
const tagName = getTagName(node.tag);
if (tagName === undefined || !MESSAGE_TAG_NAMES.includes(tagName)) {
return;
}
node.quasi.quasis.forEach((quasi: any, index: number) => {
reportMiscasedAcronyms(
quasi,
quasi.value.cooked ?? quasi.value.raw,
index > 0,
);
});
},
// The descriptor form the standard field metadata uses, and where this
// defect collected.
CallExpression: (node: any) => {
const calleeName = getTagName(node.callee);
if (
calleeName === undefined ||
!MESSAGE_TAG_NAMES.includes(calleeName)
) {
return;
}
const [argument] = node.arguments;
if (argument?.type !== 'ObjectExpression') {
return;
}
for (const property of argument.properties) {
if (
property.type !== 'Property' ||
property.key.type !== 'Identifier' ||
property.key.name !== 'message'
) {
continue;
}
const { value } = property;
if (value.type === 'TemplateLiteral') {
value.quasis.forEach((quasi: any, index: number) => {
reportMiscasedAcronyms(
quasi,
quasi.value.cooked ?? quasi.value.raw,
index > 0,
);
});
}
if (value.type === 'Literal' && typeof value.value === 'string') {
reportMiscasedAcronyms(value, value.value);
}
}
},
// Trans children only: plain JSX text is not a translatable string, so
// flagging it here would report a different problem under this rule's name.
JSXElement: (node: any) => {
if (
node.openingElement.name.type !== 'JSXIdentifier' ||
node.openingElement.name.name !== 'Trans'
) {
return;
}
for (const child of node.children) {
if (child.type === 'JSXText') {
reportMiscasedAcronyms(child, child.value);
}
}
},
};
},
});