From a54a24e1022491d5676c773020ee92f6e7a4016f Mon Sep 17 00:00:00 2001 From: florian Date: Sun, 4 Jan 2026 03:46:33 +0100 Subject: [PATCH 1/5] move table names to yml files --- src/lib/airtable.ts | 11 ++++------- src/routes/signup-pupil/+page.server.ts | 16 ++++++++++++---- src/routes/signup-pupil/+page.svelte | 4 +++- src/routes/signup-student/+page.server.ts | 16 ++++++++++++---- src/routes/signup-student/+page.svelte | 4 +++- src/signup-form/at/pupil.yml | 3 +++ src/signup-form/at/student.yml | 3 +++ src/signup-form/de/pupil.yml | 3 +++ src/signup-form/de/student.yml | 3 +++ 9 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/lib/airtable.ts b/src/lib/airtable.ts index d099c5fa..ffc35653 100644 --- a/src/lib/airtable.ts +++ b/src/lib/airtable.ts @@ -42,10 +42,6 @@ const GLOBAL_BASE_ID = import.meta.env.VITE_AIRTABLE_GLOBAL_BASE_ID || `appSswal9DNdJKRB8` const ERROR_LOG_TABLE = `Anmeldefehler` -// Airtable table names for signups -const STUDENTS_TABLE = `Nachhilfelehrkräfte` -const PUPILS_TABLE = `Lernende` - // Log signup errors to Airtable for monitoring async function log_error_to_airtable( error: Error, @@ -81,12 +77,12 @@ async function log_error_to_airtable( } } -// Prepares the form data for Airtable submission export async function prepare_signup_data_for_airtable( data: SignupStore, chapter_base_id: string, + tableName: string, ): Promise<{ status: number; data: unknown }> { - const table = data.type.value === `student` ? STUDENTS_TABLE : PUPILS_TABLE + const table = tableName // Common fields for both students and pupils let fields = { @@ -184,6 +180,7 @@ export async function signup_form_submit_handler( fields_to_validate: (keyof SignupStore)[], chapters: Chapter[], err_msg: Record, + tableName: string, ): Promise<{ error?: Error; success?: boolean }> { const signup_data = get(signup_store) @@ -217,7 +214,7 @@ export async function signup_form_submit_handler( } try { - const response = await prepare_signup_data_for_airtable(signup_data, baseId) + const response = await prepare_signup_data_for_airtable(signup_data, baseId, tableName) if (response.status < 200 || response.status >= 300) { // Include Airtable's error response for debugging diff --git a/src/routes/signup-pupil/+page.server.ts b/src/routes/signup-pupil/+page.server.ts index b8698ec5..b82af5c0 100644 --- a/src/routes/signup-pupil/+page.server.ts +++ b/src/routes/signup-pupil/+page.server.ts @@ -51,7 +51,9 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { const optionsData = (getLocaleModule(localeModules.options, locale) || {}) as Record - const rawFormData = getLocaleModule(localeModules.pupil, locale) || { + + const rawFormData = (getLocaleModule(localeModules.pupil, locale) || { + airtableTable: 'Lernende', header: { title: `Anmeldung Schüler:innen`, note: `Formular für Schüler:innen`, @@ -70,7 +72,10 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { title: `Anmeldung abschicken`, note: `Du bekommst innerhalb einer Minute eine Bestätigungs-Email von uns.`, }, - } + }) as { airtableTable?: string;[key: string]: unknown } + + // Extract airtableTable from form data (defined per form type per locale) + const airtableTable = rawFormData.airtableTable || 'Lernende' console.debug(`YAML data loaded for locale ${locale}:`, { messages: !!messagesData, @@ -111,8 +116,9 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { } for (const field of form.fields || []) { - if (field.id in optionsData) { - field.options = optionsData[field.id] + const optionValue = optionsData[field.id] + if (field.id in optionsData && Array.isArray(optionValue)) { + field.options = optionValue } else if (field.id === `chapter`) { field.options = chapters.map((chap) => chap.title) } @@ -124,6 +130,7 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { return { chapters: JSON.parse(JSON.stringify(chapters)), form: JSON.parse(JSON.stringify(form)), + airtableTable, } } catch (error) { console.error(`Error loading pupil signup form:`, error) @@ -154,6 +161,7 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { return { chapters: [], form: JSON.parse(JSON.stringify(basicForm)), + airtableTable: 'Lernende', } } } diff --git a/src/routes/signup-pupil/+page.svelte b/src/routes/signup-pupil/+page.svelte index dd84b061..9846dba4 100644 --- a/src/routes/signup-pupil/+page.svelte +++ b/src/routes/signup-pupil/+page.svelte @@ -8,6 +8,7 @@ const { data } = $props() const chapters = $derived(data.chapters) const form = $derived(data.form) + const airtableTable = $derived(data.airtableTable as string) // Add debugging and fallback $effect(() => { @@ -39,7 +40,8 @@ const response = await signup_form_submit_handler( field_ids_to_validate, chapters, - form.errMsg || {} + form.errMsg || {}, + airtableTable ) if (response.success) success = true error = response.error diff --git a/src/routes/signup-student/+page.server.ts b/src/routes/signup-student/+page.server.ts index 93670c1c..3c1ef754 100644 --- a/src/routes/signup-student/+page.server.ts +++ b/src/routes/signup-student/+page.server.ts @@ -51,7 +51,9 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { const optionsData = (getLocaleModule(localeModules.options, locale) || {}) as Record - const rawFormData = getLocaleModule(localeModules.student, locale) || { + + const rawFormData = (getLocaleModule(localeModules.student, locale) || { + airtableTable: 'Nachhilfelehrkräfte', header: { title: `Anmeldung Studierende`, note: `Formular für Studierende`, @@ -70,7 +72,10 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { title: `Anmeldung abschicken`, note: `Du bekommst innerhalb einer Minute eine Bestätigungs-Email von uns.`, }, - } + }) as { airtableTable?: string;[key: string]: unknown } + + // Extract airtableTable from form data (defined per form type per locale) + const airtableTable = rawFormData.airtableTable || 'Nachhilfelehrkräfte' console.debug(`YAML data loaded for locale ${locale}:`, { messages: !!messagesData, @@ -111,8 +116,9 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { } for (const field of form.fields || []) { - if (field.id in optionsData) { - field.options = optionsData[field.id] + const optionValue = optionsData[field.id] + if (field.id in optionsData && Array.isArray(optionValue)) { + field.options = optionValue } else if (field.id === `chapter`) { field.options = chapters.map((chap) => chap.title) } @@ -124,6 +130,7 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { return { chapters: JSON.parse(JSON.stringify(chapters)), form: JSON.parse(JSON.stringify(form)), + airtableTable, } } catch (error) { console.error(`Error loading student signup form:`, error) @@ -151,6 +158,7 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { return { chapters: [], form: JSON.parse(JSON.stringify(basicForm)), + airtableTable: 'Nachhilfelehrkräfte', } } } diff --git a/src/routes/signup-student/+page.svelte b/src/routes/signup-student/+page.svelte index 82f82767..93e06309 100644 --- a/src/routes/signup-student/+page.svelte +++ b/src/routes/signup-student/+page.svelte @@ -8,6 +8,7 @@ const { data } = $props() const chapters = $derived(data.chapters) const form = $derived(data.form) + const airtableTable = $derived(data.airtableTable as string) // Add debugging and fallback $effect(() => { @@ -40,7 +41,8 @@ const response = await signup_form_submit_handler( field_ids_to_validate, chapters, - form.errMsg || {} + form.errMsg || {}, + airtableTable ) if (response.success) success = true error = response.error diff --git a/src/signup-form/at/pupil.yml b/src/signup-form/at/pupil.yml index dffb0e54..04f40ba0 100644 --- a/src/signup-form/at/pupil.yml +++ b/src/signup-form/at/pupil.yml @@ -1,3 +1,6 @@ +# Airtable table name for this form type +airtableTable: Lernende + header: title: Anmeldung Schüler:innen note: | diff --git a/src/signup-form/at/student.yml b/src/signup-form/at/student.yml index 8db7ee4c..db1e161c 100644 --- a/src/signup-form/at/student.yml +++ b/src/signup-form/at/student.yml @@ -1,3 +1,6 @@ +# Airtable table name for this form type +airtableTable: Nachhilfelehrkräfte + header: title: Anmeldung Studierende note: | diff --git a/src/signup-form/de/pupil.yml b/src/signup-form/de/pupil.yml index dffb0e54..04f40ba0 100644 --- a/src/signup-form/de/pupil.yml +++ b/src/signup-form/de/pupil.yml @@ -1,3 +1,6 @@ +# Airtable table name for this form type +airtableTable: Lernende + header: title: Anmeldung Schüler:innen note: | diff --git a/src/signup-form/de/student.yml b/src/signup-form/de/student.yml index 8db7ee4c..db1e161c 100644 --- a/src/signup-form/de/student.yml +++ b/src/signup-form/de/student.yml @@ -1,3 +1,6 @@ +# Airtable table name for this form type +airtableTable: Nachhilfelehrkräfte + header: title: Anmeldung Studierende note: | From 7bcbdf29323fc7981e7eb1c2177d09b03b12cf5a Mon Sep 17 00:00:00 2001 From: florian Date: Sun, 4 Jan 2026 03:46:33 +0100 Subject: [PATCH 2/5] remove non required default values --- src/routes/signup-pupil/+page.server.ts | 72 ++++------------------- src/routes/signup-student/+page.server.ts | 69 ++++------------------ 2 files changed, 26 insertions(+), 115 deletions(-) diff --git a/src/routes/signup-pupil/+page.server.ts b/src/routes/signup-pupil/+page.server.ts index b82af5c0..6312e288 100644 --- a/src/routes/signup-pupil/+page.server.ts +++ b/src/routes/signup-pupil/+page.server.ts @@ -43,39 +43,21 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { console.debug(`Loading pupil signup form for locale: ${locale}`) // Load locale-specific YAML data - const messagesData = getLocaleModule(localeModules.messages, locale) || { - submitSuccess: { title: `🎉 ⭐ 🎉`, note: `Success!` }, - submitError: { title: `😢`, note: `Error occurred.` }, - errMsg: { required: `This field is required` }, + const messagesData = getLocaleModule(localeModules.messages, locale) as { + submitSuccess: { title: string; note: string } + submitError: { title: string; note: string } + errMsg: { required: string } } - const optionsData = (getLocaleModule(localeModules.options, locale) || - {}) as Record - - const rawFormData = (getLocaleModule(localeModules.pupil, locale) || { - airtableTable: 'Lernende', - header: { - title: `Anmeldung Schüler:innen`, - note: `Formular für Schüler:innen`, - }, - fields: [ - { - id: `chapter`, - title: `Standort`, - note: `Wähle einen unserer Nachhilfestandorte.`, - required: true, - type: `select`, - maxSelect: 1, - }, - ], - submit: { - title: `Anmeldung abschicken`, - note: `Du bekommst innerhalb einer Minute eine Bestätigungs-Email von uns.`, - }, - }) as { airtableTable?: string;[key: string]: unknown } + const optionsData = getLocaleModule(localeModules.options, locale) as Record + + const rawFormData = getLocaleModule(localeModules.pupil, locale) as { + airtableTable: string + [key: string]: unknown + } // Extract airtableTable from form data (defined per form type per locale) - const airtableTable = rawFormData.airtableTable || 'Lernende' + const airtableTable = rawFormData.airtableTable console.debug(`YAML data loaded for locale ${locale}:`, { messages: !!messagesData, @@ -96,7 +78,7 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { const form = parse_form_data({ ...rawFormData, ...messagesData, - } as Parameters[0]) + } as unknown as Parameters[0]) console.debug(`form parsed:`, form) // In dev mode, add a test chapter at the beginning for testing purposes if defined @@ -134,34 +116,6 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { } } catch (error) { console.error(`Error loading pupil signup form:`, error) - console.error( - `Error stack:`, - error instanceof Error ? error.stack : `Unknown error`, - ) - - // Return fallback form structure - const basicForm = { - header: { title: `Anmeldung Schüler:innen`, note: `Form loading...` }, - fields: [ - { - id: `chapter`, - title: `Standort`, - required: true, - type: `select`, - maxSelect: 1, - }, - ], - submit: { title: `Anmeldung abschicken`, note: `` }, - submitSuccess: { title: `Success`, note: `Success!` }, - submitError: { title: `Error`, note: `Error occurred` }, - errMsg: { required: `This field is required` }, - } - - console.debug(`Returning fallback form:`, basicForm) - return { - chapters: [], - form: JSON.parse(JSON.stringify(basicForm)), - airtableTable: 'Lernende', - } + throw error } } diff --git a/src/routes/signup-student/+page.server.ts b/src/routes/signup-student/+page.server.ts index 3c1ef754..14167a2d 100644 --- a/src/routes/signup-student/+page.server.ts +++ b/src/routes/signup-student/+page.server.ts @@ -43,39 +43,21 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { console.debug(`Loading student signup form for locale: ${locale}`) // Load locale-specific YAML data - const messagesData = getLocaleModule(localeModules.messages, locale) || { - submitSuccess: { title: `🎉 ⭐ 🎉`, note: `Success!` }, - submitError: { title: `😢`, note: `Error occurred.` }, - errMsg: { required: `This field is required` }, + const messagesData = getLocaleModule(localeModules.messages, locale) as { + submitSuccess: { title: string; note: string } + submitError: { title: string; note: string } + errMsg: { required: string } } - const optionsData = (getLocaleModule(localeModules.options, locale) || - {}) as Record - - const rawFormData = (getLocaleModule(localeModules.student, locale) || { - airtableTable: 'Nachhilfelehrkräfte', - header: { - title: `Anmeldung Studierende`, - note: `Formular für Studierende`, - }, - fields: [ - { - id: `chapter`, - title: `Standort`, - note: `An welchem Standort möchtest du Nachhilfe geben?`, - required: true, - type: `select`, - maxSelect: 1, - }, - ], - submit: { - title: `Anmeldung abschicken`, - note: `Du bekommst innerhalb einer Minute eine Bestätigungs-Email von uns.`, - }, - }) as { airtableTable?: string;[key: string]: unknown } + const optionsData = getLocaleModule(localeModules.options, locale) as Record + + const rawFormData = getLocaleModule(localeModules.student, locale) as { + airtableTable: string + [key: string]: unknown + } // Extract airtableTable from form data (defined per form type per locale) - const airtableTable = rawFormData.airtableTable || 'Nachhilfelehrkräfte' + const airtableTable = rawFormData.airtableTable console.debug(`YAML data loaded for locale ${locale}:`, { messages: !!messagesData, @@ -96,7 +78,7 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { const form = parse_form_data({ ...rawFormData, ...messagesData, - } as Parameters[0]) + } as unknown as Parameters[0]) console.debug(`form parsed:`, form) // In dev mode, add a test chapter at the beginning for testing purposes if defined @@ -134,31 +116,6 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { } } catch (error) { console.error(`Error loading student signup form:`, error) - console.error(`Error stack:`, (error as Error).stack) - - // Return fallback form structure - const basicForm = { - header: { title: `Anmeldung Studierende`, note: `Form loading...` }, - fields: [ - { - id: `chapter`, - title: `Standort`, - required: true, - type: `select`, - maxSelect: 1, - }, - ], - submit: { title: `Anmeldung abschicken`, note: `` }, - submitSuccess: { title: `Success`, note: `Success!` }, - submitError: { title: `Error`, note: `Error occurred` }, - errMsg: { required: `This field is required` }, - } - - console.debug(`Returning fallback form:`, basicForm) - return { - chapters: [], - form: JSON.parse(JSON.stringify(basicForm)), - airtableTable: 'Nachhilfelehrkräfte', - } + throw error } } From 291b5215ebafe9ea796b3c747736f6b89c01556b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 02:49:47 +0000 Subject: [PATCH 3/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/lib/airtable.ts | 6 +++++- src/routes/signup-pupil/+page.server.ts | 5 ++++- src/routes/signup-student/+page.server.ts | 5 ++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/lib/airtable.ts b/src/lib/airtable.ts index ffc35653..b1ef1547 100644 --- a/src/lib/airtable.ts +++ b/src/lib/airtable.ts @@ -214,7 +214,11 @@ export async function signup_form_submit_handler( } try { - const response = await prepare_signup_data_for_airtable(signup_data, baseId, tableName) + const response = await prepare_signup_data_for_airtable( + signup_data, + baseId, + tableName, + ) if (response.status < 200 || response.status >= 300) { // Include Airtable's error response for debugging diff --git a/src/routes/signup-pupil/+page.server.ts b/src/routes/signup-pupil/+page.server.ts index 6312e288..55ea2dbe 100644 --- a/src/routes/signup-pupil/+page.server.ts +++ b/src/routes/signup-pupil/+page.server.ts @@ -49,7 +49,10 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { errMsg: { required: string } } - const optionsData = getLocaleModule(localeModules.options, locale) as Record + const optionsData = getLocaleModule( + localeModules.options, + locale, + ) as Record const rawFormData = getLocaleModule(localeModules.pupil, locale) as { airtableTable: string diff --git a/src/routes/signup-student/+page.server.ts b/src/routes/signup-student/+page.server.ts index 14167a2d..e98e68da 100644 --- a/src/routes/signup-student/+page.server.ts +++ b/src/routes/signup-student/+page.server.ts @@ -49,7 +49,10 @@ export const load = async ({ fetch: customFetch }: { fetch: typeof fetch }) => { errMsg: { required: string } } - const optionsData = getLocaleModule(localeModules.options, locale) as Record + const optionsData = getLocaleModule( + localeModules.options, + locale, + ) as Record const rawFormData = getLocaleModule(localeModules.student, locale) as { airtableTable: string From ed61f8c577d9b0f9ac1245eb90843e79919480e5 Mon Sep 17 00:00:00 2001 From: florian Date: Sun, 4 Jan 2026 23:29:09 +0100 Subject: [PATCH 4/5] test if all required field are available --- tests/locale-validation.test.ts | 259 ++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 tests/locale-validation.test.ts diff --git a/tests/locale-validation.test.ts b/tests/locale-validation.test.ts new file mode 100644 index 00000000..8e4e95e1 --- /dev/null +++ b/tests/locale-validation.test.ts @@ -0,0 +1,259 @@ +import { test, expect } from '@playwright/test' +import { readFileSync, readdirSync } from 'fs' +import { join } from 'path' +import { parse } from 'yaml' + +/** + * Signup Form Locale Validation Tests + * + * Validates that: + * 1. All required YML files exist for each locale (at, de) + * 2. Form files have required structure (header, fields, airtableTable) + * 3. Fields with type:select have matching options in options.yml + * 4. Messages files have required keys + * 5. AT and DE locales have the same field IDs (cross-locale parity) + * 6. Code references to form.* have corresponding YML definitions + */ + +const LOCALES = [`at`, `de`] +const FORM_TYPES = [`student`, `pupil`] +const REQUIRED_FILES = [`messages.yml`, `options.yml`, `student.yml`, `pupil.yml`] +const SIGNUP_FORM_PATH = `src/signup-form` + +// Keys referenced by code in +page.svelte files +const REQUIRED_MESSAGE_KEYS = [ + `submitSuccess`, + `submitError`, + `submit`, + `errMsg`, +] + +const REQUIRED_FORM_KEYS = [`header`, `fields`, `airtableTable`] +const REQUIRED_FIELD_PROPS = [`id`, `title`] + +function loadYaml(filePath: string): Record { + try { + const content = readFileSync(filePath, `utf-8`) + return parse(content) as Record + } catch { + return {} + } +} + +test.describe(`Signup Form Locale Validation`, () => { + // Skip for Firefox/Safari - these are static file tests, no browser needed + test.skip(({ browserName }) => browserName !== `chromium`, `Static file validation only needs to run once`) + + test(`all required YML files exist for each locale`, () => { + for (const locale of LOCALES) { + const localePath = join(process.cwd(), SIGNUP_FORM_PATH, locale) + const files = readdirSync(localePath) + + for (const requiredFile of REQUIRED_FILES) { + expect( + files.includes(requiredFile), + `Missing ${requiredFile} in ${locale} locale`, + ).toBeTruthy() + } + } + }) + + test(`form files have required structure`, () => { + for (const locale of LOCALES) { + for (const formType of FORM_TYPES) { + const formPath = join( + process.cwd(), + SIGNUP_FORM_PATH, + locale, + `${formType}.yml`, + ) + const form = loadYaml(formPath) + + for (const key of REQUIRED_FORM_KEYS) { + expect( + form[key], + `Missing "${key}" in ${locale}/${formType}.yml`, + ).toBeDefined() + } + + // Check header structure + const header = form.header as Record + expect( + header?.title, + `Missing header.title in ${locale}/${formType}.yml`, + ).toBeDefined() + expect( + header?.note, + `Missing header.note in ${locale}/${formType}.yml`, + ).toBeDefined() + + // Check each field has required properties + const fields = form.fields as Array> + expect(Array.isArray(fields), `fields should be an array`).toBeTruthy() + + for (const field of fields) { + for (const prop of REQUIRED_FIELD_PROPS) { + expect( + field[prop], + `Field missing "${prop}" in ${locale}/${formType}.yml`, + ).toBeDefined() + } + } + } + } + }) + + test(`select fields have matching options`, () => { + for (const locale of LOCALES) { + const optionsPath = join( + process.cwd(), + SIGNUP_FORM_PATH, + locale, + `options.yml`, + ) + const options = loadYaml(optionsPath) + + for (const formType of FORM_TYPES) { + const formPath = join( + process.cwd(), + SIGNUP_FORM_PATH, + locale, + `${formType}.yml`, + ) + const form = loadYaml(formPath) + const fields = form.fields as Array> + + for (const field of fields) { + if (field.type === `select` && field.id !== `chapter`) { + // chapter is populated from chapters list, not options.yml + const fieldId = field.id as string + expect( + options[fieldId], + `Missing options for "${fieldId}" in ${locale}/options.yml`, + ).toBeDefined() + expect( + Array.isArray(options[fieldId]), + `Options for "${fieldId}" should be an array in ${locale}/options.yml`, + ).toBeTruthy() + expect( + (options[fieldId] as unknown[]).length, + `Options for "${fieldId}" should not be empty in ${locale}/options.yml`, + ).toBeGreaterThan(0) + } + } + } + } + }) + + test(`messages files have required keys`, () => { + for (const locale of LOCALES) { + const messagesPath = join( + process.cwd(), + SIGNUP_FORM_PATH, + locale, + `messages.yml`, + ) + const messages = loadYaml(messagesPath) + + for (const key of REQUIRED_MESSAGE_KEYS) { + expect( + messages[key], + `Missing "${key}" in ${locale}/messages.yml`, + ).toBeDefined() + } + + // Check nested structure + const submitSuccess = messages.submitSuccess as Record + expect( + submitSuccess?.title, + `Missing submitSuccess.title in ${locale}/messages.yml`, + ).toBeDefined() + expect( + submitSuccess?.note, + `Missing submitSuccess.note in ${locale}/messages.yml`, + ).toBeDefined() + + const submitError = messages.submitError as Record + expect( + submitError?.title, + `Missing submitError.title in ${locale}/messages.yml`, + ).toBeDefined() + expect( + submitError?.note, + `Missing submitError.note in ${locale}/messages.yml`, + ).toBeDefined() + + const submit = messages.submit as Record + expect( + submit?.title, + `Missing submit.title in ${locale}/messages.yml`, + ).toBeDefined() + expect( + submit?.note, + `Missing submit.note in ${locale}/messages.yml`, + ).toBeDefined() + + const errMsg = messages.errMsg as Record + expect( + errMsg?.required, + `Missing errMsg.required in ${locale}/messages.yml`, + ).toBeDefined() + } + }) + + test(`AT and DE locales have same field IDs (cross-locale parity)`, () => { + for (const formType of FORM_TYPES) { + const formPaths = LOCALES.map((locale) => + join(process.cwd(), SIGNUP_FORM_PATH, locale, `${formType}.yml`), + ) + + const forms = formPaths.map(loadYaml) + const fieldIdSets = forms.map((form) => { + const fields = form.fields as Array> + return new Set(fields.map((f) => f.id as string)) + }) + + const [atFields, deFields] = fieldIdSets + + // Check AT has all DE fields + for (const fieldId of deFields) { + expect( + atFields.has(fieldId), + `Field "${fieldId}" exists in de/${formType}.yml but missing in at/${formType}.yml`, + ).toBeTruthy() + } + + // Check DE has all AT fields + for (const fieldId of atFields) { + expect( + deFields.has(fieldId), + `Field "${fieldId}" exists in at/${formType}.yml but missing in de/${formType}.yml`, + ).toBeTruthy() + } + } + }) + + test(`AT and DE locales have same option keys`, () => { + const optionsPaths = LOCALES.map((locale) => + join(process.cwd(), SIGNUP_FORM_PATH, locale, `options.yml`), + ) + + const [atOptions, deOptions] = optionsPaths.map(loadYaml) + const atKeys = new Set(Object.keys(atOptions)) + const deKeys = new Set(Object.keys(deOptions)) + + for (const key of deKeys) { + expect( + atKeys.has(key), + `Option "${key}" exists in de/options.yml but missing in at/options.yml`, + ).toBeTruthy() + } + + for (const key of atKeys) { + expect( + deKeys.has(key), + `Option "${key}" exists in at/options.yml but missing in de/options.yml`, + ).toBeTruthy() + } + }) +}) From 8ccf6caa116cdfe929ca9aff7ffcd94aee0f772d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 22:29:26 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/locale-validation.test.ts | 406 ++++++++++++++++---------------- 1 file changed, 207 insertions(+), 199 deletions(-) diff --git a/tests/locale-validation.test.ts b/tests/locale-validation.test.ts index 8e4e95e1..81e14d5a 100644 --- a/tests/locale-validation.test.ts +++ b/tests/locale-validation.test.ts @@ -17,243 +17,251 @@ import { parse } from 'yaml' const LOCALES = [`at`, `de`] const FORM_TYPES = [`student`, `pupil`] -const REQUIRED_FILES = [`messages.yml`, `options.yml`, `student.yml`, `pupil.yml`] +const REQUIRED_FILES = [ + `messages.yml`, + `options.yml`, + `student.yml`, + `pupil.yml`, +] const SIGNUP_FORM_PATH = `src/signup-form` // Keys referenced by code in +page.svelte files const REQUIRED_MESSAGE_KEYS = [ - `submitSuccess`, - `submitError`, - `submit`, - `errMsg`, + `submitSuccess`, + `submitError`, + `submit`, + `errMsg`, ] const REQUIRED_FORM_KEYS = [`header`, `fields`, `airtableTable`] const REQUIRED_FIELD_PROPS = [`id`, `title`] function loadYaml(filePath: string): Record { - try { - const content = readFileSync(filePath, `utf-8`) - return parse(content) as Record - } catch { - return {} - } + try { + const content = readFileSync(filePath, `utf-8`) + return parse(content) as Record + } catch { + return {} + } } test.describe(`Signup Form Locale Validation`, () => { - // Skip for Firefox/Safari - these are static file tests, no browser needed - test.skip(({ browserName }) => browserName !== `chromium`, `Static file validation only needs to run once`) - - test(`all required YML files exist for each locale`, () => { - for (const locale of LOCALES) { - const localePath = join(process.cwd(), SIGNUP_FORM_PATH, locale) - const files = readdirSync(localePath) - - for (const requiredFile of REQUIRED_FILES) { - expect( - files.includes(requiredFile), - `Missing ${requiredFile} in ${locale} locale`, - ).toBeTruthy() - } - } - }) - - test(`form files have required structure`, () => { - for (const locale of LOCALES) { - for (const formType of FORM_TYPES) { - const formPath = join( - process.cwd(), - SIGNUP_FORM_PATH, - locale, - `${formType}.yml`, - ) - const form = loadYaml(formPath) + // Skip for Firefox/Safari - these are static file tests, no browser needed + test.skip( + ({ browserName }) => browserName !== `chromium`, + `Static file validation only needs to run once`, + ) - for (const key of REQUIRED_FORM_KEYS) { - expect( - form[key], - `Missing "${key}" in ${locale}/${formType}.yml`, - ).toBeDefined() - } + test(`all required YML files exist for each locale`, () => { + for (const locale of LOCALES) { + const localePath = join(process.cwd(), SIGNUP_FORM_PATH, locale) + const files = readdirSync(localePath) - // Check header structure - const header = form.header as Record - expect( - header?.title, - `Missing header.title in ${locale}/${formType}.yml`, - ).toBeDefined() - expect( - header?.note, - `Missing header.note in ${locale}/${formType}.yml`, - ).toBeDefined() + for (const requiredFile of REQUIRED_FILES) { + expect( + files.includes(requiredFile), + `Missing ${requiredFile} in ${locale} locale`, + ).toBeTruthy() + } + } + }) - // Check each field has required properties - const fields = form.fields as Array> - expect(Array.isArray(fields), `fields should be an array`).toBeTruthy() + test(`form files have required structure`, () => { + for (const locale of LOCALES) { + for (const formType of FORM_TYPES) { + const formPath = join( + process.cwd(), + SIGNUP_FORM_PATH, + locale, + `${formType}.yml`, + ) + const form = loadYaml(formPath) - for (const field of fields) { - for (const prop of REQUIRED_FIELD_PROPS) { - expect( - field[prop], - `Field missing "${prop}" in ${locale}/${formType}.yml`, - ).toBeDefined() - } - } - } + for (const key of REQUIRED_FORM_KEYS) { + expect( + form[key], + `Missing "${key}" in ${locale}/${formType}.yml`, + ).toBeDefined() } - }) - test(`select fields have matching options`, () => { - for (const locale of LOCALES) { - const optionsPath = join( - process.cwd(), - SIGNUP_FORM_PATH, - locale, - `options.yml`, - ) - const options = loadYaml(optionsPath) + // Check header structure + const header = form.header as Record + expect( + header?.title, + `Missing header.title in ${locale}/${formType}.yml`, + ).toBeDefined() + expect( + header?.note, + `Missing header.note in ${locale}/${formType}.yml`, + ).toBeDefined() - for (const formType of FORM_TYPES) { - const formPath = join( - process.cwd(), - SIGNUP_FORM_PATH, - locale, - `${formType}.yml`, - ) - const form = loadYaml(formPath) - const fields = form.fields as Array> + // Check each field has required properties + const fields = form.fields as Array> + expect(Array.isArray(fields), `fields should be an array`).toBeTruthy() - for (const field of fields) { - if (field.type === `select` && field.id !== `chapter`) { - // chapter is populated from chapters list, not options.yml - const fieldId = field.id as string - expect( - options[fieldId], - `Missing options for "${fieldId}" in ${locale}/options.yml`, - ).toBeDefined() - expect( - Array.isArray(options[fieldId]), - `Options for "${fieldId}" should be an array in ${locale}/options.yml`, - ).toBeTruthy() - expect( - (options[fieldId] as unknown[]).length, - `Options for "${fieldId}" should not be empty in ${locale}/options.yml`, - ).toBeGreaterThan(0) - } - } - } + for (const field of fields) { + for (const prop of REQUIRED_FIELD_PROPS) { + expect( + field[prop], + `Field missing "${prop}" in ${locale}/${formType}.yml`, + ).toBeDefined() + } } - }) + } + } + }) - test(`messages files have required keys`, () => { - for (const locale of LOCALES) { - const messagesPath = join( - process.cwd(), - SIGNUP_FORM_PATH, - locale, - `messages.yml`, - ) - const messages = loadYaml(messagesPath) + test(`select fields have matching options`, () => { + for (const locale of LOCALES) { + const optionsPath = join( + process.cwd(), + SIGNUP_FORM_PATH, + locale, + `options.yml`, + ) + const options = loadYaml(optionsPath) - for (const key of REQUIRED_MESSAGE_KEYS) { - expect( - messages[key], - `Missing "${key}" in ${locale}/messages.yml`, - ).toBeDefined() - } + for (const formType of FORM_TYPES) { + const formPath = join( + process.cwd(), + SIGNUP_FORM_PATH, + locale, + `${formType}.yml`, + ) + const form = loadYaml(formPath) + const fields = form.fields as Array> - // Check nested structure - const submitSuccess = messages.submitSuccess as Record - expect( - submitSuccess?.title, - `Missing submitSuccess.title in ${locale}/messages.yml`, - ).toBeDefined() + for (const field of fields) { + if (field.type === `select` && field.id !== `chapter`) { + // chapter is populated from chapters list, not options.yml + const fieldId = field.id as string expect( - submitSuccess?.note, - `Missing submitSuccess.note in ${locale}/messages.yml`, + options[fieldId], + `Missing options for "${fieldId}" in ${locale}/options.yml`, ).toBeDefined() - - const submitError = messages.submitError as Record expect( - submitError?.title, - `Missing submitError.title in ${locale}/messages.yml`, - ).toBeDefined() + Array.isArray(options[fieldId]), + `Options for "${fieldId}" should be an array in ${locale}/options.yml`, + ).toBeTruthy() expect( - submitError?.note, - `Missing submitError.note in ${locale}/messages.yml`, - ).toBeDefined() + (options[fieldId] as unknown[]).length, + `Options for "${fieldId}" should not be empty in ${locale}/options.yml`, + ).toBeGreaterThan(0) + } + } + } + } + }) - const submit = messages.submit as Record - expect( - submit?.title, - `Missing submit.title in ${locale}/messages.yml`, - ).toBeDefined() - expect( - submit?.note, - `Missing submit.note in ${locale}/messages.yml`, - ).toBeDefined() + test(`messages files have required keys`, () => { + for (const locale of LOCALES) { + const messagesPath = join( + process.cwd(), + SIGNUP_FORM_PATH, + locale, + `messages.yml`, + ) + const messages = loadYaml(messagesPath) - const errMsg = messages.errMsg as Record - expect( - errMsg?.required, - `Missing errMsg.required in ${locale}/messages.yml`, - ).toBeDefined() - } - }) + for (const key of REQUIRED_MESSAGE_KEYS) { + expect( + messages[key], + `Missing "${key}" in ${locale}/messages.yml`, + ).toBeDefined() + } - test(`AT and DE locales have same field IDs (cross-locale parity)`, () => { - for (const formType of FORM_TYPES) { - const formPaths = LOCALES.map((locale) => - join(process.cwd(), SIGNUP_FORM_PATH, locale, `${formType}.yml`), - ) + // Check nested structure + const submitSuccess = messages.submitSuccess as Record + expect( + submitSuccess?.title, + `Missing submitSuccess.title in ${locale}/messages.yml`, + ).toBeDefined() + expect( + submitSuccess?.note, + `Missing submitSuccess.note in ${locale}/messages.yml`, + ).toBeDefined() - const forms = formPaths.map(loadYaml) - const fieldIdSets = forms.map((form) => { - const fields = form.fields as Array> - return new Set(fields.map((f) => f.id as string)) - }) + const submitError = messages.submitError as Record + expect( + submitError?.title, + `Missing submitError.title in ${locale}/messages.yml`, + ).toBeDefined() + expect( + submitError?.note, + `Missing submitError.note in ${locale}/messages.yml`, + ).toBeDefined() - const [atFields, deFields] = fieldIdSets + const submit = messages.submit as Record + expect( + submit?.title, + `Missing submit.title in ${locale}/messages.yml`, + ).toBeDefined() + expect( + submit?.note, + `Missing submit.note in ${locale}/messages.yml`, + ).toBeDefined() - // Check AT has all DE fields - for (const fieldId of deFields) { - expect( - atFields.has(fieldId), - `Field "${fieldId}" exists in de/${formType}.yml but missing in at/${formType}.yml`, - ).toBeTruthy() - } + const errMsg = messages.errMsg as Record + expect( + errMsg?.required, + `Missing errMsg.required in ${locale}/messages.yml`, + ).toBeDefined() + } + }) - // Check DE has all AT fields - for (const fieldId of atFields) { - expect( - deFields.has(fieldId), - `Field "${fieldId}" exists in at/${formType}.yml but missing in de/${formType}.yml`, - ).toBeTruthy() - } - } - }) + test(`AT and DE locales have same field IDs (cross-locale parity)`, () => { + for (const formType of FORM_TYPES) { + const formPaths = LOCALES.map((locale) => + join(process.cwd(), SIGNUP_FORM_PATH, locale, `${formType}.yml`), + ) - test(`AT and DE locales have same option keys`, () => { - const optionsPaths = LOCALES.map((locale) => - join(process.cwd(), SIGNUP_FORM_PATH, locale, `options.yml`), - ) + const forms = formPaths.map(loadYaml) + const fieldIdSets = forms.map((form) => { + const fields = form.fields as Array> + return new Set(fields.map((f) => f.id as string)) + }) - const [atOptions, deOptions] = optionsPaths.map(loadYaml) - const atKeys = new Set(Object.keys(atOptions)) - const deKeys = new Set(Object.keys(deOptions)) + const [atFields, deFields] = fieldIdSets - for (const key of deKeys) { - expect( - atKeys.has(key), - `Option "${key}" exists in de/options.yml but missing in at/options.yml`, - ).toBeTruthy() - } + // Check AT has all DE fields + for (const fieldId of deFields) { + expect( + atFields.has(fieldId), + `Field "${fieldId}" exists in de/${formType}.yml but missing in at/${formType}.yml`, + ).toBeTruthy() + } - for (const key of atKeys) { - expect( - deKeys.has(key), - `Option "${key}" exists in at/options.yml but missing in de/options.yml`, - ).toBeTruthy() - } - }) + // Check DE has all AT fields + for (const fieldId of atFields) { + expect( + deFields.has(fieldId), + `Field "${fieldId}" exists in at/${formType}.yml but missing in de/${formType}.yml`, + ).toBeTruthy() + } + } + }) + + test(`AT and DE locales have same option keys`, () => { + const optionsPaths = LOCALES.map((locale) => + join(process.cwd(), SIGNUP_FORM_PATH, locale, `options.yml`), + ) + + const [atOptions, deOptions] = optionsPaths.map(loadYaml) + const atKeys = new Set(Object.keys(atOptions)) + const deKeys = new Set(Object.keys(deOptions)) + + for (const key of deKeys) { + expect( + atKeys.has(key), + `Option "${key}" exists in de/options.yml but missing in at/options.yml`, + ).toBeTruthy() + } + + for (const key of atKeys) { + expect( + deKeys.has(key), + `Option "${key}" exists in at/options.yml but missing in de/options.yml`, + ).toBeTruthy() + } + }) })