- {(
- [
- ['Available', stats.available],
- ['Fixed', stats.fixed],
- ['False Positive', stats.falsePositive],
- ['Skipped', stats.skipped],
- ['Already Fixed', stats.alreadyFixed],
- ["Can't Complete", stats.tooHard],
- ['Disabled', stats.disabled],
- ] as const
- ).map(([label, value]) => (
-
@@ -161,6 +205,7 @@ const StatisticsDialogContent = ({
}
export const ManageChallengeDetailContent = () => {
+ const { t } = useIntl()
const { challengeId } = useParams({ from: '/_app/manage/challenge/$challengeId/' })
const { data: challengeData, isLoading: isLoadingChallenge } = api.challenge.getChallenge(
@@ -180,14 +225,23 @@ export const ManageChallengeDetailContent = () => {
() =>
projectId != null
? [
- { label: 'create & manage', href: '/manage' },
- { label: 'projects', href: '/manage/projects' },
+ {
+ label: t('common.createManage', undefined, 'create & manage'),
+ href: '/manage',
+ },
+ {
+ label: t('common.projects2', undefined, 'projects'),
+ href: '/manage/projects',
+ },
{ label: String(projectId), href: `/manage/project/${projectId}` },
- { label: 'challenges', href: '/manage/challenges' },
+ {
+ label: t('common.challenges2', undefined, 'challenges'),
+ href: '/manage/challenges',
+ },
{ label: challengeId, href: `/manage/challenge/${challengeId}` },
]
: null,
- [projectId, challengeId]
+ [projectId, challengeId, t]
)
useSetBreadcrumbContext(breadcrumbs)
@@ -202,7 +256,7 @@ export const ManageChallengeDetailContent = () => {
@@ -216,7 +270,9 @@ export const ManageChallengeDetailContent = () => {
@@ -275,7 +343,7 @@ export const ManageChallengeDetailContent = () => {
className="w-full justify-start gap-2 rounded-full"
>
- Browse challenge
+ {t('common.browseChallenge', undefined, 'Browse challenge')}
{
className="w-full justify-start gap-2 rounded-full"
>
- Configure prioritization
+ {t(
+ 'manageChallengeDetail.detail.configurePrioritization',
+ undefined,
+ 'Configure prioritization'
+ )}
@@ -311,14 +383,14 @@ export const ManageChallengeDetailContent = () => {
onClick={() => setRebuildOpen(true)}
>
- Rebuild tasks
+ {t('common.rebuildTasks', undefined, 'Rebuild tasks')}
)}
}
- label="Statistics"
- title="Statistics"
+ label={t('manageChallengeDetail.detail.statisticsLabel', undefined, 'Statistics')}
+ title={t('manageChallengeDetail.detail.statisticsLabel', undefined, 'Statistics')}
>
{
{!isLoadingChallenge && challengeData?.id && (
}
- label="Recent Activity"
- title="Recent Activity"
+ label={t('common.recentActivity', undefined, 'Recent Activity')}
+ title={t('common.recentActivity', undefined, 'Recent Activity')}
>
@@ -341,8 +413,8 @@ export const ManageChallengeDetailContent = () => {
challengeData.description !== challengeData.blurb && (
}
- label="Description"
- title="Description"
+ label={t('common.description', undefined, 'Description')}
+ title={t('common.description', undefined, 'Description')}
>
{challengeData.description}
@@ -353,8 +425,8 @@ export const ManageChallengeDetailContent = () => {
{!isLoadingChallenge && challengeData?.instruction && (
}
- label="Instructions"
- title="Instructions"
+ label={t('common.instructions', undefined, 'Instructions')}
+ title={t('common.instructions', undefined, 'Instructions')}
>
{challengeData.instruction}
diff --git a/src/components/Pages/ManagementPages/ManageChallengeDetail/MiniChallengeMap.tsx b/src/components/Pages/ManagementPages/ManageChallengeDetail/MiniChallengeMap.tsx
index bd53ebd67..dc22d518a 100644
--- a/src/components/Pages/ManagementPages/ManageChallengeDetail/MiniChallengeMap.tsx
+++ b/src/components/Pages/ManagementPages/ManageChallengeDetail/MiniChallengeMap.tsx
@@ -24,6 +24,7 @@ import {
import { MapLoadingIndicator } from '@/components/shared/MapLoadingIndicator'
import { useDrawerPortal } from '@/components/TaskInfoPanel/DrawerPortalContext'
import { TaskInfoDrawer } from '@/components/TaskInfoPanel/TaskInfoDrawer'
+import { useIntl } from '@/i18n'
import type { Bbox2D } from '@/types/Map'
import type { TaskMarker } from '@/types/Task'
@@ -60,6 +61,7 @@ export const MiniChallengeMap = ({
selectedTask = null,
onSelectTask,
}: MiniChallengeMapProps) => {
+ const { t } = useIntl()
const mapId = useId()
const mapRef = useRef(null)
const boundsDebounceRef = useRef | null>(null)
@@ -404,7 +406,11 @@ export const MiniChallengeMap = ({
{
icon: Maximize2,
onClick: zoomToAllTags,
- tooltip: 'Zoom to all tasks',
+ tooltip: t(
+ 'manageChallengeDetail.miniMap.zoomToAllTasksTooltip',
+ undefined,
+ 'Zoom to all tasks'
+ ),
disabled: !mapLoaded,
},
]
diff --git a/src/components/Pages/ManagementPages/ManageChallengeEdit/index.tsx b/src/components/Pages/ManagementPages/ManageChallengeEdit/index.tsx
index 7eb85b332..59bd4bfc7 100644
--- a/src/components/Pages/ManagementPages/ManageChallengeEdit/index.tsx
+++ b/src/components/Pages/ManagementPages/ManageChallengeEdit/index.tsx
@@ -2,15 +2,21 @@ import { useParams } from '@tanstack/react-router'
import { ChallengeForm } from '@/components/Pages/ManagementPages/ManageChallengeNew/ChallengeForm'
import { FormCard, ManageFormLayout } from '@/components/shared/ManageFormLayout'
import { EditChallengeFormProvider, useChallengeFormContext } from '@/contexts/ChallengeFormContext'
+import { useIntl } from '@/i18n'
const EditChallengeLayout = () => {
+ const { t } = useIntl()
const { isLoading } = useChallengeFormContext()
return (
diff --git a/src/components/Pages/ManagementPages/ManageChallengeNew/ChallengeForm.test.tsx b/src/components/Pages/ManagementPages/ManageChallengeNew/ChallengeForm.test.tsx
index d33b2866f..cda68fbac 100644
--- a/src/components/Pages/ManagementPages/ManageChallengeNew/ChallengeForm.test.tsx
+++ b/src/components/Pages/ManagementPages/ManageChallengeNew/ChallengeForm.test.tsx
@@ -1,6 +1,6 @@
-import { cleanup, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { cleanup, render, screen } from '@/test/testUtils'
import type { Challenge } from '@/types/Challenge'
import type { User } from '@/types/User'
import { ChallengeForm } from './ChallengeForm.tsx'
diff --git a/src/components/Pages/ManagementPages/ManageChallengeNew/ChallengeForm.tsx b/src/components/Pages/ManagementPages/ManageChallengeNew/ChallengeForm.tsx
index e6feeb640..4d8cf110d 100644
--- a/src/components/Pages/ManagementPages/ManageChallengeNew/ChallengeForm.tsx
+++ b/src/components/Pages/ManagementPages/ManageChallengeNew/ChallengeForm.tsx
@@ -30,37 +30,78 @@ import {
import { Textarea } from '@/components/ui/Textarea'
import { useAuthContext } from '@/contexts/AuthContext'
import { useChallengeFormContext } from '@/contexts/ChallengeFormContext'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import { cn } from '@/lib/utils'
import type { Challenge } from '@/types/Challenge'
-const baseChallengeFormSchema = z.object({
- projectId: z.number().min(1, 'Please select a project'),
- name: z.string().min(3, 'Challenge name must be at least 3 characters').max(255),
- description: z.string().min(1, 'Description is required'),
- instruction: z.string().min(1, 'Instructions are required'),
- difficulty: z.number().min(1).max(3),
- dataSource: z.enum(['overpass', 'localGeoJSON', 'remoteGeoJSON']),
- overpassQL: z.string().optional().or(z.literal('')),
- localGeoJSON: z.instanceof(File).nullable().optional(),
- remoteGeoJSON: z.string().optional().or(z.literal('')),
- dataOriginDate: z.string().optional().or(z.literal('')),
- automatedEditsCodeAgreement: z.boolean(),
-})
+type T = ReturnType['t']
+
+// Building the schema requires translated validation messages, so it's built
+// from a function (called from within the component, where `t` is available)
+// rather than as a static module-level constant.
+const makeBaseChallengeFormSchema = (t: T) =>
+ z.object({
+ projectId: z
+ .number()
+ .min(1, t('common.pleaseSelectAProject', undefined, 'Please select a project')),
+ name: z
+ .string()
+ .min(
+ 3,
+ t(
+ 'manageChallengeNew.challengeForm.validation.nameMinLength',
+ undefined,
+ 'Challenge name must be at least 3 characters'
+ )
+ )
+ .max(255),
+ description: z
+ .string()
+ .min(
+ 1,
+ t(
+ 'manageChallengeNew.challengeForm.validation.descriptionRequired',
+ undefined,
+ 'Description is required'
+ )
+ ),
+ instruction: z
+ .string()
+ .min(
+ 1,
+ t(
+ 'manageChallengeNew.challengeForm.validation.instructionRequired',
+ undefined,
+ 'Instructions are required'
+ )
+ ),
+ difficulty: z.number().min(1).max(3),
+ dataSource: z.enum(['overpass', 'localGeoJSON', 'remoteGeoJSON']),
+ overpassQL: z.string().optional().or(z.literal('')),
+ localGeoJSON: z.instanceof(File).nullable().optional(),
+ remoteGeoJSON: z.string().optional().or(z.literal('')),
+ dataOriginDate: z.string().optional().or(z.literal('')),
+ automatedEditsCodeAgreement: z.boolean(),
+ })
-export type ChallengeFormValues = z.infer
+export type ChallengeFormValues = z.infer>
// When editing, the challenge's task data already lives on the server, so a
// local GeoJSON re-upload isn't required to save — only enforce it when
// creating. Overpass and remote sources still need their value either way.
-const makeChallengeFormSchema = (isEdit: boolean) =>
- baseChallengeFormSchema.superRefine((data, ctx) => {
+const makeChallengeFormSchema = (isEdit: boolean, t: T) =>
+ makeBaseChallengeFormSchema(t).superRefine((data, ctx) => {
if (data.dataSource === 'overpass') {
if (!data.overpassQL || data.overpassQL.trim().length === 0) {
ctx.addIssue({
code: 'custom',
path: ['overpassQL'],
- message: 'An Overpass query is required',
+ message: t(
+ 'manageChallengeNew.challengeForm.validation.overpassRequired',
+ undefined,
+ 'An Overpass query is required'
+ ),
})
}
} else if (data.dataSource === 'localGeoJSON') {
@@ -68,7 +109,11 @@ const makeChallengeFormSchema = (isEdit: boolean) =>
ctx.addIssue({
code: 'custom',
path: ['localGeoJSON'],
- message: 'Please upload a GeoJSON file',
+ message: t(
+ 'manageChallengeNew.challengeForm.validation.localGeoJSONRequired',
+ undefined,
+ 'Please upload a GeoJSON file'
+ ),
})
}
} else if (data.dataSource === 'remoteGeoJSON') {
@@ -76,7 +121,11 @@ const makeChallengeFormSchema = (isEdit: boolean) =>
ctx.addIssue({
code: 'custom',
path: ['remoteGeoJSON'],
- message: 'A GeoJSON URL is required',
+ message: t(
+ 'manageChallengeNew.challengeForm.validation.remoteGeoJSONRequired',
+ undefined,
+ 'A GeoJSON URL is required'
+ ),
})
}
}
@@ -85,7 +134,11 @@ const makeChallengeFormSchema = (isEdit: boolean) =>
ctx.addIssue({
code: 'custom',
path: ['automatedEditsCodeAgreement'],
- message: 'You must read and accept the Automated Edits code of conduct',
+ message: t(
+ 'manageChallengeNew.challengeForm.validation.agreementRequired',
+ undefined,
+ 'You must read and accept the Automated Edits code of conduct'
+ ),
})
}
})
@@ -133,16 +186,17 @@ interface ProjectPickerFieldProps {
// when it's outside the modal's first page of results (the previous Select
// silently dropped any project beyond the first batch).
const ProjectPickerField = ({ value, onChange, open, onOpenChange }: ProjectPickerFieldProps) => {
+ const { t } = useIntl()
const { data: selectedProject } = api.project.getProject(value > 0 ? value : undefined)
const label = selectedProject
? `${selectedProject.id} - ${selectedProject.displayName || selectedProject.name}`
: value > 0
- ? `Project #${value}`
- : 'Select a project'
+ ? t('common.projectWithId', { id: value }, 'Project #{id}')
+ : t('common.selectAProject', undefined, 'Select a project')
return (
- Project
+ {t('common.project', undefined, 'Project')}
- Select the project this challenge belongs to
+
+ {t(
+ 'manageChallengeNew.challengeForm.projectPicker.description',
+ undefined,
+ 'Select the project this challenge belongs to'
+ )}
+
{
+ const { t } = useIntl()
const { challenge, projectId, onSubmit, onCancel } = useChallengeFormContext()
const { user } = useAuthContext()
const overpassId = useId()
@@ -179,7 +240,7 @@ export const ChallengeForm = () => {
const isEdit = !!challenge
const [pickerOpen, setPickerOpen] = useState(false)
- const resolver = useMemo(() => zodResolver(makeChallengeFormSchema(isEdit)), [isEdit])
+ const resolver = useMemo(() => zodResolver(makeChallengeFormSchema(isEdit, t)), [isEdit, t])
// Drive the form off `values` (not just `defaultValues`) so it reactively
// fills once the challenge query resolves or the cache is refreshed —
// `defaultValues` alone is read only on mount. `keepDirtyValues` keeps any
@@ -205,10 +266,28 @@ export const ChallengeForm = () => {
const handleSubmit = async (values: ChallengeFormValues) => {
try {
await onSubmit(values)
- toast.success(challenge ? 'Challenge updated successfully' : 'Challenge created successfully')
+ toast.success(
+ challenge
+ ? t(
+ 'manageChallengeNew.challengeForm.updateSuccessToast',
+ undefined,
+ 'Challenge updated successfully'
+ )
+ : t(
+ 'manageChallengeNew.challengeForm.createSuccessToast',
+ undefined,
+ 'Challenge created successfully'
+ )
+ )
} catch (error) {
const errorMessage =
- error instanceof Error ? error.message : 'Failed to save challenge. Please try again.'
+ error instanceof Error
+ ? error.message
+ : t(
+ 'manageChallengeNew.challengeForm.saveErrorToast',
+ undefined,
+ 'Failed to save challenge. Please try again.'
+ )
toast.error(errorMessage)
logger.error('Failed to save challenge', { error: String(error) })
}
@@ -241,9 +320,16 @@ export const ChallengeForm = () => {
name="name"
render={({ field }) => (
- Name
+ {t('common.name', undefined, 'Name')}
-
+
@@ -255,10 +341,14 @@ export const ChallengeForm = () => {
name="description"
render={({ field }) => (
- Description
+ {t('common.description', undefined, 'Description')}
@@ -273,10 +363,14 @@ export const ChallengeForm = () => {
name="instruction"
render={({ field }) => (
- Instructions
+ {t('common.instructions', undefined, 'Instructions')}
@@ -291,20 +385,26 @@ export const ChallengeForm = () => {
name="difficulty"
render={({ field }) => (
- Difficulty
+ {t('common.difficulty', undefined, 'Difficulty')}
field.onChange(Number(value))}
defaultValue={field.value?.toString()}
>
-
+
- Easy
- Normal
- Expert
+ {t('common.easy', undefined, 'Easy')}
+ {t('common.normal', undefined, 'Normal')}
+ {t('common.expert', undefined, 'Expert')}
@@ -313,50 +413,80 @@ export const ChallengeForm = () => {
/>
{sourceReadOnly ? (
{dataSource === 'overpass' && (
-
Overpass query
+
+ {t(
+ 'manageChallengeNew.challengeForm.overpassQueryReadOnlyLabel',
+ undefined,
+ 'Overpass query'
+ )}
+
- Overpass queries cannot be edited here. Use Rebuild Tasks when managing your
- challenge to re-run the query and refresh your tasks.
+ {t(
+ 'manageChallengeNew.challengeForm.overpassQueryReadOnlyBody',
+ undefined,
+ 'Overpass queries cannot be edited here. Use Rebuild Tasks when managing your challenge to re-run the query and refresh your tasks.'
+ )}
)}
{dataSource === 'remoteGeoJSON' && (
-
GeoJSON URL
+
+ {t('common.geojsonUrl', undefined, 'GeoJSON URL')}
+
- Remote URLs cannot be edited here. Use Rebuild Tasks when managing your
- challenge to re-download the GeoJSON and refresh your tasks.
+ {t(
+ 'manageChallengeNew.challengeForm.remoteGeoJSONReadOnlyBody',
+ undefined,
+ 'Remote URLs cannot be edited here. Use Rebuild Tasks when managing your challenge to re-download the GeoJSON and refresh your tasks.'
+ )}
)}
{dataSource === 'localGeoJSON' && (
-
Uploaded GeoJSON file
+
+ {t(
+ 'manageChallengeNew.challengeForm.localGeoJSONReadOnlyLabel',
+ undefined,
+ 'Uploaded GeoJSON file'
+ )}
+
- This challenge was built from an uploaded GeoJSON file, which can't be shown
- here. To replace it with fresh GeoJSON, use Rebuild Tasks when managing your
- challenge.
+ {t(
+ 'manageChallengeNew.challengeForm.localGeoJSONReadOnlyBody',
+ undefined,
+ "This challenge was built from an uploaded GeoJSON file, which can't be shown here. To replace it with fresh GeoJSON, use Rebuild Tasks when managing your challenge."
+ )}
)}
@@ -385,10 +515,19 @@ export const ChallengeForm = () => {
>
-
I want to provide an Overpass query
+
+ {t(
+ 'manageChallengeNew.challengeForm.overpassOptionTitle',
+ undefined,
+ 'I want to provide an Overpass query'
+ )}
+
- Use Overpass QL to automatically generate tasks from OpenStreetMap
- data
+ {t(
+ 'manageChallengeNew.challengeForm.overpassOptionDescription',
+ undefined,
+ 'Use Overpass QL to automatically generate tasks from OpenStreetMap data'
+ )}
@@ -407,9 +546,19 @@ export const ChallengeForm = () => {
className="mt-1"
/>
-
I want to upload a GeoJSON file
+
+ {t(
+ 'manageChallengeNew.challengeForm.localGeoJSONOptionTitle',
+ undefined,
+ 'I want to upload a GeoJSON file'
+ )}
+
- Upload a GeoJSON file from your computer
+ {t(
+ 'manageChallengeNew.challengeForm.localGeoJSONOptionDescription',
+ undefined,
+ 'Upload a GeoJSON file from your computer'
+ )}
@@ -428,9 +577,19 @@ export const ChallengeForm = () => {
className="mt-1"
/>
-
I have a URL to the GeoJSON data
+
+ {t(
+ 'manageChallengeNew.challengeForm.remoteGeoJSONOptionTitle',
+ undefined,
+ 'I have a URL to the GeoJSON data'
+ )}
+
- Provide a URL pointing to a GeoJSON file
+ {t(
+ 'manageChallengeNew.challengeForm.remoteGeoJSONOptionDescription',
+ undefined,
+ 'Provide a URL pointing to a GeoJSON file'
+ )}
@@ -448,7 +607,13 @@ export const ChallengeForm = () => {
name="overpassQL"
render={({ field }) => (
- Overpass QL
+
+ {t(
+ 'manageChallengeNew.challengeForm.overpassQLLabel',
+ undefined,
+ 'Overpass QL'
+ )}
+
- Overpass query language to automatically generate tasks for this
- challenge. Please see the{' '}
+ {t(
+ 'manageChallengeNew.challengeForm.overpassQLDescriptionBefore',
+ undefined,
+ 'Overpass query language to automatically generate tasks for this challenge. Please see the'
+ )}{' '}
- docs
+ {t('manageChallengeNew.challengeForm.docsLinkText', undefined, 'docs')}
{' '}
- for important details and common pitfalls when creating challenges using
- Overpass queries.
+ {t(
+ 'manageChallengeNew.challengeForm.overpassQLDescriptionAfter',
+ undefined,
+ 'for important details and common pitfalls when creating challenges using Overpass queries.'
+ )}
@@ -482,7 +653,13 @@ export const ChallengeForm = () => {
name="localGeoJSON"
render={({ field: { value, onChange, ...field } }) => (
- GeoJSON File
+
+ {t(
+ 'manageChallengeNew.challengeForm.geoJSONFileLabel',
+ undefined,
+ 'GeoJSON File'
+ )}
+
{
/>
{value && (
- Selected: {value.name} ({(value.size / 1024).toFixed(2)} KB)
+ {t(
+ 'manageChallengeNew.challengeForm.geoJSONFileSelected',
+ { name: value.name, size: (value.size / 1024).toFixed(2) },
+ 'Selected: {name} ({size} KB)'
+ )}
)}
- Upload a GeoJSON file from your computer. Standard GeoJSON and{' '}
+ {t(
+ 'manageChallengeNew.challengeForm.geoJSONFileDescriptionBefore',
+ undefined,
+ 'Upload a GeoJSON file from your computer. Standard GeoJSON and'
+ )}{' '}
- line-by-line GeoJSON format
+ {t(
+ 'manageChallengeNew.challengeForm.lineByLineGeoJSONLinkText',
+ undefined,
+ 'line-by-line GeoJSON format'
+ )}
{' '}
- are supported.
+ {t(
+ 'manageChallengeNew.challengeForm.geoJSONFileDescriptionAfter',
+ undefined,
+ 'are supported.'
+ )}
@@ -526,7 +719,7 @@ export const ChallengeForm = () => {
name="remoteGeoJSON"
render={({ field }) => (
- GeoJSON URL
+ {t('common.geojsonUrl', undefined, 'GeoJSON URL')}
{
/>
- Provide a URL pointing to a GeoJSON file. The URL should point directly to
- the raw GeoJSON file, not a page that contains a link to the file.
+ {t(
+ 'manageChallengeNew.challengeForm.remoteGeoJSONDescription',
+ undefined,
+ 'Provide a URL pointing to a GeoJSON file. The URL should point directly to the raw GeoJSON file, not a page that contains a link to the file.'
+ )}
@@ -549,22 +745,35 @@ export const ChallengeForm = () => {
{!isEdit && (
- You are about to create a MapRoulette challenge. With this power comes
- responsibility. Make sure that your Challenge is designed to encourage careful
- human attention to each task, in the spirit of OpenStreetMap's{' '}
+ {t(
+ 'manageChallengeNew.challengeForm.agreementDescriptionBefore',
+ undefined,
+ "You are about to create a MapRoulette challenge. With this power comes responsibility. Make sure that your Challenge is designed to encourage careful human attention to each task, in the spirit of OpenStreetMap's"
+ )}{' '}
- Automated Edits code of conduct
+ {t(
+ 'manageChallengeNew.challengeForm.agreementLinkText',
+ undefined,
+ 'Automated Edits code of conduct'
+ )}
- . Please read this document carefully. By checking the box below, you acknowledge
- that you understand and accept this responsibility.
+ {t(
+ 'manageChallengeNew.challengeForm.agreementDescriptionAfter',
+ undefined,
+ '. Please read this document carefully. By checking the box below, you acknowledge that you understand and accept this responsibility.'
+ )}
>
}
>
@@ -582,7 +791,11 @@ export const ChallengeForm = () => {
- I have read and understand the OSM Automated Edits code of conduct
+ {t(
+ 'manageChallengeNew.challengeForm.agreementCheckboxLabel',
+ undefined,
+ 'I have read and understand the OSM Automated Edits code of conduct'
+ )}
@@ -594,14 +807,14 @@ export const ChallengeForm = () => {
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
{form.formState.isSubmitting
- ? 'Saving...'
+ ? t('common.saving2', undefined, 'Saving...')
: challenge
- ? 'Update Challenge'
- : 'Create Challenge'}
+ ? t('manageChallengeNew.challengeForm.updateButton', undefined, 'Update Challenge')
+ : t('common.createChallenge', undefined, 'Create Challenge')}
diff --git a/src/components/Pages/ManagementPages/ManageChallengeNew/index.tsx b/src/components/Pages/ManagementPages/ManageChallengeNew/index.tsx
index 3a95ad4f6..7708e52a2 100644
--- a/src/components/Pages/ManagementPages/ManageChallengeNew/index.tsx
+++ b/src/components/Pages/ManagementPages/ManageChallengeNew/index.tsx
@@ -1,18 +1,25 @@
import { ChallengeForm } from '@/components/Pages/ManagementPages/ManageChallengeNew/ChallengeForm'
import { FormCard, ManageFormLayout } from '@/components/shared/ManageFormLayout'
import { CreateChallengeFormProvider } from '@/contexts/ChallengeFormContext'
+import { useIntl } from '@/i18n'
interface ManageChallengeNewProps {
projectId?: number
}
export const ManageChallengeNew = ({ projectId }: ManageChallengeNewProps) => {
+ const { t } = useIntl()
+
return (
diff --git a/src/components/Pages/ManagementPages/ManageChallenges/ManageChallengesContent.tsx b/src/components/Pages/ManagementPages/ManageChallenges/ManageChallengesContent.tsx
index 0abe77dee..33ea63d8f 100644
--- a/src/components/Pages/ManagementPages/ManageChallenges/ManageChallengesContent.tsx
+++ b/src/components/Pages/ManagementPages/ManageChallenges/ManageChallengesContent.tsx
@@ -42,11 +42,13 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/DropdownMenu'
import { useSetHeaderActionsContext } from '@/contexts/HeaderActionsContext'
+import { useIntl } from '@/i18n'
import { cn } from '@/lib/utils'
import type { Challenge } from '@/types/Challenge'
import { useManageChallengesContext } from './ManageChallengesContext'
export const ManageChallengesContent = () => {
+ const { t } = useIntl()
const {
filteredChallenges,
isLoading,
@@ -74,7 +76,7 @@ export const ManageChallengesContent = () => {
- Create Challenge
+ {t('common.createChallenge', undefined, 'Create Challenge')}
)
@@ -92,8 +94,16 @@ export const ManageChallengesContent = () => {
e.preventDefault()
toggleChallengePin(challenge.id)
}}
- title={isPinned ? 'Unpin challenge' : 'Pin challenge'}
- aria-label={isPinned ? 'Unpin challenge' : 'Pin challenge'}
+ title={
+ isPinned
+ ? t('common.unpinChallenge', undefined, 'Unpin challenge')
+ : t('common.pinChallenge', undefined, 'Pin challenge')
+ }
+ aria-label={
+ isPinned
+ ? t('common.unpinChallenge', undefined, 'Unpin challenge')
+ : t('common.pinChallenge', undefined, 'Pin challenge')
+ }
>
{
e.preventDefault()
toggleChallengeEnabled(challenge)
}}
- title={challenge.enabled ? 'Make not discoverable' : 'Make discoverable'}
- aria-label={challenge.enabled ? 'Make not discoverable' : 'Make discoverable'}
+ title={
+ challenge.enabled
+ ? t('common.makeNotDiscoverable', undefined, 'Make not discoverable')
+ : t('common.makeDiscoverable', undefined, 'Make discoverable')
+ }
+ aria-label={
+ challenge.enabled
+ ? t('common.makeNotDiscoverable', undefined, 'Make not discoverable')
+ : t('common.makeDiscoverable', undefined, 'Make discoverable')
+ }
>
{challenge.enabled ? (
@@ -128,7 +146,7 @@ export const ManageChallengesContent = () => {
- Open menu
+ {t('common.openMenu', undefined, 'Open menu')}
@@ -140,7 +158,7 @@ export const ManageChallengesContent = () => {
className="flex cursor-pointer items-center gap-2"
>
- Start challenge
+ {t('common.startChallenge', undefined, 'Start challenge')}
)}
@@ -151,7 +169,7 @@ export const ManageChallengesContent = () => {
className="flex cursor-pointer items-center gap-2"
>
- Edit challenge
+ {t('common.editChallenge', undefined, 'Edit challenge')}
{challenge.id != null && (
@@ -160,7 +178,9 @@ export const ManageChallengesContent = () => {
className="flex cursor-pointer items-center gap-2"
>
- {challenge.isArchived ? 'Unarchive challenge' : 'Archive challenge'}
+ {challenge.isArchived
+ ? t('common.unarchiveChallenge', undefined, 'Unarchive challenge')
+ : t('common.archiveChallenge', undefined, 'Archive challenge')}
)}
{challenge.id != null && (
@@ -169,7 +189,7 @@ export const ManageChallengesContent = () => {
className="flex cursor-pointer items-center gap-2"
>
- Rebuild tasks
+ {t('common.rebuildTasks', undefined, 'Rebuild tasks')}
)}
{
className="flex cursor-pointer items-center gap-2"
>
- Copy URL
+ {t('common.copyUrl', undefined, 'Copy URL')}
{challenge.id != null && (
<>
@@ -190,7 +210,7 @@ export const ManageChallengesContent = () => {
className="flex cursor-pointer items-center gap-2 text-red-600 focus:text-red-600 dark:text-red-400 dark:focus:text-red-400"
>
- Delete challenge
+ {t('common.deleteChallenge', undefined, 'Delete challenge')}
>
)}
@@ -209,11 +229,18 @@ export const ManageChallengesContent = () => {
- About Challenges
+ {t(
+ 'manageChallenges.content.aboutChallengesTitle',
+ undefined,
+ 'About Challenges'
+ )}
- Challenges contain tasks that mappers work through to improve OpenStreetMap
- data.
+ {t(
+ 'manageChallenges.content.aboutChallengesBody',
+ undefined,
+ 'Challenges contain tasks that mappers work through to improve OpenStreetMap data.'
+ )}
{
size="icon"
className="h-8 w-8 shrink-0"
onClick={() => setShowPanel(false)}
- title="Hide panel"
+ title={t('common.hidePanel', undefined, 'Hide panel')}
>
@@ -231,27 +258,50 @@ export const ManageChallengesContent = () => {
- Write clear instructions
+ {t(
+ 'manageChallenges.content.tipWriteClearInstructionsTitle',
+ undefined,
+ 'Write clear instructions'
+ )}
- Good task instructions help mappers understand what to fix and how. Include
- examples and link to relevant wiki pages.
+ {t(
+ 'manageChallenges.content.tipWriteClearInstructionsBody',
+ undefined,
+ 'Good task instructions help mappers understand what to fix and how. Include examples and link to relevant wiki pages.'
+ )}
- Set appropriate difficulty
+ {t(
+ 'manageChallenges.content.tipSetDifficultyTitle',
+ undefined,
+ 'Set appropriate difficulty'
+ )}
- Match difficulty to the skill required. Easy tasks attract new mappers, while
- expert tasks get routed to experienced contributors.
+ {t(
+ 'manageChallenges.content.tipSetDifficultyBody',
+ undefined,
+ 'Match difficulty to the skill required. Easy tasks attract new mappers, while expert tasks get routed to experienced contributors.'
+ )}
-
Monitor progress
+
+ {t(
+ 'manageChallenges.content.tipMonitorProgressTitle',
+ undefined,
+ 'Monitor progress'
+ )}
+
- Check completion rates and review feedback. Archive challenges once all tasks
- are resolved.
+ {t(
+ 'manageChallenges.content.tipMonitorProgressBody',
+ undefined,
+ 'Check completion rates and review feedback. Archive challenges once all tasks are resolved.'
+ )}
@@ -259,7 +309,7 @@ export const ManageChallengesContent = () => {
- Quick Links
+ {t('manageChallenges.content.quickLinks', undefined, 'Quick Links')}
@@ -295,7 +349,7 @@ export const ManageChallengesContent = () => {
size="icon"
className="h-9 w-9 shrink-0"
onClick={() => setShowPanel(true)}
- title="Show panel"
+ title={t('common.showPanel', undefined, 'Show panel')}
>
@@ -303,23 +357,23 @@ export const ManageChallengesContent = () => {
{
getItemKey={(challenge) => challenge.id ?? crypto.randomUUID()}
emptyState={{
icon: ListChecks,
- title: 'No challenges found',
- description: "You haven't created any challenges yet",
- actionLabel: 'Create Challenge',
+ title: t('common.noChallengesFound', undefined, 'No challenges found'),
+ description: t(
+ 'manageChallenges.content.emptyDescription',
+ undefined,
+ "You haven't created any challenges yet"
+ ),
+ actionLabel: t('common.createChallenge', undefined, 'Create Challenge'),
actionTo: '/manage/challenge/new',
}}
/>
@@ -375,18 +433,24 @@ export const ManageChallengesContent = () => {
>
- Delete challenge?
+
+ {t('common.deleteChallenge2', undefined, 'Delete challenge?')}
+
- This will delete this challenge and all its tasks. This action cannot be undone.
+ {t(
+ 'common.deleteChallengeWarning',
+ undefined,
+ 'This will delete this challenge and all its tasks. This action cannot be undone.'
+ )}
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
- Delete
+ {t('common.delete', undefined, 'Delete')}
diff --git a/src/components/Pages/ManagementPages/ManageHome/index.tsx b/src/components/Pages/ManagementPages/ManageHome/index.tsx
index 0dce2dcf3..ac004640e 100644
--- a/src/components/Pages/ManagementPages/ManageHome/index.tsx
+++ b/src/components/Pages/ManagementPages/ManageHome/index.tsx
@@ -3,9 +3,11 @@ import { CheckSquare, FolderKanban, ListChecks } from 'lucide-react'
import { Button } from '@/components/ui/Button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'
import { useAuthContext } from '@/contexts/AuthContext'
+import { useIntl } from '@/i18n'
import { isSuperUser } from '@/lib/SuperAdminGuard'
export const ManageHome = () => {
+ const { t } = useIntl()
const { user } = useAuthContext()
const showTasksCard = user && isSuperUser(user)
@@ -18,12 +20,18 @@ export const ManageHome = () => {
- Projects
- Create and manage your MapRoulette projects
+ {t('common.projects', undefined, 'Projects')}
+
+ {t(
+ 'manageHome.projectsDescription',
+ undefined,
+ 'Create and manage your MapRoulette projects'
+ )}
+
- View Projects
+ {t('common.viewProjects', undefined, 'View Projects')}
@@ -35,12 +43,18 @@ export const ManageHome = () => {
- Challenges
- Browse and manage all your challenges
+ {t('common.challenges', undefined, 'Challenges')}
+
+ {t(
+ 'manageHome.challengesDescription',
+ undefined,
+ 'Browse and manage all your challenges'
+ )}
+
- View Challenges
+ {t('common.viewChallenges', undefined, 'View Challenges')}
@@ -53,12 +67,18 @@ export const ManageHome = () => {
- Tasks
- Open a task by ID to view or edit it
+ {t('common.tasks', undefined, 'Tasks')}
+
+ {t(
+ 'manageHome.tasksDescription',
+ undefined,
+ 'Open a task by ID to view or edit it'
+ )}
+
- Open task by ID
+ {t('common.openTaskById', undefined, 'Open task by ID')}
diff --git a/src/components/Pages/ManagementPages/ManageProjectDetail/ChallengesTableView.tsx b/src/components/Pages/ManagementPages/ManageProjectDetail/ChallengesTableView.tsx
index 143fac766..74b8b1987 100644
--- a/src/components/Pages/ManagementPages/ManageProjectDetail/ChallengesTableView.tsx
+++ b/src/components/Pages/ManagementPages/ManageProjectDetail/ChallengesTableView.tsx
@@ -30,6 +30,7 @@ import {
TableRow,
} from '@/components/ui/Table'
import { useMoveChallengeContext } from '@/contexts/MoveChallengeContext'
+import { useIntl } from '@/i18n'
import { getDifficultyLabel } from '@/lib/difficultyLevelData'
import { cn } from '@/lib/utils'
import type { Challenge } from '@/types/Challenge'
@@ -55,24 +56,35 @@ export const ChallengesTableView = ({
onRebuild,
onDelete,
}: ChallengesTableViewProps) => {
+ const { t } = useIntl()
const { openMoveModal } = useMoveChallengeContext()
return (
- Status
+ {t('common.status', undefined, 'Status')}
-
+
- Name
- ID
- Difficulty
- Tasks Left
- Description
- Actions
+ {t('common.name', undefined, 'Name')}
+
+ {t('common.id', undefined, 'ID')}
+
+
+ {t('common.difficulty', undefined, 'Difficulty')}
+
+
+ {t('manageProjectDetail.challengesTable.columnTasksLeft', undefined, 'Tasks Left')}
+
+
+ {t('common.description', undefined, 'Description')}
+
+
+ {t('common.actions', undefined, 'Actions')}
+
@@ -91,8 +103,16 @@ export const ChallengesTableView = ({
size="icon"
className="mx-auto h-8 w-8"
onClick={() => onTogglePin(challenge.id)}
- title={pinned ? 'Unpin challenge' : 'Pin challenge'}
- aria-label={pinned ? 'Unpin challenge' : 'Pin challenge'}
+ title={
+ pinned
+ ? t('common.unpinChallenge', undefined, 'Unpin challenge')
+ : t('common.pinChallenge', undefined, 'Pin challenge')
+ }
+ aria-label={
+ pinned
+ ? t('common.unpinChallenge', undefined, 'Unpin challenge')
+ : t('common.pinChallenge', undefined, 'Pin challenge')
+ }
>
onToggleEnabled(challenge)}
- title={challenge.enabled ? 'Make not discoverable' : 'Make discoverable'}
+ title={
+ challenge.enabled
+ ? t('common.makeNotDiscoverable', undefined, 'Make not discoverable')
+ : t('common.makeDiscoverable', undefined, 'Make discoverable')
+ }
aria-label={
- challenge.enabled ? 'Make not discoverable' : 'Make discoverable'
+ challenge.enabled
+ ? t('common.makeNotDiscoverable', undefined, 'Make not discoverable')
+ : t('common.makeDiscoverable', undefined, 'Make discoverable')
}
>
{challenge.enabled ? (
@@ -153,7 +179,9 @@ export const ChallengesTableView = ({
- Open menu
+
+ {t('common.openMenu', undefined, 'Open menu')}
+
@@ -165,7 +193,7 @@ export const ChallengesTableView = ({
className="flex cursor-pointer items-center gap-2"
>
- Start challenge
+ {t('common.startChallenge', undefined, 'Start challenge')}
)}
@@ -176,7 +204,7 @@ export const ChallengesTableView = ({
className="flex cursor-pointer items-center gap-2"
>
- Edit challenge
+ {t('common.editChallenge', undefined, 'Edit challenge')}
- Move challenge
+ {t('common.moveChallenge', undefined, 'Move challenge')}
{challenge.id != null && (
- Clone challenge
+ {t('common.cloneChallenge2', undefined, 'Clone challenge')}
)}
{challenge.id != null && (
@@ -204,7 +232,9 @@ export const ChallengesTableView = ({
className="flex cursor-pointer items-center gap-2"
>
- {challenge.isArchived ? 'Unarchive challenge' : 'Archive challenge'}
+ {challenge.isArchived
+ ? t('common.unarchiveChallenge', undefined, 'Unarchive challenge')
+ : t('common.archiveChallenge', undefined, 'Archive challenge')}
)}
{challenge.id != null && (
@@ -213,7 +243,7 @@ export const ChallengesTableView = ({
className="flex cursor-pointer items-center gap-2"
>
- Rebuild tasks
+ {t('common.rebuildTasks', undefined, 'Rebuild tasks')}
)}
{challenge.id != null && (
@@ -221,7 +251,9 @@ export const ChallengesTableView = ({
onClick={() => onToggleEnabled(challenge)}
className="flex cursor-pointer items-center gap-2"
>
- {challenge.enabled ? 'Disable challenge' : 'Enable challenge'}
+ {challenge.enabled
+ ? t('common.disableChallenge', undefined, 'Disable challenge')
+ : t('common.enableChallenge', undefined, 'Enable challenge')}
)}
{challenge.id != null && (
@@ -235,7 +267,7 @@ export const ChallengesTableView = ({
)}
>
- Delete challenge
+ {t('common.deleteChallenge', undefined, 'Delete challenge')}
>
)}
diff --git a/src/components/Pages/ManagementPages/ManageProjectDetail/ManageProjectDetailContent.tsx b/src/components/Pages/ManagementPages/ManageProjectDetail/ManageProjectDetailContent.tsx
index f06011b54..2accd34d4 100644
--- a/src/components/Pages/ManagementPages/ManageProjectDetail/ManageProjectDetailContent.tsx
+++ b/src/components/Pages/ManagementPages/ManageProjectDetail/ManageProjectDetailContent.tsx
@@ -48,6 +48,7 @@ import {
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/Resizable'
import { Separator } from '@/components/ui/Separator'
import { useMoveChallengeContext } from '@/contexts/MoveChallengeContext'
+import { useIntl } from '@/i18n'
import { cn } from '@/lib/utils'
import type { Challenge } from '@/types/Challenge'
import { MoveChallengeModal } from '../MoveChallengeModal'
@@ -55,6 +56,7 @@ import { ChallengesTableView } from './ChallengesTableView'
import { useManageProjectDetailContext } from './ManageProjectDetailContext'
export const ManageProjectDetailContent = () => {
+ const { t } = useIntl()
const {
projectId,
project,
@@ -106,8 +108,16 @@ export const ManageProjectDetailContent = () => {
e.preventDefault()
toggleChallengePin(challenge.id)
}}
- title={isPinned ? 'Unpin challenge' : 'Pin challenge'}
- aria-label={isPinned ? 'Unpin challenge' : 'Pin challenge'}
+ title={
+ isPinned
+ ? t('common.unpinChallenge', undefined, 'Unpin challenge')
+ : t('common.pinChallenge', undefined, 'Pin challenge')
+ }
+ aria-label={
+ isPinned
+ ? t('common.unpinChallenge', undefined, 'Unpin challenge')
+ : t('common.pinChallenge', undefined, 'Pin challenge')
+ }
>
{
e.preventDefault()
toggleChallengeEnabled(challenge)
}}
- title={challenge.enabled ? 'Make not discoverable' : 'Make discoverable'}
- aria-label={challenge.enabled ? 'Make not discoverable' : 'Make discoverable'}
+ title={
+ challenge.enabled
+ ? t('common.makeNotDiscoverable', undefined, 'Make not discoverable')
+ : t('common.makeDiscoverable', undefined, 'Make discoverable')
+ }
+ aria-label={
+ challenge.enabled
+ ? t('common.makeNotDiscoverable', undefined, 'Make not discoverable')
+ : t('common.makeDiscoverable', undefined, 'Make discoverable')
+ }
>
{challenge.enabled ? (
@@ -142,7 +160,7 @@ export const ManageProjectDetailContent = () => {
- Open menu
+ {t('common.openMenu', undefined, 'Open menu')}
@@ -154,7 +172,7 @@ export const ManageProjectDetailContent = () => {
className="flex cursor-pointer items-center gap-2"
>
- Start challenge
+ {t('common.startChallenge', undefined, 'Start challenge')}
)}
@@ -165,7 +183,7 @@ export const ManageProjectDetailContent = () => {
className="flex cursor-pointer items-center gap-2"
>
- Edit challenge
+ {t('common.editChallenge', undefined, 'Edit challenge')}
{
className="flex cursor-pointer items-center gap-2"
>
- Move challenge
+ {t('common.moveChallenge', undefined, 'Move challenge')}
{challenge.id != null && (
{
className="flex cursor-pointer items-center gap-2"
>
- Clone challenge
+ {t('common.cloneChallenge2', undefined, 'Clone challenge')}
)}
{challenge.id != null && (
@@ -192,7 +210,9 @@ export const ManageProjectDetailContent = () => {
className="flex cursor-pointer items-center gap-2"
>
- {challenge.isArchived ? 'Unarchive challenge' : 'Archive challenge'}
+ {challenge.isArchived
+ ? t('common.unarchiveChallenge', undefined, 'Unarchive challenge')
+ : t('common.archiveChallenge', undefined, 'Archive challenge')}
)}
{challenge.id != null && (
@@ -201,7 +221,7 @@ export const ManageProjectDetailContent = () => {
className="flex cursor-pointer items-center gap-2"
>
- Rebuild tasks
+ {t('common.rebuildTasks', undefined, 'Rebuild tasks')}
)}
{challenge.id != null && (
@@ -209,7 +229,9 @@ export const ManageProjectDetailContent = () => {
onClick={() => toggleChallengeEnabled(challenge)}
className="flex cursor-pointer items-center gap-2"
>
- {challenge.enabled ? 'Disable challenge' : 'Enable challenge'}
+ {challenge.enabled
+ ? t('common.disableChallenge', undefined, 'Disable challenge')
+ : t('common.enableChallenge', undefined, 'Enable challenge')}
)}
{challenge.id != null && (
@@ -220,7 +242,7 @@ export const ManageProjectDetailContent = () => {
className="flex cursor-pointer items-center gap-2 text-red-600 focus:text-red-600 dark:text-red-400 dark:focus:text-red-400"
>
- Delete challenge
+ {t('common.deleteChallenge', undefined, 'Delete challenge')}
>
)}
@@ -244,14 +266,14 @@ export const ManageProjectDetailContent = () => {
{project?.featured && (
- Featured
+ {t('common.featured', undefined, 'Featured')}
)}
{project?.isArchived && (
- Archived
+ {t('common.archived', undefined, 'Archived')}
)}
@@ -268,7 +290,9 @@ export const ManageProjectDetailContent = () => {
•
- ID {projectId}
+
+ {t('common.idNumber', { id: projectId }, 'ID {id}')}
+
)}
@@ -291,7 +315,11 @@ export const ManageProjectDetailContent = () => {
className="w-full justify-start gap-2 rounded-full"
>
- View project page
+ {t(
+ 'manageProjectDetail.content.viewProjectPage',
+ undefined,
+ 'View project page'
+ )}
{
className="w-full justify-start gap-2 rounded-full"
>
- Edit project
+ {t('common.editProject', undefined, 'Edit project')}
{
>
- Create challenge
+ {t(
+ 'manageProjectDetail.content.createChallenge',
+ undefined,
+ 'Create challenge'
+ )}
{!isLoadingProject && projectData?.id != null && (
@@ -327,7 +359,9 @@ export const ManageProjectDetailContent = () => {
className="w-full justify-start gap-2 rounded-full"
>
- {project?.isArchived ? 'Unarchive project' : 'Archive project'}
+ {project?.isArchived
+ ? t('common.unarchiveProject', undefined, 'Unarchive project')
+ : t('common.archiveProject', undefined, 'Archive project')}
{
) : (
)}
- {project?.enabled ? 'Disable project' : 'Enable project'}
+ {project?.enabled
+ ? t(
+ 'manageProjectDetail.content.disableProject',
+ undefined,
+ 'Disable project'
+ )
+ : t(
+ 'manageProjectDetail.content.enableProject',
+ undefined,
+ 'Enable project'
+ )}
{
className="w-full justify-start gap-2 rounded-full text-red-600 hover:text-red-600 dark:text-red-400 dark:hover:text-red-400"
>
- Delete project
+ {t('common.deleteProject', undefined, 'Delete project')}
>
)}
@@ -362,17 +406,23 @@ export const ManageProjectDetailContent = () => {
{!(isLoadingProject || isLoadingChallenges) && (
<>
- Challenges
+
+ {t('common.challenges', undefined, 'Challenges')}
+
{challengeSummary.total}
- Shown
+
+ {t('common.shown', undefined, 'Shown')}
+
{filteredChallenges.length}
-
Discoverable
+
+ {t('common.discoverable', undefined, 'Discoverable')}
+
{challengeSummary.enabled}
@@ -380,7 +430,7 @@ export const ManageProjectDetailContent = () => {
- Tasks remaining
+ {t('common.tasksRemaining', undefined, 'Tasks remaining')}
{challengeSummary.tasksRemaining}
@@ -394,15 +444,29 @@ export const ManageProjectDetailContent = () => {
{/* Playbook footer */}
- Project Playbook
+ {t('manageProjectDetail.content.playbookTitle', undefined, 'Project Playbook')}
- Confirm challenge instructions and QA expectations are specific and testable.
+ {t(
+ 'manageProjectDetail.content.playbookTip1',
+ undefined,
+ 'Confirm challenge instructions and QA expectations are specific and testable.'
+ )}
-
Review challenge ordering so mappers can move from easier to harder tasks.
- Assign at least one co-manager for triage, support, and archival continuity.
+ {t(
+ 'manageProjectDetail.content.playbookTip2',
+ undefined,
+ 'Review challenge ordering so mappers can move from easier to harder tasks.'
+ )}
+
+
+ {t(
+ 'manageProjectDetail.content.playbookTip3',
+ undefined,
+ 'Assign at least one co-manager for triage, support, and archival continuity.'
+ )}
{
className="mt-3 inline-flex items-center gap-2 text-xs text-zinc-700 hover:underline dark:text-zinc-200"
>
- Open project management docs
+ {t(
+ 'manageProjectDetail.content.playbookDocsLink',
+ undefined,
+ 'Open project management docs'
+ )}
@@ -428,23 +496,23 @@ export const ManageProjectDetailContent = () => {
{
getItemKey={() => ''}
emptyState={{
icon: ListChecks,
- title: 'No challenges found',
- description: 'Get started by creating your first challenge',
- actionLabel: 'Create Challenge',
+ title: t('common.noChallengesFound', undefined, 'No challenges found'),
+ description: t(
+ 'manageProjectDetail.content.emptyDescription',
+ undefined,
+ 'Get started by creating your first challenge'
+ ),
+ actionLabel: t(
+ 'manageProjectDetail.content.createChallenge',
+ undefined,
+ 'Create Challenge'
+ ),
actionTo: '/manage/challenge/new',
actionSearch: { projectId: Number(projectId) },
}}
@@ -517,9 +593,17 @@ export const ManageProjectDetailContent = () => {
getItemKey={(challenge) => challenge.id ?? crypto.randomUUID()}
emptyState={{
icon: ListChecks,
- title: 'No challenges found',
- description: 'Get started by creating your first challenge',
- actionLabel: 'Create Challenge',
+ title: t('common.noChallengesFound', undefined, 'No challenges found'),
+ description: t(
+ 'manageProjectDetail.content.emptyDescription',
+ undefined,
+ 'Get started by creating your first challenge'
+ ),
+ actionLabel: t(
+ 'manageProjectDetail.content.createChallenge',
+ undefined,
+ 'Create Challenge'
+ ),
actionTo: '/manage/challenge/new',
actionSearch: { projectId: Number(projectId) },
}}
@@ -558,18 +642,24 @@ export const ManageProjectDetailContent = () => {
>
- Delete challenge?
+
+ {t('common.deleteChallenge2', undefined, 'Delete challenge?')}
+
- This will delete this challenge and all its tasks. This action cannot be undone.
+ {t(
+ 'common.deleteChallengeWarning',
+ undefined,
+ 'This will delete this challenge and all its tasks. This action cannot be undone.'
+ )}
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
- Delete
+ {t('common.delete', undefined, 'Delete')}
@@ -578,19 +668,24 @@ export const ManageProjectDetailContent = () => {
- Delete project?
+
+ {t('common.deleteProject2', undefined, 'Delete project?')}
+
- This will delete this project and all its challenges and tasks. This action cannot be
- undone.
+ {t(
+ 'manageProjectDetail.content.deleteProjectDescription',
+ undefined,
+ 'This will delete this project and all its challenges and tasks. This action cannot be undone.'
+ )}
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
- Delete
+ {t('common.delete', undefined, 'Delete')}
diff --git a/src/components/Pages/ManagementPages/ManageProjectEdit/index.tsx b/src/components/Pages/ManagementPages/ManageProjectEdit/index.tsx
index 4456963bf..d13579455 100644
--- a/src/components/Pages/ManagementPages/ManageProjectEdit/index.tsx
+++ b/src/components/Pages/ManagementPages/ManageProjectEdit/index.tsx
@@ -5,8 +5,10 @@ import {
type ProjectFormValues,
} from '@/components/Pages/ManagementPages/ManageProjectNew/ProjectForm'
import { FormCard, ManageFormLayout } from '@/components/shared/ManageFormLayout'
+import { useIntl } from '@/i18n'
export const ManageProjectEdit = () => {
+ const { t } = useIntl()
const { projectId } = useParams({ from: '/_app/manage/project/$projectId/edit' })
const navigate = useNavigate()
@@ -35,8 +37,12 @@ export const ManageProjectEdit = () => {
return (
diff --git a/src/components/Pages/ManagementPages/ManageProjectNew/ProjectForm.test.tsx b/src/components/Pages/ManagementPages/ManageProjectNew/ProjectForm.test.tsx
index 67f832b57..faab9d6a3 100644
--- a/src/components/Pages/ManagementPages/ManageProjectNew/ProjectForm.test.tsx
+++ b/src/components/Pages/ManagementPages/ManageProjectNew/ProjectForm.test.tsx
@@ -1,6 +1,6 @@
-import { cleanup, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { cleanup, render, screen } from '@/test/testUtils'
import type { User } from '@/types/User'
import { ProjectForm } from './ProjectForm.tsx'
diff --git a/src/components/Pages/ManagementPages/ManageProjectNew/ProjectForm.tsx b/src/components/Pages/ManagementPages/ManageProjectNew/ProjectForm.tsx
index 523013fe4..7386723e4 100644
--- a/src/components/Pages/ManagementPages/ManageProjectNew/ProjectForm.tsx
+++ b/src/components/Pages/ManagementPages/ManageProjectNew/ProjectForm.tsx
@@ -1,4 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod'
+import { useMemo } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
@@ -17,19 +18,46 @@ import { Input } from '@/components/ui/Input'
import { Switch } from '@/components/ui/Switch'
import { Textarea } from '@/components/ui/Textarea'
import { useAuthContext } from '@/contexts/AuthContext'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import { isSuperUser } from '@/lib/SuperAdminGuard'
import type { Project } from '@/types/Project'
-const projectFormSchema = z.object({
- name: z.string().min(1, 'Project name is required').max(255),
- displayName: z.string().min(1, 'Display name is required').max(255),
- description: z.string().optional().or(z.literal('')),
- enabled: z.boolean(),
- featured: z.boolean(),
-})
+type T = ReturnType['t']
-export type ProjectFormValues = z.infer
+// Building the schema requires translated validation messages, so it's built
+// from a function (called from within the component, where `t` is available)
+// rather than as a static module-level constant.
+const makeProjectFormSchema = (t: T) =>
+ z.object({
+ name: z
+ .string()
+ .min(
+ 1,
+ t(
+ 'manageProjectNew.projectForm.validation.nameRequired',
+ undefined,
+ 'Project name is required'
+ )
+ )
+ .max(255),
+ displayName: z
+ .string()
+ .min(
+ 1,
+ t(
+ 'manageProjectNew.projectForm.validation.displayNameRequired',
+ undefined,
+ 'Display name is required'
+ )
+ )
+ .max(255),
+ description: z.string().optional().or(z.literal('')),
+ enabled: z.boolean(),
+ featured: z.boolean(),
+ })
+
+export type ProjectFormValues = z.infer>
interface ProjectFormProps {
project?: Project
@@ -38,11 +66,14 @@ interface ProjectFormProps {
}
export const ProjectForm = ({ project, onSubmit, onCancel }: ProjectFormProps) => {
+ const { t } = useIntl()
const { user } = useAuthContext()
const canSetFeatured = isSuperUser(user)
+ const resolver = useMemo(() => zodResolver(makeProjectFormSchema(t)), [t])
+
const form = useForm({
- resolver: zodResolver(projectFormSchema),
+ resolver,
defaultValues: {
name: project?.name || '',
displayName: project?.displayName || '',
@@ -55,10 +86,28 @@ export const ProjectForm = ({ project, onSubmit, onCancel }: ProjectFormProps) =
const handleSubmit = async (values: ProjectFormValues) => {
try {
await onSubmit(values)
- toast.success(project ? 'Project updated successfully' : 'Project created successfully')
+ toast.success(
+ project
+ ? t(
+ 'manageProjectNew.projectForm.updateSuccessToast',
+ undefined,
+ 'Project updated successfully'
+ )
+ : t(
+ 'manageProjectNew.projectForm.createSuccessToast',
+ undefined,
+ 'Project created successfully'
+ )
+ )
} catch (error) {
const errorMessage =
- error instanceof Error ? error.message : 'Failed to save project. Please try again.'
+ error instanceof Error
+ ? error.message
+ : t(
+ 'manageProjectNew.projectForm.saveErrorToast',
+ undefined,
+ 'Failed to save project. Please try again.'
+ )
toast.error(errorMessage)
logger.error('Failed to save project', { error: String(error) })
}
@@ -72,20 +121,41 @@ export const ProjectForm = ({ project, onSubmit, onCancel }: ProjectFormProps) =
>
(
- Project Name
+
+ {t('manageProjectNew.projectForm.nameLabel', undefined, 'Project Name')}
+
-
+
- A unique identifier for the project (lowercase, no spaces)
+ {t(
+ 'manageProjectNew.projectForm.nameDescription',
+ undefined,
+ 'A unique identifier for the project (lowercase, no spaces)'
+ )}
@@ -97,11 +167,26 @@ export const ProjectForm = ({ project, onSubmit, onCancel }: ProjectFormProps) =
name="displayName"
render={({ field }) => (
- Display Name
+
+ {t('manageProjectNew.projectForm.displayNameLabel', undefined, 'Display Name')}
+
-
+
- The display name shown to users
+
+ {t(
+ 'manageProjectNew.projectForm.displayNameDescription',
+ undefined,
+ 'The display name shown to users'
+ )}
+
)}
@@ -112,15 +197,25 @@ export const ProjectForm = ({ project, onSubmit, onCancel }: ProjectFormProps) =
name="description"
render={({ field }) => (
- Description
+ {t('common.description', undefined, 'Description')}
- A brief description of the project
+
+ {t(
+ 'manageProjectNew.projectForm.descriptionDescription',
+ undefined,
+ 'A brief description of the project'
+ )}
+
)}
@@ -128,8 +223,16 @@ export const ProjectForm = ({ project, onSubmit, onCancel }: ProjectFormProps) =
(
- Enabled
+
+ {t('common.enabled', undefined, 'Enabled')}
+
- Make this project visible and accessible to users
+ {t(
+ 'manageProjectNew.projectForm.enabledDescription',
+ undefined,
+ 'Make this project visible and accessible to users'
+ )}
@@ -156,8 +265,16 @@ export const ProjectForm = ({ project, onSubmit, onCancel }: ProjectFormProps) =
render={({ field }) => (
- Featured
- Feature this project on the homepage
+
+ {t('common.featured', undefined, 'Featured')}
+
+
+ {t(
+ 'manageProjectNew.projectForm.featuredDescription',
+ undefined,
+ 'Feature this project on the homepage'
+ )}
+
@@ -170,14 +287,14 @@ export const ProjectForm = ({ project, onSubmit, onCancel }: ProjectFormProps) =
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
{form.formState.isSubmitting
- ? 'Saving...'
+ ? t('common.saving2', undefined, 'Saving...')
: project
- ? 'Update Project'
- : 'Create Project'}
+ ? t('manageProjectNew.projectForm.updateButton', undefined, 'Update Project')
+ : t('common.createProject', undefined, 'Create Project')}
diff --git a/src/components/Pages/ManagementPages/ManageProjectNew/index.tsx b/src/components/Pages/ManagementPages/ManageProjectNew/index.tsx
index 5c52999b9..0444ec1cd 100644
--- a/src/components/Pages/ManagementPages/ManageProjectNew/index.tsx
+++ b/src/components/Pages/ManagementPages/ManageProjectNew/index.tsx
@@ -5,8 +5,10 @@ import {
type ProjectFormValues,
} from '@/components/Pages/ManagementPages/ManageProjectNew/ProjectForm'
import { FormCard, ManageFormLayout } from '@/components/shared/ManageFormLayout'
+import { useIntl } from '@/i18n'
export const ManageProjectNew = () => {
+ const { t } = useIntl()
const navigate = useNavigate()
const createProjectMutation = api.project.useCreateProject()
@@ -33,8 +35,12 @@ export const ManageProjectNew = () => {
return (
diff --git a/src/components/Pages/ManagementPages/ManageProjects/ManageProjectsContent.tsx b/src/components/Pages/ManagementPages/ManageProjects/ManageProjectsContent.tsx
index 73924d7e2..9d6f8bf5a 100644
--- a/src/components/Pages/ManagementPages/ManageProjects/ManageProjectsContent.tsx
+++ b/src/components/Pages/ManagementPages/ManageProjects/ManageProjectsContent.tsx
@@ -51,12 +51,14 @@ import {
EmptyTitle,
} from '@/components/ui/Empty'
import { useSetHeaderActionsContext } from '@/contexts/HeaderActionsContext'
+import { useIntl } from '@/i18n'
import { cn } from '@/lib/utils'
import type { Project } from '@/types/Project'
import { useManageProjectsContext } from './ManageProjectsContext'
import { ProjectsTableView } from './ProjectsTableView'
export const ManageProjectsContent = () => {
+ const { t } = useIntl()
const {
projectsToShow,
isLoading,
@@ -93,7 +95,7 @@ export const ManageProjectsContent = () => {
- Create Project
+ {t('common.createProject', undefined, 'Create Project')}
)
@@ -111,8 +113,16 @@ export const ManageProjectsContent = () => {
e.preventDefault()
toggleProjectPin(projectId)
}}
- title={isPinned ? 'Unpin project' : 'Pin project'}
- aria-label={isPinned ? 'Unpin project' : 'Pin project'}
+ title={
+ isPinned
+ ? t('common.unpinProject', undefined, 'Unpin project')
+ : t('common.pinProject', undefined, 'Pin project')
+ }
+ aria-label={
+ isPinned
+ ? t('common.unpinProject', undefined, 'Unpin project')
+ : t('common.pinProject', undefined, 'Pin project')
+ }
>
{
e.preventDefault()
updateProject(projectId, { enabled: !(proj.enabled ?? false) })
}}
- title={proj.enabled ? 'Make not discoverable' : 'Make discoverable'}
- aria-label={proj.enabled ? 'Make not discoverable' : 'Make discoverable'}
+ title={
+ proj.enabled
+ ? t('common.makeNotDiscoverable', undefined, 'Make not discoverable')
+ : t('common.makeDiscoverable', undefined, 'Make discoverable')
+ }
+ aria-label={
+ proj.enabled
+ ? t('common.makeNotDiscoverable', undefined, 'Make not discoverable')
+ : t('common.makeDiscoverable', undefined, 'Make discoverable')
+ }
>
{proj.enabled ? (
@@ -147,7 +165,7 @@ export const ManageProjectsContent = () => {
- Open menu
+ {t('common.openMenu', undefined, 'Open menu')}
@@ -158,7 +176,7 @@ export const ManageProjectsContent = () => {
className="flex cursor-pointer items-center gap-2"
>
- View project
+ {t('common.viewProject', undefined, 'View project')}
@@ -168,7 +186,7 @@ export const ManageProjectsContent = () => {
className="flex cursor-pointer items-center gap-2"
>
- Edit project
+ {t('common.editProject', undefined, 'Edit project')}
@@ -178,7 +196,7 @@ export const ManageProjectsContent = () => {
className="flex cursor-pointer items-center gap-2"
>
- Add challenge
+ {t('common.addChallenge', undefined, 'Add challenge')}
{projectId != null && (
@@ -187,7 +205,7 @@ export const ManageProjectsContent = () => {
className="flex cursor-pointer items-center gap-2"
>
- Export CSV
+ {t('common.exportCsv', undefined, 'Export CSV')}
)}
{
className="flex cursor-pointer items-center gap-2"
>
- Copy URL
+ {t('common.copyUrl', undefined, 'Copy URL')}
{projectId != null && (
{
className="flex cursor-pointer items-center gap-2"
>
- {proj.isArchived ? 'Unarchive project' : 'Archive project'}
+ {proj.isArchived
+ ? t('common.unarchiveProject', undefined, 'Unarchive project')
+ : t('common.archiveProject', undefined, 'Archive project')}
)}
{projectId != null && (
@@ -215,7 +235,7 @@ export const ManageProjectsContent = () => {
className="flex cursor-pointer items-center gap-2 text-red-600 focus:text-red-600 dark:text-red-400 dark:focus:text-red-400"
>
- Delete project
+ {t('common.deleteProject', undefined, 'Delete project')}
)}
@@ -233,11 +253,14 @@ export const ManageProjectsContent = () => {
- About Projects
+ {t('manageProjects.content.aboutTitle', undefined, 'About Projects')}
- Good project structure improves mapper clarity, QA consistency, and long-term
- maintenance.
+ {t(
+ 'manageProjects.content.aboutDescription',
+ undefined,
+ 'Good project structure improves mapper clarity, QA consistency, and long-term maintenance.'
+ )}
{
size="icon"
className="h-8 w-8 shrink-0"
onClick={() => setShowPanel(false)}
- title="Hide panel"
+ title={t('common.hidePanel', undefined, 'Hide panel')}
>
@@ -255,27 +278,42 @@ export const ManageProjectsContent = () => {
- Define scope early
+ {t('manageProjects.content.tipScopeTitle', undefined, 'Define scope early')}
- Use a stable naming pattern (region, theme, version) and keep each project
- focused on one clear objective.
+ {t(
+ 'manageProjects.content.tipScopeBody',
+ undefined,
+ 'Use a stable naming pattern (region, theme, version) and keep each project focused on one clear objective.'
+ )}
- Publish intentionally
+ {t(
+ 'manageProjects.content.tipPublishTitle',
+ undefined,
+ 'Publish intentionally'
+ )}
- Keep projects non-discoverable while iterating. Turn discoverable on only
- after instructions and QA checks are validated.
+ {t(
+ 'manageProjects.content.tipPublishBody',
+ undefined,
+ 'Keep projects non-discoverable while iterating. Turn discoverable on only after instructions and QA checks are validated.'
+ )}
-
Share ownership
+
+ {t('manageProjects.content.tipOwnershipTitle', undefined, 'Share ownership')}
+
- Add co-managers before launch so triage, support, and archival
- responsibilities are covered.
+ {t(
+ 'manageProjects.content.tipOwnershipBody',
+ undefined,
+ 'Add co-managers before launch so triage, support, and archival responsibilities are covered.'
+ )}
@@ -283,7 +321,7 @@ export const ManageProjectsContent = () => {
- Helpful Docs
+ {t('manageProjects.content.helpfulDocs', undefined, 'Helpful Docs')}
@@ -330,7 +368,7 @@ export const ManageProjectsContent = () => {
size="icon"
className="h-9 w-9 shrink-0"
onClick={() => setShowPanel(true)}
- title="Show panel"
+ title={t('common.showPanel', undefined, 'Show panel')}
>
@@ -338,28 +376,28 @@ export const ManageProjectsContent = () => {
{
- No projects found
- Get started by creating your first project.
+
+ {t('common.noProjectsFound', undefined, 'No projects found')}
+
+
+ {t(
+ 'manageProjects.content.noProjectsDescription',
+ undefined,
+ 'Get started by creating your first project.'
+ )}
+
- Create Project
+ {t('common.createProject', undefined, 'Create Project')}
@@ -441,9 +487,13 @@ export const ManageProjectsContent = () => {
getItemKey={(proj) => proj.id ?? crypto.randomUUID()}
emptyState={{
icon: FolderKanban,
- title: 'No projects found',
- description: 'Get started by creating your first project',
- actionLabel: 'Create Project',
+ title: t('common.noProjectsFound', undefined, 'No projects found'),
+ description: t(
+ 'manageProjects.content.noProjectsDescriptionShort',
+ undefined,
+ 'Get started by creating your first project'
+ ),
+ actionLabel: t('common.createProject', undefined, 'Create Project'),
actionTo: '/manage/project/new',
}}
/>
@@ -464,10 +514,10 @@ export const ManageProjectsContent = () => {
{isFetching ? (
<>
- Loading...
+ {t('common.loading2', undefined, 'Loading...')}
>
) : (
- 'Load More'
+ t('common.loadMore', undefined, 'Load More')
)}
@@ -475,7 +525,11 @@ export const ManageProjectsContent = () => {
- You've reached the end of the list
+ {t(
+ 'common.youveReachedTheEndOfTheList',
+ undefined,
+ "You've reached the end of the list"
+ )}
))}
@@ -489,19 +543,24 @@ export const ManageProjectsContent = () => {
>
- Delete project?
+
+ {t('common.deleteProject2', undefined, 'Delete project?')}
+
- This will delete the project "{deleteProjectConfirm?.projectName}" and all
- its challenges and tasks. This action cannot be undone.
+ {t(
+ 'manageProjects.content.deleteProjectDescription',
+ { projectName: deleteProjectConfirm?.projectName ?? '' },
+ 'This will delete the project "{projectName}" and all its challenges and tasks. This action cannot be undone.'
+ )}
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
- Delete
+ {t('common.delete', undefined, 'Delete')}
diff --git a/src/components/Pages/ManagementPages/ManageProjects/ProjectsTableView.tsx b/src/components/Pages/ManagementPages/ManageProjects/ProjectsTableView.tsx
index 238b633c0..5c7a4bc97 100644
--- a/src/components/Pages/ManagementPages/ManageProjects/ProjectsTableView.tsx
+++ b/src/components/Pages/ManagementPages/ManageProjects/ProjectsTableView.tsx
@@ -26,6 +26,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/Table'
+import { useIntl } from '@/i18n'
import type { Project } from '@/types/Project'
interface ProjectsTableViewProps {
@@ -47,22 +48,31 @@ export const ProjectsTableView = ({
onArchiveProject,
onDeleteProject,
}: ProjectsTableViewProps) => {
+ const { t } = useIntl()
return (
- Status
+ {t('common.status', undefined, 'Status')}
-
+
- Name
- ID
- Challenges
- Description
- Actions
+ {t('common.name', undefined, 'Name')}
+
+ {t('common.id', undefined, 'ID')}
+
+
+ {t('common.challenges', undefined, 'Challenges')}
+
+
+ {t('common.description', undefined, 'Description')}
+
+
+ {t('common.actions', undefined, 'Actions')}
+
@@ -80,8 +90,16 @@ export const ProjectsTableView = ({
size="icon"
className="mx-auto h-8 w-8"
onClick={() => onTogglePin(project.id as number)}
- title={pinned ? 'Unpin project' : 'Pin project'}
- aria-label={pinned ? 'Unpin project' : 'Pin project'}
+ title={
+ pinned
+ ? t('common.unpinProject', undefined, 'Unpin project')
+ : t('common.pinProject', undefined, 'Pin project')
+ }
+ aria-label={
+ pinned
+ ? t('common.unpinProject', undefined, 'Unpin project')
+ : t('common.pinProject', undefined, 'Pin project')
+ }
>
) : pinned ? (
-
+
) : (
@@ -122,7 +143,9 @@ export const ProjectsTableView = ({
- Open menu
+
+ {t('common.openMenu', undefined, 'Open menu')}
+
@@ -133,7 +156,7 @@ export const ProjectsTableView = ({
className="flex cursor-pointer items-center gap-2"
>
- View project
+ {t('common.viewProject', undefined, 'View project')}
@@ -143,7 +166,7 @@ export const ProjectsTableView = ({
className="flex cursor-pointer items-center gap-2"
>
- Edit project
+ {t('common.editProject', undefined, 'Edit project')}
@@ -153,7 +176,7 @@ export const ProjectsTableView = ({
className="flex cursor-pointer items-center gap-2"
>
- Add challenge
+ {t('common.addChallenge', undefined, 'Add challenge')}
{onExportCsv && project.id != null && (
@@ -162,7 +185,7 @@ export const ProjectsTableView = ({
className="flex cursor-pointer items-center gap-2"
>
- Export CSV
+ {t('common.exportCsv', undefined, 'Export CSV')}
)}
- Copy URL
+ {t('common.copyUrl', undefined, 'Copy URL')}
{onArchiveProject && project.id != null && (
- {project.isArchived ? 'Unarchive project' : 'Archive project'}
+ {project.isArchived
+ ? t('common.unarchiveProject', undefined, 'Unarchive project')
+ : t('common.archiveProject', undefined, 'Archive project')}
)}
{onDeleteProject && project.id != null && (
@@ -197,7 +222,7 @@ export const ProjectsTableView = ({
className="flex cursor-pointer items-center gap-2 text-red-600 focus:text-red-600 dark:text-red-400 dark:focus:text-red-400"
>
- Delete project
+ {t('common.deleteProject', undefined, 'Delete project')}
)}
diff --git a/src/components/Pages/ManagementPages/ManageTaskDetail/index.tsx b/src/components/Pages/ManagementPages/ManageTaskDetail/index.tsx
index 0a878e43a..48cf117c1 100644
--- a/src/components/Pages/ManagementPages/ManageTaskDetail/index.tsx
+++ b/src/components/Pages/ManagementPages/ManageTaskDetail/index.tsx
@@ -16,6 +16,7 @@ import {
import { useAuthContext } from '@/contexts/AuthContext'
import { useSetBreadcrumbContext } from '@/contexts/BreadcrumbContext'
import { useSetPageTitleContext } from '@/contexts/PageTitleContext'
+import { useIntl } from '@/i18n'
import { canManageChallenge } from '@/lib/challengePermissions'
import { formatDate } from '@/lib/date'
import { isSuperUser } from '@/lib/SuperAdminGuard'
@@ -45,12 +46,13 @@ const DialogActionButton = ({ icon, label, title, children }: DialogActionButton
)
export const ManageTaskDetail = () => {
+ const { t } = useIntl()
const { taskId } = useParams({ from: '/_app/manage/task/$taskId/' })
const { user } = useAuthContext()
const taskIdNum = Number(taskId)
const { data: task, isLoading, isError } = api.task.getTask(taskIdNum)
- useSetPageTitleContext(task?.name ?? `Task #${taskId}`)
+ useSetPageTitleContext(task?.name ?? t('common.taskWithTaskId', { taskId }, 'Task #{taskId}'))
const challengeId =
task && typeof task.parent === 'number' ? task.parent : (task?.parent as { id?: number })?.id
@@ -63,26 +65,39 @@ export const ManageTaskDetail = () => {
() =>
challengeId != null
? [
- { label: 'create & manage', href: '/manage' },
+ {
+ label: t('common.createManage', undefined, 'create & manage'),
+ href: '/manage',
+ },
...(projectId != null
? [
- { label: 'projects', href: '/manage/projects' },
+ {
+ label: t('common.projects2', undefined, 'projects'),
+ href: '/manage/projects',
+ },
{ label: String(projectId), href: `/manage/project/${projectId}` },
]
: []),
- { label: 'challenges', href: '/manage/challenges' },
+ {
+ label: t('common.challenges2', undefined, 'challenges'),
+ href: '/manage/challenges',
+ },
{ label: String(challengeId), href: `/manage/challenge/${challengeId}` },
- { label: 'tasks', href: '/manage/tasks' },
+ {
+ label: t('common.tasks2', undefined, 'tasks'),
+ href: '/manage/tasks',
+ },
{ label: taskId, href: `/manage/task/${taskId}` },
]
: null,
- [projectId, challengeId, taskId]
+ [projectId, challengeId, taskId, t]
)
useSetBreadcrumbContext(breadcrumbs)
const statusLabel =
task?.status != null
- ? (TASK_STATUS_LABELS[task.status as keyof typeof TASK_STATUS_LABELS] ?? 'Unknown')
+ ? (TASK_STATUS_LABELS[task.status as keyof typeof TASK_STATUS_LABELS] ??
+ t('common.unknown', undefined, 'Unknown'))
: null
const canAccess =
@@ -101,7 +116,11 @@ export const ManageTaskDetail = () => {
- Failed to load task. It may not exist or you may not have permission to view it.
+ {t(
+ 'manageTaskDetail.loadError',
+ undefined,
+ 'Failed to load task. It may not exist or you may not have permission to view it.'
+ )}
@@ -115,10 +134,14 @@ export const ManageTaskDetail = () => {
- Access denied
+ {t('common.accessDenied', undefined, 'Access denied')}
- Only challenge owners and admins can view or edit tasks.
+ {t(
+ 'manageTaskDetail.accessDeniedBody',
+ undefined,
+ 'Only challenge owners and admins can view or edit tasks.'
+ )}
@@ -134,7 +157,7 @@ export const ManageTaskDetail = () => {
{/* Header */}
- {task?.name ?? `Task #${taskId}`}
+ {task?.name ?? t('common.taskWithTaskId', { taskId }, 'Task #{taskId}')}
{!isLoading && (
@@ -148,7 +171,9 @@ export const ManageTaskDetail = () => {
)}
•
- ID {taskId}
+
+ {t('manageTaskDetail.idLabel', { taskId }, 'ID {taskId}')}
+
)}
@@ -159,7 +184,7 @@ export const ManageTaskDetail = () => {
- Edit task
+ {t('common.editTask', undefined, 'Edit task')}
{challengeId && (
@@ -174,7 +199,7 @@ export const ManageTaskDetail = () => {
className="w-full justify-start gap-2 rounded-full"
>
- Browse challenge
+ {t('common.browseChallenge', undefined, 'Browse challenge')}
)}
@@ -190,21 +215,21 @@ export const ManageTaskDetail = () => {
className="w-full justify-start gap-2 rounded-full"
>
- Manage challenge
+ {t('manageTaskDetail.manageChallenge', undefined, 'Manage challenge')}
)}
}
- label="Task information"
- title="Task information"
+ label={t('manageTaskDetail.taskInformation', undefined, 'Task information')}
+ title={t('manageTaskDetail.taskInformation', undefined, 'Task information')}
>
- Created
+ {t('common.created', undefined, 'Created')}
{task?.created ? formatDate(new Date(task.created)) : '—'}
@@ -213,19 +238,23 @@ export const ManageTaskDetail = () => {
- Modified
+ {t('common.modified', undefined, 'Modified')}
{task?.modified ? formatDate(new Date(task.modified)) : '—'}
- Status
+
+ {t('common.status', undefined, 'Status')}
+
{statusLabel ?? '—'}
{task?.errorTags && (
- MR Tags
+
+ {t('common.mrTags', undefined, 'MR Tags')}
+
{task.errorTags}
)}
@@ -235,8 +264,8 @@ export const ManageTaskDetail = () => {
{task?.instruction && (
}
- label="Instructions"
- title="Instructions"
+ label={t('common.instructions', undefined, 'Instructions')}
+ title={t('common.instructions', undefined, 'Instructions')}
>
{task.instruction}
@@ -246,8 +275,8 @@ export const ManageTaskDetail = () => {
}
- label="GeoJSON"
- title="GeoJSON"
+ label={t('common.geojson', undefined, 'GeoJSON')}
+ title={t('common.geojson', undefined, 'GeoJSON')}
>
{geometryString}
diff --git a/src/components/Pages/ManagementPages/ManageTaskEdit/TaskForm.test.tsx b/src/components/Pages/ManagementPages/ManageTaskEdit/TaskForm.test.tsx
index 8402553e1..0828943b0 100644
--- a/src/components/Pages/ManagementPages/ManageTaskEdit/TaskForm.test.tsx
+++ b/src/components/Pages/ManagementPages/ManageTaskEdit/TaskForm.test.tsx
@@ -1,6 +1,6 @@
-import { cleanup, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach, describe, expect, it, vi } from 'vitest'
+import { cleanup, render, screen } from '@/test/testUtils'
import type { TaskGetResponse } from '@/types/Task'
import { TaskForm } from './TaskForm.tsx'
diff --git a/src/components/Pages/ManagementPages/ManageTaskEdit/TaskForm.tsx b/src/components/Pages/ManagementPages/ManageTaskEdit/TaskForm.tsx
index 73596d6b3..41eefea22 100644
--- a/src/components/Pages/ManagementPages/ManageTaskEdit/TaskForm.tsx
+++ b/src/components/Pages/ManagementPages/ManageTaskEdit/TaskForm.tsx
@@ -22,6 +22,7 @@ import {
SelectValue,
} from '@/components/ui/Select'
import { Textarea } from '@/components/ui/Textarea'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import type { TaskGetResponse } from '@/types/Task'
@@ -44,6 +45,7 @@ interface TaskFormProps {
const geometriesToString = (geometries: unknown): string => JSON.stringify(geometries, null, 2)
export const TaskForm = ({ task, onSubmit, onCancel }: TaskFormProps) => {
+ const { t } = useIntl()
const form = useForm({
resolver: zodResolver(taskFormSchema),
defaultValues: {
@@ -58,10 +60,16 @@ export const TaskForm = ({ task, onSubmit, onCancel }: TaskFormProps) => {
const handleSubmit = async (values: TaskFormValues) => {
try {
await onSubmit(values)
- toast.success('Task updated successfully')
+ toast.success(t('manageTaskEdit.form.updateSuccess', undefined, 'Task updated successfully'))
} catch (error) {
const message =
- error instanceof Error ? error.message : 'Failed to save task. Please try again.'
+ error instanceof Error
+ ? error.message
+ : t(
+ 'manageTaskEdit.form.updateError',
+ undefined,
+ 'Failed to save task. Please try again.'
+ )
toast.error(message)
logger.error('Failed to save task', { error: String(error) })
}
@@ -76,11 +84,16 @@ export const TaskForm = ({ task, onSubmit, onCancel }: TaskFormProps) => {
name="name"
render={({ field }) => (
- Name
+ {t('common.name', undefined, 'Name')}
-
+
- Name of the task
+
+ {t('manageTaskEdit.form.nameDescription', undefined, 'Name of the task')}
+
)}
@@ -91,15 +104,25 @@ export const TaskForm = ({ task, onSubmit, onCancel }: TaskFormProps) => {
name="instruction"
render={({ field }) => (
- Instructions
+ {t('common.instructions', undefined, 'Instructions')}
- Instructions for users doing this specific task
+
+ {t(
+ 'manageTaskEdit.form.instructionsDescription',
+ undefined,
+ 'Instructions for users doing this specific task'
+ )}
+
)}
@@ -110,7 +133,7 @@ export const TaskForm = ({ task, onSubmit, onCancel }: TaskFormProps) => {
name="geometries"
render={({ field }) => (
- GeoJSON
+ {t('common.geojson', undefined, 'GeoJSON')}
- GeoJSON for this task (point, line or polygon). Must be valid JSON.
+ {t(
+ 'manageTaskEdit.form.geoJsonDescription',
+ undefined,
+ 'GeoJSON for this task (point, line or polygon). Must be valid JSON.'
+ )}
@@ -131,14 +158,20 @@ export const TaskForm = ({ task, onSubmit, onCancel }: TaskFormProps) => {
name="status"
render={({ field }) => (
- Status
+ {t('common.status', undefined, 'Status')}
field.onChange(Number(v))}
value={String(field.value)}
>
-
+
@@ -149,7 +182,13 @@ export const TaskForm = ({ task, onSubmit, onCancel }: TaskFormProps) => {
))}
- Current status of the task
+
+ {t(
+ 'manageTaskEdit.form.statusDescription',
+ undefined,
+ 'Current status of the task'
+ )}
+
)}
@@ -160,12 +199,23 @@ export const TaskForm = ({ task, onSubmit, onCancel }: TaskFormProps) => {
name="errorTags"
render={({ field }) => (
- MR Tags
+ {t('common.mrTags', undefined, 'MR Tags')}
-
+
- Optional MR tags to annotate this task (comma-separated)
+ {t(
+ 'manageTaskEdit.form.mrTagsDescription',
+ undefined,
+ 'Optional MR tags to annotate this task (comma-separated)'
+ )}
@@ -174,10 +224,12 @@ export const TaskForm = ({ task, onSubmit, onCancel }: TaskFormProps) => {
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
- {form.formState.isSubmitting ? 'Saving...' : 'Save'}
+ {form.formState.isSubmitting
+ ? t('common.saving2', undefined, 'Saving...')
+ : t('common.save', undefined, 'Save')}
diff --git a/src/components/Pages/ManagementPages/ManageTaskEdit/index.tsx b/src/components/Pages/ManagementPages/ManageTaskEdit/index.tsx
index 75937ad72..51b7e4d22 100644
--- a/src/components/Pages/ManagementPages/ManageTaskEdit/index.tsx
+++ b/src/components/Pages/ManagementPages/ManageTaskEdit/index.tsx
@@ -6,11 +6,13 @@ import {
} from '@/components/Pages/ManagementPages/ManageTaskEdit/TaskForm'
import { FormCard, ManageFormLayout } from '@/components/shared/ManageFormLayout'
import { useAuthContext } from '@/contexts/AuthContext'
+import { useIntl } from '@/i18n'
import { canManageChallenge } from '@/lib/challengePermissions'
import { isSuperUser } from '@/lib/SuperAdminGuard'
import type { TaskGetResponse } from '@/types/Task'
export const ManageTaskEdit = () => {
+ const { t } = useIntl()
const { taskId } = useParams({ from: '/_app/manage/task/$taskId/edit' })
const navigate = useNavigate()
const { user } = useAuthContext()
@@ -58,7 +60,11 @@ export const ManageTaskEdit = () => {
if (isLoading || !task || (task && challengeId && challengeLoading)) {
return (
-
+
@@ -69,11 +75,19 @@ export const ManageTaskEdit = () => {
return (
- You do not have permission to edit this task.
+ {t(
+ 'manageTaskEdit.index.noPermission',
+ undefined,
+ 'You do not have permission to edit this task.'
+ )}
@@ -83,8 +97,12 @@ export const ManageTaskEdit = () => {
return (
diff --git a/src/components/Pages/ManagementPages/ManageTaskNew/index.tsx b/src/components/Pages/ManagementPages/ManageTaskNew/index.tsx
index 5c686c27a..5c9b72746 100644
--- a/src/components/Pages/ManagementPages/ManageTaskNew/index.tsx
+++ b/src/components/Pages/ManagementPages/ManageTaskNew/index.tsx
@@ -1,12 +1,24 @@
import { FormCard, ManageFormLayout } from '@/components/shared/ManageFormLayout'
+import { useIntl } from '@/i18n'
export const ManageTaskNew = () => {
+ const { t } = useIntl()
return (
-
+
- Tasks are typically created in bulk through challenges. Individual task creation will be
- available in a future update.
+ {t(
+ 'manageTaskNew.body',
+ undefined,
+ 'Tasks are typically created in bulk through challenges. Individual task creation will be available in a future update.'
+ )}
diff --git a/src/components/Pages/ManagementPages/ManageTasksOpen/index.tsx b/src/components/Pages/ManagementPages/ManageTasksOpen/index.tsx
index f9250cd59..fdd483ba9 100644
--- a/src/components/Pages/ManagementPages/ManageTasksOpen/index.tsx
+++ b/src/components/Pages/ManagementPages/ManageTasksOpen/index.tsx
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/Button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'
import { Input } from '@/components/ui/Input'
import { Label } from '@/components/ui/Label'
+import { useIntl } from '@/i18n'
import { SuperAdminGuard } from '@/lib/SuperAdminGuard'
/**
@@ -12,6 +13,7 @@ import { SuperAdminGuard } from '@/lib/SuperAdminGuard'
* Enter a task ID to view or edit the task in the manage flow.
*/
export const ManageTasksOpen = () => {
+ const { t } = useIntl()
const navigate = useNavigate()
const taskIdInputId = useId()
const [taskIdInput, setTaskIdInput] = useState('')
@@ -35,9 +37,15 @@ export const ManageTasksOpen = () => {
-
Tasks
+
+ {t('common.tasks', undefined, 'Tasks')}
+
- Open a task by ID to view details or edit it (name, instructions, status, etc.).
+ {t(
+ 'manageTasksOpen.description',
+ undefined,
+ 'Open a task by ID to view details or edit it (name, instructions, status, etc.).'
+ )}
@@ -45,22 +53,27 @@ export const ManageTasksOpen = () => {
- Open task by ID
+ {t('common.openTaskById', undefined, 'Open task by ID')}
- Enter a task ID below. You can find task IDs when working on a task (URL or header) or
- from your saved/locked tasks on the dashboard.
+ {t(
+ 'manageTasksOpen.cardDescription',
+ undefined,
+ 'Enter a task ID below. You can find task IDs when working on a task (URL or header) or from your saved/locked tasks on the dashboard.'
+ )}
@@ -79,18 +92,22 @@ export const ManageTasksOpen = () => {
- Or go to{' '}
+ {t('manageTasksOpen.orGoTo', undefined, 'Or go to')}{' '}
- Projects
+ {t('common.projects', undefined, 'Projects')}
{' '}
- or{' '}
+ {t('manageTasksOpen.or', undefined, 'or')}{' '}
- Challenges
+ {t('common.challenges', undefined, 'Challenges')}
{' '}
- to find a challenge, then browse it to open a task and get its ID from the URL.
+ {t(
+ 'manageTasksOpen.findChallengeHint',
+ undefined,
+ 'to find a challenge, then browse it to open a task and get its ID from the URL.'
+ )}
diff --git a/src/components/Pages/ManagementPages/ManagementLayout.tsx b/src/components/Pages/ManagementPages/ManagementLayout.tsx
index a56fd5bdf..e52313207 100644
--- a/src/components/Pages/ManagementPages/ManagementLayout.tsx
+++ b/src/components/Pages/ManagementPages/ManagementLayout.tsx
@@ -2,23 +2,28 @@ import { Outlet } from '@tanstack/react-router'
import { SectionHeader } from '@/components/shared/SectionHeader'
import { BreadcrumbProvider } from '@/contexts/BreadcrumbContext'
import { HeaderActionsProvider } from '@/contexts/HeaderActionsContext'
+import { useIntl } from '@/i18n'
import { AuthGuard } from '@/lib/AuthGuard'
-export const ManagementLayout = () => (
-
-
-
-
-
-
-
+export const ManagementLayout = () => {
+ const { t } = useIntl()
+
+ return (
+
+
+
+
-
-
-
-
-)
+
+
+
+ )
+}
diff --git a/src/components/Pages/ManagementPages/MoveChallengeModal/index.tsx b/src/components/Pages/ManagementPages/MoveChallengeModal/index.tsx
index fe98a360a..2f785017e 100644
--- a/src/components/Pages/ManagementPages/MoveChallengeModal/index.tsx
+++ b/src/components/Pages/ManagementPages/MoveChallengeModal/index.tsx
@@ -10,10 +10,12 @@ import {
DialogTitle,
} from '@/components/ui/Dialog'
import { useMoveChallengeContext } from '@/contexts/MoveChallengeContext'
+import { useIntl } from '@/i18n'
import { cn } from '@/lib/utils'
import type { Project } from '@/types/Project'
export const MoveChallengeModal = () => {
+ const { t } = useIntl()
const {
challenge,
currentProjectId,
@@ -44,17 +46,20 @@ export const MoveChallengeModal = () => {
!open && closeMoveModal()}>
- Move Challenge
+ {t('moveChallengeModal.title', undefined, 'Move Challenge')}
- Choose a project to move "{challenge?.name ?? ''}" to. The challenge will be
- removed from the current project.
+ {t(
+ 'moveChallengeModal.description',
+ { challengeName: challenge?.name ?? '' },
+ 'Choose a project to move "{challengeName}" to. The challenge will be removed from the current project.'
+ )}
@@ -65,7 +70,11 @@ export const MoveChallengeModal = () => {
) : candidateProjects.length === 0 ? (
- No other projects found. Create another project first to move this challenge.
+ {t(
+ 'moveChallengeModal.noProjects',
+ undefined,
+ 'No other projects found. Create another project first to move this challenge.'
+ )}
) : (
@@ -85,7 +94,7 @@ export const MoveChallengeModal = () => {
{project.id != null && (
- (ID: {project.id})
+ {t('common.idNumberParenthetical', { id: project.id }, '(ID: {id})')}
)}
@@ -97,7 +106,11 @@ export const MoveChallengeModal = () => {
{isError && (
- Failed to move challenge. You may not have permission to move to that project.
+ {t(
+ 'moveChallengeModal.moveError',
+ undefined,
+ 'Failed to move challenge. You may not have permission to move to that project.'
+ )}
)}
diff --git a/src/components/Pages/ManagementPages/TaskPrioritizationPage/Editor/BoundsDrawControl.tsx b/src/components/Pages/ManagementPages/TaskPrioritizationPage/Editor/BoundsDrawControl.tsx
index 4e09fccfc..1b4b911f5 100644
--- a/src/components/Pages/ManagementPages/TaskPrioritizationPage/Editor/BoundsDrawControl.tsx
+++ b/src/components/Pages/ManagementPages/TaskPrioritizationPage/Editor/BoundsDrawControl.tsx
@@ -11,6 +11,7 @@ import {
} from 'terra-draw'
import { TerraDrawMapLibreGLAdapter } from 'terra-draw-maplibre-gl-adapter'
import { Button } from '@/components/ui/Button'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import { cn } from '@/lib/utils'
import { PRIORITY_COLOR, type TaskPriorityValue } from '@/types/Priority'
@@ -39,14 +40,28 @@ const TERRA_MODE: Record = {
rectangle: 'rectangle',
}
-const MODE_HELP: Record = {
- idle: 'Pan and zoom the map. Pick a tool to draw or edit this tier’s bounds.',
- polygon:
- 'Click to drop each vertex; double-click or press Enter to finish. Use this for any non-rectangular area.',
- rectangle: 'Click and drag to draw an axis-aligned rectangle.',
- select:
- 'Click a shape to select it, then drag a vertex to reshape, a midpoint to insert a new vertex, or the whole shape to move it. Right-click a vertex to delete it.',
-}
+const buildModeHelp = (t: ReturnType['t']): Record => ({
+ idle: t(
+ 'taskPrioritizationPage.boundsDrawControl.helpIdle',
+ undefined,
+ 'Pan and zoom the map. Pick a tool to draw or edit this tier’s bounds.'
+ ),
+ polygon: t(
+ 'taskPrioritizationPage.boundsDrawControl.helpPolygon',
+ undefined,
+ 'Click to drop each vertex; double-click or press Enter to finish. Use this for any non-rectangular area.'
+ ),
+ rectangle: t(
+ 'taskPrioritizationPage.boundsDrawControl.helpRectangle',
+ undefined,
+ 'Click and drag to draw an axis-aligned rectangle.'
+ ),
+ select: t(
+ 'taskPrioritizationPage.boundsDrawControl.helpSelect',
+ undefined,
+ 'Click a shape to select it, then drag a vertex to reshape, a midpoint to insert a new vertex, or the whole shape to move it. Right-click a vertex to delete it.'
+ ),
+})
const featureToFC = (
features: GeoJSON.Feature[] | null | undefined
@@ -81,6 +96,8 @@ const normalizeSeedFeature = (feature: GeoJSON.Feature): GeoJSONStoreFeatures =>
* the shared PreviewMap instance.
*/
export const BoundsDrawControl = ({ tier, map, mapLoaded, value, onChange, className }: Props) => {
+ const { t } = useIntl()
+ const MODE_HELP = buildModeHelp(t)
const drawRef = useRef(null)
const suppressChangeRef = useRef(false)
const [activeMode, setActiveMode] = useState('idle')
@@ -233,11 +250,19 @@ export const BoundsDrawControl = ({ tier, map, mapLoaded, value, onChange, class
size="sm"
variant={activeMode === 'idle' ? 'default' : 'ghost'}
onClick={() => setMode('idle')}
- title="Pan the map with no drawing tool active"
- aria-label="Pan mode (no tool active)"
+ title={t(
+ 'taskPrioritizationPage.boundsDrawControl.panTitle',
+ undefined,
+ 'Pan the map with no drawing tool active'
+ )}
+ aria-label={t(
+ 'taskPrioritizationPage.boundsDrawControl.panAriaLabel',
+ undefined,
+ 'Pan mode (no tool active)'
+ )}
>
- Pan
+ {t('taskPrioritizationPage.boundsDrawControl.pan', undefined, 'Pan')}
setMode('polygon')}
title={MODE_HELP.polygon}
- aria-label={`Draw polygon for ${tier} priority`}
+ aria-label={t(
+ 'taskPrioritizationPage.boundsDrawControl.drawPolygonAriaLabel',
+ { tier },
+ 'Draw polygon for {tier} priority'
+ )}
>
- Polygon
+ {t('taskPrioritizationPage.boundsDrawControl.polygon', undefined, 'Polygon')}
setMode('rectangle')}
title={MODE_HELP.rectangle}
- aria-label={`Draw rectangle for ${tier} priority`}
+ aria-label={t(
+ 'taskPrioritizationPage.boundsDrawControl.drawRectangleAriaLabel',
+ { tier },
+ 'Draw rectangle for {tier} priority'
+ )}
>
- Rectangle
+ {t('taskPrioritizationPage.boundsDrawControl.rectangle', undefined, 'Rectangle')}
setMode('select')}
title={MODE_HELP.select}
- aria-label="Select and edit an existing shape"
+ aria-label={t(
+ 'taskPrioritizationPage.boundsDrawControl.selectAriaLabel',
+ undefined,
+ 'Select and edit an existing shape'
+ )}
>
- Select
+ {t('taskPrioritizationPage.boundsDrawControl.select', undefined, 'Select')}
- Delete selected
+ {t(
+ 'taskPrioritizationPage.boundsDrawControl.deleteSelected',
+ undefined,
+ 'Delete selected'
+ )}
- Clear all
+ {t('taskPrioritizationPage.boundsDrawControl.clearAll', undefined, 'Clear all')}
diff --git a/src/components/Pages/ManagementPages/TaskPrioritizationPage/Editor/DefaultPrioritySelect.tsx b/src/components/Pages/ManagementPages/TaskPrioritizationPage/Editor/DefaultPrioritySelect.tsx
index db73c5caa..bfa5bba69 100644
--- a/src/components/Pages/ManagementPages/TaskPrioritizationPage/Editor/DefaultPrioritySelect.tsx
+++ b/src/components/Pages/ManagementPages/TaskPrioritizationPage/Editor/DefaultPrioritySelect.tsx
@@ -1,5 +1,6 @@
import { Label } from '@/components/ui/Label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup'
+import { useIntl } from '@/i18n'
import { cn } from '@/lib/utils'
import {
PRIORITY_COLOR,
@@ -10,10 +11,13 @@ import {
import { usePrioritizationContext } from '../PrioritizationContext'
export const DefaultPrioritySelect = () => {
+ const { t } = useIntl()
const { draft, setDefaultPriority } = usePrioritizationContext()
return (
-
Default priority
+
+ {t('taskPrioritizationPage.defaultPrioritySelect.label', undefined, 'Default priority')}
+
{
+ const { t } = useIntl()
const { preview } = useTaskPreview()
const warnings = preview.warnings.tier[priority]
if (warnings.length === 0) return null
@@ -24,7 +26,9 @@ export const TierWarningBadges = ({
title={w.message}
>
- {w.kind === 'dead-rule' ? 'No matches' : 'Matches all'}
+ {w.kind === 'dead-rule'
+ ? t('taskPrioritizationPage.tierWarningBadges.noMatches', undefined, 'No matches')
+ : t('taskPrioritizationPage.tierWarningBadges.matchesAll', undefined, 'Matches all')}
))}
diff --git a/src/components/Pages/ManagementPages/TaskPrioritizationPage/Preview/PreviewMap.tsx b/src/components/Pages/ManagementPages/TaskPrioritizationPage/Preview/PreviewMap.tsx
index 0b035114c..e90e0cae4 100644
--- a/src/components/Pages/ManagementPages/TaskPrioritizationPage/Preview/PreviewMap.tsx
+++ b/src/components/Pages/ManagementPages/TaskPrioritizationPage/Preview/PreviewMap.tsx
@@ -17,6 +17,7 @@ import { clusterCountLayer, clusterLayer } from '@/components/Map/TaskMarkers/cl
import { flyToClusterExpansion } from '@/components/Map/TaskMarkers/clusterUtils'
import { CLUSTER_RADIUS_PX, LAYER_IDS } from '@/components/Map/TaskMarkers/const'
import { Spinner } from '@/components/ui/Spinner'
+import { useIntl } from '@/i18n'
import { cn } from '@/lib/utils'
import type { Bbox2D } from '@/types/Map'
import { PRIORITY_COLOR, type TaskPriorityValue } from '@/types/Priority'
@@ -66,6 +67,7 @@ export const PreviewMap = ({
selectedTaskId,
children,
}: Props) => {
+ const { t } = useIntl()
const mapId = useId()
const idPrefix = useId().replace(/:/g, '-')
// Shared cluster layers from clusterLayers.ts target source id `LAYER_IDS.source`
@@ -376,7 +378,7 @@ export const PreviewMap = ({
aria-live="polite"
>
- Updating preview…
+ {t('taskPrioritizationPage.previewMap.updatingPreview', undefined, 'Updating preview…')}
)}
diff --git a/src/components/Pages/ManagementPages/TaskPrioritizationPage/PrioritizationContent.tsx b/src/components/Pages/ManagementPages/TaskPrioritizationPage/PrioritizationContent.tsx
index 77c691290..ca989fc7b 100644
--- a/src/components/Pages/ManagementPages/TaskPrioritizationPage/PrioritizationContent.tsx
+++ b/src/components/Pages/ManagementPages/TaskPrioritizationPage/PrioritizationContent.tsx
@@ -10,6 +10,7 @@ import { DrawerPortalTarget, useDrawerPortal } from '@/components/TaskInfoPanel/
import { TaskInfoDrawer } from '@/components/TaskInfoPanel/TaskInfoDrawer'
import { Button } from '@/components/ui/Button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/Tabs'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import { PRIORITY_LABEL } from '@/types/Priority'
import type { TaskMarker } from '@/types/Task'
@@ -35,6 +36,7 @@ interface PreviewMapBridgeValue {
const PreviewMapBridgeContext = createContext(null)
export const PrioritizationContent = ({ challengeId, challengeName }: Props) => {
+ const { t } = useIntl()
const { draft, isDirty, reset, markSaved, setTierBounds } = usePrioritizationContext()
const mutation = api.challenge.useUpdatePriorities()
const navigate = useNavigate()
@@ -67,14 +69,16 @@ export const PrioritizationContent = ({ challengeId, challengeName }: Props) =>
},
})
markSaved()
- toast.success('Priorities saved')
+ toast.success(t('taskPrioritizationPage.content.saveSuccess', undefined, 'Priorities saved'))
navigate({
to: '/manage/challenge/$challengeId',
params: { challengeId: String(challengeId) },
})
} catch (error) {
logger.error('Priority save failed', { error, challengeId })
- toast.error('Could not save priorities')
+ toast.error(
+ t('taskPrioritizationPage.content.saveError', undefined, 'Could not save priorities')
+ )
throw error
}
}
@@ -86,8 +90,19 @@ export const PrioritizationContent = ({ challengeId, challengeName }: Props) =>
- {challengeName ?? `Challenge #${challengeId}`} — rules run top-down; the first tier to
- match wins.
+ {t(
+ 'common.challengeWithChallengeId',
+ {
+ name:
+ challengeName ??
+ t(
+ 'taskPrioritizationPage.content.challengeNumber',
+ { challengeId },
+ 'Challenge #{challengeId}'
+ ),
+ },
+ '{name} — rules run top-down; the first tier to match wins.'
+ )}
onClick={reset}
disabled={!isDirty || mutation.isPending}
>
- Discard
+ {t('taskPrioritizationPage.content.discard', undefined, 'Discard')}
disabled={!isDirty || mutation.isPending}
aria-disabled={!isDirty || mutation.isPending}
>
- {mutation.isPending ? 'Saving…' : 'Save'}
+ {mutation.isPending
+ ? t('common.saving', undefined, 'Saving…')
+ : t('common.save', undefined, 'Save')}
diff --git a/src/components/Pages/ManagementPages/TaskPrioritizationPage/index.tsx b/src/components/Pages/ManagementPages/TaskPrioritizationPage/index.tsx
index 5a721ba20..ef7d67451 100644
--- a/src/components/Pages/ManagementPages/TaskPrioritizationPage/index.tsx
+++ b/src/components/Pages/ManagementPages/TaskPrioritizationPage/index.tsx
@@ -3,6 +3,7 @@ import { api } from '@/api'
import { processMarkersData } from '@/components/Map/TaskMarkers/utils'
import { DrawerPortalProvider } from '@/components/TaskInfoPanel/DrawerPortalContext'
import { Spinner } from '@/components/ui/Spinner'
+import { useIntl } from '@/i18n'
import { TaskPriority, type TaskPriorityValue } from '@/types/Priority'
import { PrioritizationContent } from './PrioritizationContent'
import {
@@ -23,6 +24,7 @@ const clampPriority = (value: number | null | undefined): TaskPriorityValue => {
}
export const TaskPrioritizationPage = ({ challengeId }: Props) => {
+ const { t } = useIntl()
const challengeQuery = api.challenge.getChallenge(challengeId)
const markersQuery = api.challenge.getChallengeTaskMarkers(challengeId)
@@ -66,7 +68,9 @@ export const TaskPrioritizationPage = ({ challengeId }: Props) => {
if (challengeQuery.isError || !challengeQuery.data) {
return (
-
Could not load challenge.
+
+ {t('taskPrioritizationPage.index.loadError', undefined, 'Could not load challenge.')}
+
)
}
diff --git a/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkActionsToolbar.tsx b/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkActionsToolbar.tsx
index 7d0ea73c1..42c2a87d1 100644
--- a/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkActionsToolbar.tsx
+++ b/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkActionsToolbar.tsx
@@ -18,6 +18,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/DropdownMenu'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import { BulkClearLockDialog } from './BulkClearLockDialog'
import { BulkDeleteDialog } from './BulkDeleteDialog'
@@ -31,6 +32,7 @@ interface Props {
}
export const BulkActionsToolbar = ({ selectedIds, onClearSelection }: Props) => {
+ const { t } = useIntl()
const [statusOpen, setStatusOpen] = useState(false)
const [tagOpen, setTagOpen] = useState(false)
const [deleteOpen, setDeleteOpen] = useState(false)
@@ -48,22 +50,38 @@ export const BulkActionsToolbar = ({ selectedIds, onClearSelection }: Props) =>
const handleStatus = async (status: number) => {
try {
await bulkStatus.mutateAsync({ taskIds: selectedIds, status })
- toast.success(`Updated ${selectedIds.length} tasks`)
+ toast.success(
+ t(
+ 'managementPages.bulkActionsToolbar.updatedToast',
+ { count: selectedIds.length },
+ 'Updated {count} tasks'
+ )
+ )
onClearSelection()
} catch (error) {
logger.error('Bulk status failed', { error })
- toast.error('Could not update tasks')
+ toast.error(
+ t('managementPages.bulkActionsToolbar.updateError', undefined, 'Could not update tasks')
+ )
}
}
const handleTags = async (tags: string[]) => {
try {
await bulkTags.mutateAsync({ taskIds: selectedIds, tags })
- toast.success(`Tagged ${selectedIds.length} tasks`)
+ toast.success(
+ t(
+ 'managementPages.bulkActionsToolbar.taggedToast',
+ { count: selectedIds.length },
+ 'Tagged {count} tasks'
+ )
+ )
onClearSelection()
} catch (error) {
logger.error('Bulk tag failed', { error })
- toast.error('Could not tag tasks')
+ toast.error(
+ t('managementPages.bulkActionsToolbar.tagError', undefined, 'Could not tag tasks')
+ )
}
}
@@ -72,98 +90,156 @@ export const BulkActionsToolbar = ({ selectedIds, onClearSelection }: Props) =>
const result = await bulkDelete.mutateAsync(selectedIds)
if (result.denied.length > 0) {
toast.warning(
- `Deleted ${result.deleted} tasks; ${result.denied.length} could not be deleted`
+ t(
+ 'managementPages.bulkActionsToolbar.deleteWarning',
+ { deleted: result.deleted, denied: result.denied.length },
+ 'Deleted {deleted} tasks; {denied} could not be deleted'
+ )
)
} else {
- toast.success(`Deleted ${result.deleted} tasks`)
+ toast.success(
+ t(
+ 'managementPages.bulkActionsToolbar.deleteSuccess',
+ { count: result.deleted },
+ 'Deleted {count} tasks'
+ )
+ )
}
setDeleteOpen(false)
onClearSelection()
} catch (error) {
logger.error('Bulk delete failed', { error })
- toast.error('Could not delete tasks')
+ toast.error(
+ t('managementPages.bulkActionsToolbar.deleteError', undefined, 'Could not delete tasks')
+ )
}
}
const handleArchive = async (archived: boolean) => {
try {
await bulkArchive.mutateAsync({ taskIds: selectedIds, archived })
- toast.success(`${archived ? 'Archived' : 'Unarchived'} ${selectedIds.length} tasks`)
+ toast.success(
+ archived
+ ? t(
+ 'managementPages.bulkActionsToolbar.archivedToast',
+ { count: selectedIds.length },
+ 'Archived {count} tasks'
+ )
+ : t(
+ 'managementPages.bulkActionsToolbar.unarchivedToast',
+ { count: selectedIds.length },
+ 'Unarchived {count} tasks'
+ )
+ )
onClearSelection()
} catch (error) {
logger.error('Bulk archive failed', { error })
- toast.error('Could not archive tasks')
+ toast.error(
+ t('managementPages.bulkActionsToolbar.archiveError', undefined, 'Could not archive tasks')
+ )
}
}
const handleReassign = async (userId: number) => {
try {
const result = await bulkReassign.mutateAsync({ taskIds: selectedIds, userId })
- toast.success(`Reassigned ${result.updated} of ${result.requested} tasks`)
+ toast.success(
+ t(
+ 'managementPages.bulkActionsToolbar.reassignSuccess',
+ { updated: result.updated, requested: result.requested },
+ 'Reassigned {updated} of {requested} tasks'
+ )
+ )
setReassignOpen(false)
onClearSelection()
} catch (error) {
logger.error('Bulk reassign failed', { error })
- toast.error('Could not reassign tasks')
+ toast.error(
+ t('managementPages.bulkActionsToolbar.reassignError', undefined, 'Could not reassign tasks')
+ )
}
}
const handleClearLock = async () => {
try {
await bulkClearLock.mutateAsync(selectedIds)
- toast.success(`Cleared lock on ${selectedIds.length} tasks`)
+ toast.success(
+ t(
+ 'managementPages.bulkActionsToolbar.clearLockSuccess',
+ { count: selectedIds.length },
+ 'Cleared lock on {count} tasks'
+ )
+ )
setClearLockOpen(false)
onClearSelection()
} catch (error) {
logger.error('Bulk clear lock failed', { error })
- toast.error('Could not clear locks')
+ toast.error(
+ t('managementPages.bulkActionsToolbar.clearLockError', undefined, 'Could not clear locks')
+ )
}
}
return (
-
{selectedIds.length} selected
+
+ {t(
+ 'managementPages.bulkActionsToolbar.selectedCount',
+ { count: selectedIds.length },
+ '{count} selected'
+ )}
+
- Change status
+ {t('managementPages.bulkActionsToolbar.changeStatus', undefined, 'Change status')}{' '}
+
- setStatusOpen(true)}>Pick a status…
+ setStatusOpen(true)}>
+ {t('managementPages.bulkActionsToolbar.pickStatus', undefined, 'Pick a status…')}
+
setTagOpen(true)}>
- Tag
+ {' '}
+ {t('managementPages.bulkActionsToolbar.tag', undefined, 'Tag')}
- Archive{' '}
+ {' '}
+ {t('managementPages.bulkActionsToolbar.archive', undefined, 'Archive')}{' '}
handleArchive(true)}>
- Archive
+ {' '}
+ {t('managementPages.bulkActionsToolbar.archive', undefined, 'Archive')}
handleArchive(false)}>
- Unarchive
+ {' '}
+ {t('managementPages.bulkActionsToolbar.unarchive', undefined, 'Unarchive')}
setReassignOpen(true)}>
- Reassign
+ {' '}
+ {t('managementPages.bulkActionsToolbar.reassign', undefined, 'Reassign')}
setClearLockOpen(true)}>
- Clear lock
+ {' '}
+ {t('managementPages.bulkActionsToolbar.clearLock', undefined, 'Clear lock')}
setDeleteOpen(true)}>
- Delete
+ {' '}
+ {t('common.delete', undefined, 'Delete')}
- Clear
+ {t('common.clear', undefined, 'Clear')}
(
-
-
-
-
-
- Clear lock on {count} task{count === 1 ? '' : 's'}?
-
-
- Any active locks on the selected tasks will be released. Mappers currently working on
- these tasks may lose their in-progress session.
-
-
-
- onOpenChange(false)} disabled={busy}>
- Cancel
-
-
- {busy ? 'Clearing…' : 'Clear lock'}
-
-
-
-
-)
+export const BulkClearLockDialog = ({ open, onOpenChange, onConfirm, count, busy }: Props) => {
+ const { t } = useIntl()
+ return (
+
+
+
+
+
+ {t(
+ 'managementPages.bulkActionsToolbar.clearLockDialog.title',
+ { count, suffix: count === 1 ? '' : 's' },
+ 'Clear lock on {count} task{suffix}?'
+ )}
+
+
+ {t(
+ 'managementPages.bulkActionsToolbar.clearLockDialog.description',
+ undefined,
+ 'Any active locks on the selected tasks will be released. Mappers currently working on these tasks may lose their in-progress session.'
+ )}
+
+
+
+ onOpenChange(false)} disabled={busy}>
+ {t('common.cancel', undefined, 'Cancel')}
+
+
+ {busy
+ ? t(
+ 'managementPages.bulkActionsToolbar.clearLockDialog.clearing',
+ undefined,
+ 'Clearing…'
+ )
+ : t('managementPages.bulkActionsToolbar.clearLock', undefined, 'Clear lock')}
+
+
+
+
+ )
+}
diff --git a/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkDeleteDialog.tsx b/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkDeleteDialog.tsx
index 59d4da8bc..cf17c89c5 100644
--- a/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkDeleteDialog.tsx
+++ b/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkDeleteDialog.tsx
@@ -8,6 +8,7 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/Dialog'
+import { useIntl } from '@/i18n'
interface Props {
open: boolean
@@ -17,27 +18,43 @@ interface Props {
busy?: boolean
}
-export const BulkDeleteDialog = ({ open, onOpenChange, onConfirm, count, busy }: Props) => (
-
-
-
-
-
- Delete {count} task{count === 1 ? '' : 's'}?
-
-
- This cannot be undone. Tasks and their comments, reviews, and tags are permanently
- removed.
-
-
-
- onOpenChange(false)} disabled={busy}>
- Cancel
-
-
- {busy ? 'Deleting…' : 'Delete'}
-
-
-
-
-)
+export const BulkDeleteDialog = ({ open, onOpenChange, onConfirm, count, busy }: Props) => {
+ const { t } = useIntl()
+ return (
+
+
+
+
+
+ {t(
+ 'managementPages.bulkActionsToolbar.deleteDialog.title',
+ { count, suffix: count === 1 ? '' : 's' },
+ 'Delete {count} task{suffix}?'
+ )}
+
+
+ {t(
+ 'managementPages.bulkActionsToolbar.deleteDialog.description',
+ undefined,
+ 'This cannot be undone. Tasks and their comments, reviews, and tags are permanently removed.'
+ )}
+
+
+
+ onOpenChange(false)} disabled={busy}>
+ {t('common.cancel', undefined, 'Cancel')}
+
+
+ {busy
+ ? t(
+ 'managementPages.bulkActionsToolbar.deleteDialog.deleting',
+ undefined,
+ 'Deleting…'
+ )
+ : t('common.delete', undefined, 'Delete')}
+
+
+
+
+ )
+}
diff --git a/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkReassignDialog.tsx b/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkReassignDialog.tsx
index ec50c1fdd..16ef313d8 100644
--- a/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkReassignDialog.tsx
+++ b/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkReassignDialog.tsx
@@ -12,6 +12,7 @@ import {
DialogTitle,
} from '@/components/ui/Dialog'
import { Input } from '@/components/ui/Input'
+import { useIntl } from '@/i18n'
import { initials } from '@/lib/utils'
interface Props {
@@ -23,6 +24,7 @@ interface Props {
}
export const BulkReassignDialog = ({ open, onOpenChange, onConfirm, count, busy }: Props) => {
+ const { t } = useIntl()
const [query, setQuery] = useState('')
const [selectedUserId, setSelectedUserId] = useState(null)
const { data: users = [] } = api.user.findUsers(query, 8, query.length > 0)
@@ -32,11 +34,18 @@ export const BulkReassignDialog = ({ open, onOpenChange, onConfirm, count, busy
- Reassign {count} task{count === 1 ? '' : 's'}
+ {t(
+ 'managementPages.bulkActionsToolbar.reassignDialog.title',
+ { count, suffix: count === 1 ? '' : 's' },
+ 'Reassign {count} task{suffix}'
+ )}
- Search for the reviewer to assign these task reviews to. Only tasks whose reviews are
- still open will be updated.
+ {t(
+ 'managementPages.bulkActionsToolbar.reassignDialog.description',
+ undefined,
+ 'Search for the reviewer to assign these task reviews to. Only tasks whose reviews are still open will be updated.'
+ )}
@@ -45,7 +54,7 @@ export const BulkReassignDialog = ({ open, onOpenChange, onConfirm, count, busy
setQuery(e.target.value)}
- placeholder="Search OSM username"
+ placeholder={t('common.searchOsmUsername', undefined, 'Search OSM username')}
className="pl-8"
/>
@@ -75,13 +84,19 @@ export const BulkReassignDialog = ({ open, onOpenChange, onConfirm, count, busy
onOpenChange(false)} disabled={busy}>
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
selectedUserId && onConfirm(selectedUserId)}
disabled={!selectedUserId || busy}
>
- {busy ? 'Reassigning…' : 'Reassign'}
+ {busy
+ ? t(
+ 'managementPages.bulkActionsToolbar.reassignDialog.reassigning',
+ undefined,
+ 'Reassigning…'
+ )
+ : t('managementPages.bulkActionsToolbar.reassign', undefined, 'Reassign')}
diff --git a/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkStatusDialog.tsx b/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkStatusDialog.tsx
index b67c17f0d..bf3b1c2a5 100644
--- a/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkStatusDialog.tsx
+++ b/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkStatusDialog.tsx
@@ -15,6 +15,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/Select'
+import { useIntl } from '@/i18n'
import { STATUS_LABELS } from '@/lib/taskConstants'
interface Props {
@@ -24,14 +25,27 @@ interface Props {
}
export const BulkStatusDialog = ({ open, onOpenChange, onConfirm }: Props) => {
+ const { t } = useIntl()
const [status, setStatus] = useState
('1')
return (
- Change task status
- Updates every selected task to the chosen status.
+
+ {t(
+ 'managementPages.bulkActionsToolbar.statusDialog.title',
+ undefined,
+ 'Change task status'
+ )}
+
+
+ {t(
+ 'managementPages.bulkActionsToolbar.statusDialog.description',
+ undefined,
+ 'Updates every selected task to the chosen status.'
+ )}
+
@@ -47,9 +61,11 @@ export const BulkStatusDialog = ({ open, onOpenChange, onConfirm }: Props) => {
onOpenChange(false)}>
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
+
+ onConfirm(Number(status))}>
+ {t('managementPages.bulkActionsToolbar.apply', undefined, 'Apply')}
- onConfirm(Number(status))}>Apply
diff --git a/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkTagDialog.tsx b/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkTagDialog.tsx
index f11b33318..59a2a83fe 100644
--- a/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkTagDialog.tsx
+++ b/src/components/Pages/ManagementPages/shared/BulkActionsToolbar/BulkTagDialog.tsx
@@ -8,6 +8,7 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/Dialog'
+import { useIntl } from '@/i18n'
interface Props {
open: boolean
@@ -16,21 +17,28 @@ interface Props {
}
export const BulkTagDialog = ({ open, onOpenChange, onConfirm }: Props) => {
+ const { t } = useIntl()
const [tags, setTags] = useState([])
return (
- Add tags to selected tasks
+
+ {t(
+ 'managementPages.bulkActionsToolbar.tagDialog.title',
+ undefined,
+ 'Add tags to selected tasks'
+ )}
+
onOpenChange(false)}>
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
onConfirm(tags)} disabled={tags.length === 0}>
- Apply
+ {t('managementPages.bulkActionsToolbar.apply', undefined, 'Apply')}
diff --git a/src/components/Pages/ManagementPages/shared/ChallengeStatusBanner.tsx b/src/components/Pages/ManagementPages/shared/ChallengeStatusBanner.tsx
index 782044dd0..db33b27bf 100644
--- a/src/components/Pages/ManagementPages/shared/ChallengeStatusBanner.tsx
+++ b/src/components/Pages/ManagementPages/shared/ChallengeStatusBanner.tsx
@@ -1,5 +1,6 @@
import { Loader2 } from 'lucide-react'
import { Progress } from '@/components/ui/Progress'
+import { useIntl } from '@/i18n'
export interface ChallengeStatusInfo {
status?: string
@@ -15,6 +16,7 @@ interface Props {
}
export const ChallengeStatusBanner = ({ info }: Props) => {
+ const { t } = useIntl()
if (!info?.status || !busyStatuses.has(info.status)) return null
const current = info.creatingTasks ?? info.deletingTasks ?? 0
@@ -31,7 +33,13 @@ export const ChallengeStatusBanner = ({ info }: Props) => {
aria-hidden="true"
/>
-
{info.status} tasks…
+
+ {t(
+ 'managementPages.challengeStatusBanner.tasksInProgress',
+ { status: info.status },
+ '{status} tasks…'
+ )}
+
{total > 0 && (
{current.toLocaleString()} / {total.toLocaleString()}
diff --git a/src/components/Pages/ManagementPages/shared/RebuildTasksDialog.tsx b/src/components/Pages/ManagementPages/shared/RebuildTasksDialog.tsx
index b9881e721..cee53b70d 100644
--- a/src/components/Pages/ManagementPages/shared/RebuildTasksDialog.tsx
+++ b/src/components/Pages/ManagementPages/shared/RebuildTasksDialog.tsx
@@ -14,6 +14,7 @@ import {
} from '@/components/ui/Dialog'
import { Input } from '@/components/ui/Input'
import { Label } from '@/components/ui/Label'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import type { Challenge } from '@/types/Challenge'
@@ -48,15 +49,6 @@ const detectLineByLine = (text: string): boolean => {
}
}
-const sourceIntro: Record
= {
- overpass:
- 'Rebuilding will re-run the Overpass query and rebuild the challenge tasks with the latest data:',
- remote:
- "Rebuilding will re-download the GeoJSON data from the challenge's remote URL and rebuild the challenge tasks with the latest data:",
- local:
- 'Rebuilding will allow you to upload a new local file with the latest GeoJSON data and rebuild the challenge tasks:',
-}
-
interface Props {
challengeId: number
open: boolean
@@ -65,10 +57,29 @@ interface Props {
}
export const RebuildTasksDialog = ({ challengeId, open, onOpenChange, sourceType }: Props) => {
+ const { t } = useIntl()
const [removeUnmatched, setRemoveUnmatched] = useState(false)
const [localFile, setLocalFile] = useState(null)
const [dataOriginDate, setDataOriginDate] = useState('')
+ const sourceIntro: Record = {
+ overpass: t(
+ 'managementPages.rebuildTasksDialog.introOverpass',
+ undefined,
+ 'Rebuilding will re-run the Overpass query and rebuild the challenge tasks with the latest data:'
+ ),
+ remote: t(
+ 'managementPages.rebuildTasksDialog.introRemote',
+ undefined,
+ "Rebuilding will re-download the GeoJSON data from the challenge's remote URL and rebuild the challenge tasks with the latest data:"
+ ),
+ local: t(
+ 'managementPages.rebuildTasksDialog.introLocal',
+ undefined,
+ 'Rebuilding will allow you to upload a new local file with the latest GeoJSON data and rebuild the challenge tasks:'
+ ),
+ }
+
const rebuild = api.challenge.useRebuildChallenge()
const uploadGeoJSON = api.challenge.useUploadGeoJSON()
const unmatchedId = useId()
@@ -103,12 +114,16 @@ export const RebuildTasksDialog = ({ challengeId, open, onOpenChange, sourceType
} else {
await rebuild.mutateAsync({ challengeId, removeUnmatched, skipSnapshot: true })
}
- toast.success('Rebuild started')
+ toast.success(
+ t('managementPages.rebuildTasksDialog.toastStarted', undefined, 'Rebuild started')
+ )
reset()
onOpenChange(false)
} catch (error) {
logger.error('Rebuild failed', { error: String(error) })
- toast.error('Could not start rebuild')
+ toast.error(
+ t('managementPages.rebuildTasksDialog.toastError', undefined, 'Could not start rebuild')
+ )
}
}
@@ -116,25 +131,57 @@ export const RebuildTasksDialog = ({ challengeId, open, onOpenChange, sourceType
- Rebuild Challenge Tasks
+
+ {t('managementPages.rebuildTasksDialog.title', undefined, 'Rebuild Challenge Tasks')}
+
{sourceType
? sourceIntro[sourceType]
- : 'Rebuild the challenge tasks from its source data.'}
+ : t(
+ 'managementPages.rebuildTasksDialog.descriptionDefault',
+ undefined,
+ 'Rebuild the challenge tasks from its source data.'
+ )}
- Existing tasks included in the latest data will be updated
- New tasks will be added
- If you choose to first remove incomplete tasks (below), existing{' '}
- incomplete tasks will first be removed
+ {t(
+ 'managementPages.rebuildTasksDialog.listUpdated',
+ undefined,
+ 'Existing tasks included in the latest data will be updated'
+ )}
+
+
+ {t(
+ 'managementPages.rebuildTasksDialog.listAdded',
+ undefined,
+ 'New tasks will be added'
+ )}
+
+
+ {t(
+ 'managementPages.rebuildTasksDialog.listRemoveIncompletePre',
+ undefined,
+ 'If you choose to first remove incomplete tasks (below), existing '
+ )}
+
+ {t('managementPages.rebuildTasksDialog.incomplete', undefined, 'incomplete')}
+ {' '}
+ {t(
+ 'managementPages.rebuildTasksDialog.listRemoveIncompletePost',
+ undefined,
+ 'tasks will first be removed'
+ )}
- If you do not first remove incomplete tasks, they will be left as-is, possibly leaving
- tasks that have already been addressed outside of MapRoulette
+ {t(
+ 'managementPages.rebuildTasksDialog.listKeepAsIs',
+ undefined,
+ 'If you do not first remove incomplete tasks, they will be left as-is, possibly leaving tasks that have already been addressed outside of MapRoulette'
+ )}
@@ -142,9 +189,11 @@ export const RebuildTasksDialog = ({ challengeId, open, onOpenChange, sourceType
- Warning: Rebuilding can lead to task duplication if your feature ids are not setup
- properly or if matching up old data with new data is unsuccessful. This operation
- cannot be undone!
+ {t(
+ 'managementPages.rebuildTasksDialog.warning',
+ undefined,
+ 'Warning: Rebuilding can lead to task duplication if your feature ids are not setup properly or if matching up old data with new data is unsuccessful. This operation cannot be undone!'
+ )}
- Learn More
+ {t('managementPages.rebuildTasksDialog.learnMore', undefined, 'Learn More')}
{isLocal && (
- New GeoJSON file
+
+ {t(
+ 'managementPages.rebuildTasksDialog.newGeoJsonFile',
+ undefined,
+ 'New GeoJSON file'
+ )}
+
setLocalFile(e.target.files?.[0] ?? null)}
/>
- Date data was sourced (optional)
+
+ {t(
+ 'managementPages.rebuildTasksDialog.dataOriginDate',
+ undefined,
+ 'Date data was sourced (optional)'
+ )}
+
setRemoveUnmatched(c === true)}
/>
- First remove incomplete tasks
+ {t(
+ 'managementPages.rebuildTasksDialog.removeIncomplete',
+ undefined,
+ 'First remove incomplete tasks'
+ )}
onOpenChange(false)} disabled={isPending}>
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
- {isPending ? 'Rebuilding…' : 'Proceed'}
+ {isPending
+ ? t('managementPages.rebuildTasksDialog.rebuilding', undefined, 'Rebuilding…')
+ : t('managementPages.rebuildTasksDialog.proceed', undefined, 'Proceed')}
diff --git a/src/components/Pages/ManagementPages/shared/VisibilityToggle.tsx b/src/components/Pages/ManagementPages/shared/VisibilityToggle.tsx
index b7a71dfb2..86956f9ac 100644
--- a/src/components/Pages/ManagementPages/shared/VisibilityToggle.tsx
+++ b/src/components/Pages/ManagementPages/shared/VisibilityToggle.tsx
@@ -1,6 +1,7 @@
import { useId } from 'react'
import { toast } from 'sonner'
import { Switch } from '@/components/ui/Switch'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
interface Props {
@@ -18,15 +19,18 @@ export const VisibilityToggle = ({
onToggle,
label,
disabled,
- errorMessage = 'Could not update visibility',
+ errorMessage,
}: Props) => {
+ const { t } = useIntl()
const switchId = useId()
+ const resolvedErrorMessage =
+ errorMessage ?? t('common.couldNotUpdateVisibility', undefined, 'Could not update visibility')
const handleChange = async (checked: boolean) => {
try {
await onToggle(id, checked)
} catch (error) {
logger.error('Toggle visibility failed', { error })
- toast.error(errorMessage)
+ toast.error(resolvedErrorMessage)
}
}
return (
diff --git a/src/components/Pages/NotificationsPage/NotificationFilters.tsx b/src/components/Pages/NotificationsPage/NotificationFilters.tsx
index 0790d1ef1..c121ed5d4 100644
--- a/src/components/Pages/NotificationsPage/NotificationFilters.tsx
+++ b/src/components/Pages/NotificationsPage/NotificationFilters.tsx
@@ -7,6 +7,7 @@ import {
SelectValue,
} from '@/components/ui/Select'
import { useNotificationsPageContext } from '@/contexts/NotificationsPageContext'
+import { useIntl } from '@/i18n'
import { cn } from '@/lib/utils'
import {
NOTIFICATION_CATEGORIES,
@@ -59,6 +60,7 @@ const Pill = ({ label, count, active, onClick, variant = 'category' }: PillProps
)
export const NotificationFilters = () => {
+ const { t } = useIntl()
const {
filters: {
category,
@@ -118,13 +120,15 @@ export const NotificationFilters = () => {
-
+
- All Tasks
+
+ {t('notificationsPage.filters.allTasks', undefined, 'All Tasks')}
+
{filterOptions.tasks.map((taskId: number) => (
- Task #{taskId}
+ {t('common.taskWithTaskId', { taskId }, 'Task #{taskId}')}
))}
@@ -132,13 +136,16 @@ export const NotificationFilters = () => {
-
+
- All Types
+
+ {t('notificationsPage.filters.allTypes', undefined, 'All Types')}
+
{filterOptions.types.map((typeId: number) => (
- {NOTIFICATION_TYPE_NAMES[typeId] || `Type ${typeId}`}
+ {NOTIFICATION_TYPE_NAMES[typeId] ||
+ t('notificationsPage.filters.typeOptionFallback', { typeId }, 'Type {typeId}')}
))}
@@ -146,10 +153,14 @@ export const NotificationFilters = () => {
-
+
- All Senders
+
+ {t('notificationsPage.filters.allSenders', undefined, 'All Senders')}
+
{filterOptions.fromUsers.map((username: string) => (
{username}
@@ -160,10 +171,12 @@ export const NotificationFilters = () => {
-
+
- All Challenges
+
+ {t('common.allChallenges', undefined, 'All Challenges')}
+
{filterOptions.challenges.map((challengeName: string) => (
{challengeName}
@@ -179,7 +192,7 @@ export const NotificationFilters = () => {
className="border-red-500 text-red-600 hover:bg-red-50 hover:text-red-700 dark:border-red-600 dark:text-red-400 dark:hover:bg-red-950/20 dark:hover:text-red-300"
onClick={clearFilters}
>
- Clear Filters
+ {t('notificationsPage.filters.clearFilters', undefined, 'Clear Filters')}
)}
diff --git a/src/components/Pages/NotificationsPage/NotificationItem.tsx b/src/components/Pages/NotificationsPage/NotificationItem.tsx
index 157466d2c..b89a320f0 100644
--- a/src/components/Pages/NotificationsPage/NotificationItem.tsx
+++ b/src/components/Pages/NotificationsPage/NotificationItem.tsx
@@ -4,6 +4,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar'
import { Button } from '@/components/ui/Button'
import { Checkbox } from '@/components/ui/Checkbox'
import { useNotificationsContext } from '@/contexts/NotificationsContext'
+import { useIntl } from '@/i18n'
import { formatTimeAgo } from '@/lib/date'
import { cn, initials } from '@/lib/utils'
import type { Notification } from '@/types/Notification'
@@ -35,6 +36,7 @@ export const NotificationItem = ({
onSelectChange,
onLinkClick,
}: NotificationItemProps) => {
+ const { t } = useIntl()
const {
markAsRead,
markAsUnread,
@@ -56,7 +58,8 @@ export const NotificationItem = ({
: deletingId === notification.id
const notificationTypeName =
- NOTIFICATION_TYPE_NAMES[notification.notificationType] || 'Notification'
+ NOTIFICATION_TYPE_NAMES[notification.notificationType] ||
+ t('common.notification', undefined, 'Notification')
const createdDate = new Date(notification.created)
const timeAgo = formatTimeAgo(createdDate)
@@ -129,7 +132,15 @@ export const NotificationItem = ({
indeterminate={isIndeterminate}
onCheckedChange={handleCheckboxChange}
className="mt-1.5 shrink-0"
- aria-label={`Select notification from ${notification.fromUsername || 'unknown'}`}
+ aria-label={t(
+ 'notificationsPage.item.selectFrom',
+ {
+ from:
+ notification.fromUsername ||
+ t('notificationsPage.item.unknownSender', undefined, 'unknown'),
+ },
+ 'Select notification from {from}'
+ )}
onClick={(e) => e.stopPropagation()}
/>
)}
@@ -164,7 +175,9 @@ export const NotificationItem = ({
) : notification.challengeName ? (
- Challenge:
+
+ {t('notificationsPage.item.challengeLabel', undefined, 'Challenge:')}
+
{notification.challengeId ? (
- Task #{notification.taskId}
+ {t('common.taskWithTaskId', { taskId: notification.taskId }, 'Task #{taskId}')}
>
)}
@@ -217,9 +234,17 @@ export const NotificationItem = ({
search={{ comments: 1 }}
onClick={handleLinkClick}
className="truncate font-medium hover:text-blue-600 hover:underline dark:hover:text-blue-400"
- title={`Challenge #${notification.challengeId}`}
+ title={t(
+ 'common.challengeWithChallengeId',
+ { challengeId: notification.challengeId },
+ 'Challenge #{challengeId}'
+ )}
>
- Challenge #{notification.challengeId}
+ {t(
+ 'common.challengeWithChallengeId',
+ { challengeId: notification.challengeId },
+ 'Challenge #{challengeId}'
+ )}
>
)}
@@ -233,9 +258,17 @@ export const NotificationItem = ({
params={{ projectId: String(notification.projectId) }}
onClick={handleLinkClick}
className="truncate font-medium hover:text-blue-600 hover:underline dark:hover:text-blue-400"
- title={`Project #${notification.projectId}`}
+ title={t(
+ 'notificationsPage.item.projectRef',
+ { projectId: notification.projectId },
+ 'Project #{projectId}'
+ )}
>
- Project #{notification.projectId}
+ {t(
+ 'notificationsPage.item.projectRef',
+ { projectId: notification.projectId },
+ 'Project #{projectId}'
+ )}
>
)}
@@ -253,7 +286,7 @@ export const NotificationItem = ({
)}
{!notification.isRead ? (
@@ -263,8 +296,8 @@ export const NotificationItem = ({
className={actionClasses}
disabled={isMarkingRead}
onClick={handleMarkAsRead}
- title="Mark as read"
- aria-label="Mark as read"
+ title={t('notificationsPage.item.markAsRead', undefined, 'Mark as read')}
+ aria-label={t('notificationsPage.item.markAsRead', undefined, 'Mark as read')}
>
@@ -275,8 +308,8 @@ export const NotificationItem = ({
className={actionClasses}
disabled={isMarkingUnread}
onClick={handleMarkAsUnread}
- title="Mark as unread"
- aria-label="Mark as unread"
+ title={t('notificationsPage.item.markAsUnread', undefined, 'Mark as unread')}
+ aria-label={t('notificationsPage.item.markAsUnread', undefined, 'Mark as unread')}
>
@@ -291,8 +324,12 @@ export const NotificationItem = ({
)}
disabled={isDeleting}
onClick={handleDelete}
- title="Delete notification"
- aria-label="Delete notification"
+ title={t('notificationsPage.item.deleteNotification', undefined, 'Delete notification')}
+ aria-label={t(
+ 'notificationsPage.item.deleteNotification',
+ undefined,
+ 'Delete notification'
+ )}
>
diff --git a/src/components/Pages/NotificationsPage/NotificationSelectAll.tsx b/src/components/Pages/NotificationsPage/NotificationSelectAll.tsx
index 0b7b0d0ac..33176dfa3 100644
--- a/src/components/Pages/NotificationsPage/NotificationSelectAll.tsx
+++ b/src/components/Pages/NotificationsPage/NotificationSelectAll.tsx
@@ -1,8 +1,10 @@
import { useId } from 'react'
import { Checkbox } from '@/components/ui/Checkbox'
import { useNotificationsPageContext } from '@/contexts/NotificationsPageContext'
+import { useIntl } from '@/i18n'
export const NotificationSelectAll = () => {
+ const { t } = useIntl()
const {
groupByTask,
selectedNotificationIds,
@@ -25,12 +27,20 @@ export const NotificationSelectAll = () => {
checked={allSelected}
indeterminate={someSelected && !allSelected}
onCheckedChange={(checked) => handleSelectAll(checked === true)}
- aria-label="Select all notifications"
+ aria-label={t(
+ 'notificationsPage.selectAll.ariaLabel',
+ undefined,
+ 'Select all notifications'
+ )}
/>
{selectedNotificationIds.size > 0
- ? `${selectedNotificationIds.size} of ${totalNotificationCount} selected`
- : 'Select all'}
+ ? t(
+ 'notificationsPage.selectAll.selectedCount',
+ { selected: selectedNotificationIds.size, total: totalNotificationCount },
+ '{selected} of {total} selected'
+ )
+ : t('notificationsPage.selectAll.selectAll', undefined, 'Select all')}
)
diff --git a/src/components/Pages/NotificationsPage/NotificationThreadDialog.tsx b/src/components/Pages/NotificationsPage/NotificationThreadDialog.tsx
index 7c86536b7..76feea6a3 100644
--- a/src/components/Pages/NotificationsPage/NotificationThreadDialog.tsx
+++ b/src/components/Pages/NotificationsPage/NotificationThreadDialog.tsx
@@ -16,6 +16,7 @@ import {
} from '@/components/ui/Dialog'
import { useAuthContext } from '@/contexts/AuthContext'
import { useNotificationsContext } from '@/contexts/NotificationsContext'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import { NotificationItem } from './NotificationItem'
@@ -24,6 +25,7 @@ interface NotificationThreadDialogProps {
}
export const NotificationThreadDialog = ({ onViewAll }: NotificationThreadDialogProps = {}) => {
+ const { t } = useIntl()
const { openNotificationThread: thread, closeThread, markAllAsRead } = useNotificationsContext()
const { user } = useAuthContext()
@@ -60,13 +62,13 @@ export const NotificationThreadDialog = ({ onViewAll }: NotificationThreadDialog
try {
await addCommentMutation.mutateAsync({ taskId, commentText: trimmed })
setReplyValue('')
- toast.success('Reply posted')
+ toast.success(t('notificationsPage.thread.replyPosted', undefined, 'Reply posted'))
} catch (error) {
logger.error('Failed to post reply from notification thread', {
error,
taskId,
})
- toast.error('Failed to post reply')
+ toast.error(t('notificationsPage.thread.replyFailed', undefined, 'Failed to post reply'))
// Re-throw so the composer can exit its busy state via its finally block.
throw error
}
@@ -86,15 +88,40 @@ export const NotificationThreadDialog = ({ onViewAll }: NotificationThreadDialog
{isThread
- ? `${thread?.length} notifications`
+ ? t(
+ 'notificationsPage.thread.title.count',
+ { count: thread?.length ?? 0 },
+ '{count} notifications'
+ )
: taskId
- ? `Notification for Task #${taskId}`
- : 'Notification'}
+ ? t(
+ 'notificationsPage.thread.title.task',
+ { taskId },
+ 'Notification for Task #{taskId}'
+ )
+ : t('common.notification', undefined, 'Notification')}
{isThread
- ? `Grouped together for ${taskId ? `Task #${taskId}` : (challengeRef ?? 'this thread')}`
- : 'View notification details'}
+ ? t(
+ 'common.taskWithTaskId',
+ {
+ ref: taskId
+ ? t('common.taskWithTaskId', { taskId }, 'Task #{taskId}')
+ : (challengeRef ??
+ t(
+ 'notificationsPage.thread.description.thisThread',
+ undefined,
+ 'this thread'
+ )),
+ },
+ 'Grouped together for {ref}'
+ )
+ : t(
+ 'notificationsPage.thread.description.viewDetails',
+ undefined,
+ 'View notification details'
+ )}
@@ -116,11 +143,15 @@ export const NotificationThreadDialog = ({ onViewAll }: NotificationThreadDialog
{taskId ? (
- Comments on this task
+ {t('notificationsPage.thread.commentsHeading', undefined, 'Comments on this task')}
{commentsLoading ? (
@@ -130,7 +161,11 @@ export const NotificationThreadDialog = ({ onViewAll }: NotificationThreadDialog
)}
@@ -141,14 +176,18 @@ export const NotificationThreadDialog = ({ onViewAll }: NotificationThreadDialog
value={replyValue}
onChange={setReplyValue}
onSubmit={handleReplySubmit}
- placeholder={`Reply on Task #${taskId}…`}
- submitLabel="Reply"
+ placeholder={t(
+ 'notificationsPage.thread.replyPlaceholder',
+ { taskId },
+ 'Reply on Task #{taskId}…'
+ )}
+ submitLabel={t('common.reply', undefined, 'Reply')}
disabled={addCommentMutation.isPending}
/>
) : (
- Sign in to reply
+ {t('notificationsPage.thread.signInToReply', undefined, 'Sign in to reply')}
)}
@@ -162,8 +201,13 @@ export const NotificationThreadDialog = ({ onViewAll }: NotificationThreadDialog
disabled={unreadIds.length === 0}
>
- Mark thread as read
- {unreadIds.length > 0 ? ` (${unreadIds.length})` : ''}
+ {unreadIds.length > 0
+ ? t(
+ 'notificationsPage.thread.markThreadReadCount',
+ { count: unreadIds.length },
+ 'Mark thread as read ({count})'
+ )
+ : t('notificationsPage.thread.markThreadRead', undefined, 'Mark thread as read')}
{taskId ? (
@@ -173,7 +217,7 @@ export const NotificationThreadDialog = ({ onViewAll }: NotificationThreadDialog
search={{ tab: 'comments' }}
onClick={closeThread}
>
- Open task
+ {t('notificationsPage.thread.openTask', undefined, 'Open task')}
@@ -181,7 +225,7 @@ export const NotificationThreadDialog = ({ onViewAll }: NotificationThreadDialog
{onViewAll && (
- View all notifications
+ {t('common.viewAllNotifications', undefined, 'View all notifications')}
)}
diff --git a/src/components/Pages/NotificationsPage/NotificationToolbar.tsx b/src/components/Pages/NotificationsPage/NotificationToolbar.tsx
index 591a44571..ec197c51b 100644
--- a/src/components/Pages/NotificationsPage/NotificationToolbar.tsx
+++ b/src/components/Pages/NotificationsPage/NotificationToolbar.tsx
@@ -1,8 +1,10 @@
import { Button } from '@/components/ui/Button'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/Tabs'
import { useNotificationsPageContext } from '@/contexts/NotificationsPageContext'
+import { useIntl } from '@/i18n'
export const NotificationToolbar = () => {
+ const { t } = useIntl()
const {
activeTab,
setActiveTab,
@@ -20,8 +22,12 @@ export const NotificationToolbar = () => {
setActiveTab(value as 'unread' | 'all')}>
- Unread ({filteredUnreadCount})
- All ({filteredAllCount})
+
+ {t('common.unread', { count: filteredUnreadCount }, 'Unread ({count})')}
+
+
+ {t('common.all', { count: filteredAllCount }, 'All ({count})')}
+
@@ -34,10 +40,14 @@ export const NotificationToolbar = () => {
disabled={isMarkingSelected || filteredNotifications.length === 0}
>
{isMarkingSelected
- ? 'Marking...'
+ ? t('notificationsPage.toolbar.marking', undefined, 'Marking...')
: selectedNotificationIds.size > 0
- ? `Mark ${selectedNotificationIds.size} as unread`
- : 'Mark all as unread'}
+ ? t(
+ 'notificationsPage.toolbar.markSelectedUnread',
+ { count: selectedNotificationIds.size },
+ 'Mark {count} as unread'
+ )
+ : t('notificationsPage.toolbar.markAllUnread', undefined, 'Mark all as unread')}
) : (
{
disabled={isMarkingSelected || filteredNotifications.length === 0}
>
{isMarkingSelected
- ? 'Marking...'
+ ? t('notificationsPage.toolbar.marking', undefined, 'Marking...')
: selectedNotificationIds.size > 0
- ? `Mark ${selectedNotificationIds.size} as read`
- : 'Mark all as read'}
+ ? t(
+ 'notificationsPage.toolbar.markSelectedRead',
+ { count: selectedNotificationIds.size },
+ 'Mark {count} as read'
+ )
+ : t('common.markAllAsRead', undefined, 'Mark all as read')}
)}
diff --git a/src/components/Pages/NotificationsPage/NotificationsPageContent.tsx b/src/components/Pages/NotificationsPage/NotificationsPageContent.tsx
index cf6f1d805..c93ab6813 100644
--- a/src/components/Pages/NotificationsPage/NotificationsPageContent.tsx
+++ b/src/components/Pages/NotificationsPage/NotificationsPageContent.tsx
@@ -4,6 +4,7 @@ import { Card } from '@/components/ui/Card'
import { Checkbox } from '@/components/ui/Checkbox'
import { useNotificationsContext } from '@/contexts/NotificationsContext'
import { useNotificationsPageContext } from '@/contexts/NotificationsPageContext'
+import { useIntl } from '@/i18n'
import { cn } from '@/lib/utils'
import { NotificationFilters } from './NotificationFilters'
import { NotificationItem } from './NotificationItem'
@@ -13,6 +14,7 @@ import { NotificationToolbar } from './NotificationToolbar'
import { PageHeader } from './PageHeader'
export const NotificationsPageContent = () => {
+ const { t } = useIntl()
const { notifications, isLoading } = useNotificationsContext()
const {
activeTab,
@@ -73,7 +75,14 @@ export const NotificationsPageContent = () => {
return (
-
+
{
htmlFor={groupByTaskCheckboxId}
className="cursor-pointer text-sm text-zinc-600 dark:text-slate-400"
>
- Group by Task
+ {t('notificationsPage.content.groupByTask', undefined, 'Group by Task')}
{groupByTask && (
- Notifications for the same task are grouped together
+ {t(
+ 'notificationsPage.content.groupByTaskHint',
+ undefined,
+ 'Notifications for the same task are grouped together'
+ )}
)}
@@ -98,7 +111,7 @@ export const NotificationsPageContent = () => {
{isLoading ? (
- Loading notifications...
+ {t('common.loadingNotifications', undefined, 'Loading notifications...')}
) : displayNotifications.length > 0 ? (
@@ -154,11 +167,17 @@ export const NotificationsPageContent = () => {
) : (
-
You're all up to date
+
+ {t('common.youreAllUpToDate', undefined, "You're all up to date")}
+
{activeTab === 'unread'
- ? 'You have no unread notifications at the moment.'
- : 'You have no notifications.'}
+ ? t(
+ 'common.noUnreadNotificationsYet',
+ undefined,
+ 'You have no unread notifications at the moment.'
+ )
+ : t('common.noNotificationsYet', undefined, 'You have no notifications.')}
diff --git a/src/components/Pages/NotificationsPage/SavedViewsMenu.tsx b/src/components/Pages/NotificationsPage/SavedViewsMenu.tsx
index 75964d73c..d1520d1ef 100644
--- a/src/components/Pages/NotificationsPage/SavedViewsMenu.tsx
+++ b/src/components/Pages/NotificationsPage/SavedViewsMenu.tsx
@@ -13,6 +13,7 @@ import {
import { Input } from '@/components/ui/Input'
import { useNotificationsPageContext } from '@/contexts/NotificationsPageContext'
import type { NotificationFilterState } from '@/hooks/useNotificationFilters'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import { cn } from '@/lib/utils'
@@ -55,6 +56,7 @@ const persistSavedViews = (views: SavedView[]) => {
}
export const SavedViewsMenu = () => {
+ const { t } = useIntl()
const { filters } = useNotificationsPageContext()
const { currentState, applyFilterState, hasActiveFilters } = filters
@@ -100,7 +102,9 @@ export const SavedViewsMenu = () => {
const handleSaveCurrent = () => {
const name = newName.trim()
if (!name) {
- toast.error('View name is required')
+ toast.error(
+ t('notificationsPage.savedViews.nameRequired', undefined, 'View name is required')
+ )
return
}
const next: SavedView = {
@@ -113,16 +117,18 @@ export const SavedViewsMenu = () => {
persistSavedViews(updated)
setViews(updated)
resetNamingState()
- toast.success(`Saved view "${name}"`)
+ toast.success(t('notificationsPage.savedViews.saved', { name }, 'Saved view "{name}"'))
} catch {
- toast.error('Failed to save view')
+ toast.error(t('notificationsPage.savedViews.saveFailed', undefined, 'Failed to save view'))
}
}
const handleApply = (view: SavedView) => {
applyFilterState(view.state)
setIsOpen(false)
- toast.success(`Applied view "${view.name}"`)
+ toast.success(
+ t('notificationsPage.savedViews.applied', { name: view.name }, 'Applied view "{name}"')
+ )
}
const handleDelete = (id: string) => {
@@ -130,9 +136,11 @@ export const SavedViewsMenu = () => {
try {
persistSavedViews(updated)
setViews(updated)
- toast.success('View deleted')
+ toast.success(t('notificationsPage.savedViews.deleted', undefined, 'View deleted'))
} catch {
- toast.error('Failed to delete view')
+ toast.error(
+ t('notificationsPage.savedViews.deleteFailed', undefined, 'Failed to delete view')
+ )
}
}
@@ -144,7 +152,9 @@ export const SavedViewsMenu = () => {
const handleCommitRename = (id: string) => {
const name = editingName.trim()
if (!name) {
- toast.error('View name is required')
+ toast.error(
+ t('notificationsPage.savedViews.nameRequired', undefined, 'View name is required')
+ )
return
}
const updated = views.map((v) => (v.id === id ? { ...v, name } : v))
@@ -152,9 +162,11 @@ export const SavedViewsMenu = () => {
persistSavedViews(updated)
setViews(updated)
resetEditingState()
- toast.success('View renamed')
+ toast.success(t('notificationsPage.savedViews.renamed', undefined, 'View renamed'))
} catch {
- toast.error('Failed to rename view')
+ toast.error(
+ t('notificationsPage.savedViews.renameFailed', undefined, 'Failed to rename view')
+ )
}
}
@@ -163,7 +175,7 @@ export const SavedViewsMenu = () => {
- Saved views
+ {t('notificationsPage.savedViews.title', undefined, 'Saved views')}
{views.length > 0 ? (
{views.length}
@@ -173,11 +185,11 @@ export const SavedViewsMenu = () => {
- Saved views
+ {t('notificationsPage.savedViews.title', undefined, 'Saved views')}
{views.length === 0 ? (
- No saved views yet.
+ {t('notificationsPage.savedViews.empty', undefined, 'No saved views yet.')}
) : (
views.map((view) =>
@@ -197,13 +209,17 @@ export const SavedViewsMenu = () => {
}}
autoFocus
className="h-7 text-sm"
- aria-label="Rename saved view"
+ aria-label={t(
+ 'notificationsPage.savedViews.renameInputLabel',
+ undefined,
+ 'Rename saved view'
+ )}
/>
handleCommitRename(view.id)}
- aria-label="Save name"
+ aria-label={t('notificationsPage.savedViews.saveName', undefined, 'Save name')}
>
@@ -211,7 +227,11 @@ export const SavedViewsMenu = () => {
size="icon-sm"
variant="ghost"
onClick={resetEditingState}
- aria-label="Cancel rename"
+ aria-label={t(
+ 'notificationsPage.savedViews.cancelRename',
+ undefined,
+ 'Cancel rename'
+ )}
>
@@ -238,7 +258,11 @@ export const SavedViewsMenu = () => {
e.stopPropagation()
handleStartRename(view)
}}
- aria-label={`Rename ${view.name}`}
+ aria-label={t(
+ 'notificationsPage.savedViews.renameAction',
+ { name: view.name },
+ 'Rename {name}'
+ )}
>
@@ -253,7 +277,11 @@ export const SavedViewsMenu = () => {
e.stopPropagation()
handleDelete(view.id)
}}
- aria-label={`Delete ${view.name}`}
+ aria-label={t(
+ 'notificationsPage.savedViews.deleteAction',
+ { name: view.name },
+ 'Delete {name}'
+ )}
>
@@ -277,15 +305,23 @@ export const SavedViewsMenu = () => {
}
}}
autoFocus
- placeholder="View name"
+ placeholder={t(
+ 'notificationsPage.savedViews.namePlaceholder',
+ undefined,
+ 'View name'
+ )}
className="h-7 text-sm"
- aria-label="Saved view name"
+ aria-label={t(
+ 'notificationsPage.savedViews.newNameLabel',
+ undefined,
+ 'Saved view name'
+ )}
/>
@@ -293,7 +329,7 @@ export const SavedViewsMenu = () => {
size="icon-sm"
variant="ghost"
onClick={resetNamingState}
- aria-label="Cancel save"
+ aria-label={t('notificationsPage.savedViews.cancelSave', undefined, 'Cancel save')}
>
@@ -307,7 +343,9 @@ export const SavedViewsMenu = () => {
}}
>
- Save current as…
+
+ {t('notificationsPage.savedViews.saveCurrentAs', undefined, 'Save current as…')}
+
)}
diff --git a/src/components/Pages/ProfilePage/ProfileHeader.tsx b/src/components/Pages/ProfilePage/ProfileHeader.tsx
index d0c6cd4e5..4741a3985 100644
--- a/src/components/Pages/ProfilePage/ProfileHeader.tsx
+++ b/src/components/Pages/ProfilePage/ProfileHeader.tsx
@@ -1,6 +1,7 @@
import { ExternalLink } from 'lucide-react'
import { PointsTicker } from '@/components/shared/PointsTicker'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar'
+import { useIntl } from '@/i18n'
import { initials } from '@/lib/utils'
import type { User } from '@/types/User'
@@ -10,6 +11,7 @@ interface Props {
}
export const ProfileHeader = ({ user, showLivePoints }: Props) => {
+ const { t } = useIntl()
const displayName = user.osmProfile.displayName
const avatarURL = user.osmProfile.avatarURL
const createdDate = user.created
@@ -26,7 +28,9 @@ export const ProfileHeader = ({ user, showLivePoints }: Props) => {
{displayName}
{createdDate && (
-
User since: {createdDate}
+
+ {t('profilePage.header.userSince', { date: createdDate }, 'User since: {date}')}
+
)}
{showLivePoints && (
diff --git a/src/components/Pages/ProfilePage/TimeRangeSelector.tsx b/src/components/Pages/ProfilePage/TimeRangeSelector.tsx
index 1e0d361c6..dc15d4fb4 100644
--- a/src/components/Pages/ProfilePage/TimeRangeSelector.tsx
+++ b/src/components/Pages/ProfilePage/TimeRangeSelector.tsx
@@ -1,16 +1,18 @@
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/ToggleGroup'
+import { useIntl } from '@/i18n'
import { useProfilePageContext } from './contexts/ProfilePageContext'
-const presets: Array<{ label: string; monthDuration: number }> = [
- { label: '1m', monthDuration: 1 },
- { label: '3m', monthDuration: 3 },
- { label: '6m', monthDuration: 6 },
- { label: '9m', monthDuration: 9 },
- { label: '12m', monthDuration: 12 },
- { label: 'All', monthDuration: -1 },
+const presets: Array<{ labelId: string; defaultLabel: string; monthDuration: number }> = [
+ { labelId: 'profilePage.timeRange.oneMonth', defaultLabel: '1m', monthDuration: 1 },
+ { labelId: 'profilePage.timeRange.threeMonths', defaultLabel: '3m', monthDuration: 3 },
+ { labelId: 'profilePage.timeRange.sixMonths', defaultLabel: '6m', monthDuration: 6 },
+ { labelId: 'profilePage.timeRange.nineMonths', defaultLabel: '9m', monthDuration: 9 },
+ { labelId: 'profilePage.timeRange.twelveMonths', defaultLabel: '12m', monthDuration: 12 },
+ { labelId: 'profilePage.timeRange.all', defaultLabel: 'All', monthDuration: -1 },
]
export const TimeRangeSelector = () => {
+ const { t } = useIntl()
const { timeRange, setMonthDuration } = useProfilePageContext()
return (
@@ -21,11 +23,11 @@ export const TimeRangeSelector = () => {
if (!value) return
setMonthDuration(Number(value))
}}
- aria-label="Time range"
+ aria-label={t('profilePage.timeRange.ariaLabel', undefined, 'Time range')}
>
{presets.map((preset) => (
- {preset.label}
+ {t(preset.labelId, undefined, preset.defaultLabel)}
))}
diff --git a/src/components/Pages/ProfilePage/blocks/ReviewStatsBlock.tsx b/src/components/Pages/ProfilePage/blocks/ReviewStatsBlock.tsx
index b4982b8e5..c47f71cbf 100644
--- a/src/components/Pages/ProfilePage/blocks/ReviewStatsBlock.tsx
+++ b/src/components/Pages/ProfilePage/blocks/ReviewStatsBlock.tsx
@@ -2,21 +2,23 @@ import { ClipboardCheck } from 'lucide-react'
import { api } from '@/api'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Skeleton } from '@/components/ui/Skeleton'
+import { useIntl } from '@/i18n'
import { useProfilePageContext } from '../contexts/ProfilePageContext'
-const reviewStatusLabels: Record
= {
- '0': 'Needed',
- '1': 'Approved',
- '2': 'Rejected',
- '3': 'Assisted',
- '4': 'Disputed',
- '5': 'Unnecessary',
-}
-
export const ReviewStatsBlock = () => {
+ const { t } = useIntl()
const { userId, timeRange } = useProfilePageContext()
const { data, isLoading, isError } = api.user.metrics(userId, timeRange.monthDuration)
+ const reviewStatusLabels: Record = {
+ '0': t('common.needed', undefined, 'Needed'),
+ '1': t('common.approved', undefined, 'Approved'),
+ '2': t('common.rejected', undefined, 'Rejected'),
+ '3': t('common.assisted', undefined, 'Assisted'),
+ '4': t('common.disputed', undefined, 'Disputed'),
+ '5': t('common.unnecessary', undefined, 'Unnecessary'),
+ }
+
const reviewedTotal = Object.values(data?.reviewedTasks ?? {}).reduce((a, b) => a + b, 0)
if (!isLoading && !isError && reviewedTotal === 0) {
@@ -28,20 +30,23 @@ export const ReviewStatsBlock = () => {
- Reviews Received
+ {t('profilePage.reviewStats.title', undefined, 'Reviews Received')}
{isLoading ? (
) : isError || !data ? (
- Couldn't load review stats.
+
+ {t('profilePage.reviewStats.loadError', undefined, "Couldn't load review stats.")}
+
) : (
{Object.entries(data.reviewedTasks ?? {}).map(([status, count]) => (
- {reviewStatusLabels[status] ?? `Status ${status}`}
+ {reviewStatusLabels[status] ??
+ t('common.statusWithStatus', { status }, 'Status {status}')}
{count.toLocaleString()}
diff --git a/src/components/Pages/ProfilePage/blocks/ReviewerStatsBlock.tsx b/src/components/Pages/ProfilePage/blocks/ReviewerStatsBlock.tsx
index 03877496e..19fbdb0f6 100644
--- a/src/components/Pages/ProfilePage/blocks/ReviewerStatsBlock.tsx
+++ b/src/components/Pages/ProfilePage/blocks/ReviewerStatsBlock.tsx
@@ -2,21 +2,23 @@ import { ShieldCheck } from 'lucide-react'
import { api } from '@/api'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Skeleton } from '@/components/ui/Skeleton'
+import { useIntl } from '@/i18n'
import { useProfilePageContext } from '../contexts/ProfilePageContext'
-const reviewStatusLabels: Record = {
- '0': 'Needed',
- '1': 'Approved',
- '2': 'Rejected',
- '3': 'Assisted',
- '4': 'Disputed',
- '5': 'Unnecessary',
-}
-
export const ReviewerStatsBlock = () => {
+ const { t } = useIntl()
const { userId, timeRange } = useProfilePageContext()
const { data, isLoading } = api.user.metrics(userId, timeRange.monthDuration)
+ const reviewStatusLabels: Record = {
+ '0': t('common.needed', undefined, 'Needed'),
+ '1': t('common.approved', undefined, 'Approved'),
+ '2': t('common.rejected', undefined, 'Rejected'),
+ '3': t('common.assisted', undefined, 'Assisted'),
+ '4': t('common.disputed', undefined, 'Disputed'),
+ '5': t('common.unnecessary', undefined, 'Unnecessary'),
+ }
+
const reviewerTotal = Object.values(data?.reviewTasks ?? {}).reduce((a, b) => a + b, 0)
if (!isLoading && reviewerTotal === 0) {
@@ -28,7 +30,7 @@ export const ReviewerStatsBlock = () => {
- Reviews Performed
+ {t('profilePage.reviewerStats.title', undefined, 'Reviews Performed')}
@@ -39,7 +41,8 @@ export const ReviewerStatsBlock = () => {
{Object.entries(data?.reviewTasks ?? {}).map(([status, count]) => (
- {reviewStatusLabels[status] ?? `Status ${status}`}
+ {reviewStatusLabels[status] ??
+ t('common.statusWithStatus', { status }, 'Status {status}')}
{count.toLocaleString()}
diff --git a/src/components/Pages/ProfilePage/blocks/TaskStatsBlock.tsx b/src/components/Pages/ProfilePage/blocks/TaskStatsBlock.tsx
index 856cf52e1..660bc3b3e 100644
--- a/src/components/Pages/ProfilePage/blocks/TaskStatsBlock.tsx
+++ b/src/components/Pages/ProfilePage/blocks/TaskStatsBlock.tsx
@@ -3,10 +3,12 @@ import { api } from '@/api'
import { DigitDisplay } from '@/components/shared/DigitDisplay'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Skeleton } from '@/components/ui/Skeleton'
+import { useIntl } from '@/i18n'
import { STATUS_LABELS } from '@/lib/taskConstants'
import { useProfilePageContext } from '../contexts/ProfilePageContext'
export const TaskStatsBlock = () => {
+ const { t } = useIntl()
const { userId, timeRange } = useProfilePageContext()
const { data, isLoading, isError } = api.user.metrics(userId, timeRange.monthDuration)
@@ -15,19 +17,21 @@ export const TaskStatsBlock = () => {
- Tasks
+ {t('common.tasks', undefined, 'Tasks')}
{isLoading ? (
) : isError || !data ? (
- Couldn't load task stats.
+
+ {t('profilePage.taskStats.loadError', undefined, "Couldn't load task stats.")}
+
) : (
<>
- Total completed
+ {t('profilePage.taskStats.totalCompleted', undefined, 'Total completed')}
@@ -36,7 +40,8 @@ export const TaskStatsBlock = () => {
{Object.entries(data.tasks).map(([status, count]) => (
- {STATUS_LABELS[Number(status)] ?? `Status ${status}`}
+ {STATUS_LABELS[Number(status)] ??
+ t('common.statusWithStatus', { status }, 'Status {status}')}
{count.toLocaleString()}
diff --git a/src/components/Pages/ProfilePage/index.tsx b/src/components/Pages/ProfilePage/index.tsx
index 73fc9d8ef..acd10f917 100644
--- a/src/components/Pages/ProfilePage/index.tsx
+++ b/src/components/Pages/ProfilePage/index.tsx
@@ -1,6 +1,7 @@
import { api } from '@/api'
import { Loader } from '@/components/ui/Loader'
import { useAuthContext } from '@/contexts/AuthContext'
+import { useIntl } from '@/i18n'
import type { User } from '@/types/User'
import { ProfilePageProvider } from './contexts/ProfilePageContext'
import { ProfileHeader } from './ProfileHeader'
@@ -13,6 +14,7 @@ interface Props {
}
export const ProfilePage = ({ userId }: Props = {}) => {
+ const { t } = useIntl()
const { user: authedUser } = useAuthContext()
const isViewingOther = userId !== undefined && userId !== authedUser?.id
const publicUserQuery = api.user.getUser(isViewingOther ? userId : 0)
@@ -27,8 +29,8 @@ export const ProfilePage = ({ userId }: Props = {}) => {
{isViewingOther
- ? "Couldn't load that user's profile."
- : 'Please log in to view your profile'}
+ ? t('profilePage.index.loadError', undefined, "Couldn't load that user's profile.")
+ : t('profilePage.index.loginRequired', undefined, 'Please log in to view your profile')}
)
diff --git a/src/components/Pages/ProfilePage/sections/AchievementsSection.tsx b/src/components/Pages/ProfilePage/sections/AchievementsSection.tsx
index 615d79b97..5134ff923 100644
--- a/src/components/Pages/ProfilePage/sections/AchievementsSection.tsx
+++ b/src/components/Pages/ProfilePage/sections/AchievementsSection.tsx
@@ -1,5 +1,6 @@
import { useId } from 'react'
import { AchievementBadge } from '@/components/shared/AchievementBadge'
+import { useIntl } from '@/i18n'
import {
type AchievementCategory,
achievementCategoryLabel,
@@ -11,6 +12,7 @@ interface Props {
}
export const AchievementsSection = ({ earnedIds }: Props) => {
+ const { t } = useIntl()
const headingId = useId()
const earnedSet = new Set(earnedIds)
const grouped = achievementDefinitions.reduce(
@@ -29,10 +31,14 @@ export const AchievementsSection = ({ earnedIds }: Props) => {
- Achievements
+ {t('profilePage.achievements.title', undefined, 'Achievements')}
- {earnedCount} of {totalCount} earned
+ {t(
+ 'profilePage.achievements.earnedCount',
+ { earned: earnedCount, total: totalCount },
+ '{earned} of {total} earned'
+ )}
diff --git a/src/components/Pages/ProfilePage/sections/MetricsSection.tsx b/src/components/Pages/ProfilePage/sections/MetricsSection.tsx
index 4e5a9de11..ee5e43267 100644
--- a/src/components/Pages/ProfilePage/sections/MetricsSection.tsx
+++ b/src/components/Pages/ProfilePage/sections/MetricsSection.tsx
@@ -1,16 +1,18 @@
import { useId } from 'react'
+import { useIntl } from '@/i18n'
import { ReviewerStatsBlock } from '../blocks/ReviewerStatsBlock'
import { ReviewStatsBlock } from '../blocks/ReviewStatsBlock'
import { TaskStatsBlock } from '../blocks/TaskStatsBlock'
import { TimeRangeSelector } from '../TimeRangeSelector'
export const MetricsSection = () => {
+ const { t } = useIntl()
const headingId = useId()
return (
- Metrics
+ {t('profilePage.metrics.title', undefined, 'Metrics')}
diff --git a/src/components/Pages/ProfilePage/sections/TopChallengesSection.tsx b/src/components/Pages/ProfilePage/sections/TopChallengesSection.tsx
index 56341064b..b3637d378 100644
--- a/src/components/Pages/ProfilePage/sections/TopChallengesSection.tsx
+++ b/src/components/Pages/ProfilePage/sections/TopChallengesSection.tsx
@@ -1,15 +1,17 @@
import { useId } from 'react'
import { TopChallengesList } from '@/components/shared/TopChallengesList'
+import { useIntl } from '@/i18n'
import { useProfilePageContext } from '../contexts/ProfilePageContext'
export const TopChallengesSection = () => {
+ const { t } = useIntl()
const { userId, timeRange } = useProfilePageContext()
const headingId = useId()
return (
- Top Challenges
+ {t('profilePage.topChallenges.title', undefined, 'Top Challenges')}
diff --git a/src/components/Pages/SuperAdminPages/SuperAdminAnalytics/index.tsx b/src/components/Pages/SuperAdminPages/SuperAdminAnalytics/index.tsx
index 2a8c456c6..563c3b765 100644
--- a/src/components/Pages/SuperAdminPages/SuperAdminAnalytics/index.tsx
+++ b/src/components/Pages/SuperAdminPages/SuperAdminAnalytics/index.tsx
@@ -1,8 +1,19 @@
import { Activity, BarChart3, FolderKanban, ListChecks, TrendingUp, Users } from 'lucide-react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'
import { StatCard, StatCardGrid } from '@/components/ui/StatCard'
+import { useIntl } from '@/i18n'
export const SuperAdminAnalytics = () => {
+ const { t } = useIntl()
+
+ const topProjects = [
+ t('superAdmin.analytics.projectHighwayMapping', undefined, 'Highway Mapping'),
+ t('superAdmin.analytics.projectBuildingFootprints', undefined, 'Building Footprints'),
+ t('superAdmin.analytics.projectParksAndRecreation', undefined, 'Parks and Recreation'),
+ t('superAdmin.analytics.projectStreetNames', undefined, 'Street Names'),
+ t('superAdmin.analytics.projectPoiValidation', undefined, 'POI Validation'),
+ ]
+
return (
{/* Header */}
@@ -10,61 +21,73 @@ export const SuperAdminAnalytics = () => {
- Platform Analytics
+ {t('superAdmin.analytics.title', undefined, 'Platform Analytics')}
- View comprehensive analytics and metrics across the platform.
+ {t(
+ 'superAdmin.analytics.description',
+ undefined,
+ 'View comprehensive analytics and metrics across the platform.'
+ )}
{/* Key Metrics */}
- Key Metrics
+ {t('superAdmin.analytics.keyMetrics', undefined, 'Key Metrics')}
}
description={
- +12.3% from last month
+ {t('superAdmin.analytics.totalUsersChange', undefined, '+12.3% from last month')}
}
/>
}
description={
- +8.1% from last month
+ {t('superAdmin.analytics.activeProjectsChange', undefined, '+8.1% from last month')}
}
/>
}
description={
- +15.2% from last month
+ {t(
+ 'superAdmin.analytics.activeChallengesChange',
+ undefined,
+ '+15.2% from last month'
+ )}
}
/>
}
description={
- +22.5% from last month
+ {t(
+ 'superAdmin.analytics.tasksCompletedChange',
+ undefined,
+ '+22.5% from last month'
+ )}
}
/>
@@ -75,24 +98,52 @@ export const SuperAdminAnalytics = () => {
- User Activity
- Active users over the past 30 days
+
+ {t('superAdmin.analytics.userActivityTitle', undefined, 'User Activity')}
+
+
+ {t(
+ 'superAdmin.analytics.userActivityDescription',
+ undefined,
+ 'Active users over the past 30 days'
+ )}
+
-
Chart visualization placeholder
+
+ {t(
+ 'superAdmin.analytics.chartPlaceholder',
+ undefined,
+ 'Chart visualization placeholder'
+ )}
+
- Task Completion Rate
- Tasks completed per day
+
+ {t('superAdmin.analytics.taskCompletionRateTitle', undefined, 'Task Completion Rate')}
+
+
+ {t(
+ 'superAdmin.analytics.taskCompletionRateDescription',
+ undefined,
+ 'Tasks completed per day'
+ )}
+
-
Chart visualization placeholder
+
+ {t(
+ 'superAdmin.analytics.chartPlaceholder',
+ undefined,
+ 'Chart visualization placeholder'
+ )}
+
@@ -101,16 +152,40 @@ export const SuperAdminAnalytics = () => {
{/* Performance Metrics */}
- Performance Metrics
+ {t('superAdmin.analytics.performanceMetrics', undefined, 'Performance Metrics')}
+
+
-
-
@@ -118,8 +193,16 @@ export const SuperAdminAnalytics = () => {
- Top Contributors
- Most active users this month
+
+ {t('superAdmin.analytics.topContributorsTitle', undefined, 'Top Contributors')}
+
+
+ {t(
+ 'superAdmin.analytics.topContributorsDescription',
+ undefined,
+ 'Most active users this month'
+ )}
+
@@ -133,7 +216,7 @@ export const SuperAdminAnalytics = () => {
- User {i}
+ {t('superAdmin.analytics.userLabel', { index: i }, 'User {index}')}
user{i}@example.com
@@ -141,7 +224,11 @@ export const SuperAdminAnalytics = () => {
- {Math.floor(Math.random() * 500 + 100)} tasks
+ {t(
+ 'common.tasksWithCount',
+ { count: Math.floor(Math.random() * 500 + 100) },
+ '{count} tasks'
+ )}
))}
@@ -151,18 +238,20 @@ export const SuperAdminAnalytics = () => {
- Most Active Projects
- Projects with most activity this month
+
+ {t('superAdmin.analytics.mostActiveProjectsTitle', undefined, 'Most Active Projects')}
+
+
+ {t(
+ 'superAdmin.analytics.mostActiveProjectsDescription',
+ undefined,
+ 'Projects with most activity this month'
+ )}
+
- {[
- 'Highway Mapping',
- 'Building Footprints',
- 'Parks and Recreation',
- 'Street Names',
- 'POI Validation',
- ].map((name) => (
+ {topProjects.map((name) => (
@@ -173,7 +262,11 @@ export const SuperAdminAnalytics = () => {
- {Math.floor(Math.random() * 1000 + 500)} tasks
+ {t(
+ 'common.tasksWithCount',
+ { count: Math.floor(Math.random() * 1000 + 500) },
+ '{count} tasks'
+ )}
))}
diff --git a/src/components/Pages/SuperAdminPages/SuperAdminChallenges/index.tsx b/src/components/Pages/SuperAdminPages/SuperAdminChallenges/index.tsx
index 45756de4c..598f101ea 100644
--- a/src/components/Pages/SuperAdminPages/SuperAdminChallenges/index.tsx
+++ b/src/components/Pages/SuperAdminPages/SuperAdminChallenges/index.tsx
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/Button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/Empty'
import { Progress } from '@/components/ui/Progress'
+import { useIntl } from '@/i18n'
import { getDifficultyColor, getDifficultyLabel } from '@/lib/difficultyLevelData'
import { cn } from '@/lib/utils'
@@ -50,6 +51,7 @@ const mockChallenges = [
]
const ChallengeCard = ({ challenge }: { challenge: (typeof mockChallenges)[0] }) => {
+ const { t } = useIntl()
const completionPercentage = challenge.completionPercentage || 0
return (
@@ -76,7 +78,9 @@ const ChallengeCard = ({ challenge }: { challenge: (typeof mockChallenges)[0] })
- {challenge.blurb || challenge.description || 'No description available'}
+ {challenge.blurb ||
+ challenge.description ||
+ t('common.noDescriptionAvailable', undefined, 'No description available')}
{/* Tasks Remaining */}
@@ -84,7 +88,7 @@ const ChallengeCard = ({ challenge }: { challenge: (typeof mockChallenges)[0] })
{challenge.tasksRemaining || 0}
{' '}
- tasks remaining
+ {t('common.tasksRemaining2', undefined, 'tasks remaining')}
{/* Progress Bar */}
@@ -108,10 +112,10 @@ const ChallengeCard = ({ challenge }: { challenge: (typeof mockChallenges)[0] })
- View
+ {t('common.view', undefined, 'View')}
- Edit
+ {t('common.edit', undefined, 'Edit')}
@@ -120,6 +124,7 @@ const ChallengeCard = ({ challenge }: { challenge: (typeof mockChallenges)[0] })
}
export const SuperAdminChallenges = () => {
+ const { t } = useIntl()
const [searchQuery, setSearchQuery] = useState('')
const filteredChallenges = mockChallenges.filter(
@@ -138,23 +143,27 @@ export const SuperAdminChallenges = () => {
- All Challenges
+ {t('common.allChallenges', undefined, 'All Challenges')}
- Browse and manage all challenges across the platform
+ {t(
+ 'superAdminChallenges.subtitle',
+ undefined,
+ 'Browse and manage all challenges across the platform'
+ )}
- Create New Challenge
+ {t('common.createNewChallenge', undefined, 'Create New Challenge')}
@@ -162,38 +171,54 @@ export const SuperAdminChallenges = () => {
- Total Challenges
+
+ {t('common.totalChallenges', undefined, 'Total Challenges')}
+
1,892
- +15% from last month
+
+ {t('superAdminChallenges.stats.totalChange', undefined, '+15% from last month')}
+
- Active Challenges
+
+ {t('common.activeChallenges', undefined, 'Active Challenges')}
+
1,345
- 71% of total
+
+ {t('superAdminChallenges.stats.activeShare', undefined, '71% of total')}
+
- Total Tasks
+
+ {t('superAdminChallenges.stats.totalTasks', undefined, 'Total Tasks')}
+
45.2K
- Across all challenges
+
+ {t('superAdminChallenges.stats.totalTasksNote', undefined, 'Across all challenges')}
+
- Avg. Completion
+
+ {t('common.avgCompletion', undefined, 'Avg. Completion')}
+
58%
- Platform average
+
+ {t('common.platformAverage', undefined, 'Platform average')}
+
@@ -213,8 +238,16 @@ export const SuperAdminChallenges = () => {
-
No challenges found
-
Try adjusting your search query.
+
+ {t('common.noChallengesFound', undefined, 'No challenges found')}
+
+
+ {t(
+ 'common.tryAdjustingYourSearchQuery',
+ undefined,
+ 'Try adjusting your search query.'
+ )}
+
)}
diff --git a/src/components/Pages/SuperAdminPages/SuperAdminHome/index.tsx b/src/components/Pages/SuperAdminPages/SuperAdminHome/index.tsx
index 62e34dad7..ea3e4f6d6 100644
--- a/src/components/Pages/SuperAdminPages/SuperAdminHome/index.tsx
+++ b/src/components/Pages/SuperAdminPages/SuperAdminHome/index.tsx
@@ -11,19 +11,25 @@ import {
} from 'lucide-react'
import { Button } from '@/components/ui/Button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'
+import { useIntl } from '@/i18n'
export const SuperAdminHome = () => {
+ const { t } = useIntl()
return (
- Super Admin Dashboard
+ {t('superAdminHome.title', undefined, 'Super Admin Dashboard')}
- Manage all aspects of the MapRoulette platform.
+ {t(
+ 'superAdminHome.subtitle',
+ undefined,
+ 'Manage all aspects of the MapRoulette platform.'
+ )}
@@ -35,12 +41,18 @@ export const SuperAdminHome = () => {
-
Users
-
Manage all platform users and permissions
+
{t('superAdminHome.cards.users.title', undefined, 'Users')}
+
+ {t(
+ 'superAdminHome.cards.users.description',
+ undefined,
+ 'Manage all platform users and permissions'
+ )}
+
- View Users
+ {t('superAdminHome.cards.users.button', undefined, 'View Users')}
@@ -53,12 +65,18 @@ export const SuperAdminHome = () => {
-
Projects
-
View and manage all projects across the platform
+
{t('common.projects', undefined, 'Projects')}
+
+ {t(
+ 'common.viewManageProjectsSubtitle',
+ undefined,
+ 'View and manage all projects across the platform'
+ )}
+
- View Projects
+ {t('common.viewProjects', undefined, 'View Projects')}
@@ -71,12 +89,18 @@ export const SuperAdminHome = () => {
-
Challenges
-
Browse and manage all challenges
+
{t('common.challenges', undefined, 'Challenges')}
+
+ {t(
+ 'superAdminHome.cards.challenges.description',
+ undefined,
+ 'Browse and manage all challenges'
+ )}
+
- View Challenges
+ {t('common.viewChallenges', undefined, 'View Challenges')}
@@ -89,12 +113,18 @@ export const SuperAdminHome = () => {
-
Plugins
-
Manage plugins and integrations
+
{t('superAdminHome.cards.plugins.title', undefined, 'Plugins')}
+
+ {t(
+ 'superAdminHome.cards.plugins.description',
+ undefined,
+ 'Manage plugins and integrations'
+ )}
+
- View Plugins
+ {t('superAdminHome.cards.plugins.button', undefined, 'View Plugins')}
@@ -107,12 +137,20 @@ export const SuperAdminHome = () => {
-
Analytics
-
View platform-wide analytics and metrics
+
+ {t('superAdminHome.cards.analytics.title', undefined, 'Analytics')}
+
+
+ {t(
+ 'superAdminHome.cards.analytics.description',
+ undefined,
+ 'View platform-wide analytics and metrics'
+ )}
+
- View Analytics
+ {t('superAdminHome.cards.analytics.button', undefined, 'View Analytics')}
@@ -124,12 +162,18 @@ export const SuperAdminHome = () => {
-
Database
-
Database management and maintenance
+
{t('superAdminHome.cards.database.title', undefined, 'Database')}
+
+ {t(
+ 'superAdminHome.cards.database.description',
+ undefined,
+ 'Database management and maintenance'
+ )}
+
- Coming Soon
+ {t('superAdminHome.cards.database.button', undefined, 'Coming Soon')}
@@ -141,12 +185,20 @@ export const SuperAdminHome = () => {
-
Settings
-
Platform settings and configuration
+
+ {t('superAdminHome.cards.settings.title', undefined, 'Settings')}
+
+
+ {t(
+ 'superAdminHome.cards.settings.description',
+ undefined,
+ 'Platform settings and configuration'
+ )}
+
- View Settings
+ {t('superAdminHome.cards.settings.button', undefined, 'View Settings')}
diff --git a/src/components/Pages/SuperAdminPages/SuperAdminLayout.tsx b/src/components/Pages/SuperAdminPages/SuperAdminLayout.tsx
index 493dcd974..1c2691c68 100644
--- a/src/components/Pages/SuperAdminPages/SuperAdminLayout.tsx
+++ b/src/components/Pages/SuperAdminPages/SuperAdminLayout.tsx
@@ -1,18 +1,22 @@
import { Outlet } from '@tanstack/react-router'
import { SectionHeader } from '@/components/shared/SectionHeader'
+import { useIntl } from '@/i18n'
import { SuperAdminGuard } from '@/lib/SuperAdminGuard'
-export const SuperAdminLayout = () => (
-
-
-
-
-
+export const SuperAdminLayout = () => {
+ const { t } = useIntl()
+ return (
+
+
-
-
-)
+
+ )
+}
diff --git a/src/components/Pages/SuperAdminPages/SuperAdminPlugins/index.tsx b/src/components/Pages/SuperAdminPages/SuperAdminPlugins/index.tsx
index 05eec1384..e4cc562ef 100644
--- a/src/components/Pages/SuperAdminPages/SuperAdminPlugins/index.tsx
+++ b/src/components/Pages/SuperAdminPages/SuperAdminPlugins/index.tsx
@@ -5,6 +5,7 @@ import { Badge } from '@/components/ui/Badge'
import { Button } from '@/components/ui/Button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/Empty'
+import { useIntl } from '@/i18n'
import { cn } from '@/lib/utils'
// Mock data - replace with actual API calls
@@ -67,6 +68,7 @@ const getStatusBadgeColor = (status: string) => {
}
const PluginCard = ({ plugin }: { plugin: (typeof mockPlugins)[0] }) => {
+ const { t } = useIntl()
return (
@@ -78,7 +80,11 @@ const PluginCard = ({ plugin }: { plugin: (typeof mockPlugins)[0] }) => {
{plugin.name}
- v{plugin.version} • by {plugin.author}
+ {t(
+ 'superAdminPlugins.card.versionBy',
+ { version: plugin.version, author: plugin.author },
+ 'v{version} • by {author}'
+ )}
@@ -93,18 +99,26 @@ const PluginCard = ({ plugin }: { plugin: (typeof mockPlugins)[0] }) => {
- {plugin.downloads.toLocaleString()} downloads
+
+ {t(
+ 'superAdminPlugins.card.downloads',
+ { count: plugin.downloads.toLocaleString() },
+ '{count} downloads'
+ )}
+
+
+
+ {t('superAdminPlugins.card.updated', { date: plugin.lastUpdated }, 'Updated: {date}')}
-
Updated: {plugin.lastUpdated}
- View Details
+ {t('superAdminPlugins.card.viewDetails', undefined, 'View Details')}
- Configure
+ {t('superAdminPlugins.card.configure', undefined, 'Configure')}
@@ -113,6 +127,7 @@ const PluginCard = ({ plugin }: { plugin: (typeof mockPlugins)[0] }) => {
}
export const SuperAdminPlugins = () => {
+ const { t } = useIntl()
const [searchQuery, setSearchQuery] = useState('')
const filteredPlugins = mockPlugins.filter(
@@ -131,64 +146,88 @@ export const SuperAdminPlugins = () => {
- Plugin Management
+ {t('superAdminPlugins.title', undefined, 'Plugin Management')}
- Manage plugins and integrations for the platform
+ {t(
+ 'superAdminPlugins.subtitle',
+ undefined,
+ 'Manage plugins and integrations for the platform'
+ )}
- Upload Plugin
+ {t('superAdminPlugins.uploadButton', undefined, 'Upload Plugin')}
- Install Plugin
+ {t('superAdminPlugins.installButton', undefined, 'Install Plugin')}
-
+
{/* Stats Cards */}
- Total Plugins
+
+ {t('superAdminPlugins.stats.total', undefined, 'Total Plugins')}
+
24
- +2 new this month
+
+ {t('superAdminPlugins.stats.totalChange', undefined, '+2 new this month')}
+
- Active Plugins
+
+ {t('superAdminPlugins.stats.active', undefined, 'Active Plugins')}
+
18
- 75% enabled
+
+ {t('superAdminPlugins.stats.activeShare', undefined, '75% enabled')}
+
- Total Downloads
+
+ {t('superAdminPlugins.stats.totalDownloads', undefined, 'Total Downloads')}
+
12.5K
- Across all plugins
+
+ {t('superAdminPlugins.stats.totalDownloadsNote', undefined, 'Across all plugins')}
+
- Pending Updates
+
+ {t('superAdminPlugins.stats.pendingUpdates', undefined, 'Pending Updates')}
+
3
- Updates available
+
+ {t('superAdminPlugins.stats.pendingUpdatesNote', undefined, 'Updates available')}
+
@@ -206,8 +245,16 @@ export const SuperAdminPlugins = () => {
- No plugins found
- Try adjusting your search query.
+
+ {t('superAdminPlugins.empty.title', undefined, 'No plugins found')}
+
+
+ {t(
+ 'common.tryAdjustingYourSearchQuery',
+ undefined,
+ 'Try adjusting your search query.'
+ )}
+
)}
diff --git a/src/components/Pages/SuperAdminPages/SuperAdminProjects/index.tsx b/src/components/Pages/SuperAdminPages/SuperAdminProjects/index.tsx
index 8591f1fa9..d3ba9859b 100644
--- a/src/components/Pages/SuperAdminPages/SuperAdminProjects/index.tsx
+++ b/src/components/Pages/SuperAdminPages/SuperAdminProjects/index.tsx
@@ -5,6 +5,7 @@ import { StatusBadge } from '@/components/shared/StatusBadge'
import { Button } from '@/components/ui/Button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/Empty'
+import { useIntl } from '@/i18n'
// Mock data - replace with actual API calls
const mockProjects = [
@@ -41,6 +42,7 @@ const mockProjects = [
]
const ProjectCard = ({ project }: { project: (typeof mockProjects)[0] }) => {
+ const { t } = useIntl()
return (
@@ -62,22 +64,25 @@ const ProjectCard = ({ project }: { project: (typeof mockProjects)[0] }) => {
- {project.description || 'No description available'}
+ {project.description ||
+ t('common.noDescriptionAvailable', undefined, 'No description available')}
- {project.challengeCount} challenges
+ {project.challengeCount} {' '}
+ {t('common.challenges2', undefined, 'challenges')}
- {project.completionRate}% complete
+ {project.completionRate}% {' '}
+ {t('common.complete', undefined, 'complete')}
- View
+ {t('common.view', undefined, 'View')}
- Edit
+ {t('common.edit', undefined, 'Edit')}
@@ -86,6 +91,7 @@ const ProjectCard = ({ project }: { project: (typeof mockProjects)[0] }) => {
}
export const SuperAdminProjects = () => {
+ const { t } = useIntl()
const [searchQuery, setSearchQuery] = useState('')
const filteredProjects = mockProjects.filter(
@@ -103,57 +109,83 @@ export const SuperAdminProjects = () => {
-
All Projects
+
+ {t('superAdminProjects.title', undefined, 'All Projects')}
+
- View and manage all projects across the platform
+ {t(
+ 'common.viewManageProjectsSubtitle',
+ undefined,
+ 'View and manage all projects across the platform'
+ )}
- Create New Project
+ {t('common.createNewProject', undefined, 'Create New Project')}
-
+
{/* Stats Cards */}
- Total Projects
+
+ {t('superAdminProjects.stats.total', undefined, 'Total Projects')}
+
256
- +8% from last month
+
+ {t('superAdminProjects.stats.totalChange', undefined, '+8% from last month')}
+
- Active Projects
+
+ {t('common.activeProjects', undefined, 'Active Projects')}
+
187
- 73% of total
+
+ {t('superAdminProjects.stats.activeShare', undefined, '73% of total')}
+
- Total Challenges
+
+ {t('common.totalChallenges', undefined, 'Total Challenges')}
+
1,892
- Across all projects
+
+ {t('superAdminProjects.stats.totalChallengesNote', undefined, 'Across all projects')}
+
- Avg. Completion
+
+ {t('common.avgCompletion', undefined, 'Avg. Completion')}
+
64%
- Platform average
+
+ {t('common.platformAverage', undefined, 'Platform average')}
+
@@ -171,8 +203,14 @@ export const SuperAdminProjects = () => {
- No projects found
- Try adjusting your search query.
+ {t('common.noProjectsFound', undefined, 'No projects found')}
+
+ {t(
+ 'common.tryAdjustingYourSearchQuery',
+ undefined,
+ 'Try adjusting your search query.'
+ )}
+
)}
diff --git a/src/components/Pages/SuperAdminPages/SuperAdminSettings/index.tsx b/src/components/Pages/SuperAdminPages/SuperAdminSettings/index.tsx
index f83aa9270..b59cbef52 100644
--- a/src/components/Pages/SuperAdminPages/SuperAdminSettings/index.tsx
+++ b/src/components/Pages/SuperAdminPages/SuperAdminSettings/index.tsx
@@ -6,8 +6,10 @@ import { Input } from '@/components/ui/Input'
import { Label } from '@/components/ui/Label'
import { Switch } from '@/components/ui/Switch'
import { Textarea } from '@/components/ui/Textarea'
+import { useIntl } from '@/i18n'
export const SuperAdminSettings = () => {
+ const { t } = useIntl()
const siteNameId = useId()
const siteDescriptionId = useId()
const siteUrlId = useId()
@@ -36,10 +38,16 @@ export const SuperAdminSettings = () => {
-
Platform Settings
+
+ {t('superAdminSettings.title', undefined, 'Platform Settings')}
+
- Configure platform-wide settings and preferences
+ {t(
+ 'superAdminSettings.subtitle',
+ undefined,
+ 'Configure platform-wide settings and preferences'
+ )}
@@ -49,17 +57,29 @@ export const SuperAdminSettings = () => {
- General Settings
+
+ {t('superAdminSettings.general.title', undefined, 'General Settings')}
+
- Basic platform configuration
+
+ {t(
+ 'superAdminSettings.general.description',
+ undefined,
+ 'Basic platform configuration'
+ )}
+
- Site Name
+
+ {t('superAdminSettings.general.siteName', undefined, 'Site Name')}
+
- Site Description
+
+ {t('superAdminSettings.general.siteDescription', undefined, 'Site Description')}
+
- Site URL
+
+ {t('superAdminSettings.general.siteUrl', undefined, 'Site URL')}
+
-
Maintenance Mode
+
+ {t('superAdminSettings.general.maintenanceMode', undefined, 'Maintenance Mode')}
+
- Enable maintenance mode to prevent user access
+ {t(
+ 'superAdminSettings.general.maintenanceModeDescription',
+ undefined,
+ 'Enable maintenance mode to prevent user access'
+ )}
@@ -87,28 +115,52 @@ export const SuperAdminSettings = () => {
- Email Settings
+
+ {t('superAdminSettings.email.title', undefined, 'Email Settings')}
+
- Configure email notifications and SMTP
+
+ {t(
+ 'superAdminSettings.email.description',
+ undefined,
+ 'Configure email notifications and SMTP'
+ )}
+
- SMTP Host
+
+ {t('superAdminSettings.email.smtpHost', undefined, 'SMTP Host')}
+
- SMTP Port
+
+ {t('superAdminSettings.email.smtpPort', undefined, 'SMTP Port')}
+
- From Email Address
+
+ {t('superAdminSettings.email.fromEmail', undefined, 'From Email Address')}
+
-
Enable Email Notifications
+
+ {t(
+ 'superAdminSettings.email.enableNotifications',
+ undefined,
+ 'Enable Email Notifications'
+ )}
+
- Send email notifications to users
+ {t(
+ 'superAdminSettings.email.enableNotificationsDescription',
+ undefined,
+ 'Send email notifications to users'
+ )}
@@ -121,38 +173,82 @@ export const SuperAdminSettings = () => {
- Security Settings
+
+ {t('superAdminSettings.security.title', undefined, 'Security Settings')}
+
- Configure security and authentication
+
+ {t(
+ 'superAdminSettings.security.description',
+ undefined,
+ 'Configure security and authentication'
+ )}
+
-
Require Email Verification
+
+ {t(
+ 'superAdminSettings.security.requireVerification',
+ undefined,
+ 'Require Email Verification'
+ )}
+
- Users must verify their email before accessing the platform
+ {t(
+ 'superAdminSettings.security.requireVerificationDescription',
+ undefined,
+ 'Users must verify their email before accessing the platform'
+ )}
-
Enable Two-Factor Authentication
+
+ {t(
+ 'superAdminSettings.security.twoFactor',
+ undefined,
+ 'Enable Two-Factor Authentication'
+ )}
+
- Allow users to enable 2FA for their accounts
+ {t(
+ 'superAdminSettings.security.twoFactorDescription',
+ undefined,
+ 'Allow users to enable 2FA for their accounts'
+ )}
- Session Timeout (minutes)
+
+ {t(
+ 'superAdminSettings.security.sessionTimeout',
+ undefined,
+ 'Session Timeout (minutes)'
+ )}
+
-
Strict Password Policy
+
+ {t(
+ 'superAdminSettings.security.passwordPolicy',
+ undefined,
+ 'Strict Password Policy'
+ )}
+
- Require strong passwords (min 12 chars, special chars)
+ {t(
+ 'superAdminSettings.security.passwordPolicyDescription',
+ undefined,
+ 'Require strong passwords (min 12 chars, special chars)'
+ )}
@@ -165,31 +261,53 @@ export const SuperAdminSettings = () => {
- Database Settings
+
+ {t('superAdminSettings.database.title', undefined, 'Database Settings')}
+
- Database configuration and maintenance
+
+ {t(
+ 'superAdminSettings.database.description',
+ undefined,
+ 'Database configuration and maintenance'
+ )}
+
- Database Host
+
+ {t('superAdminSettings.database.host', undefined, 'Database Host')}
+
- Database Port
+
+ {t('superAdminSettings.database.port', undefined, 'Database Port')}
+
-
Automatic Backups
+
+ {t('superAdminSettings.database.autoBackup', undefined, 'Automatic Backups')}
+
- Enable daily automatic database backups
+ {t(
+ 'superAdminSettings.database.autoBackupDescription',
+ undefined,
+ 'Enable daily automatic database backups'
+ )}
- Run Maintenance
- Create Backup
+
+ {t('superAdminSettings.database.runMaintenance', undefined, 'Run Maintenance')}
+
+
+ {t('superAdminSettings.database.createBackup', undefined, 'Create Backup')}
+
@@ -199,34 +317,68 @@ export const SuperAdminSettings = () => {
- Notification Settings
+
+ {t('superAdminSettings.notifications.title', undefined, 'Notification Settings')}
+
- Configure platform notification preferences
+
+ {t(
+ 'superAdminSettings.notifications.description',
+ undefined,
+ 'Configure platform notification preferences'
+ )}
+
-
New User Notifications
+
+ {t(
+ 'superAdminSettings.notifications.newUser',
+ undefined,
+ 'New User Notifications'
+ )}
+
- Notify admins when new users sign up
+ {t(
+ 'superAdminSettings.notifications.newUserDescription',
+ undefined,
+ 'Notify admins when new users sign up'
+ )}
-
New Project Notifications
+
+ {t(
+ 'superAdminSettings.notifications.newProject',
+ undefined,
+ 'New Project Notifications'
+ )}
+
- Notify admins when new projects are created
+ {t(
+ 'superAdminSettings.notifications.newProjectDescription',
+ undefined,
+ 'Notify admins when new projects are created'
+ )}
-
Error Notifications
+
+ {t('superAdminSettings.notifications.errors', undefined, 'Error Notifications')}
+
- Notify admins when system errors occur
+ {t(
+ 'superAdminSettings.notifications.errorsDescription',
+ undefined,
+ 'Notify admins when system errors occur'
+ )}
@@ -239,29 +391,47 @@ export const SuperAdminSettings = () => {
-
Appearance Settings
+
+ {t('superAdminSettings.appearance.title', undefined, 'Appearance Settings')}
+
- Customize the platform appearance
+
+ {t(
+ 'superAdminSettings.appearance.description',
+ undefined,
+ 'Customize the platform appearance'
+ )}
+
-
Default to Dark Mode
+
+ {t('superAdminSettings.appearance.darkMode', undefined, 'Default to Dark Mode')}
+
- Set dark mode as the default theme for new users
+ {t(
+ 'superAdminSettings.appearance.darkModeDescription',
+ undefined,
+ 'Set dark mode as the default theme for new users'
+ )}
- Logo URL
+
+ {t('superAdminSettings.appearance.logoUrl', undefined, 'Logo URL')}
+
@@ -271,7 +441,7 @@ export const SuperAdminSettings = () => {
- Save All Settings
+ {t('superAdminSettings.saveButton', undefined, 'Save All Settings')}
diff --git a/src/components/Pages/SuperAdminPages/SuperAdminUsers/index.tsx b/src/components/Pages/SuperAdminPages/SuperAdminUsers/index.tsx
index badb6f73a..8b9987520 100644
--- a/src/components/Pages/SuperAdminPages/SuperAdminUsers/index.tsx
+++ b/src/components/Pages/SuperAdminPages/SuperAdminUsers/index.tsx
@@ -14,6 +14,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/Table'
+import { useIntl } from '@/i18n'
import { formatDate } from '@/lib/date'
import { isSuperUser } from '@/lib/SuperAdminGuard'
import { cn } from '@/lib/utils'
@@ -40,6 +41,7 @@ const getRoleBadgeColor = (role: string) => {
}
export const SuperAdminUsers = () => {
+ const { t } = useIntl()
const [searchQuery, setSearchQuery] = useState('')
const [page, setPage] = useState(0)
const limit = 50
@@ -68,23 +70,31 @@ export const SuperAdminUsers = () => {
- User Management
+ {t('superAdminUsers.title', undefined, 'User Management')}
- Manage all users and their permissions across the platform
+ {t(
+ 'superAdminUsers.subtitle',
+ undefined,
+ 'Manage all users and their permissions across the platform'
+ )}
- Add New User
+ {t('superAdminUsers.addButton', undefined, 'Add New User')}
@@ -92,29 +102,41 @@ export const SuperAdminUsers = () => {
- Users on Page
+
+ {t('superAdminUsers.stats.usersOnPage', undefined, 'Users on Page')}
+
{totalUsers}
- Showing {limit} per page
+
+ {t('superAdminUsers.stats.perPage', { limit }, 'Showing {limit} per page')}
+
- Filtered Results
+
+ {t('superAdminUsers.stats.filteredResults', undefined, 'Filtered Results')}
+
{filteredUsers.length}
- Based on current search
+
+ {t('superAdminUsers.stats.basedOnSearch', undefined, 'Based on current search')}
+
- Super Admins
+
+ {t('superAdminUsers.stats.superAdmins', undefined, 'Super Admins')}
+
{superUserIds?.length ?? 0}
- Platform-wide
+
+ {t('superAdminUsers.stats.platformWide', undefined, 'Platform-wide')}
+
@@ -124,8 +146,14 @@ export const SuperAdminUsers = () => {
- All Users
- A list of all users in the system (page {page + 1})
+ {t('superAdminUsers.table.title', undefined, 'All Users')}
+
+ {t(
+ 'superAdminUsers.table.subtitle',
+ { page: page + 1 },
+ 'A list of all users in the system (page {page})'
+ )}
+
{
disabled={!hasPreviousPage || isLoading}
>
- Previous
+ {t('superAdminUsers.table.previous', undefined, 'Previous')}
{
onClick={() => setPage((p) => p + 1)}
disabled={!hasNextPage || isLoading}
>
- Next
+ {t('superAdminUsers.table.next', undefined, 'Next')}
@@ -152,20 +180,32 @@ export const SuperAdminUsers = () => {
{isLoading ? (
-
Loading users...
+
+ {t('superAdminUsers.table.loading', undefined, 'Loading users...')}
+
) : (
<>
- User
- OSM ID
- Email
- Role
- Score
- Joined
- Actions
+
+ {t('superAdminUsers.table.columns.user', undefined, 'User')}
+
+
+ {t('superAdminUsers.table.columns.osmId', undefined, 'OSM ID')}
+
+
+ {t('superAdminUsers.table.columns.email', undefined, 'Email')}
+
+
+ {t('superAdminUsers.table.columns.role', undefined, 'Role')}
+
+
+ {t('superAdminUsers.table.columns.score', undefined, 'Score')}
+
+ {t('common.joined', undefined, 'Joined')}
+ {t('common.actions', undefined, 'Actions')}
@@ -201,7 +241,8 @@ export const SuperAdminUsers = () => {
- {user.settings?.email || 'N/A'}
+ {user.settings?.email ||
+ t('superAdminUsers.table.notAvailable', undefined, 'N/A')}
@@ -221,7 +262,7 @@ export const SuperAdminUsers = () => {
- View
+ {t('common.view', undefined, 'View')}
@@ -236,8 +277,16 @@ export const SuperAdminUsers = () => {
- No users found
- Try adjusting your search query.
+
+ {t('superAdminUsers.empty.title', undefined, 'No users found')}
+
+
+ {t(
+ 'common.tryAdjustingYourSearchQuery',
+ undefined,
+ 'Try adjusting your search query.'
+ )}
+
)}
diff --git a/src/components/Pages/TaskEditPage/IdEditorView.tsx b/src/components/Pages/TaskEditPage/IdEditorView.tsx
index bf1316b4e..c1668668f 100644
--- a/src/components/Pages/TaskEditPage/IdEditorView.tsx
+++ b/src/components/Pages/TaskEditPage/IdEditorView.tsx
@@ -11,6 +11,7 @@ import {
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { api } from '@/api'
import { parseOsmFeaturesFromTask } from '@/components/TaskInfoPanel/taskUtils/osmUtils'
+import { useIntl } from '@/i18n'
import { buildChangesetComment } from '@/lib/changesetComment'
import { logger } from '@/lib/logger'
import { getOSMToken } from '@/plugins/RapidEditorPlugin/editorUtils'
@@ -47,6 +48,7 @@ interface IdEditorViewProps {
}
export const IdEditorView = ({ onClose }: IdEditorViewProps) => {
+ const { t } = useIntl()
const { task } = useTaskContext()
const { challenge } = useChallengeContext()
const { activeBundle } = useTaskBundleContext()
@@ -348,7 +350,11 @@ export const IdEditorView = ({ onClose }: IdEditorViewProps) => {
type="button"
onClick={() => setDrawerOpen(!drawerOpen)}
className="flex h-10 items-center gap-1.5 rounded-bl-lg bg-slate-900/95 pr-2.5 pl-2 shadow-md transition-colors hover:bg-slate-800"
- title={drawerOpen ? 'Collapse panel' : 'Expand panel'}
+ title={
+ drawerOpen
+ ? t('taskEditPage.idEditor.collapsePanel', undefined, 'Collapse panel')
+ : t('taskEditPage.idEditor.expandPanel', undefined, 'Expand panel')
+ }
>
{drawerOpen ? (
@@ -368,7 +374,11 @@ export const IdEditorView = ({ onClose }: IdEditorViewProps) => {
- {idUnsavedCount} unsaved change{idUnsavedCount !== 1 ? 's' : ''}
+ {t(
+ 'taskEditPage.idEditor.unsavedChanges',
+ { count: idUnsavedCount, suffix: idUnsavedCount !== 1 ? 's' : '' },
+ '{count} unsaved change{suffix}'
+ )}
)}
@@ -378,10 +388,10 @@ export const IdEditorView = ({ onClose }: IdEditorViewProps) => {
type="button"
onClick={handleResetView}
className="flex items-center gap-1.5 whitespace-nowrap rounded-md px-2.5 py-1.5 font-medium text-[11px] text-slate-300 transition-colors hover:bg-slate-700/80 hover:text-white"
- title="Reset view to task location"
+ title={t('common.resetViewToTaskLocation', undefined, 'Reset view to task location')}
>
- Re-Center
+ {t('taskEditPage.idEditor.reCenter', undefined, 'Re-Center')}
{osmEntityIds.length > 0 && (
{
selectValidEntities(ctx, iDGlobal, osmEntityIdsRef.current)
}}
className="flex items-center gap-1.5 whitespace-nowrap rounded-md px-2.5 py-1.5 font-medium text-[11px] text-slate-300 transition-colors hover:bg-slate-700/80 hover:text-white"
- title="Select task features in iD"
+ title={t(
+ 'taskEditPage.idEditor.selectTasksTitle',
+ undefined,
+ 'Select task features in iD'
+ )}
>
- Select Tasks
+ {t('taskEditPage.idEditor.selectTasks', undefined, 'Select Tasks')}
)}
{
? 'bg-purple-600/80 text-white hover:bg-purple-500'
: 'text-slate-300 hover:bg-slate-700/80 hover:text-white'
}`}
- title={focusMode ? 'Show all map features' : 'Dim other features to focus on tasks'}
+ title={
+ focusMode
+ ? t('taskEditPage.idEditor.showAllTitle', undefined, 'Show all map features')
+ : t(
+ 'taskEditPage.idEditor.focusTitle',
+ undefined,
+ 'Dim other features to focus on tasks'
+ )
+ }
>
{focusMode ? : }
- {focusMode ? 'Show All' : 'Focus'}
+ {focusMode
+ ? t('taskEditPage.idEditor.showAll', undefined, 'Show All')
+ : t('taskEditPage.idEditor.focus', undefined, 'Focus')}
- Close editor
+ {t('taskEditPage.idEditor.closeEditor', undefined, 'Close editor')}
@@ -430,7 +458,9 @@ export const IdEditorView = ({ onClose }: IdEditorViewProps) => {
-
Loading iD Editor...
+
+ {t('taskEditPage.idEditor.loading', undefined, 'Loading iD Editor...')}
+
)}
diff --git a/src/components/Pages/TaskEditPage/KeyboardShortcutsModal.tsx b/src/components/Pages/TaskEditPage/KeyboardShortcutsModal.tsx
index 75045dd84..10ddce3c0 100644
--- a/src/components/Pages/TaskEditPage/KeyboardShortcutsModal.tsx
+++ b/src/components/Pages/TaskEditPage/KeyboardShortcutsModal.tsx
@@ -11,17 +11,33 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/Dialog'
-
-// Always show the help shortcut
-const GLOBAL_SHORTCUTS: KeyboardShortcut[] = [
- { key: '?', description: 'Show keyboard shortcuts', category: 'General' },
-]
+import { useIntl } from '@/i18n'
export const KeyboardShortcutsModal = () => {
+ const { t } = useIntl()
const { shortcuts, isModalOpen, setModalOpen } = useKeyboardShortcuts()
+ // Always show the help shortcut
+ const globalShortcuts: KeyboardShortcut[] = useMemo(
+ () => [
+ {
+ key: '?',
+ description: t(
+ 'taskEditPage.keyboardShortcuts.showShortcuts',
+ undefined,
+ 'Show keyboard shortcuts'
+ ),
+ category: 'General',
+ },
+ ],
+ [t]
+ )
+
// Reason: data transformation for display grouping — merges registered and global shortcuts
- const allShortcuts = useMemo(() => [...shortcuts, ...GLOBAL_SHORTCUTS], [shortcuts])
+ const allShortcuts = useMemo(
+ () => [...shortcuts, ...globalShortcuts],
+ [shortcuts, globalShortcuts]
+ )
// Reason: data transformation for display grouping — groups shortcuts by category
const groupedShortcuts = useMemo(() => {
@@ -56,9 +72,15 @@ export const KeyboardShortcutsModal = () => {
- Keyboard Shortcuts
+ {t('taskEditPage.keyboardShortcuts.title', undefined, 'Keyboard Shortcuts')}
- Use these shortcuts to speed up your workflow
+
+ {t(
+ 'taskEditPage.keyboardShortcuts.description',
+ undefined,
+ 'Use these shortcuts to speed up your workflow'
+ )}
+
@@ -87,15 +109,25 @@ export const KeyboardShortcutsModal = () => {
{allShortcuts.length === 1 && (
- No shortcuts registered yet
+
+ {t(
+ 'taskEditPage.keyboardShortcuts.noneRegistered',
+ undefined,
+ 'No shortcuts registered yet'
+ )}
+
)}
- Press{' '}
+ {t('taskEditPage.keyboardShortcuts.pressPrefix', undefined, 'Press')}{' '}
?
{' '}
- anytime to show this dialog
+ {t(
+ 'taskEditPage.keyboardShortcuts.pressSuffix',
+ undefined,
+ 'anytime to show this dialog'
+ )}
diff --git a/src/components/Pages/TaskEditPage/TaskActionModal.tsx b/src/components/Pages/TaskEditPage/TaskActionModal.tsx
index 08ed01483..095ea5f74 100644
--- a/src/components/Pages/TaskEditPage/TaskActionModal.tsx
+++ b/src/components/Pages/TaskEditPage/TaskActionModal.tsx
@@ -24,6 +24,7 @@ import {
SelectValue,
} from '@/components/ui/Select'
import { Textarea } from '@/components/ui/Textarea'
+import { useIntl } from '@/i18n'
import { getApiErrorMessage } from '@/lib/apiError'
import { logger } from '@/lib/logger'
import { STATUS_LABELS } from '@/lib/taskConstants'
@@ -38,26 +39,35 @@ interface TaskActionModalProps {
initialStatus: number
}
-const STATUS_OPTIONS = [
- { value: 1, label: 'Fixed' },
- { value: 2, label: 'False Positive' },
- { value: 3, label: 'Skipped' },
- { value: 5, label: 'Already Fixed' },
- { value: 6, label: "Can't Complete" },
-]
-
export const TaskActionModal = ({
open,
onOpenChange,
task,
initialStatus,
}: TaskActionModalProps) => {
+ const { t } = useIntl()
const queryClient = useQueryClient()
const navigate = useNavigate()
const commentId = useId()
const tagsId = useId()
const randomId = useId()
const nearbyId = useId()
+ const STATUS_OPTIONS = [
+ { value: 1, label: t('common.fixed', undefined, 'Fixed') },
+ {
+ value: 2,
+ label: t('common.falsePositive', undefined, 'False Positive'),
+ },
+ { value: 3, label: t('common.skipped', undefined, 'Skipped') },
+ {
+ value: 5,
+ label: t('common.alreadyFixed', undefined, 'Already Fixed'),
+ },
+ {
+ value: 6,
+ label: t('common.cantComplete', undefined, "Can't Complete"),
+ },
+ ]
const [newStatus, setNewStatus] = useState(initialStatus)
const [comment, setComment] = useState('')
const [tags, setTags] = useState('')
@@ -71,7 +81,8 @@ export const TaskActionModal = ({
const updateBundleMutation = api.taskBundle.useUpdateTaskBundle()
const { activeBundle, initialBundle } = useTaskBundleContext()
const currentStatus = task.status ?? 0
- const currentStatusLabel = STATUS_LABELS[currentStatus] || 'Unknown'
+ const currentStatusLabel =
+ STATUS_LABELS[currentStatus] || t('common.unknown', undefined, 'Unknown')
useEffect(() => {
setNewStatus(initialStatus)
@@ -135,25 +146,39 @@ export const TaskActionModal = ({
addTaskCommentMutation.mutate({ taskId: task.id, commentText: comment.trim() })
}
- toast.success(`Task marked as ${STATUS_LABELS[newStatus]}`)
+ toast.success(
+ t(
+ 'taskEditPage.taskActionModal.toast.markedAs',
+ { status: STATUS_LABELS[newStatus] },
+ 'Task marked as {status}'
+ )
+ )
if (nextTaskType === 'nearby' && selectedNearbyTaskId) {
await navigate({ to: '/tasks/$taskId', params: { taskId: String(selectedNearbyTaskId) } })
} else {
- toast.info('Loading next task...')
+ toast.info(
+ t('taskEditPage.taskActionModal.toast.loadingNext', undefined, 'Loading next task...')
+ )
try {
const randomTasks = await api.challenge.getRandomTask(task.parent, queryClient)
if (randomTasks && randomTasks.length > 0) {
await navigate({ to: '/tasks/$taskId', params: { taskId: String(randomTasks[0].id) } })
} else {
- toast.info('No more tasks available in this challenge')
+ toast.info(
+ t(
+ 'common.noMoreTasksInChallenge',
+ undefined,
+ 'No more tasks available in this challenge'
+ )
+ )
await navigate({
to: '/challenge/$challengeId',
params: { challengeId: String(task.parent) },
})
}
} catch {
- toast.error('Failed to load next task')
+ toast.error(t('common.failedToLoadNextTask', undefined, 'Failed to load next task'))
await navigate({
to: '/challenge/$challengeId',
params: { challengeId: String(task.parent) },
@@ -164,7 +189,14 @@ export const TaskActionModal = ({
onOpenChange(false)
} catch (error) {
logger.error('Error updating task', { error: String(error) })
- toast.error((await getApiErrorMessage(error)) ?? 'Failed to update task. Please try again.')
+ toast.error(
+ (await getApiErrorMessage(error)) ??
+ t(
+ 'taskEditPage.taskActionModal.toast.updateFailed',
+ undefined,
+ 'Failed to update task. Please try again.'
+ )
+ )
} finally {
setIsSubmitting(false)
}
@@ -183,16 +215,24 @@ export const TaskActionModal = ({
- Complete Task Action
+
+ {t('taskEditPage.taskActionModal.title', undefined, 'Complete Task Action')}
+
- Update the task status and optionally add a comment or tags
+ {t(
+ 'taskEditPage.taskActionModal.description',
+ undefined,
+ 'Update the task status and optionally add a comment or tags'
+ )}
{/* Status Transition */}
-
Status Change
+
+ {t('taskEditPage.taskActionModal.statusChange', undefined, 'Status Change')}
+
{currentStatusLabel}
@@ -218,10 +258,16 @@ export const TaskActionModal = ({
{/* Comment */}
- Comment (Optional)
+
+ {t('taskEditPage.taskActionModal.commentLabel', undefined, 'Comment (Optional)')}
+
@@ -272,10 +338,14 @@ export const TaskActionModal = ({
className="flex cursor-pointer items-center gap-2 font-medium"
>
- Nearby Task
+ {t('taskEditPage.taskActionModal.nearbyTask.label', undefined, 'Nearby Task')}
- Select a task near the current one, or auto-select the nearest
+ {t(
+ 'taskEditPage.taskActionModal.nearbyTask.description',
+ undefined,
+ 'Select a task near the current one, or auto-select the nearest'
+ )}
@@ -297,10 +367,16 @@ export const TaskActionModal = ({
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
- {isSubmitting ? 'Submitting...' : 'Complete & Continue'}
+ {isSubmitting
+ ? t('common.submitting', undefined, 'Submitting...')
+ : t(
+ 'taskEditPage.taskActionModal.completeAndContinue',
+ undefined,
+ 'Complete & Continue'
+ )}
diff --git a/src/components/Pages/TaskEditPage/TaskActions/EditorButton.tsx b/src/components/Pages/TaskEditPage/TaskActions/EditorButton.tsx
index 6179e4dd2..d5a10588b 100644
--- a/src/components/Pages/TaskEditPage/TaskActions/EditorButton.tsx
+++ b/src/components/Pages/TaskEditPage/TaskActions/EditorButton.tsx
@@ -18,6 +18,7 @@ import {
} from '@/components/ui/DropdownMenu'
import { useAuthContext } from '@/contexts/AuthContext'
import { editorOptions } from '@/data/account.json'
+import { useIntl } from '@/i18n'
import { buildChangesetComment } from '@/lib/changesetComment'
import { logger } from '@/lib/logger'
import type { Bbox2D } from '@/types/Map'
@@ -48,6 +49,7 @@ const computeBboxForTasks = (tasks: Task[]): Bbox2D => {
}
export const EditorButton = ({ task }: EditorButtonProps) => {
+ const { t } = useIntl()
const { user } = useAuthContext()
const { challenge } = useChallengeContext()
const { activeBundle } = useTaskBundleContext()
@@ -100,7 +102,13 @@ export const EditorButton = ({ task }: EditorButtonProps) => {
case JOSM_LAYER: {
const bounds = computeBboxForTasks(tasks)
if (!bounds) {
- toast.error('Task bounds not available')
+ toast.error(
+ t(
+ 'taskEditPage.taskActions.editorButton.noBounds',
+ undefined,
+ 'Task bounds not available'
+ )
+ )
return
}
const [west, south, east, north] = bounds
@@ -117,7 +125,13 @@ export const EditorButton = ({ task }: EditorButtonProps) => {
]
if (selection) parts.push(`select=${selection}`)
editorUrl = `${JOSM_HOST}load_and_zoom?${parts.join('&')}`
- toast.info('Make sure JOSM is running with remote control enabled')
+ toast.info(
+ t(
+ 'taskEditPage.taskActions.editorButton.josmRemoteControlHint',
+ undefined,
+ 'Make sure JOSM is running with remote control enabled'
+ )
+ )
break
}
@@ -125,7 +139,13 @@ export const EditorButton = ({ task }: EditorButtonProps) => {
// load_object: select & download the specific OSM elements
const selection = formatOsmEntities(tasks, { abbreviated: false })
if (!selection) {
- toast.error('Task has no OSM feature IDs to load')
+ toast.error(
+ t(
+ 'taskEditPage.taskActions.editorButton.noOsmFeatures',
+ undefined,
+ 'Task has no OSM feature IDs to load'
+ )
+ )
return
}
const bounds = computeBboxForTasks(tasks)
@@ -141,7 +161,13 @@ export const EditorButton = ({ task }: EditorButtonProps) => {
parts.unshift(`left=${west}`, `right=${east}`, `top=${north}`, `bottom=${south}`)
}
editorUrl = `${JOSM_HOST}load_object?${parts.join('&')}`
- toast.info('Make sure JOSM is running with remote control enabled')
+ toast.info(
+ t(
+ 'taskEditPage.taskActions.editorButton.josmRemoteControlHint',
+ undefined,
+ 'Make sure JOSM is running with remote control enabled'
+ )
+ )
break
}
@@ -178,12 +204,22 @@ export const EditorButton = ({ task }: EditorButtonProps) => {
if (editorUrl) {
window.open(editorUrl, '_blank', 'noopener,noreferrer')
toast.success(
- `Opening task in ${editorOptions.find((opt) => opt.value === editorValue)?.label || 'editor'}`
+ t(
+ 'taskEditPage.taskActions.editorButton.openingTaskIn',
+ {
+ editor:
+ editorOptions.find((opt) => opt.value === editorValue)?.label ||
+ t('taskEditPage.taskActions.editorButton.editorFallback', undefined, 'editor'),
+ },
+ 'Opening task in {editor}'
+ )
)
}
} catch (error) {
logger.error('Error opening editor', { error: String(error) })
- toast.error('Failed to open editor')
+ toast.error(
+ t('taskEditPage.taskActions.editorButton.openFailed', undefined, 'Failed to open editor')
+ )
}
}
@@ -207,8 +243,22 @@ export const EditorButton = ({ task }: EditorButtonProps) => {
},
},
{
- onSuccess: () => toast.success('Default editor updated'),
- onError: () => toast.error('Failed to update default editor'),
+ onSuccess: () =>
+ toast.success(
+ t(
+ 'taskEditPage.taskActions.editorButton.defaultUpdated',
+ undefined,
+ 'Default editor updated'
+ )
+ ),
+ onError: () =>
+ toast.error(
+ t(
+ 'taskEditPage.taskActions.editorButton.defaultUpdateFailed',
+ undefined,
+ 'Failed to update default editor'
+ )
+ ),
}
)
} catch (error) {
@@ -220,14 +270,23 @@ export const EditorButton = ({ task }: EditorButtonProps) => {
// Get short label for mobile
const getShortLabel = (label: string) => {
- if (label.includes('iD')) return 'iD'
+ if (label.includes('iD'))
+ return t('taskEditPage.taskActions.editorButton.short.id', undefined, 'iD')
if (label.includes('JOSM')) {
- if (label.includes('new layer')) return 'JOSM Layer'
- if (label.includes('features')) return 'JOSM Features'
- return 'JOSM'
+ if (label.includes('new layer'))
+ return t('taskEditPage.taskActions.editorButton.short.josmLayer', undefined, 'JOSM Layer')
+ if (label.includes('features'))
+ return t(
+ 'taskEditPage.taskActions.editorButton.short.josmFeatures',
+ undefined,
+ 'JOSM Features'
+ )
+ return t('taskEditPage.taskActions.editorButton.short.josm', undefined, 'JOSM')
}
- if (label.includes('level0')) return 'Level0'
- if (label.includes('Rapid')) return 'Rapid'
+ if (label.includes('level0'))
+ return t('taskEditPage.taskActions.editorButton.short.level0', undefined, 'Level0')
+ if (label.includes('Rapid'))
+ return t('taskEditPage.taskActions.editorButton.short.rapid', undefined, 'Rapid')
return label
}
@@ -239,7 +298,11 @@ export const EditorButton = ({ task }: EditorButtonProps) => {
onClick={handleOpenEditor}
className="gap-2 rounded-r-none rounded-l-full border-r border-r-background/20"
variant="default"
- title={`Open task in ${currentEditorOption.label}`}
+ title={t(
+ 'taskEditPage.taskActions.editorButton.openTaskIn',
+ { editor: currentEditorOption.label },
+ 'Open task in {editor}'
+ )}
disabled={isSaving || updateEditorMutation.isPending}
>
{currentEditorOption.label}
@@ -251,14 +314,24 @@ export const EditorButton = ({ task }: EditorButtonProps) => {
size="sm"
variant="default"
className="rounded-r-full rounded-l-none px-2"
- title="Change default editor"
+ title={t(
+ 'taskEditPage.taskActions.editorButton.changeDefault',
+ undefined,
+ 'Change default editor'
+ )}
disabled={isSaving || updateEditorMutation.isPending}
>
- Set Default Editor:
+
+ {t(
+ 'taskEditPage.taskActions.editorButton.setDefaultEditor',
+ undefined,
+ 'Set Default Editor:'
+ )}
+
{editorOptions
.filter((opt) => opt.value !== -1) // Exclude "None" option
diff --git a/src/components/Pages/TaskEditPage/TaskActions/LockButton.tsx b/src/components/Pages/TaskEditPage/TaskActions/LockButton.tsx
index bc73e6d7e..e757250c1 100644
--- a/src/components/Pages/TaskEditPage/TaskActions/LockButton.tsx
+++ b/src/components/Pages/TaskEditPage/TaskActions/LockButton.tsx
@@ -3,25 +3,34 @@ import { toast } from 'sonner'
import { useTaskContext } from '@/components/Pages/TaskEditPage/contexts/TaskContext'
import { Button } from '@/components/ui/Button'
import { useAuthContext } from '@/contexts/AuthContext'
+import { useIntl } from '@/i18n'
export const LockButton = ({ compact = false }: { compact?: boolean }) => {
+ const { t } = useIntl()
const { isLocked, isLocking, lockTask, unlockTask } = useTaskContext()
const { isAuthenticated } = useAuthContext()
const handleLockTask = () => {
lockTask()
- toast.success('Task locked')
+ toast.success(t('taskEditPage.taskActions.lockButton.locked', undefined, 'Task locked'))
}
const handleUnlockTask = () => {
unlockTask()
- toast.success('Task unlocked')
+ toast.success(t('taskEditPage.taskActions.lockButton.unlocked', undefined, 'Task unlocked'))
}
// Don't show lock button if not authenticated
if (!isAuthenticated) {
return (
-
+
)
@@ -39,11 +48,11 @@ export const LockButton = ({ compact = false }: { compact?: boolean }) => {
? 'text-amber-600 dark:text-amber-400'
: 'gap-1.5 text-amber-600 dark:text-amber-400'
}
- aria-label="Unlock task"
- title="Unlock task"
+ aria-label={t('taskEditPage.taskActions.lockButton.unlockTask', undefined, 'Unlock task')}
+ title={t('taskEditPage.taskActions.lockButton.unlockTask', undefined, 'Unlock task')}
>
- {!compact && 'Unlock'}
+ {!compact && t('taskEditPage.taskActions.lockButton.unlock', undefined, 'Unlock')}
)
}
@@ -55,11 +64,11 @@ export const LockButton = ({ compact = false }: { compact?: boolean }) => {
onClick={handleLockTask}
disabled={isLocking}
className={compact ? undefined : 'gap-1.5'}
- aria-label="Lock task"
- title="Lock task"
+ aria-label={t('taskEditPage.taskActions.lockButton.lockTask', undefined, 'Lock task')}
+ title={t('taskEditPage.taskActions.lockButton.lockTask', undefined, 'Lock task')}
>
- {!compact && 'Lock'}
+ {!compact && t('taskEditPage.taskActions.lockButton.lock', undefined, 'Lock')}
)
}
diff --git a/src/components/Pages/TaskEditPage/TaskActions/NavigationActions.tsx b/src/components/Pages/TaskEditPage/TaskActions/NavigationActions.tsx
index 2845af0da..b4a6c506e 100644
--- a/src/components/Pages/TaskEditPage/TaskActions/NavigationActions.tsx
+++ b/src/components/Pages/TaskEditPage/TaskActions/NavigationActions.tsx
@@ -5,6 +5,7 @@ import { useState } from 'react'
import { toast } from 'sonner'
import { api } from '@/api'
import { Button } from '@/components/ui/Button'
+import { useIntl } from '@/i18n'
export const NavigationActions = ({
challengeId,
@@ -13,6 +14,7 @@ export const NavigationActions = ({
challengeId: number
taskId: number
}) => {
+ const { t } = useIntl()
const navigate = useNavigate()
const queryClient = useQueryClient()
const [isLoadingNearby, setIsLoadingNearby] = useState(false)
@@ -28,10 +30,22 @@ export const NavigationActions = ({
if (nearbyTasks && nearbyTasks.length > 0) {
await navigate({ to: '/tasks/$taskId', params: { taskId: String(nearbyTasks[0].id) } })
} else {
- toast.info('No nearby tasks available')
+ toast.info(
+ t(
+ 'taskEditPage.taskActions.navigation.noNearbyTasks',
+ undefined,
+ 'No nearby tasks available'
+ )
+ )
}
} catch {
- toast.error('Failed to load nearby task')
+ toast.error(
+ t(
+ 'taskEditPage.taskActions.navigation.loadNearbyFailed',
+ undefined,
+ 'Failed to load nearby task'
+ )
+ )
} finally {
setIsLoadingNearby(false)
}
@@ -44,14 +58,16 @@ export const NavigationActions = ({
if (randomTasks && randomTasks.length > 0) {
await navigate({ to: '/tasks/$taskId', params: { taskId: String(randomTasks[0].id) } })
} else {
- toast.info('No more tasks available in this challenge')
+ toast.info(
+ t('common.noMoreTasksInChallenge', undefined, 'No more tasks available in this challenge')
+ )
await navigate({
to: '/challenge/$challengeId',
params: { challengeId: String(challengeId) },
})
}
} catch {
- toast.error('Failed to load next task')
+ toast.error(t('common.failedToLoadNextTask', undefined, 'Failed to load next task'))
} finally {
setIsLoadingRandom(false)
}
@@ -60,7 +76,11 @@ export const NavigationActions = ({
return (
- Want to map this challenge?
+ {t(
+ 'taskEditPage.taskActions.navigation.wantToMap',
+ undefined,
+ 'Want to map this challenge?'
+ )}
{isLoadingNearby ? : }
- {isLoadingNearby ? 'Loading...' : 'Nearby task'}
+ {isLoadingNearby
+ ? t('common.loading2', undefined, 'Loading...')
+ : t('taskEditPage.taskActions.navigation.nearbyTask', undefined, 'Nearby task')}
{isLoadingRandom ? : }
- {isLoadingRandom ? 'Loading...' : 'Random task'}
+ {isLoadingRandom
+ ? t('common.loading2', undefined, 'Loading...')
+ : t('taskEditPage.taskActions.navigation.randomTask', undefined, 'Random task')}
diff --git a/src/components/Pages/TaskEditPage/TaskActions/SkipButton.tsx b/src/components/Pages/TaskEditPage/TaskActions/SkipButton.tsx
index 400cebe3b..48845fa37 100644
--- a/src/components/Pages/TaskEditPage/TaskActions/SkipButton.tsx
+++ b/src/components/Pages/TaskEditPage/TaskActions/SkipButton.tsx
@@ -5,10 +5,12 @@ import { useState } from 'react'
import { toast } from 'sonner'
import { api } from '@/api'
import { Button } from '@/components/ui/Button'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import type { Task } from '@/types/Task'
export const SkipButton = ({ task }: { task: Task }) => {
+ const { t } = useIntl()
const navigate = useNavigate()
const queryClient = useQueryClient()
const skip = api.task.useSkipTask()
@@ -29,7 +31,9 @@ export const SkipButton = ({ task }: { task: Task }) => {
params: { taskId: String(randomTasks[0].id) },
})
} else {
- toast.info('No more tasks available in this challenge')
+ toast.info(
+ t('common.noMoreTasksInChallenge', undefined, 'No more tasks available in this challenge')
+ )
await navigate({
to: '/challenge/$challengeId',
params: { challengeId: String(task.parent) },
@@ -37,7 +41,9 @@ export const SkipButton = ({ task }: { task: Task }) => {
}
} catch (error) {
logger.error('Skip failed', { error })
- toast.error('Could not skip this task')
+ toast.error(
+ t('taskEditPage.taskActions.skipButton.skipFailed', undefined, 'Could not skip this task')
+ )
} finally {
setBusy(false)
}
@@ -50,14 +56,18 @@ export const SkipButton = ({ task }: { task: Task }) => {
onClick={handleSkip}
disabled={busy}
className="gap-1.5 rounded-full border-zinc-300 text-zinc-600 hover:bg-zinc-100 dark:border-slate-600 dark:text-slate-400 dark:hover:bg-slate-700"
- title="Skip this task (preserves status)"
+ title={t(
+ 'taskEditPage.taskActions.skipButton.title',
+ undefined,
+ 'Skip this task (preserves status)'
+ )}
>
{busy ? (
) : (
)}
- Skip this task
+ {t('taskEditPage.taskActions.skipButton.label', undefined, 'Skip this task')}
)
}
diff --git a/src/components/Pages/TaskEditPage/TaskActions/StartMappingActions.tsx b/src/components/Pages/TaskEditPage/TaskActions/StartMappingActions.tsx
index 4218e774a..591400226 100644
--- a/src/components/Pages/TaskEditPage/TaskActions/StartMappingActions.tsx
+++ b/src/components/Pages/TaskEditPage/TaskActions/StartMappingActions.tsx
@@ -6,8 +6,10 @@ import { toast } from 'sonner'
import { api } from '@/api'
import { useTaskContext } from '@/components/Pages/TaskEditPage/contexts/TaskContext'
import { Button } from '@/components/ui/Button'
+import { useIntl } from '@/i18n'
export const StartMappingActions = ({ challengeId }: { challengeId: number }) => {
+ const { t } = useIntl()
const { isLocking, lockTask } = useTaskContext()
const navigate = useNavigate()
const queryClient = useQueryClient()
@@ -20,14 +22,16 @@ export const StartMappingActions = ({ challengeId }: { challengeId: number }) =>
if (randomTasks && randomTasks.length > 0) {
await navigate({ to: '/tasks/$taskId', params: { taskId: String(randomTasks[0].id) } })
} else {
- toast.info('No more tasks available in this challenge')
+ toast.info(
+ t('common.noMoreTasksInChallenge', undefined, 'No more tasks available in this challenge')
+ )
await navigate({
to: '/challenge/$challengeId',
params: { challengeId: String(challengeId) },
})
}
} catch {
- toast.error('Failed to load next task')
+ toast.error(t('common.failedToLoadNextTask', undefined, 'Failed to load next task'))
} finally {
setIsLoadingNext(false)
}
@@ -38,7 +42,9 @@ export const StartMappingActions = ({ challengeId }: { challengeId: number }) =>
{isLocking ? : }
- {isLocking ? 'Starting...' : 'Map this task'}
+ {isLocking
+ ? t('taskEditPage.taskActions.startMapping.starting', undefined, 'Starting...')
+ : t('taskEditPage.taskActions.startMapping.mapThisTask', undefined, 'Map this task')}
disabled={isLoadingNext}
>
{isLoadingNext ? : }
- {isLoadingNext ? 'Loading...' : 'Different task'}
+ {isLoadingNext
+ ? t('common.loading2', undefined, 'Loading...')
+ : t('taskEditPage.taskActions.startMapping.differentTask', undefined, 'Different task')}
diff --git a/src/components/Pages/TaskEditPage/TaskActions/TaskActions.tsx b/src/components/Pages/TaskEditPage/TaskActions/TaskActions.tsx
index 6664edb30..cf6db3000 100644
--- a/src/components/Pages/TaskEditPage/TaskActions/TaskActions.tsx
+++ b/src/components/Pages/TaskEditPage/TaskActions/TaskActions.tsx
@@ -9,14 +9,18 @@ import {
import { Button, type buttonVariants } from '@/components/ui/Button'
import { DisabledTooltip } from '@/components/ui/DisabledTooltip'
import { useAuthContext } from '@/contexts/AuthContext'
+import { useIntl } from '@/i18n'
import { TaskActionModal } from '../TaskActionModal'
import { NavigationActions } from './NavigationActions'
import { StartMappingActions } from './StartMappingActions'
-const PAUSED_MESSAGE =
- 'This challenge is currently paused. Tasks cannot be completed until it is resumed.'
-
export const TaskActions = () => {
+ const { t } = useIntl()
+ const pausedMessage = t(
+ 'taskEditPage.taskActions.main.pausedMessage',
+ undefined,
+ 'This challenge is currently paused. Tasks cannot be completed until it is resumed.'
+ )
const { task, isLocked } = useTaskContext()
const { challenge } = useChallengeContext()
const { isAuthenticated, login } = useAuthContext()
@@ -59,29 +63,45 @@ export const TaskActions = () => {
variant: 'success',
icon: ,
onClick: handleMarkAsFixed,
- title: 'Mark as Fixed (Ctrl/Cmd + F)',
- label: 'Fixed',
+ title: t(
+ 'taskEditPage.taskActions.main.markFixedTitle',
+ undefined,
+ 'Mark as Fixed (Ctrl/Cmd + F)'
+ ),
+ label: t('common.fixed', undefined, 'Fixed'),
},
{
variant: 'info',
icon: ,
onClick: handleMarkAsAlreadyFixed,
- title: 'Mark as Already Fixed',
- label: 'Already Fixed',
+ title: t(
+ 'taskEditPage.taskActions.main.markAlreadyFixedTitle',
+ undefined,
+ 'Mark as Already Fixed'
+ ),
+ label: t('common.alreadyFixed', undefined, 'Already Fixed'),
},
{
variant: 'warning',
icon: ,
onClick: handleMarkAsFalsePositive,
- title: 'Mark as False Positive (Ctrl/Cmd + P)',
- label: 'Not an Issue',
+ title: t(
+ 'taskEditPage.taskActions.main.markFalsePositiveTitle',
+ undefined,
+ 'Mark as False Positive (Ctrl/Cmd + P)'
+ ),
+ label: t('taskEditPage.taskActions.main.notAnIssue', undefined, 'Not an Issue'),
},
{
variant: 'caution',
icon: ,
onClick: handleMarkAsTooHard,
- title: "Mark as Can't Complete",
- label: "Can't Complete",
+ title: t(
+ 'taskEditPage.taskActions.main.markCantCompleteTitle',
+ undefined,
+ "Mark as Can't Complete"
+ ),
+ label: t('common.cantComplete', undefined, "Can't Complete"),
},
]
@@ -119,7 +139,7 @@ export const TaskActions = () => {
- Sign in to map this task
+ {t('taskEditPage.taskActions.main.signInToMap', undefined, 'Sign in to map this task')}
)
@@ -140,11 +160,15 @@ export const TaskActions = () => {
<>
- Completion: Set Task Status
+ {t(
+ 'taskEditPage.taskActions.main.completionHeading',
+ undefined,
+ 'Completion: Set Task Status'
+ )}
{completionActions.map((action) => (
-
+
void
}) => {
+ const { t } = useIntl()
const { challenge } = useChallengeContext()
const { isAuthenticated } = useAuthContext()
const { map, markersHidden, setMarkersHidden } = useTaskMapContext()
const { data: project } = api.project.getProject(challenge?.parent)
const status = task.status ?? 0
- const statusLabel = STATUS_LABELS[status] || 'Unknown'
+ const statusLabel = STATUS_LABELS[status] || t('common.unknown', undefined, 'Unknown')
const statusColor = STATUS_COLORS[status] || 'bg-zinc-500'
// Only show edit actions if user is authenticated, has locked the task, and status is editable
@@ -102,7 +104,7 @@ export const TaskInfoHeader = ({
{relation === 'primary' && (
- Primary
+ {t('common.primary', undefined, 'Primary')}
)}
@@ -111,8 +113,16 @@ export const TaskInfoHeader = ({
size="icon-sm"
className={cn(markersHidden && 'text-amber-600 dark:text-amber-400')}
onClick={() => setMarkersHidden(!markersHidden)}
- aria-label={markersHidden ? 'Show task markers' : 'Hide task markers'}
- title={markersHidden ? 'Show task markers' : 'Hide task markers'}
+ aria-label={
+ markersHidden
+ ? t('taskEditPage.taskInfoHeader.showMarkers', undefined, 'Show task markers')
+ : t('taskEditPage.taskInfoHeader.hideMarkers', undefined, 'Hide task markers')
+ }
+ title={
+ markersHidden
+ ? t('taskEditPage.taskInfoHeader.showMarkers', undefined, 'Show task markers')
+ : t('taskEditPage.taskInfoHeader.hideMarkers', undefined, 'Hide task markers')
+ }
>
{markersHidden ?
:
}
@@ -120,28 +130,47 @@ export const TaskInfoHeader = ({
variant="ghost"
size="icon-sm"
onClick={handleZoomToTask}
- aria-label="Zoom to task"
- title="Zoom to task"
+ aria-label={t('common.zoomToTask', undefined, 'Zoom to task')}
+ title={t('common.zoomToTask', undefined, 'Zoom to task')}
>
-
+
{osmUrl && (
-
-
+
+
@@ -152,8 +181,8 @@ export const TaskInfoHeader = ({
variant="ghost"
size="icon-sm"
onClick={onClose}
- aria-label="Close task"
- title="Close task"
+ aria-label={t('common.closeTask', undefined, 'Close task')}
+ title={t('common.closeTask', undefined, 'Close task')}
>
@@ -163,7 +192,7 @@ export const TaskInfoHeader = ({
{/* Task ID */}
- Task #{task.id}
+ {t('common.taskWithId', { id: task.id }, 'Task #{id}')}
{/* Challenge › Project breadcrumb */}
diff --git a/src/components/Pages/TaskEditPage/TaskMap.tsx b/src/components/Pages/TaskEditPage/TaskMap.tsx
index f6633244e..8610ca488 100644
--- a/src/components/Pages/TaskEditPage/TaskMap.tsx
+++ b/src/components/Pages/TaskEditPage/TaskMap.tsx
@@ -16,6 +16,7 @@ import {
} from '@/components/Pages/TaskEditPage/contexts/TaskContext'
import { useTaskMapContext } from '@/components/Pages/TaskEditPage/contexts/TaskMapContext'
import { MapLoadingIndicator } from '@/components/shared/MapLoadingIndicator'
+import { useIntl } from '@/i18n'
import type { TaskMarker } from '@/types/Task'
import { useEditorContext } from './contexts/EditorContext'
import { ClearBundleDialog } from './TaskMap/ClearBundleDialog'
@@ -49,6 +50,7 @@ const OsmIcon = ({ className }: { className?: string }) => (
)
export const TaskMap = () => {
+ const { t } = useIntl()
const mapId = useId()
const exploreSourceId = useId()
const exploreCirclesLayerId = useId()
@@ -198,10 +200,10 @@ export const TaskMap = () => {
type="button"
onClick={openIdEditor}
className="relative flex h-10 items-center gap-1.5 rounded-lg bg-zinc-800/90 px-3 font-medium text-sm text-white shadow-md transition-colors hover:bg-zinc-700"
- title="Edit in iD (inline)"
+ title={t('taskEditPage.taskMap.editInId', undefined, 'Edit in iD (inline)')}
>
- Edit in iD
+ {t('taskEditPage.taskMap.editInIdShort', undefined, 'Edit in iD')}
{idEditorMounted && (
@@ -218,7 +220,11 @@ export const TaskMap = () => {
{/* Drawing mode indicator */}
{drawingMode && (
- Click and drag to select tasks • ESC to cancel
+ {t(
+ 'taskEditPage.taskMap.drawingModeHint',
+ undefined,
+ 'Click and drag to select tasks • ESC to cancel'
+ )}
)}
diff --git a/src/components/Pages/TaskEditPage/TaskMap/ClearBundleDialog.tsx b/src/components/Pages/TaskEditPage/TaskMap/ClearBundleDialog.tsx
index c38d70799..0401450db 100644
--- a/src/components/Pages/TaskEditPage/TaskMap/ClearBundleDialog.tsx
+++ b/src/components/Pages/TaskEditPage/TaskMap/ClearBundleDialog.tsx
@@ -9,29 +9,36 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/AlertDialog'
+import { useIntl } from '@/i18n'
export const ClearBundleDialog = () => {
const { showDeleteDialog, setShowDeleteDialog, handleClearBundle, activeBundle } =
useTaskBundleContext()
+ const { t } = useIntl()
const taskCount = activeBundle?.taskIds.length ?? 0
return (
- Clear Task Bundle?
+
+ {t('taskMap.clearBundleDialog.title', undefined, 'Clear Task Bundle?')}
+
- This will unbundle all {taskCount} tasks. The tasks themselves will not be deleted, only
- separated. This action cannot be undone.
+ {t(
+ 'taskMap.clearBundleDialog.description',
+ { taskCount },
+ 'This will unbundle all {taskCount} tasks. The tasks themselves will not be deleted, only separated. This action cannot be undone.'
+ )}
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
- Clear Bundle
+ {t('taskMap.clearBundleDialog.confirm', undefined, 'Clear Bundle')}
diff --git a/src/components/Pages/TaskEditPage/TaskMap/MultiTaskPanel.tsx b/src/components/Pages/TaskEditPage/TaskMap/MultiTaskPanel.tsx
index 0af1e90e0..0470f5d7e 100644
--- a/src/components/Pages/TaskEditPage/TaskMap/MultiTaskPanel.tsx
+++ b/src/components/Pages/TaskEditPage/TaskMap/MultiTaskPanel.tsx
@@ -5,6 +5,7 @@ import {
useTaskMapContext,
} from '@/components/Pages/TaskEditPage/contexts/TaskMapContext'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/Collapsible'
+import { useIntl } from '@/i18n'
import { cn } from '@/lib/utils'
import { useTaskBundleContext } from '../contexts/TaskBundleContext'
import { useTaskEditMapContext } from './TaskEditMapContext'
@@ -15,6 +16,7 @@ export const MultiTaskPanel = () => {
const { mapLoaded } = useTaskEditMapContext()
const { resetBundle, handleClearBundle } = useTaskBundleContext()
const [multiTaskPanelOpen, setMultiTaskPanelOpen] = useState(false)
+ const { t } = useIntl()
return (
{
{activeBundle ? (
<>
- Working on {activeBundle.taskIds.length} task
- {activeBundle.taskIds.length !== 1 ? 's' : ''}
- ({MAX_SELECTED_TASKS} max)
+ {activeBundle.taskIds.length !== 1
+ ? t(
+ 'taskMap.multiTaskPanel.workingOnTasksPlural',
+ { count: activeBundle.taskIds.length },
+ 'Working on {count} tasks'
+ )
+ : t(
+ 'taskMap.multiTaskPanel.workingOnTasksSingular',
+ { count: activeBundle.taskIds.length },
+ 'Working on {count} task'
+ )}
+
+ {t('taskMap.multiTaskPanel.maxBadge', { max: MAX_SELECTED_TASKS }, '({max} max)')}
+
>
) : (
- 'Work on multiple tasks'
+ t('taskMap.multiTaskPanel.title', undefined, 'Work on multiple tasks')
)}
@@ -71,12 +84,14 @@ export const MultiTaskPanel = () => {
)}
title={
activeBundle && activeBundle.taskIds.length >= MAX_SELECTED_TASKS
- ? 'Maximum tasks reached'
- : 'Draw to add tasks (D)'
+ ? t('taskMap.multiTaskPanel.maxReached', undefined, 'Maximum tasks reached')
+ : t('taskMap.multiTaskPanel.drawTooltip', undefined, 'Draw to add tasks (D)')
}
>
- {drawingMode === 'select' ? 'Drawing...' : 'Draw to add tasks'}
+ {drawingMode === 'select'
+ ? t('taskMap.multiTaskPanel.drawing', undefined, 'Drawing...')
+ : t('taskMap.multiTaskPanel.drawButton', undefined, 'Draw to add tasks')}
{/* Clear all - only show when there's a bundle */}
@@ -87,7 +102,7 @@ export const MultiTaskPanel = () => {
className="flex items-center justify-center gap-2 rounded-lg px-3 py-2 font-medium text-red-600 text-sm transition-colors hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
>
- Work on only the primary task
+ {t('taskMap.multiTaskPanel.clearAll', undefined, 'Work on only the primary task')}
)}
@@ -103,7 +118,7 @@ export const MultiTaskPanel = () => {
className="flex items-center justify-center gap-2 rounded-lg px-3 py-2 font-medium text-sm text-zinc-600 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-slate-700"
>
- Reset to initial bundle
+ {t('taskMap.multiTaskPanel.resetBundle', undefined, 'Reset to initial bundle')}
)}
diff --git a/src/components/Pages/TaskEditPage/TaskMap/useMapControlButtons.test.tsx b/src/components/Pages/TaskEditPage/TaskMap/useMapControlButtons.test.tsx
index 4b0a838cf..99c505df3 100644
--- a/src/components/Pages/TaskEditPage/TaskMap/useMapControlButtons.test.tsx
+++ b/src/components/Pages/TaskEditPage/TaskMap/useMapControlButtons.test.tsx
@@ -1,6 +1,6 @@
-import { renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { TaskBundle } from '@/components/Pages/TaskEditPage/contexts/TaskBundleContext'
+import { renderHook } from '@/test/testUtils'
const { useTaskBundleContextMock, useTaskMapContextMock, useTaskEditMapContextMock } = vi.hoisted(
() => ({
diff --git a/src/components/Pages/TaskEditPage/TaskMap/useMapControlButtons.ts b/src/components/Pages/TaskEditPage/TaskMap/useMapControlButtons.ts
index 87dc53ea3..72dc9910f 100644
--- a/src/components/Pages/TaskEditPage/TaskMap/useMapControlButtons.ts
+++ b/src/components/Pages/TaskEditPage/TaskMap/useMapControlButtons.ts
@@ -3,6 +3,7 @@ import { useMemo } from 'react'
import type { MapControlButton } from '@/components/Map/MapControls'
import { useTaskBundleContext } from '@/components/Pages/TaskEditPage/contexts/TaskBundleContext'
import { useTaskMapContext } from '@/components/Pages/TaskEditPage/contexts/TaskMapContext'
+import { useIntl } from '@/i18n'
import { useTaskEditMapContext } from './TaskEditMapContext'
export const useMapControlButtons = (
@@ -12,6 +13,7 @@ export const useMapControlButtons = (
const { markersHidden, setMarkersHidden } = useTaskMapContext()
const { activeBundle, showBundleOnly, setShowBundleOnly } = useTaskBundleContext()
const { showExploreLayer, setShowExploreLayer } = useTaskEditMapContext()
+ const { t } = useIntl()
return useMemo(
() => [
@@ -20,14 +22,18 @@ export const useMapControlButtons = (
icon: Crosshair,
onClick: handleCenterToTask,
tooltip:
- activeBundle && activeBundle.taskIds.length > 1 ? 'Center to Bundle' : 'Center to Task',
+ activeBundle && activeBundle.taskIds.length > 1
+ ? t('taskMap.controls.centerToBundle', undefined, 'Center to Bundle')
+ : t('taskMap.controls.centerToTask', undefined, 'Center to Task'),
disabled: !mapLoaded,
},
{
id: 'toggle-markers',
icon: markersHidden ? EyeOff : Eye,
onClick: () => setMarkersHidden(!markersHidden),
- tooltip: markersHidden ? 'Show all markers' : 'Hide all markers',
+ tooltip: markersHidden
+ ? t('taskMap.controls.showAllMarkers', undefined, 'Show all markers')
+ : t('taskMap.controls.hideAllMarkers', undefined, 'Hide all markers'),
disabled: !mapLoaded,
isActive: markersHidden,
},
@@ -36,10 +42,10 @@ export const useMapControlButtons = (
icon: Filter,
onClick: () => setShowBundleOnly(!showBundleOnly),
tooltip: showBundleOnly
- ? 'Show all tasks (F)'
+ ? t('taskMap.controls.showAllTasks', undefined, 'Show all tasks (F)')
: activeBundle
- ? 'Show selected tasks only (F)'
- : 'Show primary task only (F)',
+ ? t('taskMap.controls.showSelectedOnly', undefined, 'Show selected tasks only (F)')
+ : t('taskMap.controls.showPrimaryOnly', undefined, 'Show primary task only (F)'),
disabled: !mapLoaded,
isActive: showBundleOnly,
},
@@ -48,8 +54,12 @@ export const useMapControlButtons = (
icon: Globe,
onClick: () => setShowExploreLayer(!showExploreLayer),
tooltip: showExploreLayer
- ? 'Hide tasks from other challenges'
- : 'Show tasks from other challenges',
+ ? t('taskMap.controls.hideOtherChallenges', undefined, 'Hide tasks from other challenges')
+ : t(
+ 'taskMap.controls.showOtherChallenges',
+ undefined,
+ 'Show tasks from other challenges'
+ ),
disabled: !mapLoaded,
isActive: showExploreLayer,
},
@@ -64,6 +74,7 @@ export const useMapControlButtons = (
setShowBundleOnly,
showExploreLayer,
setShowExploreLayer,
+ t,
]
)
}
diff --git a/src/components/Pages/TaskEditPage/TaskMap/useTaskMapShortcuts.test.tsx b/src/components/Pages/TaskEditPage/TaskMap/useTaskMapShortcuts.test.tsx
index 8005b2403..fe519794f 100644
--- a/src/components/Pages/TaskEditPage/TaskMap/useTaskMapShortcuts.test.tsx
+++ b/src/components/Pages/TaskEditPage/TaskMap/useTaskMapShortcuts.test.tsx
@@ -1,8 +1,8 @@
-import { renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { KeyboardShortcut } from '@/components/Pages/TaskEditPage/contexts/KeyboardShortcutsContext'
import type { TaskBundle } from '@/components/Pages/TaskEditPage/contexts/TaskBundleContext'
import type { LassoMode } from '@/components/Pages/TaskEditPage/contexts/TaskMapContext'
+import { renderHook } from '@/test/testUtils'
const { useRegisterShortcutsMock, useTaskBundleContextMock, useTaskMapContextMock } = vi.hoisted(
() => ({
diff --git a/src/components/Pages/TaskEditPage/TaskMap/useTaskMapShortcuts.ts b/src/components/Pages/TaskEditPage/TaskMap/useTaskMapShortcuts.ts
index 053b770b8..dfb643d4d 100644
--- a/src/components/Pages/TaskEditPage/TaskMap/useTaskMapShortcuts.ts
+++ b/src/components/Pages/TaskEditPage/TaskMap/useTaskMapShortcuts.ts
@@ -3,6 +3,7 @@ import {
type KeyboardShortcut,
useRegisterShortcuts,
} from '@/components/Pages/TaskEditPage/contexts/KeyboardShortcutsContext'
+import { useIntl } from '@/i18n'
import { useTaskBundleContext } from '../contexts/TaskBundleContext'
import { useTaskMapContext } from '../contexts/TaskMapContext'
@@ -11,14 +12,15 @@ export const useTaskMapShortcuts = () => {
useTaskBundleContext()
const { markersHidden, setMarkersHidden, drawingMode, startDrawing, cancelDrawing } =
useTaskMapContext()
+ const { t } = useIntl()
// Reason: stable shortcut definitions for keyboard handler registration
const taskMapShortcuts: KeyboardShortcut[] = useMemo(
() => [
{
key: 'D',
- description: 'Start drawing to add tasks',
- category: 'Multi-task',
+ description: t('taskMap.shortcuts.startDrawing', undefined, 'Start drawing to add tasks'),
+ category: t('taskMap.shortcuts.categoryMultiTask', undefined, 'Multi-task'),
handler: () => {
if (!drawingMode) {
startDrawing('select')
@@ -28,29 +30,37 @@ export const useTaskMapShortcuts = () => {
},
{
key: 'F',
- description: 'Toggle filter (show bundled tasks only)',
- category: 'Map',
+ description: t(
+ 'taskMap.shortcuts.toggleFilter',
+ undefined,
+ 'Toggle filter (show bundled tasks only)'
+ ),
+ category: t('common.map', undefined, 'Map'),
handler: () => setShowBundleOnly(!showBundleOnly),
enabled: !!activeBundle,
},
{
key: 'H',
- description: 'Toggle all markers visibility',
- category: 'Map',
+ description: t(
+ 'taskMap.shortcuts.toggleMarkers',
+ undefined,
+ 'Toggle all markers visibility'
+ ),
+ category: t('common.map', undefined, 'Map'),
handler: () => setMarkersHidden(!markersHidden),
enabled: true,
},
{
key: 'Delete',
- description: 'Exit multi-task mode',
- category: 'Multi-task',
+ description: t('taskMap.shortcuts.exitMultiTask', undefined, 'Exit multi-task mode'),
+ category: t('taskMap.shortcuts.categoryMultiTask', undefined, 'Multi-task'),
handler: () => setShowDeleteDialog(true),
enabled: !!activeBundle,
},
{
key: 'Esc',
- description: 'Cancel drawing',
- category: 'Map',
+ description: t('taskMap.shortcuts.cancelDrawing', undefined, 'Cancel drawing'),
+ category: t('common.map', undefined, 'Map'),
handler: () => cancelDrawing(),
enabled: !!drawingMode,
},
@@ -65,6 +75,7 @@ export const useTaskMapShortcuts = () => {
cancelDrawing,
startDrawing,
setShowDeleteDialog,
+ t,
]
)
useRegisterShortcuts('task-map', taskMapShortcuts)
diff --git a/src/components/Pages/TaskEditPage/TaskNearbyMap.tsx b/src/components/Pages/TaskEditPage/TaskNearbyMap.tsx
index efae6852f..850a033af 100644
--- a/src/components/Pages/TaskEditPage/TaskNearbyMap.tsx
+++ b/src/components/Pages/TaskEditPage/TaskNearbyMap.tsx
@@ -6,6 +6,7 @@ import 'maplibre-gl/dist/maplibre-gl.css'
import { MapPin } from 'lucide-react'
import { api } from '@/api'
import { getCurrentMapStyle } from '@/components/Map/mapStyles'
+import { useIntl } from '@/i18n'
import type { Task } from '@/types/Task'
interface TaskNearbyMapProps {
@@ -19,6 +20,7 @@ export const TaskNearbyMap = ({
selectedTaskId,
onTaskSelect,
}: TaskNearbyMapProps) => {
+ const { t } = useIntl()
const mapRef = useRef
(null)
const [mapLoaded, setMapLoaded] = useState(false)
const mapId = useId()
@@ -153,7 +155,7 @@ export const TaskNearbyMap = ({
- Current
+ {t('common.current', undefined, 'Current')}
@@ -161,13 +163,21 @@ export const TaskNearbyMap = ({
{/* Task count indicator */}
- {nearbyTasks.length} nearby task{nearbyTasks.length !== 1 ? 's' : ''}
+ {t(
+ 'taskEditPage.taskNearbyMap.nearbyCount',
+ { count: nearbyTasks.length, suffix: nearbyTasks.length !== 1 ? 's' : '' },
+ '{count} nearby task{suffix}'
+ )}
{/* Selected task info */}
{selectedTaskId && (
- Task #{selectedTaskId} selected
+ {t(
+ 'taskEditPage.taskNearbyMap.selectedTask',
+ { id: selectedTaskId },
+ 'Task #{id} selected'
+ )}
)}
diff --git a/src/components/Pages/TaskEditPage/contexts/OSMDataContext.tsx b/src/components/Pages/TaskEditPage/contexts/OSMDataContext.tsx
index b2a55f095..4eb3e58ac 100644
--- a/src/components/Pages/TaskEditPage/contexts/OSMDataContext.tsx
+++ b/src/components/Pages/TaskEditPage/contexts/OSMDataContext.tsx
@@ -2,6 +2,7 @@ import type { Dispatch, ReactNode, SetStateAction } from 'react'
import { createContext, useCallback, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { api } from '@/api'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import { useTaskMapContext } from './TaskMapContext'
@@ -37,6 +38,7 @@ export interface OSMDataContextType {
const OSMDataContext = createContext(undefined)
export const OSMDataProvider = ({ children }: { children: ReactNode }) => {
+ const { t } = useIntl()
const { map, mapLoaded } = useTaskMapContext()
const [showOSMData, setShowOSMData] = useState(false)
const [osmData, setOsmData] = useState(null)
@@ -71,11 +73,20 @@ export const OSMDataProvider = ({ children }: { children: ReactNode }) => {
} catch (error) {
logger.error('Error fetching OSM data', { error: String(error) })
- const errorMessage = error instanceof Error ? error.message : 'Failed to fetch OSM data'
+ const errorMessage =
+ error instanceof Error
+ ? error.message
+ : t('taskEditPage.osmData.fetchError', undefined, 'Failed to fetch OSM data')
if (errorMessage.includes('too large')) {
throw error
} else {
- throw new Error('Failed to fetch OSM data. Please try again.')
+ throw new Error(
+ t(
+ 'taskEditPage.osmData.fetchErrorRetry',
+ undefined,
+ 'Failed to fetch OSM data. Please try again.'
+ )
+ )
}
} finally {
setOsmDataLoading(false)
@@ -89,17 +100,25 @@ export const OSMDataProvider = ({ children }: { children: ReactNode }) => {
try {
await fetchOSMDataForBounds()
setShowOSMData(true)
- toast.success('OSM data loaded successfully')
+ toast.success(
+ t('taskEditPage.osmData.loadSuccess', undefined, 'OSM data loaded successfully')
+ )
} catch (error) {
setShowOSMData(false)
- const errorMessage = error instanceof Error ? error.message : 'Failed to fetch OSM data'
+ const errorMessage =
+ error instanceof Error
+ ? error.message
+ : t('taskEditPage.osmData.fetchError', undefined, 'Failed to fetch OSM data')
if (errorMessage.includes('too large')) {
- toast.error('Area too large', {
- description:
- 'Please zoom in further to view OSM features. The selected area exceeds the maximum allowed size.',
+ toast.error(t('taskEditPage.osmData.areaTooLarge', undefined, 'Area too large'), {
+ description: t(
+ 'taskEditPage.osmData.areaTooLargeDescription',
+ undefined,
+ 'Please zoom in further to view OSM features. The selected area exceeds the maximum allowed size.'
+ ),
})
} else {
- toast.error('Failed to fetch OSM data', {
+ toast.error(t('taskEditPage.osmData.fetchError', undefined, 'Failed to fetch OSM data'), {
description: errorMessage,
})
}
diff --git a/src/components/Pages/TaskEditPage/contexts/TaskBundleContext.tsx b/src/components/Pages/TaskEditPage/contexts/TaskBundleContext.tsx
index 7e56dfcc2..6f898731d 100644
--- a/src/components/Pages/TaskEditPage/contexts/TaskBundleContext.tsx
+++ b/src/components/Pages/TaskEditPage/contexts/TaskBundleContext.tsx
@@ -1,6 +1,7 @@
import type { Dispatch, ReactNode, SetStateAction } from 'react'
import { createContext, useCallback, useContext, useMemo, useState } from 'react'
import { toast } from 'sonner'
+import { useIntl } from '@/i18n'
import type { Task } from '@/types/Task'
/** Sentinel for a locally-built bundle that hasn't been persisted yet. */
@@ -36,6 +37,7 @@ export interface TaskBundleContextType {
const TaskBundleContext = createContext(undefined)
export const TaskBundleProvider = ({ children }: { children: ReactNode }) => {
+ const { t } = useIntl()
const [activeBundle, setActiveBundle] = useState(null)
const [initialBundle, setInitialBundle] = useState(null)
const [showBundleOnly, setShowBundleOnly] = useState(false)
@@ -61,7 +63,9 @@ export const TaskBundleProvider = ({ children }: { children: ReactNode }) => {
const handleClearBundle = useCallback(() => {
if (!activeBundle) return
clearBundle()
- toast.success('Now working on only the primary task')
+ toast.success(
+ t('taskEditPage.taskBundle.clearedSuccess', undefined, 'Now working on only the primary task')
+ )
setShowDeleteDialog(false)
}, [activeBundle, clearBundle])
diff --git a/src/components/Pages/TeamsPage/EditTeamPage.tsx b/src/components/Pages/TeamsPage/EditTeamPage.tsx
index a8ff04e93..7d4db6e4b 100644
--- a/src/components/Pages/TeamsPage/EditTeamPage.tsx
+++ b/src/components/Pages/TeamsPage/EditTeamPage.tsx
@@ -1,5 +1,6 @@
import { api } from '@/api'
import { Loader } from '@/components/ui/Loader'
+import { useIntl } from '@/i18n'
import { TeamForm } from './TeamForm'
interface Props {
@@ -7,14 +8,22 @@ interface Props {
}
export const EditTeamPage = ({ teamId }: Props) => {
+ const { t } = useIntl()
const { data: team, isLoading } = api.team.get(teamId)
if (isLoading) return
- if (!team) return Team not found.
+ if (!team)
+ return (
+
+ {t('common.teamNotFound', undefined, 'Team not found.')}
+
+ )
return (
-
Edit team
+
+ {t('teams.editTeam.title', undefined, 'Edit team')}
+
)
diff --git a/src/components/Pages/TeamsPage/InviteMemberDialog.tsx b/src/components/Pages/TeamsPage/InviteMemberDialog.tsx
index 4aaf476ef..9b095e7a7 100644
--- a/src/components/Pages/TeamsPage/InviteMemberDialog.tsx
+++ b/src/components/Pages/TeamsPage/InviteMemberDialog.tsx
@@ -20,6 +20,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/Select'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import { initials } from '@/lib/utils'
import type { TeamRole } from '@/types/Team'
@@ -31,6 +32,7 @@ interface Props {
}
export const InviteMemberDialog = ({ teamId, open, onOpenChange }: Props) => {
+ const { t } = useIntl()
const [query, setQuery] = useState('')
const [selectedUserId, setSelectedUserId] = useState(null)
const [role, setRole] = useState(1)
@@ -41,13 +43,13 @@ export const InviteMemberDialog = ({ teamId, open, onOpenChange }: Props) => {
if (!selectedUserId) return
try {
await invite.mutateAsync({ teamId, userId: selectedUserId, role })
- toast.success('Invitation sent')
+ toast.success(t('teams.inviteMember.sentSuccess', undefined, 'Invitation sent'))
onOpenChange(false)
setQuery('')
setSelectedUserId(null)
} catch (error) {
logger.error('Invite failed', { error })
- toast.error('Could not send invitation')
+ toast.error(t('teams.inviteMember.sendError', undefined, 'Could not send invitation'))
}
}
@@ -55,8 +57,16 @@ export const InviteMemberDialog = ({ teamId, open, onOpenChange }: Props) => {
- Invite a team member
- Search for an OSM user and pick a role for them.
+
+ {t('teams.inviteMember.title', undefined, 'Invite a team member')}
+
+
+ {t(
+ 'teams.inviteMember.description',
+ undefined,
+ 'Search for an OSM user and pick a role for them.'
+ )}
+
@@ -64,7 +74,7 @@ export const InviteMemberDialog = ({ teamId, open, onOpenChange }: Props) => {
setQuery(e.target.value)}
- placeholder="Search OSM username"
+ placeholder={t('common.searchOsmUsername', undefined, 'Search OSM username')}
className="pl-8"
/>
@@ -96,17 +106,17 @@ export const InviteMemberDialog = ({ teamId, open, onOpenChange }: Props) => {
- Member
- Admin
+ {t('common.member', undefined, 'Member')}
+ {t('common.admin', undefined, 'Admin')}
onOpenChange(false)}>
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
- Send invitation
+ {t('teams.inviteMember.sendButton', undefined, 'Send invitation')}
diff --git a/src/components/Pages/TeamsPage/NewTeamPage.tsx b/src/components/Pages/TeamsPage/NewTeamPage.tsx
index d5a7db08a..abc83c204 100644
--- a/src/components/Pages/TeamsPage/NewTeamPage.tsx
+++ b/src/components/Pages/TeamsPage/NewTeamPage.tsx
@@ -1,8 +1,14 @@
+import { useIntl } from '@/i18n'
import { TeamForm } from './TeamForm'
-export const NewTeamPage = () => (
-
-
Create a team
-
-
-)
+export const NewTeamPage = () => {
+ const { t } = useIntl()
+ return (
+
+
+ {t('teams.newTeam.title', undefined, 'Create a team')}
+
+
+
+ )
+}
diff --git a/src/components/Pages/TeamsPage/PendingInvitesSection.tsx b/src/components/Pages/TeamsPage/PendingInvitesSection.tsx
index e22b6ba05..5cd785fbc 100644
--- a/src/components/Pages/TeamsPage/PendingInvitesSection.tsx
+++ b/src/components/Pages/TeamsPage/PendingInvitesSection.tsx
@@ -3,6 +3,7 @@ import { toast } from 'sonner'
import { api } from '@/api'
import { Button } from '@/components/ui/Button'
import { Card } from '@/components/ui/Card'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import type { TeamUser } from '@/types/Team'
@@ -11,39 +12,52 @@ interface Props {
}
export const PendingInvitesSection = ({ invites }: Props) => {
+ const { t } = useIntl()
const accept = api.team.useAcceptInvite()
const decline = api.team.useDeclineInvite()
const handleAccept = async (teamId: number) => {
try {
await accept.mutateAsync(teamId)
- toast.success('Invitation accepted')
+ toast.success(t('teams.pendingInvites.acceptSuccess', undefined, 'Invitation accepted'))
} catch (error) {
logger.error('Accept invite failed', { error })
- toast.error('Could not accept invitation')
+ toast.error(t('teams.pendingInvites.acceptError', undefined, 'Could not accept invitation'))
}
}
const handleDecline = async (teamId: number) => {
try {
await decline.mutateAsync(teamId)
- toast.success('Invitation declined')
+ toast.success(t('teams.pendingInvites.declineSuccess', undefined, 'Invitation declined'))
} catch (error) {
logger.error('Decline invite failed', { error })
- toast.error('Could not decline invitation')
+ toast.error(t('teams.pendingInvites.declineError', undefined, 'Could not decline invitation'))
}
}
return (
- You have {invites.length} pending invitation{invites.length === 1 ? '' : 's'}
+ {invites.length === 1
+ ? t(
+ 'teams.pendingInvites.countSingular',
+ { count: invites.length },
+ 'You have {count} pending invitation'
+ )
+ : t(
+ 'teams.pendingInvites.countPlural',
+ { count: invites.length },
+ 'You have {count} pending invitations'
+ )}
{invites.map((invite) => (
- {invite.name || `Team #${invite.teamId}`}
+
+ {invite.name || t('common.team', { teamId: invite.teamId }, 'Team #{teamId}')}
+
{
onClick={() => handleDecline(invite.teamId)}
disabled={decline.isPending}
>
- Decline
+ {' '}
+ {t('teams.pendingInvites.declineButton', undefined, 'Decline')}
{
onClick={() => handleAccept(invite.teamId)}
disabled={accept.isPending}
>
- Accept
+ {' '}
+ {t('teams.pendingInvites.acceptButton', undefined, 'Accept')}
diff --git a/src/components/Pages/TeamsPage/TeamCard.tsx b/src/components/Pages/TeamsPage/TeamCard.tsx
index e5b8232f1..36728cc5d 100644
--- a/src/components/Pages/TeamsPage/TeamCard.tsx
+++ b/src/components/Pages/TeamsPage/TeamCard.tsx
@@ -2,6 +2,7 @@ import { Link } from '@tanstack/react-router'
import { Users } from 'lucide-react'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar'
import { Card } from '@/components/ui/Card'
+import { useIntl } from '@/i18n'
import { cn, initials } from '@/lib/utils'
import type { TeamRole, TeamUser } from '@/types/Team'
import { TeamRoleLabel } from '@/types/Team'
@@ -17,7 +18,8 @@ const roleBadge: Record = {
}
export const TeamCard = ({ membership }: Props) => {
- const name = membership.name || `Team #${membership.teamId}`
+ const { t } = useIntl()
+ const name = membership.name || t('common.team', { teamId: membership.teamId }, 'Team #{teamId}')
const role = membership.status as TeamRole
return (
@@ -36,7 +38,7 @@ export const TeamCard = ({ membership }: Props) => {
roleBadge[role]
)}
>
- {TeamRoleLabel[role] ?? 'Unknown'}
+ {TeamRoleLabel[role] ?? t('common.unknown', undefined, 'Unknown')}
diff --git a/src/components/Pages/TeamsPage/TeamDetailPage.tsx b/src/components/Pages/TeamsPage/TeamDetailPage.tsx
index dc971f827..26908332f 100644
--- a/src/components/Pages/TeamsPage/TeamDetailPage.tsx
+++ b/src/components/Pages/TeamsPage/TeamDetailPage.tsx
@@ -17,6 +17,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar'
import { Button } from '@/components/ui/Button'
import { Loader } from '@/components/ui/Loader'
import { useAuthContext } from '@/contexts/AuthContext'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import { initials } from '@/lib/utils'
import type { TeamRole, TeamUser } from '@/types/Team'
@@ -38,26 +39,27 @@ const MemberRow = ({
currentUserId: number | undefined
teamId: number
}) => {
+ const { t } = useIntl()
const changeRole = api.team.useChangeRole()
const removeMember = api.team.useRemoveMember()
const handleRole = async (role: TeamRole) => {
try {
await changeRole.mutateAsync({ teamId, userId: member.userId, role })
- toast.success('Role updated')
+ toast.success(t('teams.detail.roleUpdateSuccess', undefined, 'Role updated'))
} catch (error) {
logger.error('Role change failed', { error })
- toast.error('Could not update role')
+ toast.error(t('teams.detail.roleUpdateError', undefined, 'Could not update role'))
}
}
const handleRemove = async () => {
try {
await removeMember.mutateAsync({ teamId, userId: member.userId })
- toast.success('Member removed')
+ toast.success(t('teams.detail.memberRemoveSuccess', undefined, 'Member removed'))
} catch (error) {
logger.error('Remove failed', { error })
- toast.error('Could not remove member')
+ toast.error(t('teams.detail.memberRemoveError', undefined, 'Could not remove member'))
}
}
@@ -71,7 +73,7 @@ const MemberRow = ({
{member.name}
- {TeamRoleLabel[role] ?? 'Unknown'}
+ {TeamRoleLabel[role] ?? t('common.unknown', undefined, 'Unknown')}
{isAdmin && member.userId !== currentUserId && (
@@ -84,7 +86,7 @@ const MemberRow = ({
onClick={() => handleRole(2)}
disabled={changeRole.isPending}
>
- Promote
+ {t('teams.detail.promoteButton', undefined, 'Promote')}
)}
{role === 2 && (
@@ -95,7 +97,7 @@ const MemberRow = ({
onClick={() => handleRole(1)}
disabled={changeRole.isPending}
>
- Demote
+ {t('teams.detail.demoteButton', undefined, 'Demote')}
)}
@@ -115,6 +117,7 @@ const MemberRow = ({
}
export const TeamDetailPage = ({ teamId }: Props) => {
+ const { t } = useIntl()
const navigate = useNavigate()
const { user } = useAuthContext()
const { data: team, isLoading } = api.team.get(teamId)
@@ -126,7 +129,11 @@ export const TeamDetailPage = ({ teamId }: Props) => {
if (isLoading) return
if (!team) {
- return
Team not found.
+ return (
+
+ {t('common.teamNotFound', undefined, 'Team not found.')}
+
+ )
}
const me = members.find((m) => m.userId === user?.id)
@@ -139,11 +146,11 @@ export const TeamDetailPage = ({ teamId }: Props) => {
const handleDelete = async () => {
try {
await deleteTeam.mutateAsync(teamId)
- toast.success('Team deleted')
+ toast.success(t('teams.detail.deleteSuccess', undefined, 'Team deleted'))
navigate({ to: '/teams' })
} catch (error) {
logger.error('Team delete failed', { error })
- toast.error('Could not delete team')
+ toast.error(t('teams.detail.deleteError', undefined, 'Could not delete team'))
}
}
@@ -163,7 +170,9 @@ export const TeamDetailPage = ({ teamId }: Props) => {
)}
- {members.length} member{members.length === 1 ? '' : 's'}
+ {members.length === 1
+ ? t('teams.detail.memberCountSingular', { count: members.length }, '{count} member')
+ : t('teams.detail.memberCountPlural', { count: members.length }, '{count} members')}
@@ -171,14 +180,17 @@ export const TeamDetailPage = ({ teamId }: Props) => {
- Edit
+ {' '}
+ {t('common.edit', undefined, 'Edit')}
setInviteOpen(true)}>
- Invite
+ {' '}
+ {t('teams.detail.inviteButton', undefined, 'Invite')}
setConfirmDelete(true)}>
- Delete
+ {' '}
+ {t('common.delete', undefined, 'Delete')}
)}
@@ -186,7 +198,9 @@ export const TeamDetailPage = ({ teamId }: Props) => {
{admins.length > 0 && (
- Admins
+
+ {t('teams.detail.adminsHeading', undefined, 'Admins')}
+
{admins.map((m) => (
{
{regularMembers.length > 0 && (
- Members
+
+ {t('teams.detail.membersHeading', undefined, 'Members')}
+
{regularMembers.map((m) => (
{
{invited.length > 0 && iAmAdmin && (
- Invited
+
+ {t('common.invited', undefined, 'Invited')}
+
{invited.map((m) => (
{
- Delete this team?
+
+ {t('teams.detail.deleteConfirmTitle', undefined, 'Delete this team?')}
+
- This cannot be undone. All members will lose access.
+ {t(
+ 'teams.detail.deleteConfirmDescription',
+ undefined,
+ 'This cannot be undone. All members will lose access.'
+ )}
- Cancel
- Delete team
+ {t('common.cancel', undefined, 'Cancel')}
+
+ {t('teams.detail.deleteConfirmAction', undefined, 'Delete team')}
+
diff --git a/src/components/Pages/TeamsPage/TeamForm.tsx b/src/components/Pages/TeamsPage/TeamForm.tsx
index 07c83f18a..a3248878b 100644
--- a/src/components/Pages/TeamsPage/TeamForm.tsx
+++ b/src/components/Pages/TeamsPage/TeamForm.tsx
@@ -14,6 +14,7 @@ import {
} from '@/components/ui/Form'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
+import { useIntl } from '@/i18n'
import { logger } from '@/lib/logger'
import type { Team } from '@/types/Team'
import { type TeamFormValues, teamFormSchema } from './teamSchema'
@@ -23,6 +24,7 @@ interface Props {
}
export const TeamForm = ({ team }: Props) => {
+ const { t } = useIntl()
const navigate = useNavigate()
const create = api.team.useCreateTeam()
const update = api.team.useUpdateTeam()
@@ -46,11 +48,15 @@ export const TeamForm = ({ team }: Props) => {
const result = team
? await update.mutateAsync({ teamId: team.id, payload })
: await create.mutateAsync(payload)
- toast.success(team ? 'Team updated' : 'Team created')
+ toast.success(
+ team
+ ? t('teams.form.updateSuccess', undefined, 'Team updated')
+ : t('teams.form.createSuccess', undefined, 'Team created')
+ )
navigate({ to: '/teams/$teamId', params: { teamId: String(result.id) } })
} catch (error) {
logger.error('Team save failed', { error })
- toast.error('Could not save team')
+ toast.error(t('teams.form.saveError', undefined, 'Could not save team'))
}
}
@@ -62,7 +68,7 @@ export const TeamForm = ({ team }: Props) => {
name="name"
render={({ field }) => (
- Name
+ {t('common.name', undefined, 'Name')}
@@ -75,7 +81,7 @@ export const TeamForm = ({ team }: Props) => {
name="description"
render={({ field }) => (
- Description
+ {t('common.description', undefined, 'Description')}
@@ -88,7 +94,9 @@ export const TeamForm = ({ team }: Props) => {
name="avatarURL"
render={({ field }) => (
- Avatar URL (optional)
+
+ {t('teams.form.avatarUrlLabel', undefined, 'Avatar URL (optional)')}
+
@@ -103,10 +111,12 @@ export const TeamForm = ({ team }: Props) => {
onClick={() => navigate({ to: '/teams' })}
disabled={form.formState.isSubmitting}
>
- Cancel
+ {t('common.cancel', undefined, 'Cancel')}
- {team ? 'Save' : 'Create team'}
+ {team
+ ? t('common.save', undefined, 'Save')
+ : t('common.createTeam', undefined, 'Create team')}
diff --git a/src/components/Pages/TeamsPage/TeamsList.tsx b/src/components/Pages/TeamsPage/TeamsList.tsx
index 9266a8f0e..504465acc 100644
--- a/src/components/Pages/TeamsPage/TeamsList.tsx
+++ b/src/components/Pages/TeamsPage/TeamsList.tsx
@@ -5,10 +5,12 @@ import { Button } from '@/components/ui/Button'
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/Empty'
import { Loader } from '@/components/ui/Loader'
import { useAuthContext } from '@/contexts/AuthContext'
+import { useIntl } from '@/i18n'
import { PendingInvitesSection } from './PendingInvitesSection'
import { TeamCard } from './TeamCard'
export const TeamsList = () => {
+ const { t } = useIntl()
const { user } = useAuthContext()
const { data: memberships = [], isLoading } = api.user.teamMemberships(user?.id)
@@ -20,10 +22,11 @@ export const TeamsList = () => {
return (
-
Teams
+
{t('common.teams', undefined, 'Teams')}
- Create team
+ {' '}
+ {t('common.createTeam', undefined, 'Create team')}
@@ -36,13 +39,17 @@ export const TeamsList = () => {
-
No teams yet
+
{t('teams.list.emptyTitle', undefined, 'No teams yet')}
- Create a team to collaborate with others on challenges.
+ {t(
+ 'teams.list.emptyDescription',
+ undefined,
+ 'Create a team to collaborate with others on challenges.'
+ )}
- Create team
+ {t('common.createTeam', undefined, 'Create team')}
) : (
diff --git a/src/components/TaskInfoPanel/CommentsHistoryTab.tsx b/src/components/TaskInfoPanel/CommentsHistoryTab.tsx
index ca0d0e5d3..1fa1ae54c 100644
--- a/src/components/TaskInfoPanel/CommentsHistoryTab.tsx
+++ b/src/components/TaskInfoPanel/CommentsHistoryTab.tsx
@@ -25,6 +25,7 @@ import { ScrollArea } from '@/components/ui/ScrollArea'
import { Textarea } from '@/components/ui/Textarea'
import { useAuthContext } from '@/contexts/AuthContext'
import { useAvatarContext } from '@/contexts/AvatarContext'
+import { useIntl } from '@/i18n'
import { formatDate, formatDateTime } from '@/lib/date'
import { logger } from '@/lib/logger'
import {
@@ -81,9 +82,10 @@ const getStatusVisual = (status: number | undefined): StatusVisual | null => {
}
const StatusPill = ({ status, muted = false }: { status: number; muted?: boolean }) => {
+ const { t } = useIntl()
const Icon = STATUS_ICONS[status] ?? HelpCircle
const pill = STATUS_PILL_COLORS[status] ?? DEFAULT_PILL_CLASS
- const label = STATUS_LABELS[status] ?? `Status ${status}`
+ const label = STATUS_LABELS[status] ?? t('common.statusWithStatus', { status }, 'Status {status}')
return (
{
+ const { t } = useIntl()
const { task } = useTaskContext()
const { user } = useAuthContext()
const [commentText, setCommentText] = useState('')
@@ -112,11 +115,13 @@ export const CommentsHistoryTab = () => {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!commentText.trim()) {
- toast.error('Please enter a comment')
+ toast.error(t('common.pleaseEnterAComment', undefined, 'Please enter a comment'))
return
}
if (!user) {
- toast.error('You must be logged in to comment')
+ toast.error(
+ t('common.youMustBeLoggedInToComment', undefined, 'You must be logged in to comment')
+ )
return
}
addCommentMutation.mutate(
@@ -124,11 +129,11 @@ export const CommentsHistoryTab = () => {
{
onSuccess: () => {
setCommentText('')
- toast.success('Comment added')
+ toast.success(t('taskInfoPanel.comments.addSuccess', undefined, 'Comment added'))
},
onError: (error) => {
logger.error('Error adding comment', { error })
- toast.error('Failed to add comment')
+ toast.error(t('common.failedToAddComment', undefined, 'Failed to add comment'))
},
}
)
@@ -144,7 +149,8 @@ export const CommentsHistoryTab = () => {
const renderHistoryItem = (item: TaskHistoryAction, index: number) => {
const timestamp = new Date(item.timestamp)
- const userName = item.user?.username ?? 'System'
+ const userName =
+ item.user?.username ?? t('taskInfoPanel.comments.systemUser', undefined, 'System')
if (item.actionType === ACTION_TYPE.UPDATE) {
return null
@@ -243,16 +249,22 @@ export const CommentsHistoryTab = () => {
>
) : item.status !== undefined ? (
<>
- marked as
+
+ {t('taskInfoPanel.comments.markedAs', undefined, 'marked as')}
+
>
) : item.oldStatus !== undefined ? (
<>
- cleared
+
+ {t('taskInfoPanel.comments.cleared', undefined, 'cleared')}
+
>
) : (
- changed status
+
+ {t('taskInfoPanel.comments.changedStatus', undefined, 'changed status')}
+
)}
{formatDate(timestamp)}
@@ -276,7 +288,11 @@ export const CommentsHistoryTab = () => {
- No activity yet. Be the first to comment!
+ {t(
+ 'taskInfoPanel.comments.emptyState',
+ undefined,
+ 'No activity yet. Be the first to comment!'
+ )}
) : (
@@ -298,7 +314,7 @@ export const CommentsHistoryTab = () => {
ref={textareaRef}
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
- placeholder="Add a comment..."
+ placeholder={t('common.addAComment', undefined, 'Add a comment...')}
rows={2}
className="flex-1 resize-none text-sm"
maxLength={5000}
@@ -315,7 +331,7 @@ export const CommentsHistoryTab = () => {
) : (
- Sign in to add comments
+ {t('taskInfoPanel.comments.signInPrompt', undefined, 'Sign in to add comments')}
)}
diff --git a/src/components/TaskInfoPanel/OSMHistoryTab/AreaHistoryCard.tsx b/src/components/TaskInfoPanel/OSMHistoryTab/AreaHistoryCard.tsx
index 274dd1e84..83f1e00e9 100644
--- a/src/components/TaskInfoPanel/OSMHistoryTab/AreaHistoryCard.tsx
+++ b/src/components/TaskInfoPanel/OSMHistoryTab/AreaHistoryCard.tsx
@@ -1,4 +1,5 @@
import { ExternalLink, MapPin } from 'lucide-react'
+import { useIntl } from '@/i18n'
interface AreaHistoryCardProps {
coordinates: { lat: number; lng: number }
@@ -6,14 +7,20 @@ interface AreaHistoryCardProps {
}
export const AreaHistoryCard = ({ coordinates, osmServer }: AreaHistoryCardProps) => {
+ const { t } = useIntl()
+
return (
- Area History
+ {t('taskInfoPanel.osmHistory.areaHistory.title', undefined, 'Area History')}
- View recent changes in the area around this task
+ {t(
+ 'taskInfoPanel.osmHistory.areaHistory.description',
+ undefined,
+ 'View recent changes in the area around this task'
+ )}
- OSM History at Location
+ {t(
+ 'taskInfoPanel.osmHistory.areaHistory.osmHistoryLink',
+ undefined,
+ 'OSM History at Location'
+ )}
- Recent edits near {coordinates.lat.toFixed(4)}, {coordinates.lng.toFixed(4)}
+ {t(
+ 'taskInfoPanel.osmHistory.areaHistory.recentEditsNear',
+ { lat: coordinates.lat.toFixed(4), lng: coordinates.lng.toFixed(4) },
+ 'Recent edits near {lat}, {lng}'
+ )}
@@ -40,9 +55,19 @@ export const AreaHistoryCard = ({ coordinates, osmServer }: AreaHistoryCardProps
className="flex items-center justify-between rounded-lg bg-zinc-100 p-3 transition-colors hover:bg-zinc-200 dark:bg-slate-800/50 dark:hover:bg-slate-800"
>
-
OSMCha Area Filter
+
+ {t(
+ 'taskInfoPanel.osmHistory.areaHistory.osmchaLink',
+ undefined,
+ 'OSMCha Area Filter'
+ )}
+
- All changesets in this area
+ {t(
+ 'taskInfoPanel.osmHistory.areaHistory.osmchaDescription',
+ undefined,
+ 'All changesets in this area'
+ )}
diff --git a/src/components/TaskInfoPanel/OSMHistoryTab/ElementHistoryCard.tsx b/src/components/TaskInfoPanel/OSMHistoryTab/ElementHistoryCard.tsx
index 3a937a987..24b47a71a 100644
--- a/src/components/TaskInfoPanel/OSMHistoryTab/ElementHistoryCard.tsx
+++ b/src/components/TaskInfoPanel/OSMHistoryTab/ElementHistoryCard.tsx
@@ -2,6 +2,7 @@ import { GitCommit, History, Loader2, User } from 'lucide-react'
import { useEffect, useState } from 'react'
import { api } from '@/api'
import type { OSMHistoryElement } from '@/api/osm'
+import { useIntl } from '@/i18n'
import { formatDate } from '@/lib/date'
import type { OsmFeature } from '../taskUtils/osmUtils'
@@ -11,6 +12,7 @@ interface ElementHistoryCardProps {
}
export const ElementHistoryCard = ({ osmFeature, osmServer }: ElementHistoryCardProps) => {
+ const { t } = useIntl()
const [elementHistory, setElementHistory] = useState(null)
const [historyLoading, setHistoryLoading] = useState(false)
const [historyError, setHistoryError] = useState(null)
@@ -24,7 +26,14 @@ export const ElementHistoryCard = ({ osmFeature, osmServer }: ElementHistoryCard
const history = await api.osm.fetchOSMElementHistory(idString, true)
setElementHistory(history)
} catch (error) {
- const message = error instanceof Error ? error.message : 'Failed to fetch history'
+ const message =
+ error instanceof Error
+ ? error.message
+ : t(
+ 'taskInfoPanel.osmHistory.elementHistory.fetchError',
+ undefined,
+ 'Failed to fetch history'
+ )
setHistoryError(message)
} finally {
setHistoryLoading(false)
@@ -38,13 +47,13 @@ export const ElementHistoryCard = ({ osmFeature, osmServer }: ElementHistoryCard
- Element History
+ {t('taskInfoPanel.osmHistory.elementHistory.title', undefined, 'Element History')}
{historyLoading && (
- Loading history...
+ {t('taskInfoPanel.osmHistory.elementHistory.loading', undefined, 'Loading history...')}
)}
@@ -74,7 +83,7 @@ export const ElementHistoryCard = ({ osmFeature, osmServer }: ElementHistoryCard
{entry.visible === false && (
- deleted
+ {t('common.deleted2', undefined, 'deleted')}
)}
@@ -114,7 +123,9 @@ export const ElementHistoryCard = ({ osmFeature, osmServer }: ElementHistoryCard
)}
{elementHistory && elementHistory.length === 0 && (
- No history available.
+
+ {t('taskInfoPanel.osmHistory.elementHistory.empty', undefined, 'No history available.')}
+
)}
)
diff --git a/src/components/TaskInfoPanel/OSMHistoryTab/LinkedChangesetCard.tsx b/src/components/TaskInfoPanel/OSMHistoryTab/LinkedChangesetCard.tsx
index 19fa5f7b6..d95b15450 100644
--- a/src/components/TaskInfoPanel/OSMHistoryTab/LinkedChangesetCard.tsx
+++ b/src/components/TaskInfoPanel/OSMHistoryTab/LinkedChangesetCard.tsx
@@ -1,4 +1,5 @@
import { ExternalLink, GitCommit } from 'lucide-react'
+import { useIntl } from '@/i18n'
interface LinkedChangesetCardProps {
changesetId: number
@@ -6,12 +7,14 @@ interface LinkedChangesetCardProps {
}
export const LinkedChangesetCard = ({ changesetId, osmServer }: LinkedChangesetCardProps) => {
+ const { t } = useIntl()
+
return (