feature: localization's first implmentation done. all frontend hardcoded text is now localized.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import {
|
||||
canShareOrganizationInviteLink,
|
||||
@@ -19,6 +20,8 @@ export function CopyInvitationLinkButton({
|
||||
copying,
|
||||
onCopy,
|
||||
}: CopyInvitationLinkButtonProps) {
|
||||
const t = useTranslations('organizations');
|
||||
|
||||
if (!canShareOrganizationInviteLink(invitation)) {
|
||||
return <span className="text-xs text-text-muted">—</span>;
|
||||
}
|
||||
@@ -29,8 +32,8 @@ export function CopyInvitationLinkButton({
|
||||
className="p-2 rounded-md text-text-secondary hover:bg-background-card/80 hover:text-text-primary disabled:opacity-50"
|
||||
disabled={copying}
|
||||
onClick={onCopy}
|
||||
aria-label="Copy invitation link"
|
||||
title="Copy invitation link (generates a new link if needed)"
|
||||
aria-label={t('copyInvitationLink')}
|
||||
title={t('copyInvitationLinkTitle')}
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { DialogCloseButton } from '@/components/ui/shared/DialogCloseButton';
|
||||
import { ToastStack, type ToastMessages } from '@/components/ui/shared/Toast';
|
||||
import type { OrganizationInvitationHistoryItemDto } from '@/lib/api/organization';
|
||||
@@ -7,14 +8,6 @@ import { Badge, organizationConnectionStatusVariant } from '@/components/ui/shar
|
||||
import { Table } from '@/components/ui/shared/Table';
|
||||
import { CopyInvitationLinkButton } from '@/components/ui/organizations/CopyInvitationLinkButton';
|
||||
|
||||
function formatInvitationStatusLabel(status: OrganizationInvitationHistoryItemDto['status']): string {
|
||||
if (status === 'PENDING') return 'Invitation pending';
|
||||
if (status === 'ACTIVE') return 'Invitation accepted';
|
||||
if (status === 'REJECTED') return 'Invitation rejected';
|
||||
if (status === 'EXPIRED') return 'Invitation expired';
|
||||
return status;
|
||||
}
|
||||
|
||||
function formatTableDate(value: string): string {
|
||||
const d = new Date(value);
|
||||
if (Number.isNaN(d.getTime())) return '—';
|
||||
@@ -43,6 +36,18 @@ export function InvitationHistoryDialog({
|
||||
onCopy,
|
||||
toastMessages,
|
||||
}: InvitationHistoryDialogProps) {
|
||||
const t = useTranslations('organizations');
|
||||
|
||||
function formatInvitationStatusLabel(
|
||||
status: OrganizationInvitationHistoryItemDto['status'],
|
||||
): string {
|
||||
if (status === 'PENDING') return t('statusPending');
|
||||
if (status === 'ACTIVE') return t('statusAccepted');
|
||||
if (status === 'REJECTED') return t('statusRejected');
|
||||
if (status === 'EXPIRED') return t('statusExpired');
|
||||
return status;
|
||||
}
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
@@ -55,7 +60,7 @@ export function InvitationHistoryDialog({
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<h2 id="invitation-history-title" className="text-lg font-semibold text-text-primary pr-2">
|
||||
Invitation History
|
||||
{t('historyTitle')}
|
||||
</h2>
|
||||
<DialogCloseButton onClick={onClose} />
|
||||
</div>
|
||||
@@ -63,27 +68,27 @@ export function InvitationHistoryDialog({
|
||||
{toastMessages && <ToastStack {...toastMessages} />}
|
||||
|
||||
{loading ? (
|
||||
<p className="text-sm text-text-secondary">Loading invitation history...</p>
|
||||
<p className="text-sm text-text-secondary">{t('loadingHistory')}</p>
|
||||
) : items.length === 0 ? (
|
||||
<p className="text-sm text-text-secondary">No invitations yet.</p>
|
||||
<p className="text-sm text-text-secondary">{t('historyEmpty')}</p>
|
||||
) : (
|
||||
<Table
|
||||
headers={
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
||||
Organization
|
||||
{t('tableOrganization')}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
||||
Owner email
|
||||
{t('tableOwnerEmail')}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
||||
Date
|
||||
{t('tableDate')}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-medium text-text-muted uppercase tracking-wider">
|
||||
Status
|
||||
{t('tableStatus')}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-text-muted uppercase tracking-wider">
|
||||
Invitation link
|
||||
{t('tableInvitationLink')}
|
||||
</th>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAuth } from '@/lib/hooks/useAuth';
|
||||
import { canCreateOrganizationFromCurrentOrg } from '@/components/shared/permissions';
|
||||
import { Building2, Beaker, Mail } from 'lucide-react';
|
||||
@@ -8,6 +9,9 @@ import { Input } from '@/components/ui/shared/Input';
|
||||
import { Button } from '@/components/ui/shared/Button';
|
||||
|
||||
export function OrganizationSelectorContent() {
|
||||
const t = useTranslations('organizations');
|
||||
const tAuth = useTranslations('auth');
|
||||
const tCommon = useTranslations('common');
|
||||
const {
|
||||
organizations,
|
||||
currentOrganization,
|
||||
@@ -29,6 +33,9 @@ export function OrganizationSelectorContent() {
|
||||
const getIcon = (type: string) =>
|
||||
type === 'CLINIC' ? <Building2 className="h-8 w-8 icon-flat" /> : <Beaker className="h-8 w-8 icon-flat" />;
|
||||
|
||||
const getTypeLabel = (type: string) =>
|
||||
type === 'CLINIC' ? tAuth('dentalClinic') : tAuth('dentalLab');
|
||||
|
||||
const handleCreateOrganization = async () => {
|
||||
try {
|
||||
clearError();
|
||||
@@ -48,18 +55,16 @@ export function OrganizationSelectorContent() {
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <p className="text-text-secondary">Loading...</p>;
|
||||
return <p className="text-text-secondary">{tCommon('loadingEllipsis')}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold text-text-primary">Organizations</h1>
|
||||
<h1 className="text-3xl font-semibold text-text-primary">{t('selectorTitle')}</h1>
|
||||
<p className="text-text-secondary mt-2">
|
||||
{canCreateOrganization
|
||||
? 'Select an organization to continue, or create a new one.'
|
||||
: 'Select an organization to continue.'}
|
||||
{canCreateOrganization ? t('selectorSubtitleWithCreate') : t('selectorSubtitleSelectOnly')}
|
||||
</p>
|
||||
</div>
|
||||
{canCreateOrganization && (
|
||||
@@ -71,7 +76,7 @@ export function OrganizationSelectorContent() {
|
||||
setIsCreateOpen((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
{isCreateOpen ? 'Cancel' : 'Create Organization'}
|
||||
{isCreateOpen ? tCommon('cancel') : t('createOrganization')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -79,23 +84,23 @@ export function OrganizationSelectorContent() {
|
||||
{canCreateOrganization && isCreateOpen && (
|
||||
<div className="surface-card p-6 space-y-4">
|
||||
<Input
|
||||
label="Organization name"
|
||||
label={tAuth('organizationName')}
|
||||
value={organizationName}
|
||||
onChange={(event) => setOrganizationName(event.target.value)}
|
||||
placeholder="Sunshine Dental Clinic"
|
||||
placeholder={tAuth('organizationNamePlaceholder')}
|
||||
icon={<Building2 className="h-5 w-5 icon-flat" />}
|
||||
/>
|
||||
<Input
|
||||
label="Organization email"
|
||||
label={tAuth('organizationEmail')}
|
||||
value={organizationEmail}
|
||||
onChange={(event) => setOrganizationEmail(event.target.value)}
|
||||
placeholder="contact@sunshineclinic.com"
|
||||
placeholder={tAuth('organizationEmailPlaceholder')}
|
||||
type="email"
|
||||
icon={<Mail className="h-5 w-5 icon-flat" />}
|
||||
/>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">
|
||||
Organization type
|
||||
{tAuth('organizationType')}
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
@@ -107,7 +112,7 @@ export function OrganizationSelectorContent() {
|
||||
: 'border-border text-text-secondary hover:border-border-strong'
|
||||
}`}
|
||||
>
|
||||
Dental Clinic
|
||||
{tAuth('dentalClinic')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -118,7 +123,7 @@ export function OrganizationSelectorContent() {
|
||||
: 'border-border text-text-secondary hover:border-border-strong'
|
||||
}`}
|
||||
>
|
||||
Dental Lab
|
||||
{tAuth('dentalLab')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,7 +140,7 @@ export function OrganizationSelectorContent() {
|
||||
isLoading={isLoading}
|
||||
disabled={!organizationName.trim() || !organizationEmail.trim()}
|
||||
>
|
||||
Create and Continue
|
||||
{t('createAndContinue')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -144,9 +149,7 @@ export function OrganizationSelectorContent() {
|
||||
{!organizations.length ? (
|
||||
<div className="surface-card p-8 text-center">
|
||||
<p className="text-text-secondary">
|
||||
{canCreateOrganization
|
||||
? 'No organizations found. Create your first one to continue.'
|
||||
: 'No organizations found. Ask an organization owner to invite you.'}
|
||||
{canCreateOrganization ? t('emptyCanCreate') : t('emptyAskOwner')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -166,12 +169,12 @@ export function OrganizationSelectorContent() {
|
||||
{org.name}
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary">
|
||||
{org.type === 'CLINIC' ? 'Dental Clinic' : 'Dental Lab'}
|
||||
{getTypeLabel(org.type)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-primary text-sm">
|
||||
Continue →
|
||||
{t('continueArrow')}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user