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:
@@ -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>
|
||||
|
||||
|
||||
+9
-5
@@ -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>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getFileType } from '@/activities/files/utils/getFileType';
|
||||
import { useFileCategoryColors } from '@/file/hooks/useFileCategoryColors';
|
||||
import { IconMapping } from '@/file/utils/fileIconMappings';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type FileUIPart } from 'ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AvatarChip, Chip, ChipVariant, LinkChip } from 'twenty-ui/components';
|
||||
@@ -23,7 +24,7 @@ export const AgentChatFilePreview = ({
|
||||
useFileCategoryColors();
|
||||
|
||||
const fileName =
|
||||
file instanceof File ? file.name : (file.filename ?? 'Unknown file');
|
||||
file instanceof File ? file.name : (file.filename ?? t`Unknown file`);
|
||||
|
||||
const fileUrl = file instanceof File ? undefined : file.url;
|
||||
|
||||
@@ -54,6 +55,7 @@ export const AgentChatFilePreview = ({
|
||||
return (
|
||||
<LinkChip
|
||||
label={fileName}
|
||||
emptyLabel={t`Untitled`}
|
||||
variant={ChipVariant.Static}
|
||||
to={fileUrl}
|
||||
target="_blank"
|
||||
@@ -66,6 +68,7 @@ export const AgentChatFilePreview = ({
|
||||
return (
|
||||
<Chip
|
||||
label={fileName}
|
||||
emptyLabel={t`Untitled`}
|
||||
variant={ChipVariant.Static}
|
||||
clickable={false}
|
||||
leftComponent={leftComponent}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useGetBrowsingContext = () => {
|
||||
@@ -103,7 +104,7 @@ export const useGetBrowsingContext = () => {
|
||||
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(field) => field.id === filter.fieldMetadataId,
|
||||
);
|
||||
const fieldLabel = fieldMetadataItem?.label ?? 'Unknown field';
|
||||
const fieldLabel = fieldMetadataItem?.label ?? t`Unknown field`;
|
||||
|
||||
return `${fieldLabel} ${filter.operand} "${filter.displayValue}"`;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user