fix(i18n): close shared ui gaps
This commit is contained in:
@@ -515,7 +515,7 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
|
||||
<ListItem key={match.id} sx={{ px: 0 }}>
|
||||
<ListItemText
|
||||
primary={`${match.company} - ${match.jobTitle}`}
|
||||
secondary={`${match.reason} • ${match.status} • ${new Date(match.dateApplied).toLocaleDateString()}`}
|
||||
secondary={`${match.reason} • ${match.status} • ${new Date(match.dateApplied).toLocaleDateString(language === "nb" ? "nb-NO" : "en")}`}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
|
||||
@@ -488,7 +488,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
|
||||
) : null}
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "flex-end", mt: 0.75 }}>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>
|
||||
{isMe ? t("correspondenceMe") : t("correspondenceCompany")}{m.channel ? ` - ${m.channel}` : ""}{m.date ? ` - ${new Date(m.date).toLocaleString()}` : ""}
|
||||
{isMe ? t("correspondenceMe") : t("correspondenceCompany")}{m.channel ? ` - ${m.channel}` : ""}{m.date ? ` - ${new Date(m.date).toLocaleString(language === "nb" ? "nb-NO" : "en")}` : ""}
|
||||
</Typography>
|
||||
<IconButton size="small" aria-label={t("correspondenceDeleteTitle")} onClick={() => void deleteMessage(m.id)} sx={{ color: "text.secondary" }}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
@@ -654,7 +654,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
|
||||
))}
|
||||
</Box>
|
||||
) : null}
|
||||
{gmailStatus.lastSyncedAt ? <Chip label={t("correspondenceLastSynced", { date: new Date(gmailStatus.lastSyncedAt).toLocaleString() })} size="small" /> : null}
|
||||
{gmailStatus.lastSyncedAt ? <Chip label={t("correspondenceLastSynced", { date: new Date(gmailStatus.lastSyncedAt).toLocaleString(language === "nb" ? "nb-NO" : "en") })} size="small" /> : null}
|
||||
{gmailStatus.lastSyncAttemptedAt ? <Chip label={t("correspondenceSyncChecked", { date: new Date(gmailStatus.lastSyncAttemptedAt).toLocaleString(language === "nb" ? "nb-NO" : "en") })} size="small" variant="outlined" /> : null}
|
||||
{gmailStatus.lastSyncStatus === "error" && gmailStatus.lastSyncError ? <Chip label={t("correspondenceSyncIssue", { error: gmailStatus.lastSyncError })} size="small" color="warning" variant="outlined" /> : null}
|
||||
{linkedThreadIds.length > 0 ? (
|
||||
@@ -710,7 +710,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
|
||||
<ListItemButton sx={{ alignItems: "flex-start", px: 0, py: 1 }}>
|
||||
<ListItemText
|
||||
secondaryTypographyProps={{ component: "div" }}
|
||||
primary={<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}><Typography sx={{ fontWeight: 700 }}>{message.subject || t("correspondenceNoSubject")}</Typography><Typography variant="caption" sx={{ color: "text.secondary" }}>{message.date ? new Date(message.date).toLocaleString() : ""}</Typography></Box>}
|
||||
primary={<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}><Typography sx={{ fontWeight: 700 }}>{message.subject || t("correspondenceNoSubject")}</Typography><Typography variant="caption" sx={{ color: "text.secondary" }}>{message.date ? new Date(message.date).toLocaleString(language === "nb" ? "nb-NO" : "en") : ""}</Typography></Box>}
|
||||
secondary={<Box component="span" sx={{ mt: 0.5, display: "block" }}>
|
||||
<Typography component="span" variant="body2" sx={{ color: "text.primary", display: "block" }}>{t("correspondenceFromLabel", { value: message.from || t("correspondenceUnknown") })}</Typography>
|
||||
<Typography component="span" variant="body2" sx={{ color: "text.secondary", mt: 0.25, display: "block" }}>{message.snippet}</Typography>
|
||||
|
||||
@@ -654,7 +654,7 @@ export default function DashboardView() {
|
||||
<Box key={series.tag}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 0.5, gap: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 800, overflowWrap: "anywhere" }}>{series.tag}</Typography>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>{series.counts.reduce((sum, value) => sum + value, 0)} total</Typography>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>{t("dashboardTotalCount", { count: series.counts.reduce((sum, value) => sum + value, 0) })}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: `repeat(${series.counts.length}, 1fr)`, gap: 0.5 }}>
|
||||
{series.counts.map((count, i) => (
|
||||
|
||||
@@ -771,7 +771,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
<Table size="small" sx={{ minWidth: 980 }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell padding="checkbox"><Checkbox checked={selectedAllOnPage} indeterminate={selectedIds.length > 0 && !selectedAllOnPage} onChange={(e) => toggleSelectAll(e.target.checked)} /></TableCell>
|
||||
<TableCell padding="checkbox"><Checkbox inputProps={{ "aria-label": t("jobTableSelectAll") }} checked={selectedAllOnPage} indeterminate={selectedIds.length > 0 && !selectedAllOnPage} onChange={(e) => toggleSelectAll(e.target.checked)} /></TableCell>
|
||||
<TableCell sortDirection={sortBy === "company" ? sortDir : false}><TableSortLabel active={sortBy === "company"} direction={sortBy === "company" ? sortDir : "asc"} onClick={() => requestSort("company")}>{t("jobTableCompany")}</TableSortLabel></TableCell>
|
||||
<TableCell sortDirection={sortBy === "jobTitle" ? sortDir : false}><TableSortLabel active={sortBy === "jobTitle"} direction={sortBy === "jobTitle" ? sortDir : "asc"} onClick={() => requestSort("jobTitle")}>{t("jobTableRole")}</TableSortLabel></TableCell>
|
||||
<TableCell sortDirection={sortBy === "location" ? sortDir : false}><TableSortLabel active={sortBy === "location"} direction={sortBy === "location" ? sortDir : "asc"} onClick={() => requestSort("location")}>{t("jobTableLocation")}</TableSortLabel></TableCell>
|
||||
@@ -809,7 +809,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
"&:focus-visible": { outline: "3px solid", outlineColor: "primary.main", outlineOffset: -3 },
|
||||
}}
|
||||
>
|
||||
<TableCell padding="checkbox"><Checkbox checked={isSelected} onChange={(e) => toggleSelected(job.id, e.target.checked)} /></TableCell>
|
||||
<TableCell padding="checkbox"><Checkbox inputProps={{ "aria-label": t("jobTableSelectJob", { title: job.jobTitle }) }} checked={isSelected} onChange={(e) => toggleSelected(job.id, e.target.checked)} /></TableCell>
|
||||
<TableCell sx={{ minWidth: 140, fontWeight: 700 }}>{job.company?.name ?? ""}</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { Alert, Button, IconButton, Stack, Typography } from "@mui/material";
|
||||
import { useOptionalI18n } from "../i18n/I18nProvider";
|
||||
|
||||
type Props = {
|
||||
featureKey: string;
|
||||
@@ -13,6 +14,7 @@ function storageKey(featureKey: string) {
|
||||
}
|
||||
|
||||
export default function ProFeatureNotice({ featureKey, title, children }: Props) {
|
||||
const i18n = useOptionalI18n();
|
||||
const [dismissed, setDismissed] = useState(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
try {
|
||||
@@ -38,8 +40,8 @@ export default function ProFeatureNotice({ featureKey, title, children }: Props)
|
||||
severity="info"
|
||||
action={(
|
||||
<Stack direction="row" spacing={0.5} alignItems="center">
|
||||
<Button href="/settings" size="small">View Pro</Button>
|
||||
<IconButton size="small" aria-label={`Dismiss ${title}`} onClick={dismiss}>
|
||||
<Button href="/settings" size="small">{i18n?.t("proFeatureView") ?? "View Pro"}</Button>
|
||||
<IconButton size="small" aria-label={i18n?.t("proFeatureDismiss", { title }) ?? `Dismiss ${title}`} onClick={dismiss}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from "react";
|
||||
|
||||
import { Autocomplete, Chip, TextField } from "@mui/material";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
export default function TagsInput({
|
||||
label = "Tags",
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
@@ -11,6 +12,7 @@ export default function TagsInput({
|
||||
value: string[];
|
||||
onChange: (next: string[]) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<Autocomplete
|
||||
multiple
|
||||
@@ -28,7 +30,7 @@ export default function TagsInput({
|
||||
<Chip variant="outlined" label={option} {...getTagProps({ index })} key={option} size="small" />
|
||||
))
|
||||
}
|
||||
renderInput={(params) => <TextField {...params} label={label} placeholder="Add a tag and press Enter" />}
|
||||
renderInput={(params) => <TextField {...params} label={label ?? t("cvEditorTags")} placeholder={t("tagsInputPlaceholder")} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export const translations = {
|
||||
en: {
|
||||
appTitle: "Jobbjakt",
|
||||
appTagline: "Track your hunt",
|
||||
applicationLanguage: "Application language",
|
||||
dashboard: "Dashboard",
|
||||
jobApplications: "Job Applications",
|
||||
reminders: "Reminders",
|
||||
@@ -898,6 +899,12 @@ export const translations = {
|
||||
addJobModalTailoredCvRequiresPro: "Tailored CV generation requires Pro",
|
||||
addJobModalTailoredCvHelp: "Uses your reviewed Career Profile and keeps the result as an editable suggestion.",
|
||||
addJobModalNoAiHelp: "Create and track the job normally; no AI operation will be started.",
|
||||
tagsInputPlaceholder: "Add a tag and press Enter",
|
||||
proFeatureView: "View Pro",
|
||||
proFeatureDismiss: "Dismiss {title}",
|
||||
registrationUnavailable: "Registration is currently unavailable.",
|
||||
dashboardTotalCount: "{count} total",
|
||||
jobTableSelectJob: "Select {title}",
|
||||
characterCount: "{count} characters",
|
||||
back: "Back",
|
||||
continue: "Continue",
|
||||
@@ -2275,6 +2282,7 @@ export const translations = {
|
||||
nb: {
|
||||
appTitle: "Jobbjakt",
|
||||
appTagline: "Hold oversikt over jobbsøkingen",
|
||||
applicationLanguage: "Språk i appen",
|
||||
dashboard: "Dashboard",
|
||||
jobApplications: "Jobbsøknader",
|
||||
reminders: "Påminnelser",
|
||||
@@ -3169,6 +3177,12 @@ export const translations = {
|
||||
addJobModalTailoredCvRequiresPro: "Tilpasset CV-generering krever Pro",
|
||||
addJobModalTailoredCvHelp: "Bruker den gjennomgåtte Karriereprofilen og beholder resultatet som et redigerbart forslag.",
|
||||
addJobModalNoAiHelp: "Opprett og spor jobben som vanlig; ingen KI-operasjon startes.",
|
||||
tagsInputPlaceholder: "Legg til en tagg og trykk Enter",
|
||||
proFeatureView: "Se Pro",
|
||||
proFeatureDismiss: "Lukk {title}",
|
||||
registrationUnavailable: "Registrering er ikke tilgjengelig for øyeblikket.",
|
||||
dashboardTotalCount: "{count} totalt",
|
||||
jobTableSelectJob: "Velg {title}",
|
||||
characterCount: "{count} tegn",
|
||||
back: "Tilbake",
|
||||
continue: "Fortsett",
|
||||
|
||||
@@ -557,8 +557,9 @@ export default function AppShell({
|
||||
}
|
||||
|
||||
export function StackLanguageToggle({ language, setLanguage }: { language: "en" | "nb"; setLanguage: (language: "en" | "nb") => void }) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<ButtonGroup size="small" variant="outlined" aria-label="Application language" sx={{ flex: "0 0 auto", "& .MuiButton-root": { minWidth: 36, px: 0.6, fontWeight: 800 } }}>
|
||||
<ButtonGroup size="small" variant="outlined" aria-label={t("applicationLanguage")} sx={{ flex: "0 0 auto", "& .MuiButton-root": { minWidth: 36, px: 0.6, fontWeight: 800 } }}>
|
||||
<Button aria-label="English" aria-pressed={language === "en"} variant={language === "en" ? "contained" : "outlined"} onClick={() => setLanguage("en")}>EN</Button>
|
||||
<Button aria-label="Norsk" aria-pressed={language === "nb"} variant={language === "nb" ? "contained" : "outlined"} onClick={() => setLanguage("nb")}>NO</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -163,7 +163,7 @@ export default function LoginPage({ initialMode = "login" }: { initialMode?: "lo
|
||||
onSubmit={(e) => { e.preventDefault(); void submit(registerMode ? "register" : "login"); }}
|
||||
sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}
|
||||
>
|
||||
{registerMode && cfg && !allowReg ? <Alert severity="info">Registration is currently unavailable.</Alert> : null}
|
||||
{registerMode && cfg && !allowReg ? <Alert severity="info">{t("registrationUnavailable")}</Alert> : null}
|
||||
{cfg?.requireEmailVerification && emailNotVerified && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
|
||||
@@ -162,8 +162,8 @@ export default function ProfilePage() {
|
||||
/>
|
||||
|
||||
{loadError ? (
|
||||
<Alert severity="error" sx={{ mb: 2, borderRadius: 2.5 }} action={<Button color="inherit" size="small" onClick={() => void loadProfile()}>Retry</Button>}>
|
||||
Unable to load profile.
|
||||
<Alert severity="error" sx={{ mb: 2, borderRadius: 2.5 }} action={<Button color="inherit" size="small" onClick={() => void loadProfile()}>{t("retry")}</Button>}>
|
||||
{t("profileLoadFailed")}
|
||||
<Typography variant="body2" sx={{ mt: 0.5 }}>{loadError}</Typography>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user