feat(localization): add and update translations for multiple languages
- Introduced new localization files for Albanian, German, Spanish, Portuguese, and Chinese, enhancing the application's multilingual support. - Added new keys and updated existing translations in project-view, task-list-table, and settings files to improve user experience across different languages. - Enhanced error handling and empty state messages in task management components to provide clearer feedback to users. - Updated tooltip texts and button labels for better clarity and consistency in the user interface.
This commit is contained in:
@@ -268,7 +268,7 @@ const ProjectViewHeader = memo(() => {
|
||||
{
|
||||
key: 'import',
|
||||
label: (
|
||||
<div style={{ width: '100%', margin: 0, padding: 0 }} onClick={handleImportTaskTemplate}>
|
||||
<div style={{ width: '100%', margin: 0, padding: 0 }} onClick={handleImportTaskTemplate} title={t('importTaskTooltip')}>
|
||||
<ImportOutlined /> {t('importTask')}
|
||||
</div>
|
||||
),
|
||||
@@ -285,19 +285,21 @@ const ProjectViewHeader = memo(() => {
|
||||
|
||||
if (selectedProject.category_id) {
|
||||
elements.push(
|
||||
<Tag
|
||||
key="category"
|
||||
color={colors.vibrantOrange}
|
||||
style={{ borderRadius: 24, paddingInline: 8, margin: 0 }}
|
||||
>
|
||||
{selectedProject.category_name}
|
||||
</Tag>
|
||||
<Tooltip key="category-tooltip" title={`${t('projectCategoryTooltip')}: ${selectedProject.category_name}`}>
|
||||
<Tag
|
||||
key="category"
|
||||
color={colors.vibrantOrange}
|
||||
style={{ borderRadius: 24, paddingInline: 8, margin: 0 }}
|
||||
>
|
||||
{selectedProject.category_name}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedProject.status) {
|
||||
elements.push(
|
||||
<Tooltip key="status" title={selectedProject.status}>
|
||||
<Tooltip key="status" title={`${t('projectStatusTooltip')}: ${selectedProject.status}`}>
|
||||
<ProjectStatusIcon
|
||||
iconName={selectedProject.status_icon || ''}
|
||||
color={selectedProject.status_color || ''}
|
||||
@@ -309,6 +311,8 @@ const ProjectViewHeader = memo(() => {
|
||||
if (selectedProject.start_date || selectedProject.end_date) {
|
||||
const tooltipContent = (
|
||||
<Typography.Text style={{ color: colors.white }}>
|
||||
{t('projectDatesInfo')}
|
||||
<br />
|
||||
{selectedProject.start_date &&
|
||||
`${t('startDate')}: ${formatDate(new Date(selectedProject.start_date))}`}
|
||||
{selectedProject.end_date && (
|
||||
@@ -348,7 +352,7 @@ const ProjectViewHeader = memo(() => {
|
||||
|
||||
// Refresh button
|
||||
actions.push(
|
||||
<Tooltip key="refresh" title={t('refreshProject')}>
|
||||
<Tooltip key="refresh" title={t('refreshTooltip')}>
|
||||
<Button
|
||||
shape="circle"
|
||||
icon={<SyncOutlined spin={loadingGroups} />}
|
||||
@@ -360,7 +364,7 @@ const ProjectViewHeader = memo(() => {
|
||||
// Save as template (owner/admin only)
|
||||
if (isOwnerOrAdmin) {
|
||||
actions.push(
|
||||
<Tooltip key="template" title={t('saveAsTemplate')}>
|
||||
<Tooltip key="template" title={t('saveAsTemplateTooltip')}>
|
||||
<Button shape="circle" icon={<SaveOutlined />} onClick={handleSaveAsTemplate} />
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -368,14 +372,14 @@ const ProjectViewHeader = memo(() => {
|
||||
|
||||
// Settings button
|
||||
actions.push(
|
||||
<Tooltip key="settings" title={t('projectSettings')}>
|
||||
<Tooltip key="settings" title={t('settingsTooltip')}>
|
||||
<Button shape="circle" icon={<SettingOutlined />} onClick={handleSettingsClick} />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
// Subscribe button
|
||||
actions.push(
|
||||
<Tooltip key="subscribe" title={t('subscribe')}>
|
||||
<Tooltip key="subscribe" title={selectedProject?.subscribed ? t('unsubscribeTooltip') : t('subscribeTooltip')}>
|
||||
<Button
|
||||
shape="round"
|
||||
loading={subscriptionLoading}
|
||||
@@ -390,38 +394,44 @@ const ProjectViewHeader = memo(() => {
|
||||
// Invite button (owner/admin/project manager only)
|
||||
if (isOwnerOrAdmin || isProjectManager) {
|
||||
actions.push(
|
||||
<Button key="invite" type="primary" icon={<UsergroupAddOutlined />} onClick={handleInvite}>
|
||||
{t('invite')}
|
||||
</Button>
|
||||
<Tooltip key="invite-tooltip" title={t('inviteTooltip')}>
|
||||
<Button key="invite" type="primary" icon={<UsergroupAddOutlined />} onClick={handleInvite}>
|
||||
{t('invite')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// Create task button
|
||||
if (isOwnerOrAdmin) {
|
||||
actions.push(
|
||||
<Dropdown.Button
|
||||
key="create-task-dropdown"
|
||||
loading={creatingTask}
|
||||
type="primary"
|
||||
icon={<DownOutlined />}
|
||||
menu={{ items: dropdownItems }}
|
||||
trigger={['click']}
|
||||
onClick={handleCreateTask}
|
||||
>
|
||||
<EditOutlined /> {t('createTask')}
|
||||
</Dropdown.Button>
|
||||
<Tooltip key="create-task-tooltip" title={t('createTaskTooltip')}>
|
||||
<Dropdown.Button
|
||||
key="create-task-dropdown"
|
||||
loading={creatingTask}
|
||||
type="primary"
|
||||
icon={<DownOutlined />}
|
||||
menu={{ items: dropdownItems }}
|
||||
trigger={['click']}
|
||||
onClick={handleCreateTask}
|
||||
>
|
||||
<EditOutlined /> {t('createTask')}
|
||||
</Dropdown.Button>
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
actions.push(
|
||||
<Button
|
||||
key="create-task"
|
||||
loading={creatingTask}
|
||||
type="primary"
|
||||
icon={<EditOutlined />}
|
||||
onClick={handleCreateTask}
|
||||
>
|
||||
{t('createTask')}
|
||||
</Button>
|
||||
<Tooltip key="create-task-tooltip" title={t('createTaskTooltip')}>
|
||||
<Button
|
||||
key="create-task"
|
||||
loading={creatingTask}
|
||||
type="primary"
|
||||
icon={<EditOutlined />}
|
||||
onClick={handleCreateTask}
|
||||
>
|
||||
{t('createTask')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -451,14 +461,16 @@ const ProjectViewHeader = memo(() => {
|
||||
const pageHeaderTitle = useMemo(
|
||||
() => (
|
||||
<Flex gap={8} align="center">
|
||||
<ArrowLeftOutlined style={{ fontSize: 16 }} onClick={handleNavigateToProjects} />
|
||||
<Tooltip title={t('navigateBackTooltip')}>
|
||||
<ArrowLeftOutlined style={{ fontSize: 16, cursor: 'pointer' }} onClick={handleNavigateToProjects} />
|
||||
</Tooltip>
|
||||
<Typography.Title level={4} style={{ marginBlockEnd: 0, marginInlineStart: 12 }}>
|
||||
{selectedProject?.name}
|
||||
</Typography.Title>
|
||||
{projectAttributes}
|
||||
</Flex>
|
||||
),
|
||||
[handleNavigateToProjects, selectedProject?.name, projectAttributes]
|
||||
[handleNavigateToProjects, selectedProject?.name, projectAttributes, t]
|
||||
);
|
||||
|
||||
// Memoized page header styles
|
||||
|
||||
@@ -32,7 +32,7 @@ import { resetSelection } from '@/features/task-management/selection.slice';
|
||||
import { resetFields } from '@/features/task-management/taskListFields.slice';
|
||||
import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice';
|
||||
import { deselectAll } from '@/features/projects/bulkActions/bulkActionSlice';
|
||||
import { tabItems } from '@/lib/project/project-view-constants';
|
||||
import { tabItems, updateTabLabels } from '@/lib/project/project-view-constants';
|
||||
import {
|
||||
setSelectedTaskId,
|
||||
setShowTaskDrawer,
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
import { resetState as resetEnhancedKanbanState } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||
import { setProjectId as setInsightsProjectId } from '@/features/projects/insights/project-insights.slice';
|
||||
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// Import critical components synchronously to avoid suspense interruptions
|
||||
import TaskDrawer from '@components/task-drawer/task-drawer';
|
||||
@@ -63,13 +64,14 @@ const ProjectView = React.memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { projectId } = useParams();
|
||||
const { t } = useTranslation('project-view');
|
||||
|
||||
// Memoized selectors to prevent unnecessary re-renders
|
||||
const selectedProject = useAppSelector(state => state.projectReducer.project);
|
||||
const projectLoading = useAppSelector(state => state.projectReducer.projectLoading);
|
||||
|
||||
// Optimize document title updates
|
||||
useDocumentTitle(selectedProject?.name || 'Project View');
|
||||
useDocumentTitle(selectedProject?.name || t('projectView'));
|
||||
|
||||
// Memoize URL params to prevent unnecessary state updates
|
||||
const urlParams = useMemo(
|
||||
@@ -174,6 +176,11 @@ const ProjectView = React.memo(() => {
|
||||
setIsInitialized(false);
|
||||
}, [projectId]);
|
||||
|
||||
// Update tab labels when language changes
|
||||
useEffect(() => {
|
||||
updateTabLabels();
|
||||
}, [t]);
|
||||
|
||||
// Effect for handling task drawer opening from URL params
|
||||
useEffect(() => {
|
||||
if (taskid && isInitialized) {
|
||||
@@ -287,6 +294,7 @@ const ProjectView = React.memo(() => {
|
||||
e.stopPropagation();
|
||||
pinToDefaultTab(item.key);
|
||||
}}
|
||||
title={item.key === pinnedTab ? t('unpinTab') : t('pinTab')}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
)}
|
||||
@@ -296,7 +304,7 @@ const ProjectView = React.memo(() => {
|
||||
}));
|
||||
|
||||
return menuItems;
|
||||
}, [pinnedTab, pinToDefaultTab]);
|
||||
}, [pinnedTab, pinToDefaultTab, t]);
|
||||
|
||||
// Optimized secondary components loading with better UX
|
||||
const [shouldLoadSecondaryComponents, setShouldLoadSecondaryComponents] = useState(false);
|
||||
|
||||
@@ -24,7 +24,7 @@ const SettingSidebar: React.FC = () => {
|
||||
const items: Required<MenuProps>['items'] = accessibleSettings
|
||||
.map(item => {
|
||||
if (currentSession?.is_google && item.key === 'change-password') {
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key: item.key,
|
||||
@@ -39,7 +39,7 @@ const SettingSidebar: React.FC = () => {
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
.filter((item): item is NonNullable<typeof item> => item !== null);
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import UpdateMemberDrawer from '@/components/settings/update-member-drawer';
|
||||
@@ -43,6 +44,8 @@ const TeamMembersSettings = () => {
|
||||
const { socket } = useSocket();
|
||||
const refreshTeamMembers = useAppSelector(state => state.memberReducer.refreshTeamMembers); // Listen to refresh flag
|
||||
|
||||
useDocumentTitle(t('title') || 'Team Members');
|
||||
|
||||
const [model, setModel] = useState<ITeamMembersViewModel>({ total: 0, data: [] });
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { durationDateFormat } from '@utils/durationDateFormat';
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import EditTeamModal from '@/components/settings/edit-team-name-modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { fetchTeams } from '@features/teams/teamSlice';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
@@ -12,7 +13,8 @@ import { useDocumentTitle } from '@/hooks/useDoumentTItle';
|
||||
import { ITeamGetResponse } from '@/types/teams/team.type';
|
||||
|
||||
const TeamsSettings = () => {
|
||||
useDocumentTitle('Teams');
|
||||
const { t } = useTranslation('settings/teams');
|
||||
useDocumentTitle(t('title'));
|
||||
|
||||
const [selectedTeam, setSelectedTeam] = useState<ITeamGetResponse | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
@@ -26,27 +28,27 @@ const TeamsSettings = () => {
|
||||
const columns: TableProps['columns'] = [
|
||||
{
|
||||
key: 'name',
|
||||
title: 'Name',
|
||||
title: t('name'),
|
||||
render: (record: ITeamGetResponse) => <Typography.Text>{record.name}</Typography.Text>,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'created',
|
||||
title: 'Created',
|
||||
title: t('created'),
|
||||
render: (record: ITeamGetResponse) => (
|
||||
<Typography.Text>{durationDateFormat(record.created_at)}</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'ownsBy',
|
||||
title: 'Owns By',
|
||||
title: t('ownsBy'),
|
||||
render: (record: ITeamGetResponse) => <Typography.Text>{record.owns_by}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
key: 'actionBtns',
|
||||
width: 60,
|
||||
render: (record: ITeamGetResponse) => (
|
||||
<Tooltip title="Edit" trigger={'hover'}>
|
||||
<Tooltip title={t('edit')} trigger={'hover'}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
@@ -69,13 +71,12 @@ const TeamsSettings = () => {
|
||||
<div style={{ width: '100%' }}>
|
||||
<Flex align="center" justify="space-between" style={{ marginBlockEnd: 24 }}>
|
||||
<Typography.Title level={4} style={{ marginBlockEnd: 0 }}>
|
||||
{teamsList.length} Team
|
||||
{teamsList.length !== 1 && 's'}
|
||||
{teamsList.length} {teamsList.length === 1 ? t('team') : t('teams')}
|
||||
</Typography.Title>
|
||||
|
||||
<Tooltip title={'Click to pin this into the main menu'} trigger={'hover'}>
|
||||
<Tooltip title={t('pinTooltip')} trigger={'hover'}>
|
||||
{/* this button pin this route to navbar */}
|
||||
<PinRouteToNavbarButton name="teams" path="/worklenz/settings/teams" />
|
||||
<PinRouteToNavbarButton name="teams" path="/worklenz/settings/teams" adminOnly={true} />
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user