security: strip Node dev headers from images + lingui 5.9.5 (drops vulnerable esbuild) (#21448)
## Context
AWS Inspector flags the `prod-twenty` image (built from current main)
with 16 findings, and Dependabot alert 174 flags esbuild. This PR fixes
the OpenSSL scanner findings and the esbuild CVE. The typeorm bump
(CVE-2025-60542) was **pulled out of this PR** — see "typeorm status"
below.
## Changes
### Strip `/usr/local/include/node` from runtime stages
(`twenty-server`, `twenty-app-dev`)
15 OpenSSL CVEs (June 9 advisory, incl. CRITICAL CVE-2026-34182) are all
detected via **Node's bundled OpenSSL dev headers**: 3 GENERIC
`openssl/openssl` 3.5.6 detections per CVE at
`/usr/local/include/node/openssl/archs/linux-x86_64/{asm,asm_avx2,no-asm}/include/openssl/opensslv.h`.
The headers are only needed by node-gyp and native addons are compiled
in the build stages — nothing compiles at runtime. Dropping them clears
all 45 detection instances and permanently ends this class of finding
(third occurrence: 3.5.5 → 3.5.6 → 3.5.7). None of these CVEs are
reachable through Node (no CMS/PKCS#7 API, `pfx` is operator-supplied,
Node's QUIC uses ngtcp2, ASN.1 issues need ~2GB inputs).
**Follow-up (~June 17, 2026):** the `node` binary itself still
statically links OpenSSL 3.5.6 — invisible to the scanner after this PR
and unreachable in practice, but the real fix is bumping the pinned
`node:24-alpine` digest once the [announced June 17 Node.js security
releases](https://nodejs.org/en/blog/vulnerability/june-2026-security-releases)
ship a 24.x linking OpenSSL ≥ 3.5.7 (verify via
`deps/openssl/openssl/VERSION.dat` on the release tag — 24.16.0 is still
on 3.5.6). A dated TODO sits next to the cleanup in the Dockerfile.
### esbuild dev-server CORS CVE (Dependabot alert 174,
GHSA-67mh-4wv8-2f99)
`@lingui/cli@5.1.2` (pins `esbuild ^0.21.5`) was the last parent
resolving a vulnerable esbuild (≤ 0.24.2 lets any website send requests
to the dev server and read responses). Instead of a resolution override,
this bumps the lockstepped **lingui suite 5.1.2 → 5.9.5** (within-major;
lingui adopted `esbuild ^0.25.1` in 5.4.1), which:
- removes `esbuild@0.21.5` and all its platform packages from the
lockfile with no forced ranges;
- drops the `@lingui/core` lockstep resolution (its comment marked it
droppable on the next coordinated lingui bump — the tree now resolves a
single `@lingui/core@5.9.5`);
- `@lingui/swc-plugin` stays at `^5.11.0` (peers on `@lingui/core: 5`;
its 6.x line targets lingui 6).
**lingui 5.9.5 behavioral fallout handled here:**
- Translation functions now **throw without an active locale** (5.1.2
fell back silently). The global `i18n` singleton that backs server-side
`` t`…` `` calls only had a messages compiler set, never an activated
locale → activate the source locale in `I18nService.loadTranslations()`,
mirrored in the server jest setup (unit tests bypass Nest bootstrap).
- `msg`/`t` placeholders are now strictly typed (reject
`null`/`undefined`/`unknown`) → one server call site and 16 twenty-front
files adapted with minimal nullish-coalescing fixes that preserve
rendering.
- `.po`/compiled-catalog churn from the new extractor/compiler
(reference reordering, sorted keys — verified content-identical on
unchanged `.po` inputs) is intentionally not committed: the scheduled
i18n workflows regenerate those.
## typeorm status (pulled out)
typeorm 0.3.20 → 0.3.26 was originally in this PR but **made workspace
metadata sync intermittently lossy**: `example-app-postcard` failed
twice with a *different* field missing from the synced PostCard object
each run, and one integration shard's `DataSeedWorkspaceCommand` died
with "Could not find flat entity with universal identifier …" — versus
zero such failures on recent main. Local runs (db reset + seed, group-by
integration suite 19/19) pass, so it is a nondeterministic
CI-load-sensitive regression that needs dedicated debugging (typeorm
changed LIMIT/OFFSET 0 semantics, lazy count for `getManyAndCount`,
upsert WHERE construction, and topological-sort internals in that
range). The resolutions comment documents this as the blocker;
CVE-2025-60542 is MySQL-driver-only (`sqlstring`), so Postgres-only
Twenty is not exposed in the meantime.
## Verification
- `npx nx typecheck twenty-server` / `twenty-front` — clean (no cache)
- `npx nx test twenty-server` — full suite green
- `lingui:extract` + `lingui:compile` — clean for twenty-server /
twenty-emails / twenty-front
- `oxfmt --check` — clean for both packages
- Lockfile diff: lingui 5.9.5 entries, `esbuild@0.21.5` +
`@esbuild/*@0.21.5` platform packages removed, no typeorm changes
This commit is contained in:
+1
-2
@@ -28,7 +28,6 @@
|
||||
"resolutions": {
|
||||
"graphql": "16.8.1",
|
||||
"graphql-redis-subscriptions/ioredis": "5.10.1",
|
||||
"@lingui/core": "5.1.2",
|
||||
"@types/qs": "6.9.16",
|
||||
"@opentelemetry/api": "1.9.1",
|
||||
"chokidar": "^3.6.0",
|
||||
@@ -49,7 +48,7 @@
|
||||
"googleapis-common/uuid": "11.1.1",
|
||||
"@cyntler/react-doc-viewer/ajv": "8.20.0"
|
||||
},
|
||||
"//resolutions": "Each entry is load-bearing: it forces a version OUTSIDE some parent's declared range where no fixed upstream release exists; remove each once its blocker ships. graphql 16.8.1 -> singleton pin held below msw's ^16.12.0 dep and @nestjs/graphql's ^16.11.0 peer; drop after a validated repo-wide bump to latest 16.x; graphql-redis-subscriptions/ioredis 5.10.1 -> TS type-identity dedup: twenty-server passes its ioredis client into RedisPubSub, so this must equal the exact ioredis version pinned by twenty-server and bullmq (bump in lockstep); @lingui/core 5.1.2 -> lockstep with the @lingui/* 5.1.2 suite, drop in the next coordinated lingui bump; @types/qs 6.9.16 -> holdback below the 6.9.17 ParsedQs typing break (node-saml wants ^6.9.18); @opentelemetry/api 1.9.1 -> singleton guard for the NoopMeterProvider bug (#20231): ai 6.0.x pins 1.9.0 exact vs @sentry/node ^1.9.1, drop when workspace ai >=6.0.178 AND @scalar/agent-chat moves off ai 6.0.33; chokidar ^3 -> NestJS CLI watch needs fsevents on macOS, removed in chokidar 4/5 (#20316); tmp ^0.2.7 -> CVE, zapier-platform-cli 19 (latest) pins 0.2.5 and inquirer 7/8's external-editor wants ^0.0.33; make-fetch-happen ^15 + @electron/{rebuild,node-gyp}/tar ^7.5.16 -> tar CVE eviction for the @electron/rebuild 3.x toolchain (rebuild 3.x pins tar ^6, its node-gyp fork pins tar ^6.2.1 + mfh ^10), drop when electron-forge declares @electron/rebuild >=4; @angular-devkit/core 19.2.24 -> picomatch CVE, blocked on @nestjs/cli >11.0.23 fixing the dist/src output regression (repo held at 11.0.16); yeoman-environment 6.0.1 -> CVE, zapier-platform-cli 19 (latest) pins 4.4.3; webpack-dev-server 5.2.4 -> CVE, @electron-forge/plugin-webpack (incl. 8.x alphas) still declares ^4; express/qs + @cypress/request/qs 6.15.2 -> qs CVE for old express 4.22.0/4.22.1 pinned by @mintlify/previewing and verdaccio (verdaccio also pins @cypress/request 3.0.10; all other qs parents resolve safe naturally); next/postcss 8.5.15 -> postcss CVE, every stable next pins 8.4.31 exact (fix only in 16.3.0 canaries; @react-email/ui also pins next 16.2.6); <pkg>/uuid 11.1.1 -> uuid CVE for parents pinning uuid <11 with no fixed release (sockjs dormant since 2021; @cypress/request 3.0.10 via verdaccio; @ptc-org/nestjs-query-typeorm at latest; typeorm 0.3.20 patch:dep -- droppable by bumping to 0.3.29+ and re-rolling the patch; googleapis 105 -> common 8 drops uuid but needs the googleapis >=152 migration). Preserves the intentional uuid 13.x; @cyntler/react-doc-viewer/ajv 8.20.0 -> CVE, upstream (latest 1.17.1) pins ajv ^7 but never imports it, forcing v8 is safe",
|
||||
"//resolutions": "Each entry is load-bearing: it forces a version OUTSIDE some parent's declared range where no fixed upstream release exists; remove each once its blocker ships. graphql 16.8.1 -> singleton pin held below msw's ^16.12.0 dep and @nestjs/graphql's ^16.11.0 peer; drop after a validated repo-wide bump to latest 16.x; graphql-redis-subscriptions/ioredis 5.10.1 -> TS type-identity dedup: twenty-server passes its ioredis client into RedisPubSub, so this must equal the exact ioredis version pinned by twenty-server and bullmq (bump in lockstep); @types/qs 6.9.16 -> holdback below the 6.9.17 ParsedQs typing break (node-saml wants ^6.9.18); @opentelemetry/api 1.9.1 -> singleton guard for the NoopMeterProvider bug (#20231): ai 6.0.x pins 1.9.0 exact vs @sentry/node ^1.9.1, drop when workspace ai >=6.0.178 AND @scalar/agent-chat moves off ai 6.0.33; chokidar ^3 -> NestJS CLI watch needs fsevents on macOS, removed in chokidar 4/5 (#20316); tmp ^0.2.7 -> CVE, zapier-platform-cli 19 (latest) pins 0.2.5 and inquirer 7/8's external-editor wants ^0.0.33; make-fetch-happen ^15 + @electron/{rebuild,node-gyp}/tar ^7.5.16 -> tar CVE eviction for the @electron/rebuild 3.x toolchain (rebuild 3.x pins tar ^6, its node-gyp fork pins tar ^6.2.1 + mfh ^10), drop when electron-forge declares @electron/rebuild >=4; @angular-devkit/core 19.2.24 -> picomatch CVE, blocked on @nestjs/cli >11.0.23 fixing the dist/src output regression (repo held at 11.0.16); yeoman-environment 6.0.1 -> CVE, zapier-platform-cli 19 (latest) pins 4.4.3; webpack-dev-server 5.2.4 -> CVE, @electron-forge/plugin-webpack (incl. 8.x alphas) still declares ^4; express/qs + @cypress/request/qs 6.15.2 -> qs CVE for old express 4.22.0/4.22.1 pinned by @mintlify/previewing and verdaccio (verdaccio also pins @cypress/request 3.0.10; all other qs parents resolve safe naturally); next/postcss 8.5.15 -> postcss CVE, every stable next pins 8.4.31 exact (fix only in 16.3.0 canaries; @react-email/ui also pins next 16.2.6); <pkg>/uuid 11.1.1 -> uuid CVE for parents pinning uuid <11 with no fixed release (sockjs dormant since 2021; @cypress/request 3.0.10 via verdaccio; @ptc-org/nestjs-query-typeorm at latest; typeorm 0.3.20 patch:dep -- droppable by bumping to 0.3.26+ and re-rolling the patch, but 0.3.26 made workspace metadata sync intermittently lossy (see #21448), debug before retrying; googleapis 105 -> common 8 drops uuid but needs the googleapis >=152 migration). Preserves the intentional uuid 13.x; @cyntler/react-doc-viewer/ajv 8.20.0 -> CVE, upstream (latest 1.17.1) pins ajv ^7 but never imports it, forcing v8 is safe",
|
||||
"version": "0.2.1",
|
||||
"nx": {},
|
||||
"scripts": {
|
||||
|
||||
@@ -135,8 +135,15 @@ LABEL org.opencontainers.image.description="Twenty server image (no frontend)."
|
||||
# - example/ apps vendored inside dependencies, e.g. passport-microsoft's
|
||||
# example/login ships a package-lock.json for an old Express demo
|
||||
# (body-parser, ejs, express, ...) that is never installed or run.
|
||||
# (The OpenSSL CVE is fixed properly by the Node base bump above, not by deletion.)
|
||||
RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx && \
|
||||
# - the Node dev headers: only node-gyp needs them and native addons are
|
||||
# compiled in the build stages; their vendored openssl/opensslv.h is what
|
||||
# scanners fingerprint whenever OpenSSL patches ahead of Node releases.
|
||||
# TODO(2026-06-17): the node binary statically links OpenSSL 3.5.6 (June 9
|
||||
# OpenSSL advisory fixed in 3.5.7). Bump the pinned node:24-alpine base once
|
||||
# the announced June 17, 2026 Node security release ships a 24.x linking
|
||||
# OpenSSL >= 3.5.7 — check deps/openssl/openssl/VERSION.dat on the release tag.
|
||||
RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx \
|
||||
/usr/local/include/node && \
|
||||
find /app/node_modules -type d -name example -prune -exec rm -rf {} +
|
||||
|
||||
RUN mkdir -p /app/.local-storage /app/packages/twenty-server/.local-storage && \
|
||||
@@ -249,8 +256,10 @@ COPY --from=twenty-server-build /app/packages/twenty-client-sdk/dist /app/packag
|
||||
# Frontend static build
|
||||
COPY --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
|
||||
|
||||
# Source maps are not needed in the dev image (no Sentry)
|
||||
RUN find /app/packages/twenty-server/dist -name '*.js.map' -delete
|
||||
# Source maps are not needed in the dev image (no Sentry). Node dev headers are
|
||||
# dropped for the same scanner-hygiene reasons as in twenty-server above.
|
||||
RUN find /app/packages/twenty-server/dist -name '*.js.map' -delete \
|
||||
&& rm -rf /usr/local/include/node
|
||||
|
||||
# s6 service definitions
|
||||
COPY packages/twenty-docker/twenty-app-dev/rootfs/ /
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"build": "npx vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lingui/core": "^5.1.2",
|
||||
"@lingui/react": "^5.1.2",
|
||||
"@lingui/core": "^5.9.5",
|
||||
"@lingui/react": "^5.9.5",
|
||||
"@react-email/components": "^0.5.3",
|
||||
"twenty-shared": "workspace:*"
|
||||
},
|
||||
@@ -19,9 +19,9 @@
|
||||
"react-dom": "^18.2.0 || ^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "^5.1.2",
|
||||
"@lingui/cli": "^5.9.5",
|
||||
"@lingui/swc-plugin": "^5.11.0",
|
||||
"@lingui/vite-plugin": "^5.1.2",
|
||||
"@lingui/vite-plugin": "^5.9.5",
|
||||
"@react-email/ui": "6.5.0",
|
||||
"@tiptap/core": "^3.4.2",
|
||||
"@types/react": "^19",
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
"@graphiql/react": "^0.23.0",
|
||||
"@hello-pangea/dnd": "^16.2.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@lingui/core": "^5.1.2",
|
||||
"@lingui/detect-locale": "^5.2.0",
|
||||
"@lingui/react": "^5.1.2",
|
||||
"@lingui/core": "^5.9.5",
|
||||
"@lingui/detect-locale": "^5.9.5",
|
||||
"@lingui/react": "^5.9.5",
|
||||
"@mantine/core": "^8.3.11",
|
||||
"@mantine/hooks": "^8.3.11",
|
||||
"@mantine/utils": "^6.0.22",
|
||||
@@ -154,9 +154,9 @@
|
||||
"@graphql-codegen/typed-document-node": "^6.1.8",
|
||||
"@graphql-codegen/typescript": "^5.0.10",
|
||||
"@graphql-codegen/typescript-operations": "^5.1.0",
|
||||
"@lingui/cli": "^5.1.2",
|
||||
"@lingui/cli": "^5.9.5",
|
||||
"@lingui/swc-plugin": "^5.11.0",
|
||||
"@lingui/vite-plugin": "^5.1.2",
|
||||
"@lingui/vite-plugin": "^5.9.5",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@storybook-community/storybook-addon-cookie": "^5.0.0",
|
||||
"@storybook/addon-coverage": "^3.0.0",
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@ export const AIChatNoMoreBillingCreditsBanner = () => {
|
||||
const message = isTrialing
|
||||
? t`You've hit your usage limit. Subscribe for more usage.`
|
||||
: isDefined(nextPrice)
|
||||
? t`You've hit your usage limit. \nUpgrade to ${nextResourceCreditsAmount} credits for $${nextResourceCreditPrice}/${nextTierInterval}.`
|
||||
? t`You've hit your usage limit. \nUpgrade to ${nextResourceCreditsAmount ?? ''} credits for $${nextResourceCreditPrice ?? ''}/${nextTierInterval ?? ''}.`
|
||||
: t`You've hit your usage limit. \nReach to our support team to upgrade.`;
|
||||
|
||||
const buttonTitle = isTrialing
|
||||
@@ -138,7 +138,7 @@ export const AIChatNoMoreBillingCreditsBanner = () => {
|
||||
<ConfirmationModal
|
||||
modalInstanceId={AI_CHAT_UPGRADE_CREDIT_PLAN_MODAL_ID}
|
||||
title={t`Get more credits`}
|
||||
subtitle={t`Upgrade to ${nextResourceCreditsAmount} credits for $${nextResourceCreditPrice}/${nextTierInterval}.`}
|
||||
subtitle={t`Upgrade to ${nextResourceCreditsAmount ?? ''} credits for $${nextResourceCreditPrice ?? ''}/${nextTierInterval ?? ''}.`}
|
||||
onConfirmClick={handleUpgradeConfirm}
|
||||
confirmButtonText={t`Upgrade`}
|
||||
confirmButtonAccent="blue"
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ export const LogicFunctionExecutionResult = ({
|
||||
const isError =
|
||||
logicFunctionTestData.output.status === LogicFunctionExecutionStatus.ERROR;
|
||||
|
||||
const duration = logicFunctionTestData.output.duration;
|
||||
const duration = logicFunctionTestData.output.duration ?? 0;
|
||||
const status: ExecutionStatus = {
|
||||
isSuccess,
|
||||
isError,
|
||||
|
||||
+1
-1
@@ -150,7 +150,7 @@ export const AdvancedFilterCompositeSubFieldSelectMenu = ({
|
||||
});
|
||||
}}
|
||||
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown.icon)}
|
||||
text={t`Any ${fieldLabel} field`}
|
||||
text={t`Any ${fieldLabel ?? ''} field`}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
|
||||
+2
-2
@@ -48,8 +48,8 @@ export const RecordGroupMenuItemDraggable = ({
|
||||
{
|
||||
Icon: recordGroupDefinition.isVisible ? IconEyeOff : IconEye,
|
||||
ariaLabel: recordGroupDefinition.isVisible
|
||||
? t`Hide group ${groupValue}`
|
||||
: t`Show group ${groupValue}`,
|
||||
? t`Hide group ${groupValue ?? ''}`
|
||||
: t`Show group ${groupValue ?? ''}`,
|
||||
dataTestId: recordGroupDefinition.isVisible
|
||||
? `hide-group-${recordGroupDefinition.value?.toLowerCase().replace(' ', '-') ?? ''}`
|
||||
: `show-group-${recordGroupDefinition.value?.toLowerCase().replace(' ', '-') ?? ''}`,
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ export const SettingsAdminChatThreadMessageList = ({
|
||||
)}
|
||||
{toolParts.map((part, index) => (
|
||||
<StyledMessageContent key={index} isUser={false}>
|
||||
{t`Tool call: ${part.toolName}`}
|
||||
{t`Tool call: ${part.toolName ?? ''}`}
|
||||
</StyledMessageContent>
|
||||
))}
|
||||
<StyledTimestamp>
|
||||
|
||||
+5
-3
@@ -22,10 +22,12 @@ const getExpandableContainerTitle = (
|
||||
)?.label;
|
||||
|
||||
const unmatchedCount =
|
||||
'matchedOptions' in column &&
|
||||
column.matchedOptions?.filter((option) => !isDefined(option.value)).length;
|
||||
'matchedOptions' in column
|
||||
? column.matchedOptions.filter((option) => !isDefined(option.value))
|
||||
.length
|
||||
: 0;
|
||||
|
||||
return t`Match ${fieldLabel} (${unmatchedCount} Unmatched)`;
|
||||
return t`Match ${fieldLabel ?? ''} (${unmatchedCount} Unmatched)`;
|
||||
};
|
||||
|
||||
type UnmatchColumnProps = {
|
||||
|
||||
@@ -129,7 +129,7 @@ const parseAdditionalPhones = (additionalPhones?: any) => {
|
||||
try {
|
||||
return JSON.parse(additionalPhones);
|
||||
} catch (error) {
|
||||
logError(t`Error parsing additional phones: ${error}`);
|
||||
logError(t`Error parsing additional phones: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -62,8 +62,6 @@ export const MobileBreadcrumb = ({
|
||||
? previousLink.children
|
||||
: '';
|
||||
|
||||
const linkText = previousLink.children;
|
||||
|
||||
return (
|
||||
<StyledWrapper className={className}>
|
||||
{previousLink?.href ? (
|
||||
@@ -71,7 +69,7 @@ export const MobileBreadcrumb = ({
|
||||
<IconChevronLeft size={theme.icon.size.md} />
|
||||
<StyledLinkContainer>
|
||||
<Link title={text} to={previousLink.href}>
|
||||
{t`Back to ${linkText}`}
|
||||
{t`Back to ${text}`}
|
||||
</Link>
|
||||
</StyledLinkContainer>
|
||||
</>
|
||||
|
||||
+1
-1
@@ -250,7 +250,7 @@ export const WorkflowEditActionUpsertRecord = ({
|
||||
}
|
||||
placeholder={t`Object unique fields`}
|
||||
readonly
|
||||
hint={t`We match on these fields. If a ${objectLabelSingular} already exists, we update it. Otherwise, we create a new one.`}
|
||||
hint={t`We match on these fields. If a ${objectLabelSingular ?? ''} already exists, we update it. Otherwise, we create a new one.`}
|
||||
actionType="UPSERT_RECORD"
|
||||
/>
|
||||
)}
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ export const getDayOfMonthDescription = (
|
||||
}
|
||||
const lastDay = ordinalDays.pop();
|
||||
const remainingDays = ordinalDays.join(', ');
|
||||
return t`on the ${remainingDays} and ${lastDay} of the month`;
|
||||
return t`on the ${remainingDays} and ${lastDay ?? ''} of the month`;
|
||||
}
|
||||
|
||||
// Single day value
|
||||
|
||||
+1
-1
@@ -145,7 +145,7 @@ export const getDayOfWeekDescription = (
|
||||
}
|
||||
const lastDay = dayNames.pop();
|
||||
const remainingDays = dayNames.join(', ');
|
||||
return t`only on ${remainingDays} and ${lastDay}`;
|
||||
return t`only on ${remainingDays} and ${lastDay ?? ''}`;
|
||||
}
|
||||
|
||||
// Single day value
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ export const getHoursDescription = (
|
||||
}
|
||||
const lastTime = formattedTimes.pop();
|
||||
const remainingTimes = formattedTimes.join(', ');
|
||||
return t`at ${remainingTimes} and ${lastTime}`;
|
||||
return t`at ${remainingTimes} and ${lastTime ?? ''}`;
|
||||
}
|
||||
|
||||
const hourNum = parseInt(hours, 10);
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ export const getMinutesDescription = (
|
||||
}
|
||||
const lastValue = values.pop();
|
||||
const remainingValues = values.join(', ');
|
||||
return t`at minutes ${remainingValues} and ${lastValue}`;
|
||||
return t`at minutes ${remainingValues} and ${lastValue ?? ''}`;
|
||||
}
|
||||
|
||||
const minuteNum = parseInt(minutes, 10);
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@ export const getMonthsDescription = (
|
||||
}
|
||||
const lastMonth = monthNames.pop();
|
||||
const remainingMonths = monthNames.join(', ');
|
||||
return t`only in ${remainingMonths} and ${lastMonth}`;
|
||||
return t`only in ${remainingMonths} and ${lastMonth ?? ''}`;
|
||||
}
|
||||
|
||||
// Single month value
|
||||
|
||||
+1
-1
@@ -424,7 +424,7 @@ export const SettingsAdminAiProviderDetail = () => {
|
||||
|
||||
<ConfirmationModal
|
||||
modalInstanceId={REMOVE_PROVIDER_MODAL_ID}
|
||||
title={t`Remove provider "${provider?.label ?? providerName}"`}
|
||||
title={t`Remove provider "${provider?.label ?? providerName ?? ''}"`}
|
||||
subtitle={t`This will disconnect all models from this provider. Models will no longer be available until a new provider is configured.`}
|
||||
onConfirmClick={handleRemoveProvider}
|
||||
confirmButtonText={t`Remove`}
|
||||
|
||||
+1
-1
@@ -155,7 +155,7 @@ export const SettingsApplicationDetailAboutTab = ({
|
||||
title={
|
||||
isUpgrading
|
||||
? t`Upgrading...`
|
||||
: t`Upgrade to ${latestAvailableVersion}`
|
||||
: t`Upgrade to ${latestAvailableVersion ?? ''}`
|
||||
}
|
||||
variant={'secondary'}
|
||||
accent={'blue'}
|
||||
|
||||
@@ -42,9 +42,9 @@
|
||||
"@graphql-yoga/nestjs": "2.1.0",
|
||||
"@jrmdayn/googleapis-batcher": "^0.10.1",
|
||||
"@lifeomic/axios-fetch": "^3.1.0",
|
||||
"@lingui/conf": "5.1.2",
|
||||
"@lingui/core": "^5.1.2",
|
||||
"@lingui/format-po": "5.1.2",
|
||||
"@lingui/conf": "5.9.5",
|
||||
"@lingui/core": "^5.9.5",
|
||||
"@lingui/format-po": "5.9.5",
|
||||
"@microsoft/microsoft-graph-client": "3.0.7",
|
||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||
"@nestjs/axios": "3.1.2",
|
||||
@@ -172,7 +172,7 @@
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "^5.1.2",
|
||||
"@lingui/cli": "^5.9.5",
|
||||
"@nestjs/cli": "^11.0.16",
|
||||
"@nestjs/schematics": "^11.0.9",
|
||||
"@nestjs/testing": "11.1.24",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
// Add Jest matchers for toThrowError and other missing methods
|
||||
export {};
|
||||
import { i18n } from '@lingui/core';
|
||||
import { compileMessage } from '@lingui/message-utils/compileMessage';
|
||||
|
||||
// Lingui 5.9+ throws when a translation function runs without an active
|
||||
// locale. Production activates the global singleton in I18nService; unit
|
||||
// tests bypass Nest bootstrap, so mirror that here. The messages compiler
|
||||
// makes t`...` fall back to the English source text.
|
||||
i18n.setMessagesCompiler(compileMessage);
|
||||
i18n.load('en', {});
|
||||
i18n.activate('en');
|
||||
|
||||
declare global {
|
||||
namespace jest {
|
||||
|
||||
+4
-2
@@ -112,7 +112,7 @@ export class GraphqlQueryFilterFieldParser {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Invalid filter value for field ${key}. Expected non-empty array`,
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
{ userFriendlyMessage: msg`Invalid filter value: "${value}"` },
|
||||
{ userFriendlyMessage: msg`Invalid filter value: "${String(value)}"` },
|
||||
);
|
||||
}
|
||||
const { sql, params } = computeWhereConditionParts({
|
||||
@@ -254,7 +254,9 @@ export class GraphqlQueryFilterFieldParser {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Invalid filter value for field ${subFieldKey}. Expected non-empty array`,
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
{ userFriendlyMessage: msg`Invalid filter value: "${value}"` },
|
||||
{
|
||||
userFriendlyMessage: msg`Invalid filter value: "${String(value)}"`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,8 +49,12 @@ export class I18nService implements OnModuleInit {
|
||||
|
||||
async loadTranslations() {
|
||||
// The global i18n singleton backs server-side t`…` calls and has no
|
||||
// compiled catalog, so it needs a runtime message compiler.
|
||||
// compiled catalog, so it needs a runtime message compiler. Since lingui
|
||||
// 5.9 it also throws unless a locale is activated, so activate the source
|
||||
// locale (t`…` then renders the English source text via the compiler).
|
||||
i18n.setMessagesCompiler(compileMessage);
|
||||
i18n.load(SOURCE_LOCALE, enMessages);
|
||||
i18n.activate(SOURCE_LOCALE);
|
||||
|
||||
const messagesByLocale: Record<keyof typeof APP_LOCALES, Messages> = {
|
||||
en: enMessages,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-typescript": "^7.24.6",
|
||||
"@lingui/core": "^5.1.2",
|
||||
"@lingui/core": "^5.9.5",
|
||||
"@prettier/sync": "^0.5.2",
|
||||
"@swc/core": "^1.15.11",
|
||||
"@swc/jest": "^0.2.39",
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
"@calcom/embed-react": "^1.5.3",
|
||||
"@linaria/core": "^7.0.0",
|
||||
"@linaria/react": "^7.0.1",
|
||||
"@lingui/core": "^5.1.2",
|
||||
"@lingui/react": "^5.1.2",
|
||||
"@lingui/core": "^5.9.5",
|
||||
"@lingui/react": "^5.9.5",
|
||||
"@lottiefiles/dotlottie-react": "^0.18.10",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@tabler/icons-react": "^3.41.1",
|
||||
@@ -41,9 +41,9 @@
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "^5.1.2",
|
||||
"@lingui/conf": "5.1.2",
|
||||
"@lingui/format-po": "5.1.2",
|
||||
"@lingui/cli": "^5.9.5",
|
||||
"@lingui/conf": "5.9.5",
|
||||
"@lingui/format-po": "5.9.5",
|
||||
"@lingui/swc-plugin": "^5.11.0",
|
||||
"@opennextjs/cloudflare": "^1.0.0",
|
||||
"@swc/core": "^1.15.11",
|
||||
|
||||
Reference in New Issue
Block a user