feat(localization): update and enhance localization files for multiple languages

- Updated localization files for various languages, including English, German, Spanish, Portuguese, and Chinese, to ensure consistency and accuracy across the application.
- Added new keys and updated existing ones to support recent UI changes and features, particularly in project views, task lists, and admin center settings.
- Enhanced the structure of localization files to improve maintainability and facilitate future updates.
- Implemented performance optimizations in the frontend components to better handle localization data.
This commit is contained in:
chamiakJ
2025-07-28 07:19:55 +05:30
parent fc88c14b94
commit 591d348ae5
315 changed files with 9956 additions and 6116 deletions

View File

@@ -117,16 +117,16 @@ const ProjectList: React.FC = () => {
statuses: requestParams.statuses,
categories: requestParams.categories,
};
// Create a stable key for comparison
const paramsKey = JSON.stringify(params);
// Only return new params if they've actually changed
if (paramsKey !== lastQueryParamsRef.current) {
lastQueryParamsRef.current = paramsKey;
return params;
}
// Return the previous params to maintain reference stability
return JSON.parse(lastQueryParamsRef.current || '{}');
}, [requestParams]);
@@ -147,8 +147,6 @@ const ProjectList: React.FC = () => {
skip: viewMode === ProjectViewType.GROUP,
});
// Add performance monitoring
const performanceRef = useRef<{ startTime: number | null }>({ startTime: null });
@@ -163,15 +161,17 @@ const ProjectList: React.FC = () => {
// Optimized debounced search with better cleanup and performance
const debouncedSearch = useCallback(
debounce((searchTerm: string) => {
debounce((searchTerm: string) => {
// Clear any error messages when starting a new search
setErrorMessage(null);
if (viewMode === ProjectViewType.LIST) {
dispatch(setRequestParams({
search: searchTerm,
index: 1 // Reset to first page on search
}));
dispatch(
setRequestParams({
search: searchTerm,
index: 1, // Reset to first page on search
})
);
} else if (viewMode === ProjectViewType.GROUP) {
const newGroupedParams = {
...groupedRequestParams,
@@ -179,12 +179,12 @@ const ProjectList: React.FC = () => {
index: 1,
};
dispatch(setGroupedRequestParams(newGroupedParams));
// Add timeout for grouped search to prevent rapid API calls
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
searchTimeoutRef.current = setTimeout(() => {
dispatch(fetchGroupedProjects(newGroupedParams));
}, 100);
@@ -208,21 +208,21 @@ const ProjectList: React.FC = () => {
const handleSearchChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newSearchValue = e.target.value;
// Validate input length to prevent excessive API calls
if (newSearchValue.length > 100) {
return; // Prevent extremely long search terms
}
setSearchValue(newSearchValue);
trackMixpanelEvent(evt_projects_search);
// Clear any existing timeout
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
searchTimeoutRef.current = null;
}
// Debounce the actual search execution
debouncedSearch(newSearchValue);
},
@@ -381,7 +381,7 @@ const ProjectList: React.FC = () => {
trackMixpanelEvent(evt_projects_refresh_click);
setIsLoading(true);
setErrorMessage(null);
if (viewMode === ProjectViewType.LIST) {
await refetchProjects();
} else if (viewMode === ProjectViewType.GROUP && groupBy) {
@@ -398,7 +398,7 @@ const ProjectList: React.FC = () => {
const emptyContent = useMemo(() => {
if (errorMessage) {
return (
<Empty
<Empty
description={
<div>
<p>{errorMessage}</p>
@@ -406,7 +406,7 @@ const ProjectList: React.FC = () => {
Retry
</Button>
</div>
}
}
/>
);
}
@@ -455,7 +455,11 @@ const ProjectList: React.FC = () => {
const newOrder = Array.isArray(sorter) ? sorter[0].order : sorter.order;
const newField = (Array.isArray(sorter) ? sorter[0].columnKey : sorter.columnKey) as string;
if (newOrder && newField && (newOrder !== requestParams.order || newField !== requestParams.field)) {
if (
newOrder &&
newField &&
(newOrder !== requestParams.order || newField !== requestParams.field)
) {
updates.order = newOrder ?? 'ascend';
updates.field = newField ?? 'name';
setSortingValues(updates.field, updates.order);
@@ -463,7 +467,10 @@ const ProjectList: React.FC = () => {
}
// Handle pagination
if (newPagination.current !== requestParams.index || newPagination.pageSize !== requestParams.size) {
if (
newPagination.current !== requestParams.index ||
newPagination.pageSize !== requestParams.size
) {
updates.index = newPagination.current || 1;
updates.size = newPagination.pageSize || DEFAULT_PAGE_SIZE;
hasChanges = true;
@@ -494,9 +501,12 @@ const ProjectList: React.FC = () => {
index: newPagination.current || 1,
size: newPagination.pageSize || DEFAULT_PAGE_SIZE,
};
// Only update if values actually changed
if (newParams.index !== groupedRequestParams.index || newParams.size !== groupedRequestParams.size) {
if (
newParams.index !== groupedRequestParams.index ||
newParams.size !== groupedRequestParams.size
) {
dispatch(setGroupedRequestParams(newParams));
}
},
@@ -511,19 +521,23 @@ const ProjectList: React.FC = () => {
// Batch updates to reduce re-renders
const baseUpdates = { filter: newFilterIndex, index: 1 };
dispatch(setRequestParams(baseUpdates));
dispatch(setGroupedRequestParams({
...groupedRequestParams,
...baseUpdates,
}));
dispatch(
setGroupedRequestParams({
...groupedRequestParams,
...baseUpdates,
})
);
// Only trigger data fetch for group view (list view will auto-refetch via query)
if (viewMode === ProjectViewType.GROUP && groupBy) {
dispatch(fetchGroupedProjects({
...groupedRequestParams,
...baseUpdates,
}));
dispatch(
fetchGroupedProjects({
...groupedRequestParams,
...baseUpdates,
})
);
}
},
[filters, setFilterIndex, dispatch, groupedRequestParams, viewMode, groupBy]
@@ -697,26 +711,28 @@ const ProjectList: React.FC = () => {
useEffect(() => {
const filterIndex = getFilterIndex();
const initialParams = { filter: filterIndex };
// Only update if values are different
if (requestParams.filter !== filterIndex) {
dispatch(setRequestParams(initialParams));
}
// Initialize grouped request params with proper groupBy value
if (!groupedRequestParams.groupBy) {
const initialGroupBy = groupBy || ProjectGroupBy.CATEGORY;
dispatch(setGroupedRequestParams({
filter: filterIndex,
index: 1,
size: DEFAULT_PAGE_SIZE,
field: 'name',
order: 'ascend',
search: '',
groupBy: initialGroupBy,
statuses: null,
categories: null,
}));
dispatch(
setGroupedRequestParams({
filter: filterIndex,
index: 1,
size: DEFAULT_PAGE_SIZE,
field: 'name',
order: 'ascend',
search: '',
groupBy: initialGroupBy,
statuses: null,
categories: null,
})
);
}
}, [dispatch, getFilterIndex, groupBy]); // Add groupBy to deps to handle initial state
@@ -732,8 +748,9 @@ const ProjectList: React.FC = () => {
// 2. We have a groupBy value (either from Redux or default)
if (viewMode === ProjectViewType.GROUP && groupBy) {
// Always ensure grouped request params are properly set with current groupBy
const shouldUpdateParams = !groupedRequestParams.groupBy || groupedRequestParams.groupBy !== groupBy;
const shouldUpdateParams =
!groupedRequestParams.groupBy || groupedRequestParams.groupBy !== groupBy;
if (shouldUpdateParams) {
const updatedParams = {
...groupedRequestParams,
@@ -757,7 +774,7 @@ const ProjectList: React.FC = () => {
useEffect(() => {
const loadLookups = async () => {
const promises = [];
if (projectStatuses.length === 0) {
promises.push(dispatch(fetchProjectStatuses()));
}
@@ -767,19 +784,20 @@ const ProjectList: React.FC = () => {
if (projectHealths.length === 0) {
promises.push(dispatch(fetchProjectHealth()));
}
// Load all lookups in parallel
if (promises.length > 0) {
await Promise.allSettled(promises);
}
};
loadLookups();
}, [dispatch]); // Remove length dependencies to avoid re-runs
// Sync search input value with Redux state
useEffect(() => {
const currentSearch = viewMode === ProjectViewType.LIST ? requestParams.search : groupedRequestParams.search;
const currentSearch =
viewMode === ProjectViewType.LIST ? requestParams.search : groupedRequestParams.search;
if (searchValue !== (currentSearch || '')) {
setSearchValue(currentSearch || '');
}
@@ -788,13 +806,13 @@ const ProjectList: React.FC = () => {
// Optimize loading state management
useEffect(() => {
let newLoadingState = false;
if (viewMode === ProjectViewType.LIST) {
newLoadingState = loadingProjects || isFetchingProjects;
} else {
newLoadingState = groupedProjects.loading;
}
// Only update if loading state actually changed
if (isLoading !== newLoadingState) {
setIsLoading(newLoadingState);

View File

@@ -1,5 +1,13 @@
import React, { useEffect, useState } from 'react';
import { Avatar, Checkbox, DatePicker, Flex, Tag, Tooltip, Typography } from '@/shared/antd-imports';
import {
Avatar,
Checkbox,
DatePicker,
Flex,
Tag,
Tooltip,
Typography,
} from '@/shared/antd-imports';
import { useAppSelector } from '@/hooks/useAppSelector';
import { columnList } from '@/pages/projects/project-view-1/taskList/taskListTable/columns/columnList';

View File

@@ -1,7 +1,21 @@
import { Badge, Button, Collapse, ConfigProvider, Dropdown, Flex, Input, Typography } from '@/shared/antd-imports';
import {
Badge,
Button,
Collapse,
ConfigProvider,
Dropdown,
Flex,
Input,
Typography,
} from '@/shared/antd-imports';
import { useState } from 'react';
import { TaskType } from '@/types/task.types';
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@/shared/antd-imports';
import {
EditOutlined,
EllipsisOutlined,
RetweetOutlined,
RightOutlined,
} from '@/shared/antd-imports';
import { colors } from '@/styles/colors';
import './task-list-table-wrapper.css';
import TaskListTable from '../table-v2';

View File

@@ -1,4 +1,8 @@
import { CaretDownFilled, SortAscendingOutlined, SortDescendingOutlined } from '@/shared/antd-imports';
import {
CaretDownFilled,
SortAscendingOutlined,
SortDescendingOutlined,
} from '@/shared/antd-imports';
import { Badge, Button, Card, Checkbox, Dropdown, List, Space } from '@/shared/antd-imports';
import React, { useState } from 'react';
import { colors } from '../../../../../styles/colors';

View File

@@ -1,6 +1,20 @@
import { Badge, Button, Collapse, ConfigProvider, Dropdown, Flex, Input, Typography } from '@/shared/antd-imports';
import {
Badge,
Button,
Collapse,
ConfigProvider,
Dropdown,
Flex,
Input,
Typography,
} from '@/shared/antd-imports';
import { useState } from 'react';
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@/shared/antd-imports';
import {
EditOutlined,
EllipsisOutlined,
RetweetOutlined,
RightOutlined,
} from '@/shared/antd-imports';
import { colors } from '../../../../../styles/colors';
import './taskListTableWrapper.css';
import TaskListTable from './TaskListTable';

View File

@@ -1,6 +1,14 @@
import { useCallback, useState } from 'react';
import dayjs, { Dayjs } from 'dayjs';
import { Col, Flex, Typography, List, Dropdown, MenuProps, Popconfirm } from '@/shared/antd-imports';
import {
Col,
Flex,
Typography,
List,
Dropdown,
MenuProps,
Popconfirm,
} from '@/shared/antd-imports';
import {
UserAddOutlined,
DeleteOutlined,

View File

@@ -184,19 +184,19 @@ const ProjectViewHeader = memo(() => {
const handleSettingsClick = useCallback(() => {
if (selectedProject?.id) {
console.log('Opening project drawer from project view for project:', selectedProject.id);
// Set project ID first
dispatch(setProjectId(selectedProject.id));
// Then fetch project data
dispatch(fetchProjectData(selectedProject.id))
.unwrap()
.then((projectData) => {
.then(projectData => {
console.log('Project data fetched successfully from project view:', projectData);
// Open drawer after data is fetched
dispatch(toggleProjectDrawer());
})
.catch((error) => {
.catch(error => {
console.error('Failed to fetch project data from project view:', error);
// Still open drawer even if fetch fails, so user can see error state
dispatch(toggleProjectDrawer());
@@ -270,7 +270,11 @@ const ProjectViewHeader = memo(() => {
{
key: 'import',
label: (
<div style={{ width: '100%', margin: 0, padding: 0 }} onClick={handleImportTaskTemplate} title={t('importTaskTooltip')}>
<div
style={{ width: '100%', margin: 0, padding: 0 }}
onClick={handleImportTaskTemplate}
title={t('importTaskTooltip')}
>
<ImportOutlined /> {t('importTask')}
</div>
),
@@ -287,7 +291,10 @@ const ProjectViewHeader = memo(() => {
if (selectedProject.category_id) {
elements.push(
<Tooltip key="category-tooltip" title={`${t('projectCategoryTooltip')}: ${selectedProject.category_name}`}>
<Tooltip
key="category-tooltip"
title={`${t('projectCategoryTooltip')}: ${selectedProject.category_name}`}
>
<Tag
key="category"
color={colors.vibrantOrange}
@@ -381,7 +388,10 @@ const ProjectViewHeader = memo(() => {
// Subscribe button
actions.push(
<Tooltip key="subscribe" title={selectedProject?.subscribed ? t('unsubscribeTooltip') : t('subscribeTooltip')}>
<Tooltip
key="subscribe"
title={selectedProject?.subscribed ? t('unsubscribeTooltip') : t('subscribeTooltip')}
>
<Button
shape="round"
loading={subscriptionLoading}
@@ -464,7 +474,10 @@ const ProjectViewHeader = memo(() => {
() => (
<Flex gap={4} align="center">
<Tooltip title={t('navigateBackTooltip')}>
<ArrowLeftOutlined style={{ fontSize: 16, cursor: 'pointer' }} onClick={handleNavigateToProjects} />
<ArrowLeftOutlined
style={{ fontSize: 16, cursor: 'pointer' }}
onClick={handleNavigateToProjects}
/>
</Tooltip>
<Typography.Title level={4} style={{ marginBlockEnd: 0, marginInlineStart: 8 }}>
{selectedProject?.name}

View File

@@ -26,5 +26,3 @@
[data-theme="dark"] .project-view-tabs .ant-tabs-ink-bar {
background-color: #ffffff;
}

View File

@@ -8,7 +8,12 @@ import Dropdown from 'antd/es/dropdown';
import Input from 'antd/es/input';
import Typography from 'antd/es/typography';
import { MenuProps } from 'antd/es/menu';
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@/shared/antd-imports';
import {
EditOutlined,
EllipsisOutlined,
RetweetOutlined,
RightOutlined,
} from '@/shared/antd-imports';
import { colors } from '@/styles/colors';
import { useAppSelector } from '@/hooks/useAppSelector';

View File

@@ -17,7 +17,12 @@ import {
DragEndEvent,
DragStartEvent,
} from '@dnd-kit/core';
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@/shared/antd-imports';
import {
EditOutlined,
EllipsisOutlined,
RetweetOutlined,
RightOutlined,
} from '@/shared/antd-imports';
import { colors } from '@/styles/colors';
import { useAppSelector } from '@/hooks/useAppSelector';

View File

@@ -1,4 +1,13 @@
import { Badge, Card, Dropdown, Empty, Flex, Menu, MenuProps, Typography } from '@/shared/antd-imports';
import {
Badge,
Card,
Dropdown,
Empty,
Flex,
Menu,
MenuProps,
Typography,
} from '@/shared/antd-imports';
import React, { useState, useEffect } from 'react';
import { DownOutlined } from '@/shared/antd-imports';
// custom css file

View File

@@ -1,4 +1,13 @@
import { Badge, Card, Dropdown, Empty, Flex, Menu, MenuProps, Typography } from '@/shared/antd-imports';
import {
Badge,
Card,
Dropdown,
Empty,
Flex,
Menu,
MenuProps,
Typography,
} from '@/shared/antd-imports';
import React, { useState, useEffect } from 'react';
import { DownOutlined } from '@/shared/antd-imports';
// custom css file

View File

@@ -43,8 +43,8 @@ import {
import { themeWiseColor } from '@/utils/themeWiseColor';
import KeyTypeColumn from './key-type-column/key-type-column';
import logger from '@/utils/errorLogger';
import {
fetchTasksV3,
import {
fetchTasksV3,
fetchTaskListColumns,
addCustomColumn,
deleteCustomColumn as deleteCustomColumnFromTaskManagement,
@@ -91,8 +91,6 @@ const CustomColumnModal = () => {
// Use the column data passed from TaskListV2
const openedColumn = currentColumnData;
// Function to reset all form and Redux state
const resetModalData = () => {
mainForm.resetFields();
@@ -104,11 +102,12 @@ const CustomColumnModal = () => {
const handleDeleteColumn = async () => {
// The customColumnId should now be the UUID passed from TaskListV2
// But also check the column data as a fallback, prioritizing uuid over id
const columnUUID = customColumnId ||
openedColumn?.uuid ||
openedColumn?.id ||
openedColumn?.custom_column_obj?.uuid ||
openedColumn?.custom_column_obj?.id;
const columnUUID =
customColumnId ||
openedColumn?.uuid ||
openedColumn?.id ||
openedColumn?.custom_column_obj?.uuid ||
openedColumn?.custom_column_obj?.id;
if (!customColumnId || !columnUUID) {
message.error('Cannot delete column: Missing UUID');
@@ -260,10 +259,10 @@ const CustomColumnModal = () => {
dispatch(addCustomColumn(newColumn));
dispatch(toggleCustomColumnModalOpen(false));
resetModalData();
// Show success message
message.success(t('customColumns.modal.createSuccessMessage'));
// Refresh tasks and columns to include the new custom column values
if (projectId) {
dispatch(fetchTaskListColumns(projectId));
@@ -301,11 +300,12 @@ const CustomColumnModal = () => {
: null;
// Get the correct UUID for the update operation, prioritizing uuid over id
const updateColumnUUID = customColumnId ||
openedColumn?.uuid ||
openedColumn?.id ||
openedColumn?.custom_column_obj?.uuid ||
openedColumn?.custom_column_obj?.id;
const updateColumnUUID =
customColumnId ||
openedColumn?.uuid ||
openedColumn?.id ||
openedColumn?.custom_column_obj?.uuid ||
openedColumn?.custom_column_obj?.id;
if (updatedColumn && updateColumnUUID) {
try {
@@ -377,7 +377,11 @@ const CustomColumnModal = () => {
return (
<Modal
title={customColumnModalType === 'create' ? t('customColumns.modal.addFieldTitle') : t('customColumns.modal.editFieldTitle')}
title={
customColumnModalType === 'create'
? t('customColumns.modal.addFieldTitle')
: t('customColumns.modal.editFieldTitle')
}
centered
open={isCustomColumnModalOpen}
onCancel={() => {
@@ -490,7 +494,10 @@ const CustomColumnModal = () => {
]}
required={false}
>
<Input placeholder={t('customColumns.modal.columnTitlePlaceholder')} style={{ minWidth: '100%', width: 300 }} />
<Input
placeholder={t('customColumns.modal.columnTitlePlaceholder')}
style={{ minWidth: '100%', width: 300 }}
/>
</Form.Item>
<Form.Item
@@ -541,10 +548,14 @@ const CustomColumnModal = () => {
)}
<Flex gap={8}>
<Button onClick={() => {
dispatch(toggleCustomColumnModalOpen(false));
resetModalData();
}}>{t('customColumns.modal.cancelButton')}</Button>
<Button
onClick={() => {
dispatch(toggleCustomColumnModalOpen(false));
resetModalData();
}}
>
{t('customColumns.modal.cancelButton')}
</Button>
{customColumnModalType === 'create' ? (
<Button type="primary" htmlType="submit">
{t('customColumns.modal.createButton')}

View File

@@ -35,8 +35,6 @@ const SelectionTypeColumn = () => {
// Use the current column data passed from TaskListV2
const openedColumn = currentColumnData;
// Load existing selections when in edit mode
useEffect(() => {
if (customColumnModalType === 'edit' && openedColumn?.custom_column_obj?.selectionsList) {

View File

@@ -539,7 +539,7 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
const updatedTasks = [...sourceGroup.tasks];
updatedTasks.splice(fromIndex, 1);
updatedTasks.splice(toIndex, 0, task);
updatedTasks.forEach((task, index) => {
taskUpdates.push({
task_id: task.id,
@@ -550,7 +550,7 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
// Different groups - update both source and target
const updatedSourceTasks = sourceGroup.tasks.filter((_, i) => i !== fromIndex);
const updatedTargetTasks = [...targetGroup.tasks];
if (isTargetGroupEmpty) {
updatedTargetTasks.push(task);
} else if (toIndex >= 0 && toIndex <= updatedTargetTasks.length) {
@@ -573,7 +573,7 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
task_id: task.id,
sort_order: index + 1,
};
// Add group-specific updates
if (groupBy === IGroupBy.STATUS) {
update.status_id = targetGroup.id;
@@ -582,7 +582,7 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
} else if (groupBy === IGroupBy.PHASE) {
update.phase_id = targetGroup.id;
}
taskUpdates.push(update);
});
}

View File

@@ -9,7 +9,12 @@ import Dropdown from 'antd/es/dropdown';
import Input from 'antd/es/input';
import Typography from 'antd/es/typography';
import { MenuProps } from 'antd/es/menu';
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@/shared/antd-imports';
import {
EditOutlined,
EllipsisOutlined,
RetweetOutlined,
RightOutlined,
} from '@/shared/antd-imports';
import { colors } from '@/styles/colors';
import './task-list-table-wrapper.css';
import TaskListTable from '../task-list-table';

View File

@@ -944,8 +944,6 @@ const SelectionFieldCell: React.FC<{
columnKey: string;
updateValue: (taskId: string, columnKey: string, value: string) => void;
}> = ({ selectionsList, value, task, columnKey, updateValue }) => {
return (
<CustomColumnSelectionCell
selectionsList={selectionsList}

View File

@@ -1,4 +1,13 @@
import { Button, ConfigProvider, Flex, Form, Mentions, Space, Tooltip, Typography } from '@/shared/antd-imports';
import {
Button,
ConfigProvider,
Flex,
Form,
Mentions,
Space,
Tooltip,
Typography,
} from '@/shared/antd-imports';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAppSelector } from '../../../../hooks/useAppSelector';