Skip to content

Commit

Permalink
Merge pull request #1744 from digitalfabrik/1623-add-i18n-for-adminis…
Browse files Browse the repository at this point in the history
…tration

1623: Add i18n for administration
  • Loading branch information
ztefanie authored Nov 18, 2024
2 parents 28e12eb + 98cc2ac commit 5275a30
Show file tree
Hide file tree
Showing 30 changed files with 461 additions and 284 deletions.
2 changes: 2 additions & 0 deletions administration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"csv-stringify": "^6.4.6",
"date-fns": "^2.30.0",
"graphql": "^16.8.1",
"i18next": "^23.16.4",
"localforage": "^1.10.0",
"normalize-strings": "^1.1.1",
"normalize.css": "^8.0.1",
Expand All @@ -35,6 +36,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-flip-move": "^3.0.5",
"react-i18next": "^15.1.0",
"react-router-dom": "^6.14.0",
"styled-components": "^5.3.11"
},
Expand Down
1 change: 1 addition & 0 deletions administration/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AuthProvider from './AuthProvider'
import Router from './Router'
import { AppToasterProvider } from './bp-modules/AppToaster'
import useMetaTags from './hooks/useMetaTags'
import './i18n'
import { ProjectConfigProvider } from './project-configs/ProjectConfigContext'

if (!process.env.REACT_APP_API_BASE_URL) {
Expand Down
21 changes: 17 additions & 4 deletions administration/src/bp-modules/applications/JsonFieldArray.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Classes, Collapse, H6, Icon } from '@blueprintjs/core'
import React, { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'

import { printAwareCss } from './ApplicationCard'
Expand Down Expand Up @@ -52,10 +53,22 @@ const JsonFieldArray = ({
attachmentAccessible,
expandedRoot,
}: JsonFieldViewProps<JsonField<'Array'>>) => {
const { t } = useTranslation('application')
const [isExpanded, setIsExpanded] = useState(hierarchyIndex !== 1 || expandedRoot)
const children = jsonField.value.map((jsonField, index: number) => (
const getTranslationKey = () =>
[
'organizationContact',
'organization',
'volunteerServiceEntitlement',
'honoredByMinisterPresidentEntitlement',
].includes(jsonField.name)
? `${jsonField.name}.title`
: jsonField.name

const children = jsonField.value.map((jsonFieldIt, index: number) => (
<JsonFieldView
jsonField={jsonField}
jsonField={jsonFieldIt}
parentName={['organizationContact', 'organization'].includes(jsonField.name) ? jsonField.name : undefined}
baseUrl={baseUrl}
// This is the best key we have as jsonField.name is not unique
// eslint-disable-next-line react/no-array-index-key
Expand All @@ -65,13 +78,13 @@ const JsonFieldArray = ({
expandedRoot={expandedRoot}
/>
))
return jsonField.translations.de.length === 0 ? (
return jsonField.name === 'application' ? (
<>{children}</>
) : (
<ParentOfBorder $hierarchyIndex={hierarchyIndex}>
<CollapsableHeader onClick={() => setIsExpanded(!isExpanded)}>
<PrintableCaret icon={isExpanded ? 'caret-up' : 'caret-down'} />
{jsonField.translations.de}
{t(getTranslationKey())}
</CollapsableHeader>
<PrintableCollapse keepChildrenMounted isOpen={isExpanded}>
{children}
Expand Down
18 changes: 12 additions & 6 deletions administration/src/bp-modules/applications/JsonFieldElemental.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Colors, Icon, Tag } from '@blueprintjs/core'
import React, { memo, useContext } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'

import { AuthContext } from '../../AuthProvider'
Expand Down Expand Up @@ -29,6 +30,7 @@ const JsonFieldAttachment = memo(
({ jsonField, baseUrl, attachmentAccessible }: JsonFieldViewProps<JsonField<'Attachment'>>) => {
const appToaster = useAppToaster()
const token = useContext(AuthContext).data?.token
const { t } = useTranslation('application')

const attachment = jsonField.value
if (attachmentAccessible) {
Expand Down Expand Up @@ -62,7 +64,7 @@ const JsonFieldAttachment = memo(
}
return (
<p>
{jsonField.translations.de}:&nbsp;
{t(jsonField.name)}:&nbsp;
<PrintAwareTag
round
rightIcon={<Icon icon='download' color={Colors.GRAY1} />}
Expand All @@ -75,7 +77,7 @@ const JsonFieldAttachment = memo(
}
return (
<p>
{jsonField.translations.de}:&nbsp;
{t(jsonField.name)}:&nbsp;
<span>eingereicht, nicht sichtbar</span>
</p>
)
Expand All @@ -84,31 +86,35 @@ const JsonFieldAttachment = memo(

const JsonFieldElemental = ({
jsonField,
parentName,
...rest
}: JsonFieldViewProps<Exclude<GeneralJsonField, JsonField<'Array'>>>) => {
const { t } = useTranslation('application')
const getTranslationKey = () => (parentName ? `${parentName}.${jsonField.name}` : jsonField.name)

switch (jsonField.type) {
case 'String':
return (
<p>
{jsonField.translations.de}: {jsonField.value}
{t(getTranslationKey())}: {jsonField.value}
</p>
)
case 'Date':
return (
<p>
{jsonField.translations.de}: {new Date(jsonField.value).toLocaleDateString('de')}
{t(getTranslationKey())}: {new Date(jsonField.value).toLocaleDateString('de')}
</p>
)
case 'Number':
return (
<p>
{jsonField.translations.de}: {jsonField.value}
{t(getTranslationKey())}: {jsonField.value}
</p>
)
case 'Boolean':
return (
<p>
{jsonField.translations.de}:&nbsp;
{t(getTranslationKey())}:&nbsp;
{jsonField.value ? (
<>
<Icon icon='tick' intent='success' /> Ja
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import JsonFieldElemental from './JsonFieldElemental'

export type JsonField<T extends keyof JsonFieldValueByType> = {
name: string
translations: { de: string }
type: T
value: JsonFieldValueByType[T]
}
Expand Down Expand Up @@ -35,6 +34,7 @@ export const findValue = <T extends keyof JsonFieldValueByType>(

export type JsonFieldViewProps<JsonFieldType extends GeneralJsonField> = {
jsonField: JsonFieldType
parentName?: string
hierarchyIndex: number
baseUrl: string
attachmentAccessible: boolean
Expand Down
23 changes: 23 additions & 0 deletions administration/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'

import deTranslations from './util/translations/de.json'

type Translation = typeof deTranslations

type Translations = {
de: Translation
}

export const loadTranslations = (): Translations => ({
de: {
application: deTranslations.application,
},
})

i18n.use(initReactI18next).init({
fallbackLng: 'de',
resources: loadTranslations(),
})

export default i18n
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import '@fontsource/roboto/700.css'
import { CircularProgress, DialogActions, Typography } from '@mui/material'
import { SnackbarProvider, useSnackbar } from 'notistack'
import React, { ReactElement, useCallback, useContext, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'

import getMessageFromApolloError from '../../errors/getMessageFromApolloError'
Expand All @@ -31,6 +32,7 @@ const SuccessContent = styled.div`
`

const ApplyController = (): React.ReactElement | null => {
const { t } = useTranslation('application')
const [formSubmitted, setFormSubmitted] = useState<boolean>(false)
const { enqueueSnackbar } = useSnackbar()
const { status, state, setState } = useVersionedLocallyStoredState(
Expand Down Expand Up @@ -98,7 +100,7 @@ const ApplyController = (): React.ReactElement | null => {
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'start', margin: '16px' }}>
<div style={{ maxWidth: '1000px', width: '100%' }}>
<Typography variant='h4' component='h1' style={{ textAlign: 'center', margin: '16px' }}>
{formSubmitted ? 'Erfolgreich gesendet' : 'Bayerische Ehrenamtskarte beantragen'}
{formSubmitted ? t('sentSuccessfully') : t('title')}
</Typography>
{formSubmitted ? (
<SuccessContent>
Expand Down
92 changes: 48 additions & 44 deletions administration/src/mui-modules/application/forms/AddressForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react'
import { useTranslation } from 'react-i18next'

import { AddressInput } from '../../../generated/graphql'
import { useUpdateStateCallback } from '../hooks/useUpdateStateCallback'
Expand Down Expand Up @@ -27,53 +28,56 @@ const AddressForm: Form<State, ValidatedInput> = {
initialState: { ...createCompoundInitialState(SubForms), country: { shortText: 'Deutschland' } },
getArrayBufferKeys: createCompoundGetArrayBufferKeys(SubForms),
validate: createCompoundValidate(SubForms, {}),
Component: ({ state, setState }: FormComponentProps<State>) => (
<>
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap' }}>
<div style={{ flex: '3' }}>
<SubForms.street.Component
state={state.street}
setState={useUpdateStateCallback(setState, 'street')}
label='Straße'
/>
</div>
<div style={{ flex: '1' }}>
<SubForms.houseNumber.Component
state={state.houseNumber}
setState={useUpdateStateCallback(setState, 'houseNumber')}
label='Hausnummer'
minWidth={100}
/>
Component: ({ state, setState }: FormComponentProps<State>) => {
const { t } = useTranslation('application')
return (
<>
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap' }}>
<div style={{ flex: '3' }}>
<SubForms.street.Component
state={state.street}
setState={useUpdateStateCallback(setState, 'street')}
label={t('street')}
/>
</div>
<div style={{ flex: '1' }}>
<SubForms.houseNumber.Component
state={state.houseNumber}
setState={useUpdateStateCallback(setState, 'houseNumber')}
label={t('houseNumber')}
minWidth={100}
/>
</div>
</div>
</div>
<SubForms.addressSupplement.Component
label='Adresszusatz'
state={state.addressSupplement}
setState={useUpdateStateCallback(setState, 'addressSupplement')}
/>
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap' }}>
<div style={{ flex: '1' }}>
<SubForms.postalCode.Component
state={state.postalCode}
setState={useUpdateStateCallback(setState, 'postalCode')}
label='Postleitzahl'
/>
</div>
<div style={{ flex: '3' }}>
<SubForms.location.Component
state={state.location}
setState={useUpdateStateCallback(setState, 'location')}
label='Ort'
<SubForms.addressSupplement.Component
label={t('addressSupplement')}
state={state.addressSupplement}
setState={useUpdateStateCallback(setState, 'addressSupplement')}
/>
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap' }}>
<div style={{ flex: '1' }}>
<SubForms.postalCode.Component
state={state.postalCode}
setState={useUpdateStateCallback(setState, 'postalCode')}
label={t('postalCode')}
/>
</div>
<div style={{ flex: '3' }}>
<SubForms.location.Component
state={state.location}
setState={useUpdateStateCallback(setState, 'location')}
label={t('location')}
/>
</div>
<SubForms.country.Component
state={state.country}
setState={useUpdateStateCallback(setState, 'country')}
label={t('country')}
/>
</div>
<SubForms.country.Component
state={state.country}
setState={useUpdateStateCallback(setState, 'country')}
label='Land'
/>
</div>
</>
),
</>
)
},
}

export default AddressForm
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react'

import { BlueCardEntitlementInput, BlueCardEntitlementType } from '../../../generated/graphql'
import i18next from '../../../i18n'
import SwitchComponent from '../SwitchComponent'
import { useUpdateStateCallback } from '../hooks/useUpdateStateCallback'
import { createRadioGroupForm } from '../primitive-inputs/RadioGroupForm'
Expand All @@ -20,15 +21,11 @@ import WorkAtOrganizationsEntitlementForm from './WorkAtOrganizationsEntitlement
const EntitlementTypeRadioGroupForm = createRadioGroupForm<BlueCardEntitlementType>()
const entitlementTypeOptions: { labelByValue: { [K in BlueCardEntitlementType]: string } } = {
labelByValue: {
[BlueCardEntitlementType.WorkAtOrganizations]:
'Ich engagiere mich ehrenamtlich seit mindestens zwei Jahren freiwillig mindestens fünf Stunden pro Woche oder bei Projektarbeiten mindestens 250 Stunden jährlich.',
[BlueCardEntitlementType.Juleica]: 'Ich bin Inhaber:in einer JuLeiCa (Jugendleiter:in-Card).',
[BlueCardEntitlementType.WorkAtDepartment]:
'Ich bin aktiv in der Freiwilligen Feuerwehr mit abgeschlossener Truppmannausbildung bzw. abgeschlossenem Basis-Modul der Modularen Truppausbildung (MTA), oder im Katastrophenschutz oder im Rettungsdienst mit abgeschlossener Grundausbildung.',
[BlueCardEntitlementType.MilitaryReserve]:
'Ich habe in den vergangenen zwei Kalenderjahren als Reservist regelmäßig aktiven Wehrdienst in der Bundeswehr geleistet, indem ich insgesamt mindestens 40 Tage Reservisten-Dienstleistung erbracht habe oder ständige:r Angehörige:r eines Bezirks- oder Kreisverbindungskommandos war.',
[BlueCardEntitlementType.VolunteerService]:
'Ich leiste einen Freiwilligendienst ab in einem Freiwilligen Sozialen Jahr (FSJ), einem Freiwilligen Ökologischen Jahr (FÖJ) oder einem Bundesfreiwilligendienst (BFD).',
[BlueCardEntitlementType.WorkAtOrganizations]: i18next.t('application:blueCardEntitlementType.WorkAtOrganizations'),
[BlueCardEntitlementType.Juleica]: i18next.t('application:blueCardEntitlementType.Juleica'),
[BlueCardEntitlementType.WorkAtDepartment]: i18next.t('application:blueCardEntitlementType.WorkAtDepartment'),
[BlueCardEntitlementType.MilitaryReserve]: i18next.t('application:blueCardEntitlementType.MilitaryReserve'),
[BlueCardEntitlementType.VolunteerService]: i18next.t('application:blueCardEntitlementType.VolunteerService'),
},
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react'

import { GoldenCardEntitlementInput, GoldenCardEntitlementType } from '../../../generated/graphql'
import i18next from '../../../i18n'
import SwitchComponent from '../SwitchComponent'
import { useUpdateStateCallback } from '../hooks/useUpdateStateCallback'
import { createRadioGroupForm } from '../primitive-inputs/RadioGroupForm'
Expand All @@ -18,14 +19,14 @@ import WorkAtOrganizationsEntitlementForm from './WorkAtOrganizationsEntitlement

const entitlementTypeOptions: { labelByValue: { [K in GoldenCardEntitlementType]: string } } = {
labelByValue: {
[GoldenCardEntitlementType.WorkAtOrganizations]:
'Ich bin seit seit mindestens 25 Jahren mindestens 5 Stunden pro Woche oder 250 Stunden pro Jahr bei einem Verein oder einer Organisation ehrenamtlich tätig.',
[GoldenCardEntitlementType.HonoredByMinisterPresident]:
'Ich bin Inhaber:in des Ehrenzeichens für Verdienstete im Ehrenamt des Bayerischen Ministerpräsidenten.',
[GoldenCardEntitlementType.WorkAtDepartment]:
'Ich bin Feuerwehrdienstleistende:r oder Einsatzkraft im Rettungsdienst oder in Einheiten des Katastrophenschutzes und habe eine Dienstzeitauszeichnung nach dem Feuerwehr- und Hilfsorganisationen-Ehrenzeichengesetz (FwHOEzG) erhalten.',
[GoldenCardEntitlementType.MilitaryReserve]:
'Ich leiste als Reservist:in seit mindestens 25 Jahren regelmäßig aktiven Wehrdienst in der Bundeswehr, indem ich in dieser Zeit entweder insgesamt mindestens 500 Tage Reservisten-Dienstleistung erbracht habe oder in dieser Zeit ständige:r Angehörige:r eines Bezirks- oder Kreisverbindungskommandos war.',
[GoldenCardEntitlementType.WorkAtOrganizations]: i18next.t(
'application:goldenCardEntitlementType.WorkAtOrganizations'
),
[GoldenCardEntitlementType.HonoredByMinisterPresident]: i18next.t(
'application:goldenCardEntitlementType.HonoredByMinisterPresident'
),
[GoldenCardEntitlementType.WorkAtDepartment]: i18next.t('application:goldenCardEntitlementType.WorkAtDepartment'),
[GoldenCardEntitlementType.MilitaryReserve]: i18next.t('application:goldenCardEntitlementType.MilitaryReserve'),
},
}

Expand Down
Loading

0 comments on commit 5275a30

Please sign in to comment.