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:
chamikaJ
2025-07-08 15:26:55 +05:30
parent e750023fdc
commit f06851fa37
53 changed files with 700 additions and 117 deletions

View File

@@ -5,8 +5,15 @@ import { PushpinFilled, PushpinOutlined } from '@ant-design/icons';
import { colors } from '../styles/colors';
import { navRoutes, NavRoutesType } from '../features/navbar/navRoutes';
// Props type for the component
type PinRouteToNavbarButtonProps = {
name: string;
path: string;
adminOnly?: boolean;
};
// this component pin the given path to navbar
const PinRouteToNavbarButton = ({ name, path }: NavRoutesType) => {
const PinRouteToNavbarButton = ({ name, path, adminOnly = false }: PinRouteToNavbarButtonProps) => {
const navRoutesList: NavRoutesType[] = getJSONFromLocalStorage('navRoutes') || navRoutes;
const [isPinned, setIsPinned] = useState(
@@ -18,7 +25,7 @@ const PinRouteToNavbarButton = ({ name, path }: NavRoutesType) => {
const handlePinToNavbar = (name: string, path: string) => {
let newNavRoutesList;
const route: NavRoutesType = { name, path };
const route: NavRoutesType = { name, path, adminOnly };
if (isPinned) {
newNavRoutesList = navRoutesList.filter(item => item.name !== route.name);

View File

@@ -1,5 +1,6 @@
import { Divider, Form, Input, message, Modal, Typography } from 'antd';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { editTeamName, fetchTeams } from '@/features/teams/teamSlice';
import { ITeamGetResponse } from '@/types/teams/team.type';
@@ -11,6 +12,7 @@ interface EditTeamNameModalProps {
}
const EditTeamNameModal = ({ team, isModalOpen, onCancel }: EditTeamNameModalProps) => {
const { t } = useTranslation('settings/teams');
const dispatch = useAppDispatch();
const [form] = Form.useForm();
const [updating, setUpdating] = useState(false);
@@ -33,7 +35,7 @@ const EditTeamNameModal = ({ team, isModalOpen, onCancel }: EditTeamNameModalPro
}
setUpdating(false);
} catch (error) {
message.error('Team name change failed!');
message.error(t('updateFailed'));
} finally {
setUpdating(false);
}
@@ -49,13 +51,13 @@ const EditTeamNameModal = ({ team, isModalOpen, onCancel }: EditTeamNameModalPro
width: '100%',
}}
>
Edit Team Name
{t('editTeamName')}
<Divider />
</Typography.Text>
}
open={isModalOpen}
onOk={form.submit}
okText="Update Name"
okText={t('updateName')}
onCancel={() => {
onCancel();
setUpdating(false);
@@ -67,15 +69,15 @@ const EditTeamNameModal = ({ team, isModalOpen, onCancel }: EditTeamNameModalPro
<Form form={form} layout="vertical" onFinish={handleFormSubmit}>
<Form.Item
name="name"
label="Name"
label={t('name')}
rules={[
{
required: true,
message: 'Please enter a Name',
message: t('nameRequired'),
},
]}
>
<Input placeholder="Name" />
<Input placeholder={t('namePlaceholder')} />
</Form.Item>
</Form>
</Modal>

View File

@@ -65,7 +65,7 @@ const UpdateMemberDrawer = ({ selectedMemberId, onRoleUpdate }: UpdateMemberDraw
setJobTitles(res.body.data || []);
}
} catch (error) {
console.error('Error fetching job titles:', error);
logger.error('Error fetching job titles:', error);
message.error(t('jobTitlesFetchError'));
} finally {
setLoading(false);

View File

@@ -154,7 +154,7 @@ const TaskDrawer = () => {
onClick={handleAddTimeLog}
style={{ width: '100%' }}
>
Add new time log
{t('taskTimeLogTab.addTimeLog')}
</Button>
</Flex>
);

View File

@@ -519,7 +519,7 @@ const TaskListV2: React.FC = () => {
// Loading and error states
if (loading || loadingColumns) return <Skeleton active />;
if (error) return <div>Error: {error}</div>;
if (error) return <div>{t('emptyStates.errorPrefix')} {error}</div>;
// Show message when no data
if (groups.length === 0 && !loading) {
@@ -531,10 +531,10 @@ const TaskListV2: React.FC = () => {
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="text-lg font-medium text-gray-900 dark:text-white mb-2">
No task groups found
{t('emptyStates.noTaskGroups')}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Tasks will appear here when they are created or when filters are applied.
{t('emptyStates.noTaskGroupsDescription')}
</div>
</div>
</div>
@@ -623,7 +623,7 @@ const TaskListV2: React.FC = () => {
<div className="text-sm font-medium text-gray-900 dark:text-white">
{allTasks.find(task => task.id === activeId)?.name ||
allTasks.find(task => task.id === activeId)?.title ||
'Task'}
t('emptyStates.dragTaskFallback')}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
{allTasks.find(task => task.id === activeId)?.task_key}

View File

@@ -213,7 +213,7 @@ const useFilterData = (position: 'board' | 'list'): FilterSection[] => {
return [
{
id: 'priority',
label: 'Priority',
label: t('priorityText'),
options: filterData.priorities.map((p: any) => ({
value: p.id,
label: p.name,
@@ -288,7 +288,7 @@ const useFilterData = (position: 'board' | 'list'): FilterSection[] => {
return [
{
id: 'priority',
label: 'Priority',
label: t('priorityText'),
options: filterData.priorities.map((p: any) => ({
value: p.id,
label: p.name,
@@ -719,7 +719,34 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
isDarkMode,
}) => {
const { t } = useTranslation('task-list-filters');
const { t: tTable } = useTranslation('task-list-table');
const dispatch = useAppDispatch();
// Helper function to get translated field label using existing task-list-table translations
const getFieldLabel = useCallback((fieldKey: string) => {
const keyMappings: Record<string, string> = {
'KEY': 'keyColumn',
'DESCRIPTION': 'descriptionColumn',
'PROGRESS': 'progressColumn',
'ASSIGNEES': 'assigneesColumn',
'LABELS': 'labelsColumn',
'PHASE': 'phaseColumn',
'STATUS': 'statusColumn',
'PRIORITY': 'priorityColumn',
'TIME_TRACKING': 'timeTrackingColumn',
'ESTIMATION': 'estimationColumn',
'START_DATE': 'startDateColumn',
'DUE_DATE': 'dueDateColumn',
'DUE_TIME': 'dueTimeColumn',
'COMPLETED_DATE': 'completedDateColumn',
'CREATED_DATE': 'createdDateColumn',
'LAST_UPDATED': 'lastUpdatedColumn',
'REPORTER': 'reporterColumn',
};
const translationKey = keyMappings[fieldKey];
return translationKey ? tTable(translationKey) : fieldKey;
}, [tTable]);
const fieldsRaw = useSelector((state: RootState) => state.taskManagementFields);
const columns = useSelector(selectColumns);
const projectId = useAppSelector(state => state.projectReducer.projectId);
@@ -857,7 +884,7 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
{/* Label and Count */}
<div className="flex-1 flex items-center justify-between">
<span className="truncate">{field.label}</span>
<span className="truncate">{getFieldLabel(field.key)}</span>
</div>
</button>
);

View File

@@ -1,5 +1,6 @@
import React, { ReactNode, Suspense } from 'react';
import { InlineSuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
import i18n from '@/i18n';
// Import core components synchronously to avoid suspense in main tabs
import ProjectViewEnhancedBoard from '@/pages/projects/projectView/enhancedBoard/project-view-enhanced-board';
@@ -28,26 +29,31 @@ type TabItems = {
element: ReactNode;
};
// Function to get translated labels
const getTabLabel = (key: string): string => {
return i18n.t(`project-view:${key}`);
};
// settings all element items use for tabs
export const tabItems: TabItems[] = [
{
index: 0,
key: 'tasks-list',
label: 'Task List',
label: getTabLabel('taskList'),
isPinned: true,
element: React.createElement(TaskListV2),
},
{
index: 1,
key: 'board',
label: 'Board',
label: getTabLabel('board'),
isPinned: true,
element: React.createElement(ProjectViewEnhancedBoard),
},
{
index: 2,
key: 'project-insights-member-overview',
label: 'Insights',
label: getTabLabel('insights'),
element: React.createElement(
Suspense,
{ fallback: React.createElement(InlineSuspenseFallback) },
@@ -57,7 +63,7 @@ export const tabItems: TabItems[] = [
{
index: 3,
key: 'all-attachments',
label: 'Files',
label: getTabLabel('files'),
element: React.createElement(
Suspense,
{ fallback: React.createElement(InlineSuspenseFallback) },
@@ -67,7 +73,7 @@ export const tabItems: TabItems[] = [
{
index: 4,
key: 'members',
label: 'Members',
label: getTabLabel('members'),
element: React.createElement(
Suspense,
{ fallback: React.createElement(InlineSuspenseFallback) },
@@ -77,7 +83,7 @@ export const tabItems: TabItems[] = [
{
index: 5,
key: 'updates',
label: 'Updates',
label: getTabLabel('updates'),
element: React.createElement(
Suspense,
{ fallback: React.createElement(InlineSuspenseFallback) },
@@ -85,3 +91,29 @@ export const tabItems: TabItems[] = [
),
},
];
// Function to update tab labels when language changes
export const updateTabLabels = () => {
tabItems.forEach(item => {
switch (item.key) {
case 'tasks-list':
item.label = getTabLabel('taskList');
break;
case 'board':
item.label = getTabLabel('board');
break;
case 'project-insights-member-overview':
item.label = getTabLabel('insights');
break;
case 'all-attachments':
item.label = getTabLabel('files');
break;
case 'members':
item.label = getTabLabel('members');
break;
case 'updates':
item.label = getTabLabel('updates');
break;
}
});
};

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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);

View File

@@ -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>

View File

@@ -4,9 +4,56 @@ import { getLanguageFromLocalStorage } from './language-utils';
export const currentDateString = (): string => {
const date = dayjs();
const localeString = getLanguageFromLocalStorage();
const locale = localeString === 'en' ? 'en' : localeString === 'es' ? 'es' : 'pt';
// Map language codes to dayjs locales
let locale = 'en'; // Default to English
switch (localeString) {
case 'en':
locale = 'en';
break;
case 'es':
locale = 'es';
break;
case 'pt':
locale = 'pt';
break;
case 'de':
locale = 'de';
break;
case 'zh_cn':
locale = 'zh-cn';
break;
case 'alb':
locale = 'sq'; // Albanian locale code for dayjs
break;
default:
locale = 'en';
}
// Get localized "Today is" text
let todayText = 'Today is'; // Default English
switch (localeString) {
case 'en':
todayText = 'Today is';
break;
case 'es':
todayText = 'Hoy es';
break;
case 'pt':
todayText = 'Hoje é';
break;
case 'de':
todayText = 'Heute ist';
break;
case 'zh_cn':
todayText = '今天是';
break;
case 'alb':
todayText = 'Sot është';
break;
default:
todayText = 'Today is';
}
const todayText =
localeString === 'en' ? 'Today is' : localeString === 'es' ? 'Hoy es' : 'Hoje é';
return `${todayText} ${date.locale(locale).format('dddd, MMMM DD, YYYY')}`;
};

View File

@@ -49,5 +49,16 @@ export const greetingString = (name: string): string => {
evening = '晚上好';
}
return `${greetingPrefix} ${name}, ${greetingSuffix} ${greet}!`;
// Get the localized time period based on the current time
let localizedTimePeriod;
if (greet === 'morning') localizedTimePeriod = morning;
else if (greet === 'afternoon') localizedTimePeriod = afternoon;
else localizedTimePeriod = evening;
// Handle Chinese language which has different structure
if (language === 'zh_cn') {
return `${greetingPrefix} ${name}, ${localizedTimePeriod}!`;
}
return `${greetingPrefix} ${name}, ${greetingSuffix} ${localizedTimePeriod}!`;
};