feat: add lingui/no-unlocalized-strings ESLint rule and fix translations (#16610)

## Summary
This PR adds the `lingui/no-unlocalized-strings` ESLint rule to detect
untranslated strings and fixes translation issues across multiple
components.

## Changes

### ESLint Configuration (`eslint.config.react.mjs`)
- Added comprehensive `ignore` patterns for non-translatable strings
(CSS values, HTML attributes, technical identifiers)
- Added `ignoreNames` for props that don't need translation (className,
data-*, aria-*, etc.)
- Added `ignoreFunctions` for console methods, URL APIs, and other
non-user-facing functions
- Disabled rule for debug files, storybook, and test files

### Components Fixed (~19 files)
- Object record components (field inputs, pickers, merge dialogs)
- Settings components (accounts, admin panel)
- Serverless function components
- Record table and title cell components

## Status
🚧 **Work in Progress** - ~124 files remaining to fix

This PR is being submitted as draft to allow progressive fixing of
remaining translation issues.

## Testing
- Run `npx eslint "src/**/*.tsx"` in `packages/twenty-front` to check
remaining issues
This commit is contained in:
Félix Malfait
2025-12-17 22:08:33 +01:00
committed by GitHub
parent c13b955a36
commit 1088f7bbab
368 changed files with 56823 additions and 836 deletions
+6 -2
View File
@@ -56,7 +56,9 @@ npx nx storybook:build twenty-front # Build Storybook
npx nx storybook:serve-and-test:static # Run Storybook tests
# Development
npx nx lint twenty-front # Run linter
npx nx lint:changed twenty-front # Lint changed files only (fastest)
npx nx lint:changed twenty-front --configuration=fix # Auto-fix changed files
npx nx lint twenty-front # Lint all files (slower)
npx nx typecheck twenty-front # Type checking
npx nx run twenty-front:graphql:generate # Generate GraphQL types
```
@@ -70,7 +72,9 @@ npx nx run twenty-server:database:migrate:prod # Run migrations
# Development
npx nx run twenty-server:start # Start the server
npx nx run twenty-server:lint # Run linter (add --fix to auto-fix)
npx nx lint:changed twenty-server # Lint changed files only (fastest)
npx nx lint:changed twenty-server --configuration=fix # Auto-fix changed files
npx nx run twenty-server:lint # Lint all files (slower)
npx nx run twenty-server:typecheck # Type checking
npx nx run twenty-server:test # Run unit tests
npx nx run twenty-server:test:integration:with-db-reset # Run integration tests
+15 -2
View File
@@ -12,7 +12,13 @@ alwaysApply: true
npx nx run twenty-front:build
npx nx run twenty-server:test
# Run target for all projects
# Lint changed files only (recommended - much faster!)
npx nx lint:changed twenty-front # Lint only files changed vs main
npx nx lint:changed twenty-server
npx nx lint:changed twenty-front --configuration=fix # Auto-fix changed files
npx nx lint:changed twenty-front --configuration=staged # Lint staged files
# Run target for all projects (slower)
npx nx run-many --target=build --all
npx nx run-many --target=test --projects=twenty-front,twenty-server
@@ -43,12 +49,19 @@ npx nx g @nx/react:component my-component
}
```
## Linting Strategy
For faster development, always prefer linting only changed files:
- Use `npx nx lint:changed <project>` to lint only files changed vs main branch
- Use `--configuration=fix` to auto-fix issues in changed files
- Use `--configuration=staged` to lint only staged files (useful for pre-commit hooks)
- Only use `npx nx lint <project>` when you need to lint the entire project
## Dependency Graph
```bash
# View project dependencies
npx nx graph
# Check what's affected by changes
# Check what's affected by changes (runs target on affected projects)
npx nx affected --target=test
npx nx affected --target=build --base=main
```
+10 -4
View File
@@ -36,10 +36,16 @@ When testing the UI end to end, click on "Continue with Email" and use the prefi
### Code Quality
```bash
# Linting
npx nx lint twenty-front # Frontend linting
npx nx lint twenty-server # Backend linting
npx nx lint twenty-front --fix # Auto-fix linting issues
# Linting (changed files only - fastest)
npx nx lint:changed twenty-front # Lint only changed files vs main
npx nx lint:changed twenty-server # Lint only changed files vs main
npx nx lint:changed twenty-front --configuration=fix # Auto-fix changed files
npx nx lint:changed twenty-front --configuration=staged # Lint staged files only
# Linting (full project)
npx nx lint twenty-front # Lint all files in frontend
npx nx lint twenty-server # Lint all files in backend
npx nx lint twenty-front --fix # Auto-fix all linting issues
# Type checking
npx nx typecheck twenty-front
+489 -4
View File
@@ -128,6 +128,375 @@ export default [
additionalHooks: 'useRecoilCallback',
},
],
// Lingui - detect untranslated strings
'lingui/no-unlocalized-strings': [
'error',
{
ignore: [
// Ignore strings which are a single "word" (no spaces) and don't start with uppercase
'^(?![A-Z])\\S+$',
// Ignore UPPERCASE literals (constants, env vars)
'^[A-Z0-9_-]+$',
// Ignore strings that look like code/technical (contain special chars)
'^[\\s]*$', // whitespace only
'.*[{}/<>].*', // contains code-like characters
'^\\d+(\\.\\d+)?(px|rem|em|%|vh|vw|s|ms)?$', // CSS units
'^[\\d.]+(px|rem|em|%|vh|vw|fr|s|ms)?(\\s+[\\d.]+(px|rem|em|%|vh|vw|fr|s|ms)?)*$', // CSS values like "200px 1fr 20px"
'^#[0-9a-fA-F]{3,8}$', // hex colors
'^rgba?\\(.*\\)$', // rgb/rgba colors
'^(auto|none|inherit|initial|unset|flex|grid|block|inline|inline-block|relative|absolute|fixed|sticky)$', // CSS keywords
'^color:.*$', // CSS color declarations
'^font-.*$', // CSS font declarations
'^\\d+$', // numbers only
'^https?:\\/\\/.*', // URLs
'^@.*', // @ mentions or decorators
'^\\/.*', // paths starting with /
'^[HhMmSsYyDdAaPp:.,\\s-]+$', // date format patterns (HH:mm, yyyy-MM-dd, etc.)
'^Arrow(Up|Down|Left|Right)$', // keyboard keys
'^(Enter|Escape|Tab|Space|Backspace|Delete)$', // keyboard keys
'^Text$', // clipboard data type
'^(allow-|sandbox)', // iframe sandbox values
'^Id$', // technical identifier suffix (e.g., fieldNameId)
'^(string|number|boolean|void|any|unknown|never|object)$', // TypeScript type keywords
'^(Dark|Light)$', // color schemes
'^translate\\(.*\\)$', // CSS transform strings
'^svg .*$', // CSS selectors
'^Icon[A-Z]\\w*$', // Icon names like IconDefault, IconTable, IconSettings
'^\\w*Icon$', // Icon names that end with Icon like FieldIcon
'^%c.*$', // Console format strings
// Common item IDs for selectable lists
'^(Group|CalendarView|CalendarDateField|Compact view)$',
'^(Layout|Visibility|Fields|Delete view|Copy link to view|Create custom view)$',
'^(GroupBy|Sort|HideEmptyGroups|HiddenGroups)$',
// HTTP headers and auth (technical, not user-facing)
'^Authorization$',
'^Bearer .*',
// Allow object keys that are technical identifiers
'^(topLeft|topRight|bottomLeft|bottomRight)$',
// Color schemes and CSS media queries
'^System$',
'^\\(prefers-color-scheme:',
// GraphQL query names (used in refetchQueries)
'^Get[A-Z]\\w*$',
// React Context names (technical identifiers)
'.*Context$',
// SVG paths (geometric coordinates, not translatable)
'^M[0-9 LML]+$',
'^[ML][0-9 ]+$',
// Database ordering values (technical, backend API)
'^(Asc|Desc)Nulls(First|Last)$',
// Calendar response status values (backend enum values, not user-facing)
'^(Yes|No|Maybe)$',
// Email validation error prefixes (combined with dynamic content)
'^Invalid email(s)?:',
// GraphQL type construction patterns
'.*FilterInput$',
'.*OrderByInput.*',
'^\\$filter.*',
'^\\$orderBy.*',
'^\\$after.*',
'^\\$before.*',
'^\\$first.*',
'^\\$last.*',
// Logger names (technical identifiers)
'^Twenty(-\\w+)?$',
// Cookie names and cookie string patterns
'^twenty_session_id$',
'^; domain=',
// Context names for createRequiredContext
'^[A-Z][a-zA-Z]+$',
// JSON-like filter patterns
'^%"type":',
'^%"objectNameSingular":',
],
ignoreNames: [
// HTML/React attributes that shouldn't be translated
{ regex: { pattern: 'className', flags: 'i' } },
{ regex: { pattern: 'styleName', flags: 'i' } },
{ regex: { pattern: 'testId', flags: 'i' } },
'data-testid',
'dataTestId',
'src',
'srcSet',
'href',
'target',
'rel',
'type',
'id',
'key',
'name',
'htmlFor',
'width',
'height',
'fill',
'stroke',
'viewBox',
'clipPath',
'd', // SVG path
'transform',
'displayName',
'defaultValue',
'to', // router links
'path',
'pathname',
'hash',
'componentInstanceId',
'hotkeyScope',
'dropdownId',
'recoilScopeId',
'modalId',
'dialogId',
'itemId',
'selectableItemIdArray',
'listenerId',
'focusId',
'color', // color prop values
'variant', // component variants
'size', // size prop values
'position', // position values
'align', // alignment values
'justify', // justification values
'direction', // direction values
'orientation', // orientation values
'status', // status values
'state', // state values
'mode', // mode values
'accent', // accent values
// CSS-related props
'gridAutoColumns',
'gridAutoRows',
'gridTemplateColumns',
'gridTemplateRows',
'gridColumn',
'gridRow',
'gap',
'margin',
'padding',
'border',
'borderRadius',
'boxShadow',
'flex',
'flexDirection',
'flexWrap',
'justifyContent',
'alignItems',
'alignContent',
'overflow',
'display',
'cursor',
'zIndex',
'opacity',
'fontWeight',
'fontSize',
'lineHeight',
'textAlign',
'textDecoration',
'whiteSpace',
'wordBreak',
'objectFit',
'backgroundSize',
'backgroundPosition',
'minWidth',
'maxWidth',
'minHeight',
'maxHeight',
'mobileGridAutoColumns',
'tabletGridAutoColumns',
// Styled components
'css',
'theme',
'animation',
'transition',
// GraphQL
'query',
'mutation',
'subscription',
'fragment',
'operationName',
'variables',
'__typename',
// Technical identifiers
'fieldName',
'columnName',
'objectNameSingular',
'objectNamePlural',
'metadataId',
'nameSingular',
'namePlural',
// Event types
'eventName',
'event',
'action',
'actionType',
// Icon names
'iconName',
{ regex: { pattern: '^Icon[A-Z]' } },
// UPPER_CASE names (constants)
{ regex: { pattern: '^[A-Z][A-Z0-9_]*$' } },
// Sort direction values (backend API)
'orderBy',
{ regex: { pattern: '^(Asc|Desc)(NullsFirst|NullsLast)?$' } },
// HTTP headers (technical, not user-facing)
'Authorization',
],
ignoreFunctions: [
// Console and logging
'console.*',
'*.log',
'*.warn',
'*.error',
'*.debug',
'*.info',
'*.trace',
'logDebug',
'formatTitle',
// Error handling (technical messages, not user-facing)
'Error',
'TypeError',
'RangeError',
'SyntaxError',
'throw',
'assertUnreachable',
'CustomError',
'parseInitialBlocknote',
// Testing
'describe',
'it',
'test',
'expect',
'jest.*',
'*.toBe',
'*.toEqual',
'*.toContain',
'*.toMatch',
'*.toThrow',
// React/Libraries internals
'require',
'import',
'styled',
'styled.*',
'css',
'keyframes',
'createGlobalStyle',
// Router
'useNavigate',
'navigate',
'useLocation',
'useParams',
// Date formatting (patterns are not translatable)
'format',
'formatDate',
'formatDateTime',
'formatTime',
'parseISO',
'parse',
// Navigation
'useNavigationSection',
// Recoil
'atom',
'atomFamily',
'selector',
'selectorFamily',
'useSetRecoilState',
'useRecoilState',
'useRecoilValue',
// GraphQL operations
'gql',
'useQuery',
'useMutation',
'useLazyQuery',
'useSubscription',
// Type checking and validation
'*.includes',
'*.indexOf',
'*.startsWith',
'*.endsWith',
'*.split',
'*.join',
'*.match',
'*.replace',
'*.test',
'Object.keys',
'Object.values',
'Object.entries',
'Array.isArray',
// DOM operations
'*.getElementById',
'*.getElementsByClassName',
'*.querySelector',
'*.querySelectorAll',
'*.getAttribute',
'*.setAttribute',
'*.addEventListener',
'*.removeEventListener',
'*.dispatchEvent',
'*.createElement',
// Storage
'localStorage.*',
'sessionStorage.*',
'searchParams.*',
'*.get',
'*.set',
'*.has',
'*.delete',
// Misc utilities
'cva',
'cn',
'clsx',
'classNames',
'track',
'*.postMessage',
'*.dispatch',
'*.commit',
// Event handlers (typically receive enum values, not user-facing text)
'onChange',
'onClick',
'onSelect',
'onSubmit',
'onFocus',
'onBlur',
'onKeyDown',
'onKeyUp',
'onMouseEnter',
'onMouseLeave',
// Logging functions (technical messages, not user-facing)
'logError',
'logDebug',
'logInfo',
'logWarn',
'loggerLink',
// Context creation (technical names)
'createRequiredContext',
// GraphQL refetch queries (technical identifiers)
'refetchQueries',
],
},
],
},
},
@@ -175,7 +544,7 @@ export default [
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports'
},
@@ -208,11 +577,115 @@ export default [
},
},
// Storybook files
// Storybook files and story-related files
{
files: ['*.stories.@(ts|tsx|js|jsx)'],
files: [
'**/*.stories.ts',
'**/*.stories.tsx',
'**/*.stories.js',
'**/*.stories.jsx',
'**/__stories__/**/*',
],
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'lingui/no-unlocalized-strings': 'off',
},
},
// Debug files - development only, not user-facing
{
files: [
'**/Debug*.tsx',
'**/*Debug*.tsx',
'**/*DebugDisplay*.tsx',
'**/*DebugHelper*.tsx',
'**/*DebugObserver*.tsx',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Testing utilities and mock data - not user-facing
{
files: [
'**/testing/**/*.tsx',
'**/testing/**/*.ts',
'**/__mocks__/**/*',
'**/*mock*.ts',
'**/*Mock*.ts',
'**/perf/**/*',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Constants files - technical values, not user-facing
{
files: [
'**/constants/**/*.ts',
'**/*.constants.ts',
'**/validation-schemas/**/*.ts',
'**/*Schema.ts',
'**/*-schema.ts',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Layout configuration files - titles are translated at consumption time
{
files: ['**/layouts/**/*.ts'],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Service files - contain technical strings (logger names, HTTP headers, etc.)
{
files: ['**/services/**/*.ts'],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// State files - contain technical default values
{
files: ['**/states/**/*.ts'],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Utility files - technical/developer-facing
{
files: [
'**/utils/**/*.ts',
'**/*Utils.ts',
'**/*-utils.ts',
'**/*Util.ts',
'**/*-util.ts',
'**/errors/**/*.ts',
'**/*Error.ts',
'**/*-error.ts',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Config and setup files - not user-facing
{
files: [
'**/*.config.ts',
'**/*.config.js',
'**/vite.config.ts',
'**/.storybook/**/*',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
@@ -250,7 +723,18 @@ export default [
// Test files
{
files: [
'*.test.@(ts|tsx|js|jsx)',
'**/*.test.ts',
'**/*.test.tsx',
'**/*.test.js',
'**/*.test.jsx',
'**/*.spec.ts',
'**/*.spec.tsx',
'**/*.spec.js',
'**/*.spec.jsx',
'**/__tests__/**/*.ts',
'**/__tests__/**/*.tsx',
'**/__mocks__/**/*.ts',
'**/__mocks__/**/*.tsx',
],
languageOptions: {
globals: {
@@ -266,6 +750,7 @@ export default [
},
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'lingui/no-unlocalized-strings': 'off',
},
},
+16
View File
@@ -59,6 +59,22 @@
"fix": {}
}
},
"lint:changed": {
"executor": "nx:run-commands",
"cache": false,
"options": {
"command": "git diff --name-only --diff-filter=d {args.base} | grep -E '\\.(ts|tsx|js|jsx)$' | grep '^packages/twenty-front/' | xargs -r npx eslint --config packages/twenty-front/eslint.config.mjs",
"base": "main"
},
"configurations": {
"fix": {
"command": "git diff --name-only --diff-filter=d {args.base} | grep -E '\\.(ts|tsx|js|jsx)$' | grep '^packages/twenty-front/' | xargs -r npx eslint --config packages/twenty-front/eslint.config.mjs --fix"
},
"staged": {
"command": "git diff --cached --name-only --diff-filter=d | grep -E '\\.(ts|tsx|js|jsx)$' | grep '^packages/twenty-front/' | xargs -r npx eslint --config packages/twenty-front/eslint.config.mjs"
}
}
},
"fmt": {
"options": {
"files": "src"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,5 @@
import { type ReactNode, useContext } from 'react';
import { t } from '@lingui/core/macro';
import { ActionDisplay } from '@/action-menu/actions/display/components/ActionDisplay';
import { ActionConfigContext } from '@/action-menu/contexts/ActionConfigContext';
@@ -25,7 +26,7 @@ export const ActionModal = ({
title,
subtitle,
onConfirmClick,
confirmButtonText = 'Confirm',
confirmButtonText = t`Confirm`,
confirmButtonAccent = 'danger',
isLoading = false,
closeSidePanelOnShowPageOptionsActionExecution,
@@ -46,8 +46,5 @@ export const ActionDisplay = ({
return <ActionDropdownItem action={action} onClick={onClick} to={to} />;
}
return assertUnreachable(
displayType,
`Unsupported display type: ${displayType}`,
);
return assertUnreachable(displayType, 'Unsupported display type');
};
@@ -17,6 +17,7 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useContext } from 'react';
import { IconLayoutSidebarRightExpand } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
@@ -32,6 +33,7 @@ const StyledDropdownMenuContainer = styled.div`
`;
export const RecordIndexActionMenuDropdown = () => {
const { t } = useLingui();
const { actions } = useContext(ActionMenuContext);
const recordIndexActions = actions.filter(
@@ -103,7 +105,7 @@ export const RecordIndexActionMenuDropdown = () => {
openCommandMenu();
}}
focused={selectedItemId === 'more-actions'}
text="More actions"
text={t`More actions`}
/>
</SelectableListItem>
</SelectableList>
@@ -1,5 +1,6 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { CalendarEventParticipantsResponseStatus } from '@/activities/calendar/components/CalendarEventParticipantsResponseStatus';
import { type CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
@@ -77,6 +78,7 @@ const StyledPropertyBox = styled(PropertyBox)`
export const CalendarEventDetails = ({
calendarEvent,
}: CalendarEventDetailsProps) => {
const { t } = useLingui();
const theme = useTheme();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
@@ -155,14 +157,14 @@ export const CalendarEventDetails = ({
variant={ChipVariant.Highlighted}
clickable={false}
leftComponent={<IconCalendarEvent size={theme.icon.size.md} />}
label="Event"
label={t`Event`}
/>
<StyledHeader>
<StyledTitle canceled={calendarEvent.isCanceled}>
{calendarEvent.title}
</StyledTitle>
<StyledCreatedAt>
Created{' '}
{t`Created`}{' '}
{beautifyPastDateRelativeToNow(
new Date(calendarEvent.externalCreatedAt),
)}
@@ -22,7 +22,7 @@ export const CalendarEventParticipantsResponseStatus = ({
}
});
const responseStatusOrder: ('Yes' | 'Maybe' | 'No')[] = [
const responseStatusOrder: Array<'Yes' | 'Maybe' | 'No'> = [
'Yes',
'Maybe',
'No',
@@ -1,5 +1,6 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { format } from 'date-fns';
import { useRecoilValue } from 'recoil';
@@ -93,7 +94,7 @@ export const CalendarEventRow = ({
const hasEnded = hasCalendarEventEnded(calendarEvent);
const startTimeLabel = calendarEvent.isFullDay
? 'All day'
? t`All day`
: format(startsAt, 'HH:mm');
const endTimeLabel = calendarEvent.isFullDay ? '' : format(endsAt, 'HH:mm');
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { format, getYear } from 'date-fns';
import { useRecoilValue } from 'recoil';
@@ -46,6 +47,7 @@ const StyledTitleContainer = styled.div`
`;
export const CalendarEventsCard = () => {
const { t } = useLingui();
const targetRecord = useTargetRecord();
const { localeCatalog } = useRecoilValue(dateLocaleState);
@@ -95,6 +97,8 @@ export const CalendarEventsCard = () => {
}
};
const objectName = targetRecord.targetObjectNameSingular;
if (firstQueryLoading) {
return <SkeletonLoader />;
}
@@ -109,11 +113,10 @@ export const CalendarEventsCard = () => {
<AnimatedPlaceholder type="noMatchRecord" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
No Events
{t`No Events`}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
No events have been scheduled with this{' '}
{targetRecord.targetObjectNameSingular} yet.
{t`No events have been scheduled with this ${objectName} yet.`}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
</AnimatedPlaceholderEmptyContainer>
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useInView } from 'react-intersection-observer';
type CustomResolverFetchMoreLoaderProps = {
@@ -34,7 +35,7 @@ export const CustomResolverFetchMoreLoader = ({
return (
<StyledContainer ref={tbodyRef}>
{loading && <StyledText>Loading more...</StyledText>}
{loading && <StyledText>{t`Loading more...`}</StyledText>}
</StyledContainer>
);
};
@@ -1,3 +1,4 @@
import { t } from '@lingui/core/macro';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
@@ -11,7 +12,7 @@ export const EmailLoader = ({ loadingText }: { loadingText?: string }) => (
<AnimatedPlaceholder type="loadingMessages" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
{loadingText || 'Loading emails'}
{loadingText || t`Loading emails`}
</AnimatedPlaceholderEmptyTitle>
<Loader />
</AnimatedPlaceholderEmptyTextContainer>
@@ -1,5 +1,6 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { AppTooltip, IconLock, TooltipDelay } from 'twenty-ui/display';
import { MessageChannelVisibility } from '~/generated/graphql';
@@ -31,6 +32,7 @@ type EmailThreadNotSharedProps = {
export const EmailThreadNotShared = ({
visibility,
}: EmailThreadNotSharedProps) => {
const { t } = useLingui();
const theme = useTheme();
const containerId = 'email-thread-not-shared';
const isCompact = visibility === MessageChannelVisibility.SUBJECT;
@@ -39,12 +41,12 @@ export const EmailThreadNotShared = ({
<>
<StyledContainer id={containerId} isCompact={isCompact}>
<IconLock size={theme.icon.size.sm} />
{'Not shared'}
{t`Not shared`}
</StyledContainer>
{visibility === MessageChannelVisibility.SUBJECT && (
<AppTooltip
anchorSelect={`#${containerId}`}
content="Only the subject is shared"
content={t`Only the subject is shared`}
delay={TooltipDelay.mediumDelay}
noArrow
place="bottom"
@@ -3,6 +3,7 @@ import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useLingui } from '@lingui/react/macro';
import {
IconDotsVertical,
IconDownload,
@@ -27,6 +28,7 @@ export const AttachmentDropdown = ({
attachmentId,
hasDownloadPermission,
}: AttachmentDropdownProps) => {
const { t } = useLingui();
const dropdownId = `${attachmentId}-attachment-dropdown`;
const { closeDropdown } = useCloseDropdown();
@@ -57,18 +59,18 @@ export const AttachmentDropdown = ({
<DropdownMenuItemsContainer>
{hasDownloadPermission && (
<MenuItem
text="Download"
text={t`Download`}
LeftIcon={IconDownload}
onClick={handleDownload}
/>
)}
<MenuItem
text="Rename"
text={t`Rename`}
LeftIcon={IconPencil}
onClick={handleRename}
/>
<MenuItem
text="Delete"
text={t`Delete`}
accent="danger"
LeftIcon={IconTrash}
onClick={handleDelete}
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { lazy, type ReactElement, Suspense, useState } from 'react';
import { createPortal } from 'react-dom';
@@ -240,7 +241,7 @@ export const AttachmentList = ({
fallback={
<StyledLoadingContainer>
<StyledLoadingText>
Loading document viewer...
{t`Loading document viewer...`}
</StyledLoadingText>
</StyledLoadingContainer>
}
@@ -6,7 +6,7 @@ import DocViewer, { DocViewerRenderers } from '@cyntler/react-doc-viewer';
import '@cyntler/react-doc-viewer/dist/index.css';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Trans } from '@lingui/react/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconDownload } from 'twenty-ui/display';
@@ -99,6 +99,7 @@ export const DocumentViewer = ({
documentName,
documentUrl,
}: DocumentViewerProps) => {
const { t } = useLingui();
const theme = useTheme();
const [csvPreview, setCsvPreview] = useState<string | undefined>(undefined);
@@ -141,7 +142,7 @@ export const DocumentViewer = ({
</StyledMessage>
<Button
Icon={IconDownload}
title="Download File"
title={t`Download File`}
onClick={() => downloadFile(documentUrl, documentName)}
variant="secondary"
/>
@@ -1,5 +1,6 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useDropzone } from 'react-dropzone';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
@@ -47,6 +48,7 @@ export const DropZone = ({
setIsDraggingFile,
onUploadFiles,
}: DropZoneProps) => {
const { t } = useLingui();
const theme = useTheme();
const { maxFileSize } = useSpreadsheetImportInternal();
@@ -85,9 +87,9 @@ export const DropZone = ({
stroke={theme.icon.stroke.sm}
size={theme.icon.size.lg}
/>
<StyledUploadDragTitle>Upload files</StyledUploadDragTitle>
<StyledUploadDragTitle>{t`Upload files`}</StyledUploadDragTitle>
<StyledUploadDragSubTitle>
Drag and Drop Here
{t`Drag and Drop Here`}
</StyledUploadDragSubTitle>
</>
)}
@@ -1,4 +1,5 @@
import { useContext } from 'react';
import { t } from '@lingui/core/macro';
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
@@ -95,7 +96,7 @@ export const ActivityTargetsInlineCell = ({
}}
/>
),
label: 'Relations',
label: t`Relations`,
displayModeContent: (
<ActivityTargetChips
activityTargetObjectRecords={activityTargetObjectRecords}
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { useActivityTargetsComponentInstanceId } from '@/activities/inline-cell/hooks/useActivityTargetsComponentInstanceId';
@@ -88,7 +89,7 @@ export const NoteTile = ({
})
}
>
<StyledNoteTitle>{note.title ?? 'Task Title'}</StyledNoteTitle>
<StyledNoteTitle>{note.title ?? t`Task Title`}</StyledNoteTitle>
<StyledCardContent>{body}</StyledCardContent>
</StyledCardDetailsContainer>
<StyledFooter>
@@ -68,10 +68,10 @@ export const NotesCard = () => {
<AnimatedPlaceholder type="noNote" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
No notes
{t`No notes`}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
There are no associated notes with this record.
{t`There are no associated notes with this record.`}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
{hasObjectUpdatePermissions && (
@@ -78,10 +78,10 @@ export const TaskGroups = ({ targetableObject }: TaskGroupsProps) => {
<AnimatedPlaceholder type="noTask" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
Mission accomplished!
{t`Mission accomplished!`}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
All tasks addressed. Maintain the momentum.
{t`All tasks addressed. Maintain the momentum.`}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
{hasObjectUpdatePermissions && (
@@ -1,5 +1,6 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
@@ -114,7 +115,7 @@ export const TaskRow = ({ task }: { task: Task }) => {
/>
</StyledCheckboxContainer>
<StyledTaskTitle completed={task.status === 'DONE'}>
{task.title || <StyledPlaceholder>Task title</StyledPlaceholder>}
{task.title || <StyledPlaceholder>{t`Task title`}</StyledPlaceholder>}
</StyledTaskTitle>
<StyledTaskBody>
<OverflowingTextWithTooltip text={body} />
@@ -72,10 +72,10 @@ export const TimelineCard = () => {
<AnimatedPlaceholder type="emptyTimeline" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
No activity yet
{t`No activity yet`}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
There is no activity associated with this record.
{t`There is no activity associated with this record.`}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
</EmptyContainer>
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import {
type EventRowDynamicComponentProps,
@@ -91,7 +92,7 @@ export const EventRowActivity = ({
return event.linkedRecordCachedName;
}
return 'Untitled';
return t`Untitled`;
};
const activityTitle = computeActivityTitle();
@@ -103,7 +104,7 @@ export const EventRowActivity = ({
<StyledRow>
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
<StyledEventRowItemAction>
{`${eventAction} a related ${eventObject}`}
{t`${eventAction} a related ${eventObject}`}
</StyledEventRowItemAction>
<StyledLinkedActivity
onClick={() =>
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isUndefined } from '@sniptt/guards';
import { CalendarEventNotSharedContent } from '@/activities/calendar/components/CalendarEventNotSharedContent';
@@ -138,14 +139,14 @@ export const EventCardCalendarEvent = ({
);
if (shouldHandleNotFound) {
return <div>Calendar event not found</div>;
return <div>{t`Calendar event not found`}</div>;
}
return <div>Error loading calendar event</div>;
return <div>{t`Error loading calendar event`}</div>;
}
if (loading || isUndefined(calendarEvent)) {
return <div>Loading...</div>;
return <div>{t`Loading...`}</div>;
}
const startsAtDate = calendarEvent?.startsAt;
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { EventCardCalendarEvent } from '@/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent';
@@ -30,6 +31,7 @@ export const EventRowCalendarEvent = ({
authorFullName,
labelIdentifierValue,
}: EventRowCalendarEventProps) => {
const { t } = useLingui();
const [, eventAction] = event.name.split('.');
const [isOpen, setIsOpen] = useState(false);
@@ -42,7 +44,7 @@ export const EventRowCalendarEvent = ({
<StyledRowContainer>
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
<StyledEventRowItemAction>
linked a calendar event with {labelIdentifierValue}
{t`linked a calendar event with ${labelIdentifierValue}`}
</StyledEventRowItemAction>
<EventCardToggleButton isOpen={isOpen} setIsOpen={setIsOpen} />
</StyledRowContainer>
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { EventCard } from '@/activities/timeline-activities/rows/components/EventCard';
@@ -54,6 +55,7 @@ export const EventRowMainObjectUpdated = ({
mainObjectMetadataItem,
createdAt,
}: EventRowMainObjectUpdatedProps) => {
const { t } = useLingui();
const diff: Record<string, { before: any; after: any }> =
event.properties?.diff;
@@ -70,12 +72,15 @@ export const EventRowMainObjectUpdated = ({
throw new Error('Cannot render update description without changes');
}
const fieldCount = diffEntries.length;
const recordLabel = labelIdentifierValue;
return (
<StyledEventRowMainObjectUpdatedContainer>
<StyledRowContainer>
<StyledRow>
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
updated
{t`updated`}
{diffEntries.length === 1 && (
<EventFieldDiffContainer
mainObjectMetadataItem={mainObjectMetadataItem}
@@ -87,9 +92,7 @@ export const EventRowMainObjectUpdated = ({
)}
{diffEntries.length > 1 && (
<>
<span>
{diffEntries.length} fields on {labelIdentifierValue}
</span>
<span>{t`${fieldCount} fields on ${recordLabel}`}</span>
<EventCardToggleButton isOpen={isOpen} setIsOpen={setIsOpen} />
</>
)}
@@ -7,7 +7,7 @@ import { useOpenEmailThreadInCommandMenu } from '@/command-menu/hooks/useOpenEma
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { Trans } from '@lingui/react/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
@@ -59,6 +59,7 @@ export const EventCardMessage = ({
messageId: string;
authorFullName: string;
}) => {
const { t } = useLingui();
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const { openEmailThreadInCommandMenu } = useOpenEmailThreadInCommandMenu();
@@ -142,7 +143,7 @@ export const EventCardMessage = ({
{message.subject !==
FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
? message.subject
: `Subject not shared`}
: t`Subject not shared`}
</StyledEmailTitle>
<StyledEmailParticipants>
<OverflowingTextWithTooltip text={messageParticipantHandles} />
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { EventCard } from '@/activities/timeline-activities/rows/components/EventCard';
@@ -42,7 +43,7 @@ export const EventRowMessage = ({
<StyledRowContainer>
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
<StyledEventRowItemAction>
linked an email with
{t`linked an email with`}
</StyledEventRowItemAction>
<StyledEventRowItemColumn>
{labelIdentifierValue}
@@ -5,6 +5,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { type Editor } from '@tiptap/react';
import { useId } from 'react';
import { IconPilcrow } from 'twenty-ui/display';
@@ -50,7 +51,7 @@ export const TurnIntoBlockDropdown = ({
const options = useTurnIntoBlockOptions(editor);
const activeItem = options.find((option) => option.isActive());
const { icon: ActiveIcon = IconPilcrow, title: activeTitle = 'Paragraph' } =
const { icon: ActiveIcon = IconPilcrow, title: activeTitle = t`Paragraph` } =
activeItem ?? {};
return (
@@ -1,3 +1,4 @@
import { useLingui } from '@lingui/react/macro';
import { type Editor, useEditorState } from '@tiptap/react';
import {
type IconComponent,
@@ -17,12 +18,14 @@ export type TurnIntoBlockOptions = {
};
export const useTurnIntoBlockOptions = (editor: Editor) => {
const { t } = useLingui();
return useEditorState({
editor,
selector: ({ editor }): TurnIntoBlockOptions[] => [
{
id: 'paragraph',
title: 'Paragraph',
title: t`Paragraph`,
icon: IconPilcrow,
onClick: () => {
return editor.chain().focus().setParagraph().run();
@@ -36,7 +39,7 @@ export const useTurnIntoBlockOptions = (editor: Editor) => {
},
{
id: 'heading1',
title: 'Heading 1',
title: t`Heading 1`,
icon: IconH1,
onClick: () => {
return editor.chain().focus().setHeading({ level: 1 }).run();
@@ -50,7 +53,7 @@ export const useTurnIntoBlockOptions = (editor: Editor) => {
},
{
id: 'heading2',
title: 'Heading 2',
title: t`Heading 2`,
icon: IconH2,
onClick: () => {
return editor.chain().focus().setHeading({ level: 2 }).run();
@@ -64,7 +67,7 @@ export const useTurnIntoBlockOptions = (editor: Editor) => {
},
{
id: 'heading3',
title: 'Heading 3',
title: t`Heading 3`,
icon: IconH3,
onClick: () => {
return editor.chain().focus().setHeading({ level: 3 }).run();
@@ -51,16 +51,18 @@ export const useUploadWorkflowFile = () => {
createdAt: uploadedFile.createdAt,
};
const fileName = file.name;
enqueueSuccessSnackBar({
message: `File "${file.name}" uploaded successfully`,
message: t`File "${fileName}" uploaded successfully`,
});
return workflowFile;
} catch (error) {
logError(`Failed to upload workflow file "${file.name}": ${error}`);
const fileNameForError = file.name;
enqueueErrorSnackBar({
message: `Failed to upload "${file.name}"`,
message: t`Failed to upload "${fileNameForError}"`,
});
return null;
@@ -1,5 +1,6 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { AvatarChip, ChipVariant, LinkChip } from 'twenty-ui/components';
@@ -29,6 +30,7 @@ export const RecordLink = ({
return (
<LinkChip
label={displayName}
emptyLabel={t`Untitled`}
to={linkToShowPage}
variant={ChipVariant.Highlighted}
leftComponent={
@@ -152,45 +152,51 @@ type TimingTabProps = {
};
const TimingTab = ({ debug }: TimingTabProps) => {
const { t } = useLingui();
const totalTime =
debug.agentExecutionStartTimeMs !== undefined
? `${debug.agentExecutionStartTimeMs + (debug.agentExecutionTimeMs || 0)}ms`
: undefined;
const totalCost =
debug.totalCostInCredits !== undefined
? formatNumber(debug.totalCostInCredits)
: undefined;
return (
<StyledTimingSection>
<TimingRow
label="Routing decision"
label={t`Routing decision`}
value={debug.routingTimeMs && `${debug.routingTimeMs}ms`}
/>
<TimingRow
label="Context building (routing)"
label={t`Context building (routing)`}
value={debug.contextBuildTimeMs && `${debug.contextBuildTimeMs}ms`}
/>
<TimingRow
label="Context building (agent)"
label={t`Context building (agent)`}
value={
debug.agentContextBuildTimeMs && `${debug.agentContextBuildTimeMs}ms`
}
/>
<TimingRow
label="Tool generation"
label={t`Tool generation`}
value={debug.toolGenerationTimeMs && `${debug.toolGenerationTimeMs}ms`}
/>
<TimingRow
label="AI request prep"
label={t`AI request prep`}
value={debug.aiRequestPrepTimeMs && `${debug.aiRequestPrepTimeMs}ms`}
/>
<TimingRow
label="Agent execution"
label={t`Agent execution`}
value={debug.agentExecutionTimeMs && `${debug.agentExecutionTimeMs}ms`}
/>
<TimingRow label="Total time" value={totalTime} />
<TimingRow label="Available tools" value={debug.toolCount} />
<TimingRow label="Tool calls made" value={debug.toolCallCount} />
<TimingRow label="Context records" value={debug.contextRecordCount} />
<TimingRow label={t`Total time`} value={totalTime} />
<TimingRow label={t`Available tools`} value={debug.toolCount} />
<TimingRow label={t`Tool calls made`} value={debug.toolCallCount} />
<TimingRow label={t`Context records`} value={debug.contextRecordCount} />
<TimingRow
label="Context size"
label={t`Context size`}
value={
debug.contextSizeBytes !== undefined
? formatBytes(debug.contextSizeBytes)
@@ -198,7 +204,7 @@ const TimingTab = ({ debug }: TimingTabProps) => {
}
/>
<TimingRow
label="Routing tokens"
label={t`Routing tokens`}
value={
debug.routingTotalTokens !== undefined
? formatTokenBreakdown(
@@ -210,7 +216,7 @@ const TimingTab = ({ debug }: TimingTabProps) => {
}
/>
<TimingRow
label="Agent tokens"
label={t`Agent tokens`}
value={
debug.agentTotalTokens !== undefined
? formatTokenBreakdown(
@@ -222,12 +228,8 @@ const TimingTab = ({ debug }: TimingTabProps) => {
}
/>
<TimingRow
label="Total cost"
value={
debug.totalCostInCredits !== undefined
? `${formatNumber(debug.totalCostInCredits)} credits`
: undefined
}
label={t`Total cost`}
value={totalCost !== undefined ? t`${totalCost} credits` : undefined}
/>
</StyledTimingSection>
);
@@ -279,7 +281,7 @@ const ContextTab = ({ debug, copyToClipboard }: ContextTabProps) => {
if (!debug.context) {
return (
<StyledTimingLabel>
No context was provided for this request
{t`No context was provided for this request`}
</StyledTimingLabel>
);
}
@@ -302,9 +304,10 @@ const ContextTab = ({ debug, copyToClipboard }: ContextTabProps) => {
</StyledJsonTreeContainer>
);
} catch {
const contextValue = debug.context;
return (
<StyledTimingLabel>
Failed to parse context: {debug.context}
{t`Failed to parse context: ${contextValue}`}
</StyledTimingLabel>
);
}
@@ -315,6 +318,7 @@ type RoutingDebugDisplayProps = {
};
export const RoutingDebugDisplay = ({ debug }: RoutingDebugDisplayProps) => {
const { t } = useLingui();
const theme = useTheme();
const { copyToClipboard } = useCopyToClipboard();
const [isExpanded, setIsExpanded] = useState(false);
@@ -323,7 +327,7 @@ export const RoutingDebugDisplay = ({ debug }: RoutingDebugDisplayProps) => {
return (
<StyledContainer>
<StyledToggleButton onClick={() => setIsExpanded(!isExpanded)}>
<StyledTimingLabel>Debug Info</StyledTimingLabel>
<StyledTimingLabel>{t`Debug Info`}</StyledTimingLabel>
{isExpanded ? (
<IconChevronUp size={theme.icon.size.sm} />
) : (
@@ -338,20 +342,20 @@ export const RoutingDebugDisplay = ({ debug }: RoutingDebugDisplayProps) => {
isActive={activeTab === 'timing'}
onClick={() => setActiveTab('timing')}
>
Timing
{t`Timing`}
</StyledTab>
<StyledTab
isActive={activeTab === 'details'}
onClick={() => setActiveTab('details')}
>
Details
{t`Details`}
</StyledTab>
{debug.context && (
<StyledTab
isActive={activeTab === 'context'}
onClick={() => setActiveTab('context')}
>
Context
{t`Context`}
</StyledTab>
)}
</StyledTabContainer>
@@ -190,7 +190,7 @@ export const ToolStepRenderer = ({ toolPart }: { toolPart: ToolUIPart }) => {
}
const displayMessage = hasError
? 'Tool execution failed'
? t`Tool execution failed`
: output &&
typeof output === 'object' &&
'message' in output &&
@@ -240,13 +240,13 @@ export const ToolStepRenderer = ({ toolPart }: { toolPart: ToolUIPart }) => {
isActive={activeTab === 'output'}
onClick={() => setActiveTab('output')}
>
Output
{t`Output`}
</StyledTab>
<StyledTab
isActive={activeTab === 'input'}
onClick={() => setActiveTab('input')}
>
Input
{t`Input`}
</StyledTab>
</StyledTabContainer>
@@ -1,5 +1,6 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { ProgressBar } from 'twenty-ui/feedback';
@@ -103,6 +104,7 @@ const formatTokenCount = (count: number): string => {
};
export const AIChatContextUsageButton = () => {
const { t } = useLingui();
const theme = useTheme();
const [isHovered, setIsHovered] = useState(false);
const agentChatUsage = useRecoilValue(agentChatUsageState);
@@ -125,6 +127,8 @@ export const AIChatContextUsageButton = () => {
const formattedPercentage = percentage.toFixed(1);
const totalCredits =
agentChatUsage.inputCredits + agentChatUsage.outputCredits;
const inputCredits = agentChatUsage.inputCredits.toLocaleString();
const outputCredits = agentChatUsage.outputCredits.toLocaleString();
return (
<StyledContainer
@@ -162,23 +166,23 @@ export const AIChatContextUsageButton = () => {
<StyledBody>
<StyledRow>
<StyledLabel>Input</StyledLabel>
<StyledLabel>{t`Input`}</StyledLabel>
<StyledValue>
{formatTokenCount(agentChatUsage.inputTokens)} {' '}
{agentChatUsage.inputCredits.toLocaleString()} credits
{t`${inputCredits} credits`}
</StyledValue>
</StyledRow>
<StyledRow>
<StyledLabel>Output</StyledLabel>
<StyledLabel>{t`Output`}</StyledLabel>
<StyledValue>
{formatTokenCount(agentChatUsage.outputTokens)} {' '}
{agentChatUsage.outputCredits.toLocaleString()} credits
{t`${outputCredits} credits`}
</StyledValue>
</StyledRow>
</StyledBody>
<StyledFooter>
<StyledLabel>Total credits</StyledLabel>
<StyledLabel>{t`Total credits`}</StyledLabel>
<StyledPercentage>{totalCredits.toLocaleString()}</StyledPercentage>
</StyledFooter>
</StyledHoverCard>

Some files were not shown because too many files have changed in this diff Show More