diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index c5066b34f00d5603035839ff6d8bb88032c13174..37ddd870e6fe5c04af3623ece944136c53b530e6 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -6583,13 +6583,23 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001271",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz",
-      "integrity": "sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==",
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/browserslist"
-      }
+      "version": "1.0.30001512",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001512.tgz",
+      "integrity": "sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ]
     },
     "node_modules/capture-exit": {
       "version": "2.0.0",
@@ -28885,9 +28895,9 @@
       }
     },
     "caniuse-lite": {
-      "version": "1.0.30001271",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz",
-      "integrity": "sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA=="
+      "version": "1.0.30001512",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001512.tgz",
+      "integrity": "sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw=="
     },
     "capture-exit": {
       "version": "2.0.0",
diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json
index e89574e0620232d852cd2af5f41838240756ed7b..63be9f8e67b31261c6d341b4a5e80ce877198fef 100644
--- a/frontend/public/locales/en/common.json
+++ b/frontend/public/locales/en/common.json
@@ -125,7 +125,9 @@
   "nationalIdNumber": "Norwegian national ID number",
   "validation": {
     "firstNameRequired": "First name is required",
+    "firstNameContainsInvalidChars": "First name contains invalid characters",
     "lastNameRequired": "Last name is required",
+    "lastNameContainsInvalidChars": "Last name contains invalid characters",
     "dateOfBirthRequired": "Date of birth is required",
     "invalidIdNumber": "Invalid Norwegian national ID number",
     "nationalIdNumberRequired": "Norwegian national ID number required",
diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json
index 5e9091fe936b4bf6a4242f15f0ce21d28b35ab55..a9fa27f4502231e81c4e90efcc7514921aac29d0 100644
--- a/frontend/public/locales/nb/common.json
+++ b/frontend/public/locales/nb/common.json
@@ -125,7 +125,9 @@
   "nationalIdNumber": "Fødselsnummer/D-nummer",
   "validation": {
     "firstNameRequired": "Fornavn er obligatorisk",
+    "firstNameContainsInvalidChars": "Fornavn inneholder ugyldige karakterer",
     "lastNameRequired": "Etternavn er obligatorisk",
+    "lastNameContainsInvalidChars": "Etternavn inneholder ugyldige karakterer",
     "dateOfBirthRequired": "Fødselsdato er obligatorisk",
     "invalidIdNumber": "Ugyldig fødselsnummer/D-nummer",
     "nationalIdNumberRequired": "Fødselsnummer/D-nummer er obligatorisk",
diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json
index 6a93167351aa5b53a0d18736922ab66578740aaf..d8e96c9976b9828a15ba22e7471217a1e5c610f5 100644
--- a/frontend/public/locales/nn/common.json
+++ b/frontend/public/locales/nn/common.json
@@ -125,7 +125,9 @@
   "nationalIdNumber": "Fødselsnummer/D-nummer",
   "validation": {
     "firstNameRequired": "Fornamn er påkrevd",
+    "firstNameContainsInvalidChars": "Fornamn inneheld ugyldige karakterar",
     "lastNameRequired": "Etternamn er påkrevd",
+    "lastNameContainsInvalidChars": "Etternamn inneheld ugyldige karakterar",
     "dateOfBirthRequired": "Fødselsdato er påkrevd",
     "invalidIdNumber": "Ugyldig fødselsnummer/D-nummer",
     "nationalIdNumberRequired": "Fødselsnummer/D-nummer er påkrevd",
@@ -235,4 +237,4 @@
   "update": {
     "email": "E-postadressa ble endra"
   }
-}
+}
\ No newline at end of file
diff --git a/frontend/src/routes/guest/register/index.test.tsx b/frontend/src/routes/guest/register/index.test.tsx
index 4969ed4dddb82c1351300f2353f9d1b356072c4a..624a3841cfad396b3fb05a8832f7fdbc938ffd1a 100644
--- a/frontend/src/routes/guest/register/index.test.tsx
+++ b/frontend/src/routes/guest/register/index.test.tsx
@@ -4,17 +4,15 @@ import AdapterDateFns from '@mui/lab/AdapterDateFns'
 import { LocalizationProvider } from '@mui/lab'
 import { BrowserRouter } from 'react-router-dom'
 
-import { render, screen } from 'test-utils'
+import { render, screen, act, waitFor, fireEvent } from 'test-utils'
 import { FeatureContext } from 'contexts'
-import { fireEvent } from '@testing-library/react'
 import GuestRegister from './index'
-import { waitFor } from '../../../test-utils'
 
 enableFetchMocks()
 
 const testData = {
   person: {
-    first_name: 'Test20',
+    first_name: 'TestTwenty',
     last_name: 'Tester',
     private_mobile: '+4797543910',
     private_email: 'test@example.org',
@@ -176,10 +174,10 @@ test('Gender is remembered when going back', async () => {
 
   // Go forward and then back in wizard
   const nextButton = screen.getByTestId('button-next')
-  nextButton.click()
+  act(() => nextButton.click())
   await screen.findByTestId('button-back')
   const backButton = screen.getByTestId('button-back')
-  backButton.click()
+  act(() => backButton.click())
 
   // The selection should still be male
   await waitFor(() => screen.getByText('input.male'))
@@ -218,10 +216,10 @@ test('Gender not required when gender field is not shown', async () => {
   // No gender is set, but it should still be possible
   // to go forward and back in wizard
   let nextButton = screen.getByTestId('button-next')
-  nextButton.click()
+  act(() => nextButton.click())
   await screen.findByTestId('button-back')
   const backButton = screen.getByTestId('button-back')
-  backButton.click()
+  act(() => backButton.click())
   nextButton = screen.getByTestId('button-next')
-  nextButton.click()
+  act(() => nextButton.click())
 })
diff --git a/frontend/src/routes/guest/register/steps/register.test.tsx b/frontend/src/routes/guest/register/steps/register.test.tsx
index ad034c1186e352217b3804e7a13ebbc227f4384f..f6669fe5490015759ed08b264afa17d2c001aa34 100644
--- a/frontend/src/routes/guest/register/steps/register.test.tsx
+++ b/frontend/src/routes/guest/register/steps/register.test.tsx
@@ -194,7 +194,7 @@ test('Gender required to be set if gender field is showing', async () => {
 
   const formData: GuestRegisterData = {
     firstName: 'Test',
-    lastName: 'Test2',
+    lastName: 'TestTwo',
     mobilePhoneCountry: 'NO',
     mobilePhone: '97543980',
     nationalIdNumber: '',
@@ -249,7 +249,7 @@ test('Gender not required to be set if gender field is not showing', async () =>
 
   const formData: GuestRegisterData = {
     firstName: 'Test',
-    lastName: 'Test2',
+    lastName: 'TestTwo',
     mobilePhoneCountry: 'NO',
     mobilePhone: '97543980',
     nationalIdNumber: '',
@@ -297,7 +297,7 @@ test('Guest not allowed to proceed in wizard if phone number is not valid', asyn
 
   const formData: GuestRegisterData = {
     firstName: 'Test',
-    lastName: 'Test2',
+    lastName: 'TestTwo',
     mobilePhoneCountry: 'NO',
     mobilePhone: '50',
     nationalIdNumber: '',
@@ -341,7 +341,7 @@ test('Guest allowed to proceed in wizard if data is valid', async () => {
 
   const formData: GuestRegisterData = {
     firstName: 'Test',
-    lastName: 'Test2',
+    lastName: 'TestTwo',
     mobilePhoneCountry: 'NO',
     mobilePhone: '97543992',
     nationalIdNumber: '',
@@ -391,7 +391,7 @@ test('Default country code gets set in form values', async () => {
   // be populated with a default value
   const formData: GuestRegisterData = {
     firstName: 'Test',
-    lastName: 'Test2',
+    lastName: 'TestTwo',
     mobilePhoneCountry: '',
     mobilePhone: '97543992',
     nationalIdNumber: '',
@@ -441,7 +441,7 @@ test('Foreign country code gets set in form values', async () => {
 
   const formData: GuestRegisterData = {
     firstName: 'Test',
-    lastName: 'Test2',
+    lastName: 'TestTwo',
     mobilePhoneCountry: '',
     mobilePhone: '3892778472',
     nationalIdNumber: '',
diff --git a/frontend/src/routes/guest/register/steps/register.tsx b/frontend/src/routes/guest/register/steps/register.tsx
index 576b9b2aafd985649b5eeb5eccec99bfbac82950..86f52bd241962c59cc61df154f82ce69a3af1430 100644
--- a/frontend/src/routes/guest/register/steps/register.tsx
+++ b/frontend/src/routes/guest/register/steps/register.tsx
@@ -28,7 +28,9 @@ import { getAlpha2Codes, getName } from 'i18n-iso-countries'
 import { DatePicker } from '@mui/lab'
 import { subYears } from 'date-fns/fp'
 import {
+  isValidFirstName,
   isValidFnr,
+  isValidLastName,
   isValidMobilePhoneNumber,
   extractGenderOrBlank,
   extractBirthdateFromNationalId,
@@ -320,7 +322,7 @@ const GuestRegisterStep = forwardRef(
         (initialGuestData.authentication_method ===
           AuthenticationMethod.Feide ||
           initialGuestData.authentication_method ===
-            AuthenticationMethod.IdPorten) &&
+          AuthenticationMethod.IdPorten) &&
         initialGuestData.fnr !== null &&
         initialGuestData.fnr?.length !== 0,
     }
@@ -372,7 +374,7 @@ const GuestRegisterStep = forwardRef(
                 name="firstName"
                 control={control}
                 rules={{
-                  required: t('common:validation.firstNameRequired').toString(),
+                  validate: isValidFirstName,
                 }}
                 render={({ field: { onChange, value } }) => (
                   <TextField
@@ -394,7 +396,7 @@ const GuestRegisterStep = forwardRef(
                 name="lastName"
                 control={control}
                 rules={{
-                  required: t('common:validation.lastNameRequired').toString(),
+                  validate: isValidLastName,
                 }}
                 render={({ field: { onChange, value } }) => (
                   <TextField
diff --git a/frontend/src/routes/sponsor/register/index.test.tsx b/frontend/src/routes/sponsor/register/index.test.tsx
index 1e22d17f029e82b8bb46e274277dc05682bdfc11..420299133d7f51feda07fb4b81be651e7bc4b0c3 100644
--- a/frontend/src/routes/sponsor/register/index.test.tsx
+++ b/frontend/src/routes/sponsor/register/index.test.tsx
@@ -4,7 +4,7 @@ import AdapterDateFns from '@mui/lab/AdapterDateFns'
 import { LocalizationProvider } from '@mui/lab'
 import { BrowserRouter } from 'react-router-dom'
 
-import { render, waitFor, screen } from 'test-utils'
+import { render, waitFor, screen, act } from 'test-utils'
 import StepRegistration from './stepRegistration'
 
 jest.mock('hooks/useOus', () => () => ({
@@ -25,7 +25,7 @@ test('Validation message showing if last name is missing', async () => {
 
   // Try to go to the next step and check that the validation message is showing
   const submitButton = screen.getByTestId('button-next')
-  userEvent.click(submitButton)
+  act(() => userEvent.click(submitButton))
 
   const validationMessage = await waitFor(() =>
     screen.getByText('validation.lastNameRequired')
@@ -36,12 +36,12 @@ test('Validation message showing if last name is missing', async () => {
 
   const inputValue = 'Test input value'
   // Note that we need to use the test-ID of the input field inside the Material UI TextField-component
-  userEvent.type(screen.getByTestId('lastName-input-field'), inputValue)
+  act(() => userEvent.type(screen.getByTestId('lastName-input-field'), inputValue))
   expect(screen.getByDisplayValue(inputValue)).toBeInTheDocument()
 
   // Type in text, the message should disappear
   screen.queryByText('validation.lastNameRequired')
-  userEvent.click(submitButton)
+  act(() => userEvent.click(submitButton))
 
   await waitFor(
     () => {
diff --git a/frontend/src/routes/sponsor/register/stepPersonForm.tsx b/frontend/src/routes/sponsor/register/stepPersonForm.tsx
index e9c8f5a15160262a9647743ed9b7edac23fcd90c..c6075fe6de4daf5d4eeaed7482eb4511ad0c9a5f 100644
--- a/frontend/src/routes/sponsor/register/stepPersonForm.tsx
+++ b/frontend/src/routes/sponsor/register/stepPersonForm.tsx
@@ -22,7 +22,7 @@ import useOus, { enSort, nbSort, OuData } from 'hooks/useOus'
 import useRoleTypes, { RoleTypeData } from 'hooks/useRoleTypes'
 import Autocomplete from '@mui/material/Autocomplete'
 import Loading from 'components/loading'
-import { isValidEmail } from 'utils'
+import { isValidEmail, isValidFirstName, isValidLastName } from 'utils'
 import { FeatureContext } from 'contexts'
 import { RegisterFormData } from './formData'
 import { PersonFormMethods } from './personFormMethods'
@@ -138,7 +138,7 @@ const StepPersonForm = forwardRef(
 
       const roleEnd = getValues('role_end')
       if (roleEnd && startDate > roleEnd) {
-        // The role end date is set, but is is before the start date
+        // The role end date is set, but is before the start date
         return t('validation.startDateMustBeBeforeEndDate')
       }
       return true
@@ -147,20 +147,17 @@ const StepPersonForm = forwardRef(
     function getFullOptionLabel(ouData: OuData) {
       switch (i18n.language) {
         case 'en':
-          return `${ouData.en}${
-            ouData.identifier_1 ? ` (${ouData.identifier_1})` : ''
-          }${ouData.identifier_2 ? ` (${ouData.identifier_2})` : ''}`
+          return `${ouData.en}${ouData.identifier_1 ? ` (${ouData.identifier_1})` : ''
+            }${ouData.identifier_2 ? ` (${ouData.identifier_2})` : ''}`
 
         case 'nn':
-          return `${ouData.nb}${
-            ouData.identifier_1 ? ` (${ouData.identifier_1})` : ''
-          }${ouData.identifier_2 ? ` (${ouData.identifier_2})` : ''}`
+          return `${ouData.nb}${ouData.identifier_1 ? ` (${ouData.identifier_1})` : ''
+            }${ouData.identifier_2 ? ` (${ouData.identifier_2})` : ''}`
         default:
           // There should always be a Norwegian bokmaal acronym set
 
-          return `${ouData.nb}${
-            ouData.identifier_1 ? ` (${ouData.identifier_1})` : ''
-          }${ouData.identifier_2 ? ` (${ouData.identifier_2})` : ''}`
+          return `${ouData.nb}${ouData.identifier_1 ? ` (${ouData.identifier_1})` : ''
+            }${ouData.identifier_2 ? ` (${ouData.identifier_2})` : ''}`
       }
     }
 
@@ -198,7 +195,7 @@ const StepPersonForm = forwardRef(
                 error={!!errors.first_name}
                 helperText={errors.first_name && errors.first_name.message}
                 {...register(`first_name`, {
-                  required: t<string>('validation.firstNameRequired'),
+                  validate: isValidFirstName,
                 })}
               />
               <TextField
@@ -207,7 +204,7 @@ const StepPersonForm = forwardRef(
                 error={!!errors.last_name}
                 helperText={errors.last_name && errors.last_name.message}
                 {...register(`last_name`, {
-                  required: t<string>('validation.lastNameRequired'),
+                  validate: isValidLastName,
                 })}
                 inputProps={{ 'data-testid': 'lastName-input-field' }}
               />
diff --git a/frontend/src/test-utils.tsx b/frontend/src/test-utils.tsx
index fa8739024553136211812cbfeb356c9840f2fd8c..78d064435400956f779fa2d27be563be7edecc48 100644
--- a/frontend/src/test-utils.tsx
+++ b/frontend/src/test-utils.tsx
@@ -1,12 +1,32 @@
-import React from 'react'
-import { render } from '@testing-library/react'
 import { ThemeProvider } from '@mui/material/styles'
+import { render } from '@testing-library/react'
+import React from 'react'
+import { I18nextProvider, initReactI18next } from 'react-i18next'
+import i18n from 'i18next'
 
 import { defaultTheme } from 'themes'
 
+i18n.use(initReactI18next).init({
+  lng: 'en',
+  fallbackLng: 'en',
+
+  ns: ['translationsNS'],
+  defaultNS: 'translationsNS',
+
+  debug: false,
+
+  interpolation: {
+    escapeValue: false,
+  },
+
+  resources: { en: { translationsNS: {} } },
+})
+
 // Providers used in test rendering
 const AllTheProviders = ({ children }: any) => (
-  <ThemeProvider theme={defaultTheme}> {children} </ThemeProvider>
+  <I18nextProvider i18n={i18n}>
+    <ThemeProvider theme={defaultTheme}> {children} </ThemeProvider>
+  </I18nextProvider>
 )
 
 // Custom testing-library/react renderer using our providers.
@@ -18,72 +38,3 @@ export * from '@testing-library/react'
 
 // override render method
 export { customRender as render }
-
-// The reason for this complex mock of react-i18next is to make the Trans-component work when running tests
-jest.mock('react-i18next', (): object => {
-  // Need to require React at this level as well to avoid getting an error
-  // when running the tests saying that the module factory is not allowed
-  // to reference out-of-scope variables
-  // eslint-disable-next-line @typescript-eslint/no-shadow,global-require
-  const React = require('react')
-
-  const hasChildren = (node: any): boolean =>
-    node && (node.children || (node.props && node.props.children))
-
-  const getChildrenFromProps = (node: any): any =>
-    node.props && node.props.children ? node.props.children : null
-
-  const getChildren = (node: any): any =>
-    node && node.children ? node.children : getChildrenFromProps(node)
-
-  const renderNodes = (reactNodes: any): any => {
-    if (typeof reactNodes === 'string') {
-      return reactNodes
-    }
-
-    return Object.keys(reactNodes).map((key, i) => {
-      const child = reactNodes[key]
-
-      if (typeof child === 'string') {
-        return child
-      }
-
-      if (hasChildren(child)) {
-        const inner = renderNodes(getChildren(child))
-        // eslint-disable-next-line react/no-array-index-key
-        return React.cloneElement(child, { ...child.props, key: i }, inner)
-      }
-
-      const isElement = React.isValidElement(child)
-      if (typeof child === 'object' && !isElement) {
-        return Object.keys(child).reduce(
-          (str, childKey) => `${str}${child[childKey]}`,
-          ''
-        )
-      }
-
-      return child
-    })
-  }
-
-  const useMock: any = [(k: any) => k, {}]
-  // Mock react-i18next module to return a translation that just returns the key
-  useMock.t = (k: any) => k
-  useMock.i18n = {
-    // Return "en" as the selected language if the code asks for it
-    language: 'en',
-    changeLanguage: () => new Promise(() => {}),
-  }
-
-  return {
-    withTranslation:
-      () =>
-      (Component: any): any =>
-      (props: any): any =>
-        <Component t={(k: any): any => k} {...props} />,
-    Trans: ({ children }: any) => renderNodes(children),
-    Translation: ({ children }: any) =>
-      children((k: any): any => k, { i18n: {} }),
-    useTranslation: () => useMock,
-  }
-})
diff --git a/frontend/src/utils/index.test.ts b/frontend/src/utils/index.test.ts
index 5067541ad736df52b192d97638303fbbdb857e0c..bdc3108c327e6449021557ba70ed5c9155aec2c9 100644
--- a/frontend/src/utils/index.test.ts
+++ b/frontend/src/utils/index.test.ts
@@ -4,7 +4,9 @@ import {
   deleteCookie,
   setCookie,
   isValidEmail,
+  isValidFirstName,
   isValidFnr,
+  isValidLastName,
   isValidMobilePhoneNumber,
   maybeCsrfToken,
   submitJsonOpts,
@@ -54,6 +56,46 @@ test('Invalid e-mail', async () => {
   })
 })
 
+test('Valid first name', async () => {
+  expect(isValidFirstName('AZ az ÀÖ ØÞßö øÿ Āſ')).toEqual(true)
+})
+
+test('Invalid first name', async () => {
+  expect(isValidFirstName('')).toEqual('common:validation.firstNameRequired')
+  expect(isValidFirstName('aaƂåå')).toEqual(
+    'common:validation.firstNameContainsInvalidChars'
+  )
+  expect(isValidFirstName('!')).toEqual(
+    'common:validation.firstNameContainsInvalidChars'
+  )
+  expect(isValidFirstName('÷')).toEqual(
+    'common:validation.firstNameContainsInvalidChars'
+  )
+  expect(isValidFirstName('汉字')).toEqual(
+    'common:validation.firstNameContainsInvalidChars'
+  )
+})
+
+test('Valid last name', async () => {
+  expect(isValidLastName('AZ az ÀÖ ØÞßö øÿ Āſ')).toEqual(true)
+})
+
+test('Invalid last name', async () => {
+  expect(isValidLastName('')).toEqual('common:validation.lastNameRequired')
+  expect(isValidLastName('aaƂåå')).toEqual(
+    'common:validation.lastNameContainsInvalidChars'
+  )
+  expect(isValidLastName('!')).toEqual(
+    'common:validation.lastNameContainsInvalidChars'
+  )
+  expect(isValidLastName('÷')).toEqual(
+    'common:validation.lastNameContainsInvalidChars'
+  )
+  expect(isValidLastName('汉字')).toEqual(
+    'common:validation.lastNameContainsInvalidChars'
+  )
+})
+
 test('Body has values', async () => {
   const data = { foo: 'bar' }
   expect(submitJsonOpts('POST', data)).toEqual({
diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts
index 0dd69ab5f46aa54bea2dbbb177cf5942fed45e33..d85c7f3ff3b966fe094f9bb1c21e8a35856c1bd5 100644
--- a/frontend/src/utils/index.ts
+++ b/frontend/src/utils/index.ts
@@ -121,6 +121,46 @@ export async function isValidEmail(data: string | undefined) {
   return i18n.t<string>('common:validation.invalidEmail')
 }
 
+function stringContainsIllegalChars(string: string): boolean {
+  // Only allow the following characters:
+  // ----- Basic Latin -----
+  // U+0020 (Space)
+  // U+0041 - U+005A (Latin Alphabet: Uppercase)
+  // U+0061 - U+007A (Latin Alphabet: Lowercase)
+  // ----- Latin-1 Supplement -----
+  // U+00C0 - U+00D6 (Letters: Uppercase)
+  // U+00D8 - U+00DE (Letters: Uppercase)
+  // U+00DF - U+00F6 (Letters: Lowercase)
+  // U+00F8 - U+00FF (Letters: Lowercase)
+  // ----- Latin Extended-A -----
+  // U+0100 - U+017F (European Latin)
+
+  // eslint-disable-next-line no-control-regex
+  return /[^\u0020\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u017F]/g.test(
+    string
+  )
+}
+
+export function isValidFirstName(data: string | undefined): string | true {
+  if (!data) {
+    return i18n.t<string>('common:validation.firstNameRequired')
+  }
+  if (stringContainsIllegalChars(data)) {
+    return i18n.t<string>('common:validation.firstNameContainsInvalidChars')
+  }
+  return true
+}
+
+export function isValidLastName(data: string | undefined): string | true {
+  if (!data) {
+    return i18n.t<string>('common:validation.lastNameRequired')
+  }
+  if (stringContainsIllegalChars(data)) {
+    return i18n.t<string>('common:validation.lastNameContainsInvalidChars')
+  }
+  return true
+}
+
 /**
  * Splits a phone number into a country code and the national number.
  *
diff --git a/greg/tests/test_utils.py b/greg/tests/test_utils.py
index c77def931927a4c19f342045b15fe4b7eaf73025..75b5687e76f180d6eb1afae793f318de6b2e4a89 100644
--- a/greg/tests/test_utils.py
+++ b/greg/tests/test_utils.py
@@ -1,5 +1,11 @@
+import pytest
 from django.conf import settings
-from greg.utils import is_valid_id_number, is_valid_so_number
+
+from greg.utils import (
+    is_valid_id_number,
+    is_valid_so_number,
+    string_contains_illegal_chars,
+)
 
 
 def test_so_number():
@@ -21,3 +27,17 @@ def test_not_valid_so_number():
     settings.ALLOW_SO_NUMBERS = True
     assert not is_valid_id_number(so_number)
     assert not is_valid_so_number(so_number)
+
+
+@pytest.mark.parametrize(
+    "string, expected_output",
+    [
+        ("AZ az ÀÖ ØÞßö øÿ Āſ", False),
+        ("aaƂåå", True),
+        ("!", True),
+        ("÷", True),
+        ("汉字", True),
+    ],
+)
+def test_string_contains_illegal_chars(string, expected_output):
+    assert string_contains_illegal_chars(string) == expected_output
diff --git a/greg/utils.py b/greg/utils.py
index bc108f354af6f8aefe306b0022f2a46345e9b92a..b60d9b948709f913495c86f63fc321e921b0f38f 100644
--- a/greg/utils.py
+++ b/greg/utils.py
@@ -220,3 +220,24 @@ def role_invitation_date_validator(
         raise serializers.ValidationError(
             f"New end date too far into the future for this type. Must be before {max_date.strftime('%Y-%m-%d')}."
         )
+
+
+def string_contains_illegal_chars(string: str) -> bool:
+    # Only allow the following characters:
+    # ----- Basic Latin -----
+    # U+0020 (Space)
+    # U+0041 - U+005A (Latin Alphabet: Uppercase)
+    # U+0061 - U+007A (Latin Alphabet: Lowercase)
+    # ----- Latin-1 Supplement -----
+    # U+00C0 - U+00D6 (Letters: Uppercase)
+    # U+00D8 - U+00DE (Letters: Uppercase)
+    # U+00DF - U+00F6 (Letters: Lowercase)
+    # U+00F8 - U+00FF (Letters: Lowercase)
+    # ----- Latin Extended-A -----
+    # U+0100 - U+017F (European Latin)
+    return bool(
+        re.search(
+            r"[^\u0020\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u017F]",
+            string,
+        )
+    )
diff --git a/gregui/api/serializers/guest.py b/gregui/api/serializers/guest.py
index d18f56ff280dc95ced2430c1ef7c975acfb6d656..444c37bf28f2db5d9817f9d1e3cdec2b80844148 100644
--- a/gregui/api/serializers/guest.py
+++ b/gregui/api/serializers/guest.py
@@ -16,7 +16,7 @@ from greg.models import (
     Person,
     InvitationLink,
 )
-from greg.utils import is_identity_duplicate
+from greg.utils import is_identity_duplicate, string_contains_illegal_chars
 from gregui.api.serializers.identity import (
     PartialIdentitySerializer,
     IdentityDuplicateError,
@@ -144,6 +144,16 @@ class GuestRegisterSerializer(serializers.ModelSerializer):
                 consent_instance.choice = choice
                 consent_instance.save()
 
+    def validate_first_name(self, first_name):
+        if string_contains_illegal_chars(first_name):
+            raise serializers.ValidationError("First name contains illegal characters")
+        return first_name
+
+    def validate_last_name(self, last_name):
+        if string_contains_illegal_chars(last_name):
+            raise serializers.ValidationError("Last name contains illegal characters")
+        return last_name
+
     def validate_date_of_birth(self, date_of_birth):
         today = datetime.date.today()
 
diff --git a/gregui/api/serializers/invitation.py b/gregui/api/serializers/invitation.py
index add333aa7655ec35efd159134c7f9e32ab466584..7feddf953af023c830781363fbffe2e1ed2339e2 100644
--- a/gregui/api/serializers/invitation.py
+++ b/gregui/api/serializers/invitation.py
@@ -1,13 +1,19 @@
 from rest_framework import serializers
 
 from greg.models import Person, Identity
-from greg.utils import create_objects_for_invitation, is_identity_duplicate
+from greg.utils import (
+    create_objects_for_invitation,
+    is_identity_duplicate,
+    string_contains_illegal_chars,
+)
 from gregui.api.serializers.identity import IdentityDuplicateError
 from gregui.api.serializers.role import InviteRoleSerializerUi
 from gregui.models import GregUserProfile
 
 
 class InviteGuestSerializer(serializers.ModelSerializer):
+    first_name = serializers.CharField(required=True)
+    last_name = serializers.CharField(required=True)
     email = serializers.EmailField(required=True)
     role = InviteRoleSerializerUi(required=True)
     uuid = serializers.UUIDField(read_only=True)
@@ -29,6 +35,16 @@ class InviteGuestSerializer(serializers.ModelSerializer):
 
         return person
 
+    def validate_first_name(self, first_name):
+        if string_contains_illegal_chars(first_name):
+            raise serializers.ValidationError("First name contains illegal characters")
+        return first_name
+
+    def validate_last_name(self, last_name):
+        if string_contains_illegal_chars(last_name):
+            raise serializers.ValidationError("Last name contains illegal characters")
+        return last_name
+
     def validate_email(self, email):
         # The e-mail in the invite is the private e-mail
         if is_identity_duplicate(Identity.IdentityType.PRIVATE_EMAIL, email):
diff --git a/gregui/tests/api/views/test_invitation.py b/gregui/tests/api/views/test_invitation.py
index 54370a365113cf68e94e63b9c2e1f8bafe1a9ba0..8c8cb8669f14c9b42cfc8c0706d6c36e664e88b0 100644
--- a/gregui/tests/api/views/test_invitation.py
+++ b/gregui/tests/api/views/test_invitation.py
@@ -747,7 +747,7 @@ def test_session_type_id_porten(
     data = {
         "person": {
             "first_name": "Updated",
-            "last_name": "Updated2",
+            "last_name": "UpdatedTwo",
             "private_mobile": "+4797543992",
         }
     }
@@ -763,7 +763,7 @@ def test_session_type_id_porten(
     person.refresh_from_db()
 
     assert person.first_name == "Updated"
-    assert person.last_name == "Updated2"
+    assert person.last_name == "UpdatedTwo"
 
 
 @pytest.mark.django_db
diff --git a/gregui/tests/api/views/test_invite_guest.py b/gregui/tests/api/views/test_invite_guest.py
index 51e9f5450703a51cc175c5e8b8f9ef6558f76cba..06d28720c026357560c3dee858fbae585514f891 100644
--- a/gregui/tests/api/views/test_invite_guest.py
+++ b/gregui/tests/api/views/test_invite_guest.py
@@ -21,8 +21,8 @@ def test_invite_guest(client, user_sponsor, unit_foo, role_type_foo, mocker):
     role_end_date = datetime.datetime.today() + datetime.timedelta(days=10)
 
     data = {
-        "first_name": "foo木👍أ",
-        "last_name": "غbar水",
+        "first_name": "ſfooþ",
+        "last_name": "barÅ®",
         "email": "test@example.com",
         "role": {
             "start_date": (role_start_date).strftime("%Y-%m-%d"),
@@ -47,8 +47,8 @@ def test_invite_guest(client, user_sponsor, unit_foo, role_type_foo, mocker):
 
     assert Person.objects.count() == 1
     person = Person.objects.first()
-    assert person.first_name == "foo木👍أ"
-    assert person.last_name == "غbar水"
+    assert person.first_name == "ſfooþ"
+    assert person.last_name == "barÅ®"
 
     assert Identity.objects.filter(
         person=person,
@@ -66,6 +66,39 @@ def test_invite_guest(client, user_sponsor, unit_foo, role_type_foo, mocker):
     send_invite_mock_function.assert_called()
 
 
+@pytest.mark.django_db
+def test_invite_guest__illegal_name(client, user_sponsor, unit_foo, role_type_foo):
+    test_comment = "This is a test comment"
+    contact_person_unit = "This is a test contact person"
+    role_start_date = datetime.datetime.today() + datetime.timedelta(days=1)
+    role_end_date = datetime.datetime.today() + datetime.timedelta(days=10)
+
+    data = {
+        "first_name": "foo木👍أ",
+        "last_name": "غbar水",
+        "email": "test@example.com",
+        "role": {
+            "start_date": (role_start_date).strftime("%Y-%m-%d"),
+            "end_date": (role_end_date).strftime("%Y-%m-%d"),
+            "orgunit": unit_foo.id,
+            "type": role_type_foo.id,
+            "comments": test_comment,
+            "contact_person_unit": contact_person_unit,
+        },
+    }
+
+    assert len(Person.objects.all()) == 0
+
+    request = APIRequestFactory().post(
+        path=reverse("gregui-v1:invitation"), data=data, format="json"
+    )
+    force_authenticate(request, user=user_sponsor)
+
+    response = InvitationView.as_view()(request)
+
+    assert response.status_code == status.HTTP_400_BAD_REQUEST
+
+
 @pytest.mark.django_db
 def test_invite_cancel(client, invitation_link, invitation, role, log_in, user_sponsor):
     # TODO: Should all sponsors be allowed to delete arbitrary invitations?