From 5d698609f2af60196f4edce2678534c4cd1c09b8 Mon Sep 17 00:00:00 2001 From: Harry Turnbull <52940497+hturnbull93@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:27:35 +0100 Subject: [PATCH 1/7] feat: add i18n to onboarding flow via paraglide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces all hardcoded UI strings in the onboarding pipeline with paraglide message functions, enabling translation into any locale. - Add 160+ onboarding_* keys to messages/en.json with translator instructions for HTML-containing strings - Add getter functions to options.ts (getDiscoveryOptions, getMotivations, getSkills, getWeeklyHours, getIntentOptions) returning {value, label} pairs — English values submitted to Airtable unchanged, labels translated at render time - Wire up paraglide in OnboardingFlow.svelte, ActionCards.svelte, +page.svelte and +page.server.ts - Add isomorphic-dompurify safeHtml() helper to sanitize all {@html} calls, guarding against malicious LLM translation output - Accept ?locale= query param on the embed page to override locale server- and client-side - Allow PARAGLIDE_LOCALES to include locales not in default-settings.js on local dev (warn instead of error); CI still hard-errors to prevent deploying untranslated locales Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_015aKhZeP6x8pgBkDEraXGUs --- messages/en.json | 192 ++++++++- package.json | 2 + pnpm-lock.yaml | 295 +++++++++++++- .../components/onboarding/ActionCards.svelte | 37 +- .../onboarding/OnboardingFlow.svelte | 377 ++++++++---------- src/lib/components/onboarding/options.ts | 94 ++++- src/lib/env.ts | 21 +- src/lib/sanitize.ts | 10 + .../embed/onboarding-form/+page.server.ts | 33 +- src/routes/embed/onboarding-form/+page.svelte | 12 +- 10 files changed, 804 insertions(+), 269 deletions(-) create mode 100644 src/lib/sanitize.ts diff --git a/messages/en.json b/messages/en.json index ba93fed34..90699da92 100644 --- a/messages/en.json +++ b/messages/en.json @@ -104,5 +104,195 @@ "learn_building_the_pause_button": "[Building the Pause button](/building-the-pause-button). What it takes to pause AI.", "learn_faq": "[FAQ](/faq). Commonly asked questions about AI safety and PauseAI.", "learn_action": "[Action](/action). What you can do to help (with links to many action-related guides)", - "footer_info_about": "About Us" + "footer_info_about": "About Us", + "onboarding_page_title": "Get involved", + "onboarding_page_description": "Find the highest-impact way for you to help pause the development of superhuman AI.", + "onboarding_browse_banner": "You're browsing without signing up, leave your email below so we can tell you when new opportunities go live.", + "onboarding_field_full_name": "Full name *", + "onboarding_placeholder_full_name": "Full name", + "onboarding_field_email": "Email *", + "onboarding_placeholder_email": "Email", + "onboarding_helper_email_gmail": "Preferably Gmail if you have one.", + "onboarding_field_country": "Country of residence *", + "onboarding_placeholder_country": "Select your country", + "onboarding_field_city": "City / town of residence *", + "onboarding_placeholder_city": "City / town", + "onboarding_btn_continue": "Continue →", + "onboarding_btn_browse": "👀 I just want to take action now", + "onboarding_browse_helper": "Skip ahead to see the actions you can take today, no email required.", + "onboarding_step2_heading": "What brings you here?", + "onboarding_intent_keep_informed_label": "Keep me informed", + "onboarding_intent_keep_informed_sub": "Connect me with my local PauseAI chapter, and keep me updated on global campaigns.", + "onboarding_intent_newsletter_label": "Subscribe to our Substack", + "onboarding_intent_newsletter_sub": "General news on AI, delivered via our Substack newsletter.", + "onboarding_intent_more_optional": "Want to do more? (optional)", + "onboarding_intent_act_now_label": "I want to take action now", + "onboarding_intent_act_now_sub": "Show me what I can do today.", + "onboarding_intent_volunteer_label": "I want to volunteer regularly", + "onboarding_intent_volunteer_sub": "Help me find a role that fits.", + "onboarding_intent_lead_label": "I want to lead", + "onboarding_intent_lead_sub": "I'm ready to organize in my country or region.", + "onboarding_gdpr_consent__instructions": "Preserve the HTML tag and href attribute exactly. Only translate the visible text around and inside the tag.", + "onboarding_gdpr_consent": "I agree to the Privacy Policy, including sharing my details with my local PauseAI chapter (which may be a separate entity to PauseAI Global) for local coordination. *", + "onboarding_btn_submit": "Submit →", + "onboarding_btn_submitting": "Submitting...", + "onboarding_btn_back": "← Back", + "onboarding_confirm_keep_informed": "We'll connect you with your local PauseAI chapter and keep you updated on global campaigns.", + "onboarding_confirm_newsletter": "You're subscribed to our Substack newsletter for general news on AI.", + "onboarding_confirm_a_title": "You're in.", + "onboarding_confirm_b_title": "You're in, thanks for joining us.", + "onboarding_confirm_b_sub": "You're all set. Here are a few ways to make a difference today.", + "onboarding_confirm_volunteer_title": "Welcome to the team.", + "onboarding_confirm_volunteer_sub": "You're on the volunteer list. We'll be in touch soon.", + "onboarding_browse_header_title": "Take action right now.", + "onboarding_browse_header_sub": "Below are some actions you can take right now. The best way to stay informed about new opportunities is to sign up below.", + "onboarding_browse_signed_up": "✓ You're in. We'll connect you with your local PauseAI chapter and keep you updated on global campaigns.", + "onboarding_browse_keep_informed_title": "Keep me informed", + "onboarding_browse_keep_informed_sub": "Connect me with my local PauseAI chapter and keep me updated on global campaigns.", + "onboarding_btn_sign_me_up": "Sign me up →", + "onboarding_btn_signing_up": "Signing up...", + "onboarding_section_ways_to_help": "A few ways to help today", + "onboarding_next_step_title": "★ Recommended next step", + "onboarding_next_step_p1__instructions": "Preserve the HTML tag and href attribute exactly. Only translate the visible text.", + "onboarding_next_step_p1": "Join one of our Member Community Welcome Meetings, or a local social event, to find out more about PauseAI's community: see upcoming events.", + "onboarding_next_step_p2__instructions": "Preserve the HTML tag and href attribute exactly. Only translate the visible text.", + "onboarding_next_step_p2": "Want to get kick-started into action straight away? Check out our list of actions.", + "onboarding_footer_join_conversation": "Join the conversation", + "onboarding_footer_discord_btn": "Join PauseAI on Discord", + "onboarding_footer_follow_us": "Follow us", + "onboarding_step_about": "About you", + "onboarding_step_intent": "Intent", + "onboarding_step_volunteer_form": "Volunteer form", + "onboarding_step_confirmed": "Confirmed", + "onboarding_step_next_steps": "Next steps", + "onboarding_volunteer_title": "Sign up to volunteer", + "onboarding_volunteer_intro": "Tell us a bit about yourself so we can find a role that fits.", + "onboarding_field_zip": "Zip code", + "onboarding_placeholder_zip": "e.g. 02134", + "onboarding_helper_zip": "Your 5-digit zip code is used to find your Local Group.", + "onboarding_field_discord": "Discord username", + "onboarding_helper_discord__instructions": "Preserve the HTML tag and href attribute exactly. Only translate the visible text.", + "onboarding_helper_discord": "If you don't have a Discord account, we encourage you to create one here.", + "onboarding_field_phone": "Phone number", + "onboarding_placeholder_dial_code": "+1", + "onboarding_placeholder_phone": "(555) 000-0000", + "onboarding_field_languages": "What languages do you speak? *", + "onboarding_placeholder_languages": "Select languages", + "onboarding_field_discovery": "How did you find out about PauseAI?", + "onboarding_placeholder_discovery": "Select an option", + "onboarding_field_motivations": "What motivated you to join?", + "onboarding_field_skills": "Skills & interests", + "onboarding_field_hours": "How much time can you commit weekly? *", + "onboarding_placeholder_specify": "Please specify", + "onboarding_agree_volunteer__instructions": "Preserve the HTML tag and href attribute exactly. Only translate the visible text.", + "onboarding_agree_volunteer": "I agree with the Volunteer Agreement *", + "onboarding_agree_conduct__instructions": "Preserve the HTML tag and href attribute exactly. Only translate the visible text.", + "onboarding_agree_conduct": "I agree with the Code of Conduct *", + "onboarding_lead_title": "{role} Volunteer Description", + "onboarding_lead_meta": "Part-time volunteer role · 5–15 hours/week", + "onboarding_lead_intro": "Pause AI Global is looking for leaders around the world to run local groups that lead organising efforts in their area. This is a part-time volunteer role requiring 5-15 hours per week. Each group leader will be responsible for planning direct actions, mobilising volunteers and coordinating with the Global PauseAI team.", + "onboarding_lead_us_branch__instructions": "Preserve all HTML tags (, ) and href attributes exactly. Only translate the visible text.", + "onboarding_lead_us_branch": "Since you're based in the United States, please apply through PauseAI US: PauseAI US application form.", + "onboarding_lead_has_chapter__instructions": "Preserve all HTML tags (, ) and href attributes exactly. The {country} placeholder must remain unchanged — it is replaced at runtime. Only translate the surrounding visible text.", + "onboarding_lead_has_chapter": "{country} already has a PauseAI chapter, so rather than founding a national group, you could lead a regional or city group within it. Find your chapter at pauseai.info/communities, or email our Organizing Director below to talk it through.", + "onboarding_lead_check_chapter__instructions": "Preserve all HTML tags (, ) and href attributes exactly. Only translate the visible text.", + "onboarding_lead_check_chapter": "First, check that your country doesn't already have a chapter: pauseai.info/communities.", + "onboarding_lead_what_you_do_title": "What you'll do", + "onboarding_lead_do_1": "Recruit and grow your local group by welcoming new volunteers and organising events together.", + "onboarding_lead_do_2": "Build relationships with local activist groups and journalists.", + "onboarding_lead_do_3": "Meet monthly with PauseAI's Global team to swap ideas and stay coordinated with the global strategy and quarterly campaigns.", + "onboarding_lead_do_4": "Share what your chapter is up to on social media and help promote events.", + "onboarding_lead_looking_for_title": "What we're looking for", + "onboarding_lead_looking_1": "Ability to plan and execute direct actions, including workshops and public events.", + "onboarding_lead_looking_2": "Excellent communication skills, eager and able to engage diverse communities.", + "onboarding_lead_looking_3": "Self-motivated, with the ability to work independently and as part of a team.", + "onboarding_lead_looking_4": "Passion for AI safety and alignment with PauseAI's mission.", + "onboarding_lead_looking_5": "Comfortable communicating in English", + "onboarding_lead_looking_6": "Adhere and practice to a non-violent, legal approach.", + "onboarding_lead_nice_to_have_title": "What would be nice to have", + "onboarding_lead_nice_1": "Strong organising skills and experience in grassroots activism.", + "onboarding_lead_nice_2": "Lobbying skills and policymaker engagement experience", + "onboarding_lead_nice_3": "Media and content writing experience", + "onboarding_lead_imperfect": "If you do not meet all of the requirements, we still want to hear from you. The commitment to AI Safety and the belief we can make an impact is the most important factor; we can provide training, resources and a community to lean on.", + "onboarding_lead_support_title": "Support you'll get", + "onboarding_lead_support": "You'll join a global network of organisers who are all figuring this out together. We meet monthly, share resources across our chat platforms and help each other troubleshoot. It's a great way to learn new skills, meet thoughtful people and be part of a fast-growing movement that's working on one of the most important issues of our time.", + "onboarding_lead_next_steps_title": "Next steps", + "onboarding_lead_next_steps__instructions": "Preserve the HTML tag and href attribute exactly. The email address inside the tag is a proper noun and must not be translated.", + "onboarding_lead_next_steps": "If you'd like to learn more, please contact our PauseAI Global organising director Irina Tavera at Irina@pauseai.info to schedule an informal exploratory chat.", + "onboarding_lead_email_btn": "✉️ Email our Organizing Director", + "onboarding_lead_please_mention": "Please let her know the following:", + "onboarding_lead_mention_1": "Your name", + "onboarding_lead_mention_2": "Your country of residence", + "onboarding_lead_mention_3": "A few sentences on why you'd like to become a lead", + "onboarding_discovery_pauseai_social": "PauseAI affiliated social media", + "onboarding_discovery_other_social": "Non-PauseAI affiliated social media", + "onboarding_discovery_referral": "Friend/Family referral", + "onboarding_discovery_news": "News article", + "onboarding_discovery_event": "Event/Presentation", + "onboarding_discovery_search": "Internet search", + "onboarding_discovery_other": "Other", + "onboarding_motivation_ai_safety": "AI Safety", + "onboarding_motivation_democratic_oversight": "Need for democratic oversight", + "onboarding_motivation_ethical_tech": "Ethical technology", + "onboarding_motivation_ai_governance": "AI Governance", + "onboarding_motivation_job_displacement": "Job Displacement", + "onboarding_motivation_misinformation": "Misinformation", + "onboarding_motivation_deepfakes": "Deepfake scams and harassment", + "onboarding_motivation_power": "Concentration of power", + "onboarding_motivation_privacy": "Privacy loss", + "onboarding_motivation_environment": "Environmental damage", + "onboarding_motivation_addiction": "Technology addiction", + "onboarding_motivation_weapons": "Autonomous weapons", + "onboarding_motivation_cyber": "Cyberattacks", + "onboarding_motivation_bioweapons": "Bioweapons", + "onboarding_motivation_other": "Other", + "onboarding_skill_software": "Software Development", + "onboarding_skill_video": "Video Creation", + "onboarding_skill_social_media": "Social Media Management", + "onboarding_skill_events": "Event Organization", + "onboarding_skill_speaking": "Public Speaking/ Presentation", + "onboarding_skill_writing": "Writing", + "onboarding_skill_design": "Graphic Design/ Visual Arts", + "onboarding_skill_research": "Research", + "onboarding_skill_comms": "Communications/ PR", + "onboarding_skill_fundraising": "Fundraising", + "onboarding_skill_community": "Community Organizing", + "onboarding_skill_advocacy": "Political Advocacy/ Lobbying", + "onboarding_skill_education": "Education/ Teaching", + "onboarding_skill_admin": "Administrative Support", + "onboarding_skill_legal": "Legal Knowledge", + "onboarding_skill_other": "Other", + "onboarding_hours_under_3": "Less than 3 hours", + "onboarding_hours_3_6": "3-6 hours", + "onboarding_hours_6_10": "6-10 hours", + "onboarding_hours_10_20": "10-20 hours", + "onboarding_hours_20_plus": "20+ hours", + "onboarding_action_ai_concerns_title": "Share your AI concerns", + "onboarding_action_ai_concerns_sub": "Tell your story and add your voice to the public record.", + "onboarding_action_email_reps_title": "Email your elected representatives", + "onboarding_action_email_reps_sub": "Use our Email Builder for a pre-drafted message you can personalize and send.", + "onboarding_action_lobby_title": "Contact your elected officials", + "onboarding_action_lobby_sub": "Our lobby tips walk you through what to say.", + "onboarding_action_talk_title": "Talk to someone in your life about AI safety", + "onboarding_action_talk_sub__instructions": "Preserve the HTML tag and href attribute exactly. Only translate the visible text.", + "onboarding_action_talk_sub": "A friend, neighbour, colleague, or family member. Our counterarguments guide can help you answer tough questions and encourage others to act.", + "onboarding_action_microcommit_title": "Microcommit — weekly bite-sized actions", + "onboarding_action_microcommit_sub": "Pick from a list of small ongoing actions and track your contribution over time.", + "onboarding_action_petitions_title": "Sign the petitions", + "onboarding_action_petitions_sub": "Add your name alongside thousands of others calling for safer AI.", + "onboarding_action_donate_title": "Donate or pick up some gear", + "onboarding_action_donate_sub__instructions": "Preserve both HTML tags and their href attributes exactly. Only translate the visible text inside and around the tags.", + "onboarding_action_donate_sub": "Make a donation or pick up some gear from our store.", + "onboarding_error_missing_fields": "Please fill in your name, email, country and city.", + "onboarding_error_invalid_email": "Please enter a valid email address.", + "onboarding_error_invalid_country": "Please select a country from the list.", + "onboarding_error_missing_intent": "Please choose what brings you here.", + "onboarding_error_missing_gdpr": "Please agree to the data processing consent to continue.", + "onboarding_error_missing_languages": "Please select at least one language you speak.", + "onboarding_error_missing_hours": "Please tell us how much time you can commit weekly.", + "onboarding_error_missing_agreements": "Please agree to the Volunteer Agreement and Code of Conduct.", + "onboarding_error_invalid_discovery": "Please select a valid option for how you found us.", + "onboarding_error_save_failed": "Sorry, we couldn't save your details. Please try again.", + "onboarding_error_generic": "Something went wrong. Please try again.", + "onboarding_error_unexpected": "An unexpected error occurred. Please try again." } diff --git a/package.json b/package.json index a30666998..b04ea7b1d 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@sveltejs/adapter-netlify": "^5.2.4", "@sveltejs/kit": "^2.65.2", "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@types/dompurify": "^3.2.0", "@types/escape-html": "^1.0.4", "@types/glidejs__glide": "^3.6.6", "@types/minimist": "^1.2.5", @@ -104,6 +105,7 @@ "github-slugger": "^2.0.0", "html-to-image": "^1.11.13", "http-status-codes": "^2.3.0", + "isomorphic-dompurify": "^3.18.0", "maplibre-gl": "^5.24.0", "maplibregl-mapbox-request-transformer": "^0.0.3", "micromark": "^4.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7677696c5..3ddeac7e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: http-status-codes: specifier: ^2.3.0 version: 2.3.0 + isomorphic-dompurify: + specifier: ^3.18.0 + version: 3.18.0 maplibre-gl: specifier: ^5.24.0 version: 5.24.0 @@ -138,6 +141,9 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: ^7.1.2 version: 7.1.2(svelte@5.56.3(@typescript-eslint/types@8.62.1))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + '@types/dompurify': + specifier: ^3.2.0 + version: 3.2.0 '@types/escape-html': specifier: ^1.0.4 version: 1.0.4 @@ -260,7 +266,7 @@ importers: version: 1.4.11(rolldown@1.0.3)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) vitest: specifier: ^4.1.9 - version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages: @@ -278,6 +284,21 @@ packages: '@apm-js-collab/tracing-hooks@0.10.0': resolution: {integrity: sha512-2/Z3NTewJTruUkmsSnBC5bJlLNUd9keuD1OLlTEpim4FyLhm6m2Rnfv+wrFdUvFfhmH8CRdiDZBqBrn+wyaGuA==} + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -345,6 +366,10 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@cacheable/memory@2.0.9': resolution: {integrity: sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==} @@ -355,6 +380,10 @@ packages: resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==} engines: {node: '>=18'} + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} + engines: {node: '>=20.19.0'} + '@csstools/css-calc@3.2.1': resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} engines: {node: '>=20.19.0'} @@ -362,6 +391,13 @@ packages: '@csstools/css-parser-algorithms': ^4.0.0 '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-color-parser@4.1.9': + resolution: {integrity: sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-parser-algorithms@4.0.0': resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} engines: {node: '>=20.19.0'} @@ -797,6 +833,15 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@fontsource/roboto-slab@5.2.8': resolution: {integrity: sha512-8+iMCsoUZsDwQUe5omwCp7JPNTVdyAgay5AdhmnFZPEVIVabujrmYaFkSuZ1+GUemPEWlzEQ6aQkg2mPL84SAA==} @@ -1713,6 +1758,10 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/dompurify@3.2.0': + resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} + deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. + '@types/escape-html@1.0.4': resolution: {integrity: sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==} @@ -1983,6 +2032,9 @@ packages: bcp-47@2.1.0: resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} @@ -2112,6 +2164,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2121,6 +2177,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} @@ -2170,6 +2229,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.4.11: + resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -2198,6 +2260,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -2616,6 +2682,10 @@ packages: hookified@2.2.0: resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-tags@5.1.0: resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==} engines: {node: '>=20.10'} @@ -2706,6 +2776,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} @@ -2723,6 +2796,10 @@ packages: resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} engines: {node: '>=18'} + isomorphic-dompurify@3.18.0: + resolution: {integrity: sha512-ajp0D8laIHeoYlhBTevpE2HUhqWaqLXFk6K/wV3Ok8kDraBZpZsifwVWaY8IfJntMRIo1VSksgKV+lXyet9Q7A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -2740,6 +2817,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -2964,6 +3050,10 @@ packages: resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} engines: {node: 20 || >=22} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -3292,6 +3382,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3502,6 +3595,10 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + schema-dts-lib@1.0.0: resolution: {integrity: sha512-9MEO5vpQH9JdBioUupqluzxSYxPLjhmqRUudk15adUl/ypnRsM2/M1kN3AmVJQeG7nZqcL68H8JlGqQQT6vy9A==} engines: {node: '>=14.0.0'} @@ -3680,6 +3777,9 @@ packages: svg-tags@1.0.0: resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + table@6.9.0: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} @@ -3708,6 +3808,13 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tldts-core@7.4.5: + resolution: {integrity: sha512-pGrwzZDvPwKe+7NNUqAunb6rqTfynr0VOUhCMdqbu5xlvNiszsAJygRzwvpVycdzejlbpY+SWJOn+s75Og7FEA==} + + tldts@7.4.5: + resolution: {integrity: sha512-RfEzKWcq5fHUOFq7J3rl3Oz6ylKGtcHqUznzj4EcXsxLSIjJcvpbXAQtWGeJQ0xKnimR5e0Cn+cn9TssfMzm+g==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3716,9 +3823,17 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} @@ -3782,6 +3897,10 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + unicorn-magic@0.4.0: resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} engines: {node: '>=20'} @@ -3965,6 +4084,10 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + walk-up-path@4.0.0: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} @@ -3976,9 +4099,21 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4009,6 +4144,13 @@ packages: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -4072,6 +4214,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -4172,6 +4334,10 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@cacheable/memory@2.0.9': dependencies: '@cacheable/utils': 2.4.1 @@ -4188,11 +4354,20 @@ snapshots: dependencies: fontkitten: 1.0.3 + '@csstools/color-helpers@6.1.0': {} + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-color-parser@4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-tokenizer': 4.0.0 @@ -4483,6 +4658,8 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 + '@exodus/bytes@1.15.1': {} + '@fontsource/roboto-slab@5.2.8': {} '@fontsource/saira-condensed@5.2.8': {} @@ -5241,6 +5418,10 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/dompurify@3.2.0': + dependencies: + dompurify: 3.4.11 + '@types/escape-html@1.0.4': {} '@types/esrecurse@4.3.1': {} @@ -5543,6 +5724,10 @@ snapshots: is-alphanumerical: 2.0.1 is-decimal: 2.0.1 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -5657,10 +5842,19 @@ snapshots: csstype@3.2.3: {} + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + debug@4.4.3: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 @@ -5697,6 +5891,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.4.11: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -5721,6 +5919,8 @@ snapshots: entities@4.5.0: {} + entities@8.0.0: {} + env-paths@2.2.1: {} error-ex@1.3.4: @@ -6189,6 +6389,12 @@ snapshots: hookified@2.2.0: {} + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + html-tags@5.1.0: {} html-to-image@1.11.13: {} @@ -6259,6 +6465,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} + is-reference@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -6271,6 +6479,14 @@ snapshots: isexe@3.1.5: {} + isomorphic-dompurify@3.18.0: + dependencies: + dompurify: 3.4.11 + jsdom: 29.1.1 + transitivePeerDependencies: + - '@noble/hashes' + - canvas + jiti@2.7.0: {} js-sha256@0.11.1: {} @@ -6283,6 +6499,32 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.5(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.28.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -6454,6 +6696,8 @@ snapshots: lru-cache@11.2.7: {} + lru-cache@11.5.1: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -7049,6 +7293,10 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse5@8.0.1: + dependencies: + entities: 8.0.0 + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -7241,6 +7489,10 @@ snapshots: dependencies: mri: 1.2.0 + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + schema-dts-lib@1.0.0(typescript@6.0.3): dependencies: typescript: 6.0.3 @@ -7514,6 +7766,8 @@ snapshots: svg-tags@1.0.0: {} + symbol-tree@3.2.4: {} + table@6.9.0: dependencies: ajv: 8.20.0 @@ -7539,14 +7793,28 @@ snapshots: tinyrainbow@3.1.0: {} + tldts-core@7.4.5: {} + + tldts@7.4.5: + dependencies: + tldts-core: 7.4.5 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 totalist@3.0.1: {} + tough-cookie@6.0.1: + dependencies: + tldts: 7.4.5 + tr46@0.0.3: {} + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + trough@2.2.0: {} ts-api-utils@2.5.0(typescript@6.0.3): @@ -7600,6 +7868,8 @@ snapshots: undici-types@7.18.2: {} + undici@7.28.0: {} + unicorn-magic@0.4.0: {} unified@11.0.5: @@ -7735,7 +8005,7 @@ snapshots: optionalDependencies: vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) - vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): + vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.9 '@vitest/mocker': 4.1.9(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) @@ -7760,17 +8030,34 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 24.13.2 + jsdom: 29.1.1 transitivePeerDependencies: - msw + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + walk-up-path@4.0.0: {} web-streams-polyfill@4.0.0-beta.3: {} webidl-conversions@3.0.1: {} + webidl-conversions@8.0.1: {} + webpack-virtual-modules@0.6.2: {} + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -7799,6 +8086,10 @@ snapshots: dependencies: signal-exit: 4.1.0 + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + yallist@3.1.1: {} yaml@1.10.3: {} diff --git a/src/lib/components/onboarding/ActionCards.svelte b/src/lib/components/onboarding/ActionCards.svelte index 090d62914..961af2395 100644 --- a/src/lib/components/onboarding/ActionCards.svelte +++ b/src/lib/components/onboarding/ActionCards.svelte @@ -1,39 +1,37 @@
  • -

    Share your AI concerns

    -

    Tell your story and add your voice to the public record.

    +

    {m.onboarding_action_ai_concerns_title()}

    +

    {m.onboarding_action_ai_concerns_sub()}

  • -

    Email your elected representatives

    -

    Use our Email Builder for a pre-drafted message you can personalize and send.

    +

    {m.onboarding_action_email_reps_title()}

    +

    {m.onboarding_action_email_reps_sub()}

  • -

    Contact your elected officials

    -

    Our lobby tips walk you through what to say.

    +

    {m.onboarding_action_lobby_title()}

    +

    {m.onboarding_action_lobby_sub()}

  • -

    Talk to someone in your life about AI safety

    -

    - A friend, neighbour, colleague, or family member. Our - counterarguments guide can help you answer tough questions - and encourage others to act. -

    +

    {m.onboarding_action_talk_title()}

    +

    {@html safeHtml(m.onboarding_action_talk_sub())}

  • - Microcommit — weekly bite-sized actions + {m.onboarding_action_microcommit_title()}

    -

    Pick from a list of small ongoing actions and track your contribution over time.

    +

    {m.onboarding_action_microcommit_sub()}

  • -

    Sign the petitions

    -

    Add your name alongside thousands of others calling for safer AI.

    +

    {m.onboarding_action_petitions_title()}

    +

    {m.onboarding_action_petitions_sub()}

    • PauseAI Statement
    • Say No To Superintelligent AI
    • @@ -51,11 +49,8 @@
  • -

    Donate or pick up some gear

    -

    - Make a donation or pick up some gear from our - store. -

    +

    {m.onboarding_action_donate_title()}

    +

    {@html safeHtml(m.onboarding_action_donate_sub())}

diff --git a/src/lib/components/onboarding/OnboardingFlow.svelte b/src/lib/components/onboarding/OnboardingFlow.svelte index 985c88f65..aad5e38f2 100644 --- a/src/lib/components/onboarding/OnboardingFlow.svelte +++ b/src/lib/components/onboarding/OnboardingFlow.svelte @@ -5,52 +5,35 @@ import type { NationalGroupsApiResponse } from '$api/national-groups/+server.js' import type { OnboardingModeApiResponse } from '$api/onboarding-mode/+server.js' import { toast } from 'svelte-french-toast' + import { safeHtml } from '$lib/sanitize' import Link from '$lib/components/Link.svelte' import LinkWithoutIcon from '$lib/components/LinkWithoutIcon.svelte' import Socials from '$lib/components/Socials.svelte' import Combobox from '$lib/components/Combobox.svelte' import ActionCards from './ActionCards.svelte' import Stepper from './Stepper.svelte' + import * as m from '$lib/paraglide/messages.js' import { COUNTRIES, COUNTRY_DIAL_CODES, - DISCOVERY_OPTIONS, DISCOVERY_SPECIFY_TRIGGERS, LANGUAGES, - MOTIVATIONS, - SKILLS, - WEEKLY_HOURS, - type Intent + getDiscoveryOptions, + getMotivations, + getSkills, + getWeeklyHours, + getIntentOptions, + type Intent, + type IntentKey } from './options' - type IntentKey = 'act-now' | 'volunteer' | 'lead' - const INTENT_VALUES: Record = { 'act-now': 'Act now', volunteer: 'Volunteer', lead: 'Lead' } - const intentOptions: { key: IntentKey; icon: string; label: string; sub: string }[] = [ - { - key: 'act-now', - icon: '✊', - label: 'I want to take action now', - sub: 'Show me what I can do today.' - }, - { - key: 'volunteer', - icon: '🤝', - label: 'I want to volunteer regularly', - sub: 'Help me find a role that fits.' - }, - { - key: 'lead', - icon: '🚀', - label: 'I want to lead', - sub: "I'm ready to organize in my country or region." - } - ] + const intentOptions = $derived(getIntentOptions()) let { initialEmail = '', @@ -164,10 +147,15 @@ const stepperLabels = $derived( intent === 'volunteer' - ? ['About you', 'Intent', 'Volunteer form', 'Confirmed'] + ? [ + m.onboarding_step_about(), + m.onboarding_step_intent(), + m.onboarding_step_volunteer_form(), + m.onboarding_step_confirmed() + ] : intent === 'lead' - ? ['About you', 'Intent', 'Next steps'] - : ['About you', 'Intent', 'Confirmed'] + ? [m.onboarding_step_about(), m.onboarding_step_intent(), m.onboarding_step_next_steps()] + : [m.onboarding_step_about(), m.onboarding_step_intent(), m.onboarding_step_confirmed()] ) // Lead path: if the country already has a chapter, offer regional/city @@ -239,9 +227,9 @@ } onSuccess(result.data) } else if (result.type === 'failure') { - toast.error(String(result.data?.message ?? 'Something went wrong. Please try again.')) + toast.error(String(result.data?.message ?? m.onboarding_error_generic())) } else { - toast.error('An unexpected error occurred. Please try again.') + toast.error(m.onboarding_error_unexpected()) } } } @@ -268,7 +256,7 @@ name="country" options={COUNTRIES} required - placeholder="Select your country" + placeholder={m.onboarding_placeholder_country()} bind:value={basics.country} /> {/snippet} @@ -283,21 +271,25 @@ {/if} {/snippet} -{#snippet selectCards(name: string, options: string[], selectedList: string[])} +{#snippet selectCards( + name: string, + options: { value: string; label: string }[], + selectedList: string[] +)}
- {#each options as option (option)} + {#each options as option (option.value)} {/each}
@@ -312,13 +304,13 @@ {#if keepInformed}
  • - We'll connect you with your local PauseAI chapter and keep you updated on global campaigns. + {m.onboarding_confirm_keep_informed()}
  • {/if} {#if basics.newsletter}
  • - You're subscribed to our Substack newsletter for general news on AI. + {m.onboarding_confirm_newsletter()}
  • {/if} @@ -329,35 +321,25 @@ {/snippet} {#snippet nextStepBlock()}
    -

    ★ Recommended next step

    -

    - Join one of our Member Community Welcome Meetings, or a local social event, to find out more - about PauseAI's community: see upcoming events. -

    -

    - Want to get kick-started into action straight away? Check out our - list of actions. -

    +

    {m.onboarding_next_step_title()}

    +

    {@html safeHtml(m.onboarding_next_step_p1())}

    +

    {@html safeHtml(m.onboarding_next_step_p2())}

    {/snippet} {#snippet confirmationFooter()} {/snippet} @@ -365,8 +347,7 @@
    {#if mode === 'browse' && !browseSignedUp}
    - You're browsing without signing up, leave your email below so we can tell you when new - opportunities go live. + {m.onboarding_browse_banner()}
    {/if} @@ -379,55 +360,55 @@
    - +
    {@render honeypotField('ob-nickname')}
    - + -

    Preferably Gmail if you have one.

    +

    {m.onboarding_helper_email_gmail()}

    - + {@render countrySelect('ob-country')}
    - +
    - +

    - Skip ahead to see the actions you can take today, no email required. + {m.onboarding_browse_helper()}

    @@ -452,7 +433,7 @@ {#if keepInformed} {/if} -

    What brings you here?

    +

    {m.onboarding_step2_heading()}

    - +
    {#each intentOptions as option (option.key)} - + {:else if step === 3 && !intent}
    -

    You're in.

    +

    {m.onboarding_confirm_a_title()}

    {@render checkboxConfirmations()} {@render nextStepBlock()} {@render confirmationFooter()} @@ -539,27 +522,25 @@ {#if mode === 'contact'}
    -

    You're in, thanks for joining us.

    -

    You're all set. Here are a few ways to make a difference today.

    +

    {m.onboarding_confirm_b_title()}

    +

    {m.onboarding_confirm_b_sub()}

    {@render checkboxConfirmations()}
    {:else}
    -

    Take action right now.

    +

    {m.onboarding_browse_header_title()}

    - Below are some actions you can take right now. The best way to stay informed about new - opportunities is to sign up below. + {m.onboarding_browse_header_sub()}

    {#if browseSignedUp}
    - ✓ You're in. We'll connect you with your local PauseAI chapter and keep you updated on - global campaigns. + {m.onboarding_browse_signed_up()}
    {:else}
    -

    Keep me informed

    -

    Connect me with my local PauseAI chapter and keep me updated on global campaigns.

    +

    {m.onboarding_browse_keep_informed_title()}

    +

    {m.onboarding_browse_keep_informed_sub()}

    - +
    - + @@ -596,35 +577,35 @@

    -->
    - + {@render countrySelect('loop-country')}
    - +
    {@render gdprConsentField()}
    {/if} - + {/if} {@render confirmationFooter()} {:else if step === 3 && intent === 'volunteer'} -

    Sign up to volunteer

    -

    Tell us a bit about yourself so we can find a role that fits.

    +

    {m.onboarding_volunteer_title()}

    +

    {m.onboarding_volunteer_intro()}

    {@render checkboxConfirmations()}
    - + -

    Your 5-digit zip code is used to find your Local Group.

    +

    {m.onboarding_helper_zip()}

    {/if}
    - + -

    - If you don't have a Discord account, we encourage you to - create one here. -

    +

    {@html safeHtml(m.onboarding_helper_discord())}

    - +
    (dialCodeEdited = true)} /> @@ -685,99 +663,99 @@ type="tel" id="vol-phone" class="phone-number" - placeholder="07123 456789" + placeholder={m.onboarding_placeholder_phone()} bind:value={volunteer.phone} />
    - + {#if volunteer.languages.includes('Other')} {/if}
    - + {#if showDiscoverySpecify} {/if}
    - What motivated you to join? + {m.onboarding_field_motivations()}
    - {@render selectCards('motivations', MOTIVATIONS, volunteer.motivations)} + {@render selectCards('motivations', getMotivations(), volunteer.motivations)}
    {#if volunteer.motivations.includes('Other')} {/if}
    - Skills & interests + {m.onboarding_field_skills()}
    - {@render selectCards('skills', SKILLS, volunteer.skills)} + {@render selectCards('skills', getSkills(), volunteer.skills)}
    {#if volunteer.skills.includes('Other')} {/if}
    - How much time can you commit weekly? * + {m.onboarding_field_hours()}
    - {#each WEEKLY_HOURS as option (option)} + {#each getWeeklyHours() as option (option.value)} {/each}
    @@ -791,121 +769,78 @@ bind:checked={agreements.volunteer} /> - I agree with the Volunteer Agreement * + {@html safeHtml(m.onboarding_agree_volunteer())} - + {:else if step === 3 && intent === 'lead'} -

    {leadRole} Volunteer Description

    -

    Part-time volunteer role · 5–15 hours/week

    +

    {m.onboarding_lead_title({ role: leadRole })}

    +

    {m.onboarding_lead_meta()}

    {@render checkboxConfirmations()}
    -

    - Pause AI Global is looking for leaders around the world to run local groups that lead - organising efforts in their area. This is a part-time volunteer role requiring 5-15 hours - per week. Each group leader will be responsible for planning direct actions, mobilising - volunteers and coordinating with the Global PauseAI team. -

    +

    {m.onboarding_lead_intro()}

    {#if basics.country === 'United States'} -

    - Since you're based in the United States, please apply through PauseAI - US: - - PauseAI US application form - . -

    +

    {@html safeHtml(m.onboarding_lead_us_branch())}

    {:else if countryHasChapter} -

    - {basics.country} already has a PauseAI chapter, so rather than founding - a national group, you could lead a regional or city group within it. Find your chapter - at - pauseai.info/communities, or email our Organizing - Director below to talk it through. -

    +

    {@html safeHtml(m.onboarding_lead_has_chapter({ country: basics.country }))}

    {:else} -

    - First, check that your country doesn't already have a chapter: - pauseai.info/communities. -

    +

    {@html safeHtml(m.onboarding_lead_check_chapter())}

    {/if} -

    What you'll do

    +

    {m.onboarding_lead_what_you_do_title()}

      -
    • - Recruit and grow your local group by welcoming new volunteers and organising events - together. -
    • -
    • Build relationships with local activist groups and journalists.
    • -
    • - Meet monthly with PauseAI's Global team to swap ideas and stay coordinated with the - global strategy and quarterly campaigns. -
    • -
    • Share what your chapter is up to on social media and help promote events.
    • +
    • {m.onboarding_lead_do_1()}
    • +
    • {m.onboarding_lead_do_2()}
    • +
    • {m.onboarding_lead_do_3()}
    • +
    • {m.onboarding_lead_do_4()}
    -

    What we're looking for

    +

    {m.onboarding_lead_looking_for_title()}

      -
    • - Ability to plan and execute direct actions, including workshops and public events. -
    • -
    • Excellent communication skills, eager and able to engage diverse communities.
    • -
    • Self-motivated, with the ability to work independently and as part of a team.
    • -
    • Passion for AI safety and alignment with PauseAI’s mission.
    • -
    • Comfortable communicating in English
    • -
    • Adhere and practice to a non-violent, legal approach.
    • +
    • {m.onboarding_lead_looking_1()}
    • +
    • {m.onboarding_lead_looking_2()}
    • +
    • {m.onboarding_lead_looking_3()}
    • +
    • {m.onboarding_lead_looking_4()}
    • +
    • {m.onboarding_lead_looking_5()}
    • +
    • {m.onboarding_lead_looking_6()}
    -

    What would be nice to have

    +

    {m.onboarding_lead_nice_to_have_title()}

      -
    • Strong organising skills and experience in grassroots activism.
    • -
    • Lobbying skills and policymaker engagement experience
    • -
    • Media and content writing experience
    • +
    • {m.onboarding_lead_nice_1()}
    • +
    • {m.onboarding_lead_nice_2()}
    • +
    • {m.onboarding_lead_nice_3()}
    -

    - If you do not meet all of the requirements, we still want to hear from you. The commitment - to AI Safety and the belief we can make an impact is the most important factor; we can - provide training, resources and a community to lean on. -

    -

    Support you'll get

    -

    - You'll join a global network of organisers who are all figuring this out together. We meet - monthly, share resources across our chat platforms and help each other troubleshoot. It's - a great way to learn new skills, meet thoughtful people and be part of a fast-growing - movement that's working on one of the most important issues of our time. -

    -

    Next steps

    -

    - If you’d like to learn more, please contact our PauseAI Global organising director Irina - Tavera at - Irina@pauseai.info to schedule an informal exploratory - chat. -

    +

    {m.onboarding_lead_imperfect()}

    +

    {m.onboarding_lead_support_title()}

    +

    {m.onboarding_lead_support()}

    +

    {m.onboarding_lead_next_steps_title()}

    +

    {@html safeHtml(m.onboarding_lead_next_steps())}

    - ✉️ Email our Organizing Director + {m.onboarding_lead_email_btn()} -

    Please let her know the following:

    +

    {m.onboarding_lead_please_mention()}

      -
    • Your name
    • -
    • Your country of residence
    • -
    • A few sentences on why you’d like to become a lead
    • +
    • {m.onboarding_lead_mention_1()}
    • +
    • {m.onboarding_lead_mention_2()}
    • +
    • {m.onboarding_lead_mention_3()}
    {@render confirmationFooter()} {:else if step === 4 && intent === 'volunteer'}
    -

    Welcome to the team.

    -

    You're on the volunteer list. We'll be in touch soon.

    +

    {m.onboarding_confirm_volunteer_title()}

    +

    {m.onboarding_confirm_volunteer_sub()}

    {@render checkboxConfirmations()} {@render nextStepBlock()} {@render confirmationFooter()} diff --git a/src/lib/components/onboarding/options.ts b/src/lib/components/onboarding/options.ts index 483f0fbb1..d55d1c037 100644 --- a/src/lib/components/onboarding/options.ts +++ b/src/lib/components/onboarding/options.ts @@ -1,9 +1,10 @@ // Controlled vocabularies for the onboarding pipeline form. -// Sources: `Tally wbGvKe Field Spec.md` (extracted 2026-06-10) and -// `Volunteer Onboarding Pipeline - Plan of Action.md` (decisions 2026-06-10). + +import * as m from '$lib/paraglide/messages.js' export const INTENTS = ['Keep informed', 'Act now', 'Volunteer', 'Lead'] as const export type Intent = (typeof INTENTS)[number] +export type IntentKey = 'act-now' | 'volunteer' | 'lead' export const SIGNUP_SOURCE = 'June 2026 onboarding flow' @@ -510,3 +511,92 @@ export const WEEKLY_HOURS = [ '10-20 hours', '20+ hours' ] + +export function getDiscoveryOptions(): { value: string; label: string }[] { + return [ + { value: 'PauseAI affiliated social media', label: m.onboarding_discovery_pauseai_social() }, + { value: 'Non-PauseAI affiliated social media', label: m.onboarding_discovery_other_social() }, + { value: 'Friend/Family referral', label: m.onboarding_discovery_referral() }, + { value: 'News article', label: m.onboarding_discovery_news() }, + { value: 'Event/Presentation', label: m.onboarding_discovery_event() }, + { value: 'Internet search', label: m.onboarding_discovery_search() }, + { value: 'Other', label: m.onboarding_discovery_other() } + ] +} + +export function getMotivations(): { value: string; label: string }[] { + return [ + { value: 'AI Safety', label: m.onboarding_motivation_ai_safety() }, + { + value: 'Need for democratic oversight', + label: m.onboarding_motivation_democratic_oversight() + }, + { value: 'Ethical technology', label: m.onboarding_motivation_ethical_tech() }, + { value: 'AI Governance', label: m.onboarding_motivation_ai_governance() }, + { value: 'Job Displacement', label: m.onboarding_motivation_job_displacement() }, + { value: 'Misinformation', label: m.onboarding_motivation_misinformation() }, + { value: 'Deepfake scams and harassment', label: m.onboarding_motivation_deepfakes() }, + { value: 'Concentration of power', label: m.onboarding_motivation_power() }, + { value: 'Privacy loss', label: m.onboarding_motivation_privacy() }, + { value: 'Environmental damage', label: m.onboarding_motivation_environment() }, + { value: 'Technology addiction', label: m.onboarding_motivation_addiction() }, + { value: 'Autonomous weapons', label: m.onboarding_motivation_weapons() }, + { value: 'Cyberattacks', label: m.onboarding_motivation_cyber() }, + { value: 'Bioweapons', label: m.onboarding_motivation_bioweapons() }, + { value: 'Other', label: m.onboarding_motivation_other() } + ] +} + +export function getSkills(): { value: string; label: string }[] { + return [ + { value: 'Software Development', label: m.onboarding_skill_software() }, + { value: 'Video Creation', label: m.onboarding_skill_video() }, + { value: 'Social Media Management', label: m.onboarding_skill_social_media() }, + { value: 'Event Organization', label: m.onboarding_skill_events() }, + { value: 'Public Speaking/ Presentation', label: m.onboarding_skill_speaking() }, + { value: 'Writing', label: m.onboarding_skill_writing() }, + { value: 'Graphic Design/ Visual Arts', label: m.onboarding_skill_design() }, + { value: 'Research', label: m.onboarding_skill_research() }, + { value: 'Communications/ PR', label: m.onboarding_skill_comms() }, + { value: 'Fundraising', label: m.onboarding_skill_fundraising() }, + { value: 'Community Organizing', label: m.onboarding_skill_community() }, + { value: 'Political Advocacy/ Lobbying', label: m.onboarding_skill_advocacy() }, + { value: 'Education/ Teaching', label: m.onboarding_skill_education() }, + { value: 'Administrative Support', label: m.onboarding_skill_admin() }, + { value: 'Legal Knowledge', label: m.onboarding_skill_legal() }, + { value: 'Other', label: m.onboarding_skill_other() } + ] +} + +export function getWeeklyHours(): { value: string; label: string }[] { + return [ + { value: 'Less than 3 hours', label: m.onboarding_hours_under_3() }, + { value: '3-6 hours', label: m.onboarding_hours_3_6() }, + { value: '6-10 hours', label: m.onboarding_hours_6_10() }, + { value: '10-20 hours', label: m.onboarding_hours_10_20() }, + { value: '20+ hours', label: m.onboarding_hours_20_plus() } + ] +} + +export function getIntentOptions(): { key: IntentKey; icon: string; label: string; sub: string }[] { + return [ + { + key: 'act-now', + icon: '✊', + label: m.onboarding_intent_act_now_label(), + sub: m.onboarding_intent_act_now_sub() + }, + { + key: 'volunteer', + icon: '🤝', + label: m.onboarding_intent_volunteer_label(), + sub: m.onboarding_intent_volunteer_sub() + }, + { + key: 'lead', + icon: '🚀', + label: m.onboarding_intent_lead_label(), + sub: m.onboarding_intent_lead_sub() + } + ] +} diff --git a/src/lib/env.ts b/src/lib/env.ts index 61aefc29d..ef20f03ab 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -74,13 +74,20 @@ export function possiblyOverriddenLocales( if (inclusive) { const unknown = namedLocales.filter((l) => l !== 'all' && !defaults.locales.includes(l)) if (unknown.length > 0) { - console.error( - `Error: ${source} requests "${unknown.join(', ')}" but default-settings.js only has [${defaults.locales.join(', ')}].` - ) - console.error( - `Add the locale(s) to default-settings.js or fix your .env / PARAGLIDE_LOCALES.` - ) - process.exit(1) + if (isDev(env)) { + console.warn( + `Warning: ${source} requests "${unknown.join(', ')}" which are not in default-settings.js. Adding for local dev only.` + ) + result.push(...unknown) + } else { + console.error( + `Error: ${source} requests "${unknown.join(', ')}" but default-settings.js only has [${defaults.locales.join(', ')}].` + ) + console.error( + `Add the locale(s) to default-settings.js or fix your .env / PARAGLIDE_LOCALES.` + ) + process.exit(1) + } } } console.log(`Locale source: ${source} → [${result.join(', ')}]`) diff --git a/src/lib/sanitize.ts b/src/lib/sanitize.ts new file mode 100644 index 000000000..2660e8fff --- /dev/null +++ b/src/lib/sanitize.ts @@ -0,0 +1,10 @@ +import DOMPurify from 'isomorphic-dompurify' + +const CONFIG = { + ALLOWED_TAGS: ['a', 'strong'], + ALLOWED_ATTR: ['href'] +} + +export function safeHtml(dirty: string): string { + return DOMPurify.sanitize(dirty, CONFIG) as string +} diff --git a/src/routes/embed/onboarding-form/+page.server.ts b/src/routes/embed/onboarding-form/+page.server.ts index b82d87be7..2d119fc22 100644 --- a/src/routes/embed/onboarding-form/+page.server.ts +++ b/src/routes/embed/onboarding-form/+page.server.ts @@ -1,6 +1,8 @@ import { fail } from '@sveltejs/kit' +import * as m from '$lib/paraglide/messages.js' import type { FieldSet } from 'airtable' -import type { Actions } from './$types' +import type { Actions, PageServerLoad } from './$types' +import { isLocale, setLocale } from '$lib/paraglide/runtime' import type { NationalGroupsApiResponse } from '$api/national-groups/+server.js' import { createRecord, updateRecord } from '$lib/airtable' import { isOnboardingLive } from '$lib/server/onboarding' @@ -20,6 +22,13 @@ import { export const prerender = false +export const load: PageServerLoad = ({ url }) => { + const locale = url.searchParams.get('locale') + if (locale && isLocale(locale)) { + setLocale(locale) + } +} + // Write target per the Plan of Action: base "PauseAI Volunteers & Actions", // table "Members". const AIRTABLE_BASE_ID = 'appWPTGqZmUcs3NWu' @@ -90,21 +99,21 @@ export const actions: Actions = { const hasVolunteerDetails = data.get('volunteer_details') === 'on' if (!fullName || !email || !country || !city) { - return fail(400, { message: 'Please fill in your name, email, country and city.' }) + return fail(400, { message: m.onboarding_error_missing_fields() }) } if (!/^\S+@\S+\.\S+$/.test(email)) { - return fail(400, { message: 'Please enter a valid email address.' }) + return fail(400, { message: m.onboarding_error_invalid_email() }) } if (!COUNTRIES.includes(country)) { - return fail(400, { message: 'Please select a country from the list.' }) + return fail(400, { message: m.onboarding_error_invalid_country() }) } if (!isIntent(intent)) { - return fail(400, { message: 'Please choose what brings you here.' }) + return fail(400, { message: m.onboarding_error_missing_intent() }) } // Updates (volunteer step 3) carry an existing record id and already // consented at step 2; require consent only when creating a record. if (!existingRecordId && !gdprAgreed) { - return fail(400, { message: 'Please agree to the data processing consent to continue.' }) + return fail(400, { message: m.onboarding_error_missing_gdpr() }) } const fields: FieldSet = { @@ -129,18 +138,18 @@ export const actions: Actions = { const hours = getString(data, 'hours') if (!languages.length) { - return fail(400, { message: 'Please select at least one language you speak.' }) + return fail(400, { message: m.onboarding_error_missing_languages() }) } if (!WEEKLY_HOURS.includes(hours)) { - return fail(400, { message: 'Please tell us how much time you can commit weekly.' }) + return fail(400, { message: m.onboarding_error_missing_hours() }) } if (data.get('agree_volunteer') !== 'on' || data.get('agree_conduct') !== 'on') { return fail(400, { - message: 'Please agree to the Volunteer Agreement and Code of Conduct.' + message: m.onboarding_error_missing_agreements() }) } if (discovery && !DISCOVERY_OPTIONS.includes(discovery)) { - return fail(400, { message: 'Please select a valid option for how you found us.' }) + return fail(400, { message: m.onboarding_error_invalid_discovery() }) } fields['Discord Username'] = getString(data, 'discord_username') @@ -176,14 +185,14 @@ export const actions: Actions = { if (recordId) { const updated = await updateRecord(AIRTABLE_BASE_ID, MEMBERS_TABLE_ID, recordId, fields) if (!updated) { - return fail(502, { message: "Sorry, we couldn't save your details. Please try again." }) + return fail(502, { message: m.onboarding_error_save_failed() }) } } else { recordId = await createRecord(AIRTABLE_BASE_ID, MEMBERS_TABLE_ID, fields) // A failed write returns undefined; surface it instead of telling // the user they're signed up when no record was created. if (!recordId) { - return fail(502, { message: "Sorry, we couldn't save your details. Please try again." }) + return fail(502, { message: m.onboarding_error_save_failed() }) } // Subscription happens on the initial (step 2) submission only; // the volunteer-form update never re-subscribes. diff --git a/src/routes/embed/onboarding-form/+page.svelte b/src/routes/embed/onboarding-form/+page.svelte index da51566b8..118f4e0ea 100644 --- a/src/routes/embed/onboarding-form/+page.svelte +++ b/src/routes/embed/onboarding-form/+page.svelte @@ -3,10 +3,16 @@ import { page } from '$app/state' import OnboardingFlow from '$lib/components/onboarding/OnboardingFlow.svelte' import PostMeta from '$lib/components/PostMeta.svelte' + import * as m from '$lib/paraglide/messages.js' + import { isLocale, setLocale } from '$lib/paraglide/runtime' - const title = 'Get involved' - const description = - 'Find the highest-impact way for you to help pause the development of superhuman AI.' + $effect(() => { + const locale = page.url.searchParams.get('locale') + if (locale && isLocale(locale)) setLocale(locale) + }) + + const title = $derived(m.onboarding_page_title()) + const description = $derived(m.onboarding_page_description()) const initialCountry = $derived(page.url.searchParams.get('country') ?? '') From bd61ffd40c837698ae37f3daa50d106ebb2c9192 Mon Sep 17 00:00:00 2001 From: Harry Turnbull <52940497+hturnbull93@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:54:53 +0100 Subject: [PATCH 2/7] fix: replace isomorphic-dompurify with runtime-agnostic safeHtml isomorphic-dompurify instantiates jsdom at module load time, which fails in Netlify's Deno edge runtime. Replace with a dependency-free allowlist sanitizer that works in any JS environment. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_015aKhZeP6x8pgBkDEraXGUs --- package.json | 2 - pnpm-lock.yaml | 295 +------------------------------------------- src/lib/sanitize.ts | 21 ++-- 3 files changed, 15 insertions(+), 303 deletions(-) diff --git a/package.json b/package.json index b04ea7b1d..a30666998 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "@sveltejs/adapter-netlify": "^5.2.4", "@sveltejs/kit": "^2.65.2", "@sveltejs/vite-plugin-svelte": "^7.1.2", - "@types/dompurify": "^3.2.0", "@types/escape-html": "^1.0.4", "@types/glidejs__glide": "^3.6.6", "@types/minimist": "^1.2.5", @@ -105,7 +104,6 @@ "github-slugger": "^2.0.0", "html-to-image": "^1.11.13", "http-status-codes": "^2.3.0", - "isomorphic-dompurify": "^3.18.0", "maplibre-gl": "^5.24.0", "maplibregl-mapbox-request-transformer": "^0.0.3", "micromark": "^4.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ddeac7e8..7677696c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,9 +77,6 @@ importers: http-status-codes: specifier: ^2.3.0 version: 2.3.0 - isomorphic-dompurify: - specifier: ^3.18.0 - version: 3.18.0 maplibre-gl: specifier: ^5.24.0 version: 5.24.0 @@ -141,9 +138,6 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: ^7.1.2 version: 7.1.2(svelte@5.56.3(@typescript-eslint/types@8.62.1))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) - '@types/dompurify': - specifier: ^3.2.0 - version: 3.2.0 '@types/escape-html': specifier: ^1.0.4 version: 1.0.4 @@ -266,7 +260,7 @@ importers: version: 1.4.11(rolldown@1.0.3)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) vitest: specifier: ^4.1.9 - version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages: @@ -284,21 +278,6 @@ packages: '@apm-js-collab/tracing-hooks@0.10.0': resolution: {integrity: sha512-2/Z3NTewJTruUkmsSnBC5bJlLNUd9keuD1OLlTEpim4FyLhm6m2Rnfv+wrFdUvFfhmH8CRdiDZBqBrn+wyaGuA==} - '@asamuzakjp/css-color@5.1.11': - resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - '@asamuzakjp/dom-selector@7.1.1': - resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - '@asamuzakjp/generational-cache@1.0.1': - resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - '@asamuzakjp/nwsapi@2.3.9': - resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} - '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -366,10 +345,6 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@bramus/specificity@2.4.2': - resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} - hasBin: true - '@cacheable/memory@2.0.9': resolution: {integrity: sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==} @@ -380,10 +355,6 @@ packages: resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==} engines: {node: '>=18'} - '@csstools/color-helpers@6.1.0': - resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} - engines: {node: '>=20.19.0'} - '@csstools/css-calc@3.2.1': resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} engines: {node: '>=20.19.0'} @@ -391,13 +362,6 @@ packages: '@csstools/css-parser-algorithms': ^4.0.0 '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-color-parser@4.1.9': - resolution: {integrity: sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==} - engines: {node: '>=20.19.0'} - peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-parser-algorithms@4.0.0': resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} engines: {node: '>=20.19.0'} @@ -833,15 +797,6 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@exodus/bytes@1.15.1': - resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - peerDependencies: - '@noble/hashes': ^1.8.0 || ^2.0.0 - peerDependenciesMeta: - '@noble/hashes': - optional: true - '@fontsource/roboto-slab@5.2.8': resolution: {integrity: sha512-8+iMCsoUZsDwQUe5omwCp7JPNTVdyAgay5AdhmnFZPEVIVabujrmYaFkSuZ1+GUemPEWlzEQ6aQkg2mPL84SAA==} @@ -1758,10 +1713,6 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/dompurify@3.2.0': - resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} - deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. - '@types/escape-html@1.0.4': resolution: {integrity: sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==} @@ -2032,9 +1983,6 @@ packages: bcp-47@2.1.0: resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==} - bidi-js@1.0.3: - resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} - brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} @@ -2164,10 +2112,6 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - data-urls@7.0.0: - resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2177,9 +2121,6 @@ packages: supports-color: optional: true - decimal.js@10.6.0: - resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} @@ -2229,9 +2170,6 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.4.11: - resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} - domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -2260,10 +2198,6 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - entities@8.0.0: - resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} - engines: {node: '>=20.19.0'} - env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -2682,10 +2616,6 @@ packages: hookified@2.2.0: resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} - html-encoding-sniffer@6.0.0: - resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - html-tags@5.1.0: resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==} engines: {node: '>=20.10'} @@ -2776,9 +2706,6 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} @@ -2796,10 +2723,6 @@ packages: resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} engines: {node: '>=18'} - isomorphic-dompurify@3.18.0: - resolution: {integrity: sha512-ajp0D8laIHeoYlhBTevpE2HUhqWaqLXFk6K/wV3Ok8kDraBZpZsifwVWaY8IfJntMRIo1VSksgKV+lXyet9Q7A==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} - jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -2817,15 +2740,6 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsdom@29.1.1: - resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} - peerDependencies: - canvas: ^3.0.0 - peerDependenciesMeta: - canvas: - optional: true - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -3050,10 +2964,6 @@ packages: resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} engines: {node: 20 || >=22} - lru-cache@11.5.1: - resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} - engines: {node: 20 || >=22} - lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -3382,9 +3292,6 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - parse5@8.0.1: - resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3595,10 +3502,6 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} - saxes@6.0.0: - resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} - engines: {node: '>=v12.22.7'} - schema-dts-lib@1.0.0: resolution: {integrity: sha512-9MEO5vpQH9JdBioUupqluzxSYxPLjhmqRUudk15adUl/ypnRsM2/M1kN3AmVJQeG7nZqcL68H8JlGqQQT6vy9A==} engines: {node: '>=14.0.0'} @@ -3777,9 +3680,6 @@ packages: svg-tags@1.0.0: resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} - symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - table@6.9.0: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} @@ -3808,13 +3708,6 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} - tldts-core@7.4.5: - resolution: {integrity: sha512-pGrwzZDvPwKe+7NNUqAunb6rqTfynr0VOUhCMdqbu5xlvNiszsAJygRzwvpVycdzejlbpY+SWJOn+s75Og7FEA==} - - tldts@7.4.5: - resolution: {integrity: sha512-RfEzKWcq5fHUOFq7J3rl3Oz6ylKGtcHqUznzj4EcXsxLSIjJcvpbXAQtWGeJQ0xKnimR5e0Cn+cn9TssfMzm+g==} - hasBin: true - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3823,17 +3716,9 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tough-cookie@6.0.1: - resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} - engines: {node: '>=16'} - tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@6.0.0: - resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} - engines: {node: '>=20'} - trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} @@ -3897,10 +3782,6 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - undici@7.28.0: - resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} - engines: {node: '>=20.18.1'} - unicorn-magic@0.4.0: resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} engines: {node: '>=20'} @@ -4084,10 +3965,6 @@ packages: jsdom: optional: true - w3c-xmlserializer@5.0.0: - resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} - engines: {node: '>=18'} - walk-up-path@4.0.0: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} @@ -4099,21 +3976,9 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@8.0.1: - resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} - engines: {node: '>=20'} - webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - whatwg-mimetype@5.0.0: - resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} - engines: {node: '>=20'} - - whatwg-url@16.0.1: - resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4144,13 +4009,6 @@ packages: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} - xml-name-validator@5.0.0: - resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} - engines: {node: '>=18'} - - xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -4214,26 +4072,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@asamuzakjp/css-color@5.1.11': - dependencies: - '@asamuzakjp/generational-cache': 1.0.1 - '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-color-parser': 4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - - '@asamuzakjp/dom-selector@7.1.1': - dependencies: - '@asamuzakjp/generational-cache': 1.0.1 - '@asamuzakjp/nwsapi': 2.3.9 - bidi-js: 1.0.3 - css-tree: 3.2.1 - is-potential-custom-element-name: 1.0.1 - - '@asamuzakjp/generational-cache@1.0.1': {} - - '@asamuzakjp/nwsapi@2.3.9': {} - '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -4334,10 +4172,6 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@bramus/specificity@2.4.2': - dependencies: - css-tree: 3.2.1 - '@cacheable/memory@2.0.9': dependencies: '@cacheable/utils': 2.4.1 @@ -4354,20 +4188,11 @@ snapshots: dependencies: fontkitten: 1.0.3 - '@csstools/color-helpers@6.1.0': {} - '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-color-parser@4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': - dependencies: - '@csstools/color-helpers': 6.1.0 - '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-tokenizer': 4.0.0 @@ -4658,8 +4483,6 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@exodus/bytes@1.15.1': {} - '@fontsource/roboto-slab@5.2.8': {} '@fontsource/saira-condensed@5.2.8': {} @@ -5418,10 +5241,6 @@ snapshots: '@types/deep-eql@4.0.2': {} - '@types/dompurify@3.2.0': - dependencies: - dompurify: 3.4.11 - '@types/escape-html@1.0.4': {} '@types/esrecurse@4.3.1': {} @@ -5724,10 +5543,6 @@ snapshots: is-alphanumerical: 2.0.1 is-decimal: 2.0.1 - bidi-js@1.0.3: - dependencies: - require-from-string: 2.0.2 - brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -5842,19 +5657,10 @@ snapshots: csstype@3.2.3: {} - data-urls@7.0.0: - dependencies: - whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 - transitivePeerDependencies: - - '@noble/hashes' - debug@4.4.3: dependencies: ms: 2.1.3 - decimal.js@10.6.0: {} - decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 @@ -5891,10 +5697,6 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.4.11: - optionalDependencies: - '@types/trusted-types': 2.0.7 - domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -5919,8 +5721,6 @@ snapshots: entities@4.5.0: {} - entities@8.0.0: {} - env-paths@2.2.1: {} error-ex@1.3.4: @@ -6389,12 +6189,6 @@ snapshots: hookified@2.2.0: {} - html-encoding-sniffer@6.0.0: - dependencies: - '@exodus/bytes': 1.15.1 - transitivePeerDependencies: - - '@noble/hashes' - html-tags@5.1.0: {} html-to-image@1.11.13: {} @@ -6465,8 +6259,6 @@ snapshots: is-plain-obj@4.1.0: {} - is-potential-custom-element-name@1.0.1: {} - is-reference@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -6479,14 +6271,6 @@ snapshots: isexe@3.1.5: {} - isomorphic-dompurify@3.18.0: - dependencies: - dompurify: 3.4.11 - jsdom: 29.1.1 - transitivePeerDependencies: - - '@noble/hashes' - - canvas - jiti@2.7.0: {} js-sha256@0.11.1: {} @@ -6499,32 +6283,6 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@29.1.1: - dependencies: - '@asamuzakjp/css-color': 5.1.11 - '@asamuzakjp/dom-selector': 7.1.1 - '@bramus/specificity': 2.4.2 - '@csstools/css-syntax-patches-for-csstree': 1.1.5(css-tree@3.2.1) - '@exodus/bytes': 1.15.1 - css-tree: 3.2.1 - data-urls: 7.0.0 - decimal.js: 10.6.0 - html-encoding-sniffer: 6.0.0 - is-potential-custom-element-name: 1.0.1 - lru-cache: 11.5.1 - parse5: 8.0.1 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 6.0.1 - undici: 7.28.0 - w3c-xmlserializer: 5.0.0 - webidl-conversions: 8.0.1 - whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 - xml-name-validator: 5.0.0 - transitivePeerDependencies: - - '@noble/hashes' - jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -6696,8 +6454,6 @@ snapshots: lru-cache@11.2.7: {} - lru-cache@11.5.1: {} - lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -7293,10 +7049,6 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parse5@8.0.1: - dependencies: - entities: 8.0.0 - path-exists@4.0.0: {} path-key@3.1.1: {} @@ -7489,10 +7241,6 @@ snapshots: dependencies: mri: 1.2.0 - saxes@6.0.0: - dependencies: - xmlchars: 2.2.0 - schema-dts-lib@1.0.0(typescript@6.0.3): dependencies: typescript: 6.0.3 @@ -7766,8 +7514,6 @@ snapshots: svg-tags@1.0.0: {} - symbol-tree@3.2.4: {} - table@6.9.0: dependencies: ajv: 8.20.0 @@ -7793,28 +7539,14 @@ snapshots: tinyrainbow@3.1.0: {} - tldts-core@7.4.5: {} - - tldts@7.4.5: - dependencies: - tldts-core: 7.4.5 - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 totalist@3.0.1: {} - tough-cookie@6.0.1: - dependencies: - tldts: 7.4.5 - tr46@0.0.3: {} - tr46@6.0.0: - dependencies: - punycode: 2.3.1 - trough@2.2.0: {} ts-api-utils@2.5.0(typescript@6.0.3): @@ -7868,8 +7600,6 @@ snapshots: undici-types@7.18.2: {} - undici@7.28.0: {} - unicorn-magic@0.4.0: {} unified@11.0.5: @@ -8005,7 +7735,7 @@ snapshots: optionalDependencies: vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) - vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): + vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.9 '@vitest/mocker': 4.1.9(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) @@ -8030,34 +7760,17 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 24.13.2 - jsdom: 29.1.1 transitivePeerDependencies: - msw - w3c-xmlserializer@5.0.0: - dependencies: - xml-name-validator: 5.0.0 - walk-up-path@4.0.0: {} web-streams-polyfill@4.0.0-beta.3: {} webidl-conversions@3.0.1: {} - webidl-conversions@8.0.1: {} - webpack-virtual-modules@0.6.2: {} - whatwg-mimetype@5.0.0: {} - - whatwg-url@16.0.1: - dependencies: - '@exodus/bytes': 1.15.1 - tr46: 6.0.0 - webidl-conversions: 8.0.1 - transitivePeerDependencies: - - '@noble/hashes' - whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -8086,10 +7799,6 @@ snapshots: dependencies: signal-exit: 4.1.0 - xml-name-validator@5.0.0: {} - - xmlchars@2.2.0: {} - yallist@3.1.1: {} yaml@1.10.3: {} diff --git a/src/lib/sanitize.ts b/src/lib/sanitize.ts index 2660e8fff..1a379688c 100644 --- a/src/lib/sanitize.ts +++ b/src/lib/sanitize.ts @@ -1,10 +1,15 @@ -import DOMPurify from 'isomorphic-dompurify' - -const CONFIG = { - ALLOWED_TAGS: ['a', 'strong'], - ALLOWED_ATTR: ['href'] -} - export function safeHtml(dirty: string): string { - return DOMPurify.sanitize(dirty, CONFIG) as string + // Rewrite tags: keep only href, block javascript:/data: URIs + let result = dirty.replace(/]*>/gi, (match) => { + const hrefMatch = match.match(/\bhref\s*=\s*"([^"]*)"/i) + if (!hrefMatch) return '' + const href = hrefMatch[1] + if (/^\s*(javascript|data|vbscript):/i.test(href)) return '' + return `` + }) + // Strip attributes from + result = result.replace(/]*>/gi, '') + // Strip all other tags (including event handlers on any tag) + result = result.replace(/<(?!\/?(a|strong)\b)[^>]+>/gi, '') + return result } From 1e7cb69abaf35d7a2d96c0e754b10007380c8410 Mon Sep 17 00:00:00 2001 From: Harry Turnbull <52940497+hturnbull93@users.noreply.github.com> Date: Thu, 2 Jul 2026 08:20:03 +0100 Subject: [PATCH 3/7] refactor: replace paraglide with hardcoded translations for onboarding form Removes the dependency on the l10n cage and paraglide runtime for the onboarding embed. Translations are now hardcoded in TypeScript, making them reviewable via PR and deployable without any external tooling. - Add messages.ts with OnboardingMessages type + en/de translations - Add i18n.svelte.ts with reactive locale state (setOnboardingLocale, isOnboardingLocale, getMessages) - Update OnboardingFlow, ActionCards, +page.svelte to use getMessages() instead of paraglide m.* calls - Update options.ts getter functions to accept OnboardingMessages param - Drop paraglide locale handling from +page.server.ts (locale is now purely client-side, driven by ?locale= URL param) - Remove onboarding_* keys from messages/en.json (no longer used) - Revert env.ts PARAGLIDE_LOCALES dev-override (no longer needed) ?locale=de now works on any deploy without cage access or OpenRouter key. Adding a new language is a single PR adding a new locale object to messages.ts. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_015aKhZeP6x8pgBkDEraXGUs --- messages/en.json | 192 +----- .../components/onboarding/ActionCards.svelte | 32 +- .../onboarding/OnboardingFlow.svelte | 248 ++++---- src/lib/components/onboarding/i18n.svelte.ts | 25 + src/lib/components/onboarding/messages.ts | 591 ++++++++++++++++++ src/lib/components/onboarding/options.ts | 112 ++-- src/lib/env.ts | 21 +- .../embed/onboarding-form/+page.server.ts | 33 +- src/routes/embed/onboarding-form/+page.svelte | 14 +- 9 files changed, 844 insertions(+), 424 deletions(-) create mode 100644 src/lib/components/onboarding/i18n.svelte.ts create mode 100644 src/lib/components/onboarding/messages.ts diff --git a/messages/en.json b/messages/en.json index 90699da92..ba93fed34 100644 --- a/messages/en.json +++ b/messages/en.json @@ -104,195 +104,5 @@ "learn_building_the_pause_button": "[Building the Pause button](/building-the-pause-button). What it takes to pause AI.", "learn_faq": "[FAQ](/faq). Commonly asked questions about AI safety and PauseAI.", "learn_action": "[Action](/action). What you can do to help (with links to many action-related guides)", - "footer_info_about": "About Us", - "onboarding_page_title": "Get involved", - "onboarding_page_description": "Find the highest-impact way for you to help pause the development of superhuman AI.", - "onboarding_browse_banner": "You're browsing without signing up, leave your email below so we can tell you when new opportunities go live.", - "onboarding_field_full_name": "Full name *", - "onboarding_placeholder_full_name": "Full name", - "onboarding_field_email": "Email *", - "onboarding_placeholder_email": "Email", - "onboarding_helper_email_gmail": "Preferably Gmail if you have one.", - "onboarding_field_country": "Country of residence *", - "onboarding_placeholder_country": "Select your country", - "onboarding_field_city": "City / town of residence *", - "onboarding_placeholder_city": "City / town", - "onboarding_btn_continue": "Continue →", - "onboarding_btn_browse": "👀 I just want to take action now", - "onboarding_browse_helper": "Skip ahead to see the actions you can take today, no email required.", - "onboarding_step2_heading": "What brings you here?", - "onboarding_intent_keep_informed_label": "Keep me informed", - "onboarding_intent_keep_informed_sub": "Connect me with my local PauseAI chapter, and keep me updated on global campaigns.", - "onboarding_intent_newsletter_label": "Subscribe to our Substack", - "onboarding_intent_newsletter_sub": "General news on AI, delivered via our Substack newsletter.", - "onboarding_intent_more_optional": "Want to do more? (optional)", - "onboarding_intent_act_now_label": "I want to take action now", - "onboarding_intent_act_now_sub": "Show me what I can do today.", - "onboarding_intent_volunteer_label": "I want to volunteer regularly", - "onboarding_intent_volunteer_sub": "Help me find a role that fits.", - "onboarding_intent_lead_label": "I want to lead", - "onboarding_intent_lead_sub": "I'm ready to organize in my country or region.", - "onboarding_gdpr_consent__instructions": "Preserve the HTML tag and href attribute exactly. Only translate the visible text around and inside the tag.", - "onboarding_gdpr_consent": "I agree to the Privacy Policy, including sharing my details with my local PauseAI chapter (which may be a separate entity to PauseAI Global) for local coordination. *", - "onboarding_btn_submit": "Submit →", - "onboarding_btn_submitting": "Submitting...", - "onboarding_btn_back": "← Back", - "onboarding_confirm_keep_informed": "We'll connect you with your local PauseAI chapter and keep you updated on global campaigns.", - "onboarding_confirm_newsletter": "You're subscribed to our Substack newsletter for general news on AI.", - "onboarding_confirm_a_title": "You're in.", - "onboarding_confirm_b_title": "You're in, thanks for joining us.", - "onboarding_confirm_b_sub": "You're all set. Here are a few ways to make a difference today.", - "onboarding_confirm_volunteer_title": "Welcome to the team.", - "onboarding_confirm_volunteer_sub": "You're on the volunteer list. We'll be in touch soon.", - "onboarding_browse_header_title": "Take action right now.", - "onboarding_browse_header_sub": "Below are some actions you can take right now. The best way to stay informed about new opportunities is to sign up below.", - "onboarding_browse_signed_up": "✓ You're in. We'll connect you with your local PauseAI chapter and keep you updated on global campaigns.", - "onboarding_browse_keep_informed_title": "Keep me informed", - "onboarding_browse_keep_informed_sub": "Connect me with my local PauseAI chapter and keep me updated on global campaigns.", - "onboarding_btn_sign_me_up": "Sign me up →", - "onboarding_btn_signing_up": "Signing up...", - "onboarding_section_ways_to_help": "A few ways to help today", - "onboarding_next_step_title": "★ Recommended next step", - "onboarding_next_step_p1__instructions": "Preserve the HTML tag and href attribute exactly. Only translate the visible text.", - "onboarding_next_step_p1": "Join one of our Member Community Welcome Meetings, or a local social event, to find out more about PauseAI's community: see upcoming events.", - "onboarding_next_step_p2__instructions": "Preserve the HTML tag and href attribute exactly. Only translate the visible text.", - "onboarding_next_step_p2": "Want to get kick-started into action straight away? Check out our list of actions.", - "onboarding_footer_join_conversation": "Join the conversation", - "onboarding_footer_discord_btn": "Join PauseAI on Discord", - "onboarding_footer_follow_us": "Follow us", - "onboarding_step_about": "About you", - "onboarding_step_intent": "Intent", - "onboarding_step_volunteer_form": "Volunteer form", - "onboarding_step_confirmed": "Confirmed", - "onboarding_step_next_steps": "Next steps", - "onboarding_volunteer_title": "Sign up to volunteer", - "onboarding_volunteer_intro": "Tell us a bit about yourself so we can find a role that fits.", - "onboarding_field_zip": "Zip code", - "onboarding_placeholder_zip": "e.g. 02134", - "onboarding_helper_zip": "Your 5-digit zip code is used to find your Local Group.", - "onboarding_field_discord": "Discord username", - "onboarding_helper_discord__instructions": "Preserve the HTML tag and href attribute exactly. Only translate the visible text.", - "onboarding_helper_discord": "If you don't have a Discord account, we encourage you to create one here.", - "onboarding_field_phone": "Phone number", - "onboarding_placeholder_dial_code": "+1", - "onboarding_placeholder_phone": "(555) 000-0000", - "onboarding_field_languages": "What languages do you speak? *", - "onboarding_placeholder_languages": "Select languages", - "onboarding_field_discovery": "How did you find out about PauseAI?", - "onboarding_placeholder_discovery": "Select an option", - "onboarding_field_motivations": "What motivated you to join?", - "onboarding_field_skills": "Skills & interests", - "onboarding_field_hours": "How much time can you commit weekly? *", - "onboarding_placeholder_specify": "Please specify", - "onboarding_agree_volunteer__instructions": "Preserve the HTML tag and href attribute exactly. Only translate the visible text.", - "onboarding_agree_volunteer": "I agree with the Volunteer Agreement *", - "onboarding_agree_conduct__instructions": "Preserve the HTML tag and href attribute exactly. Only translate the visible text.", - "onboarding_agree_conduct": "I agree with the Code of Conduct *", - "onboarding_lead_title": "{role} Volunteer Description", - "onboarding_lead_meta": "Part-time volunteer role · 5–15 hours/week", - "onboarding_lead_intro": "Pause AI Global is looking for leaders around the world to run local groups that lead organising efforts in their area. This is a part-time volunteer role requiring 5-15 hours per week. Each group leader will be responsible for planning direct actions, mobilising volunteers and coordinating with the Global PauseAI team.", - "onboarding_lead_us_branch__instructions": "Preserve all HTML tags (, ) and href attributes exactly. Only translate the visible text.", - "onboarding_lead_us_branch": "Since you're based in the United States, please apply through PauseAI US: PauseAI US application form.", - "onboarding_lead_has_chapter__instructions": "Preserve all HTML tags (, ) and href attributes exactly. The {country} placeholder must remain unchanged — it is replaced at runtime. Only translate the surrounding visible text.", - "onboarding_lead_has_chapter": "{country} already has a PauseAI chapter, so rather than founding a national group, you could lead a regional or city group within it. Find your chapter at pauseai.info/communities, or email our Organizing Director below to talk it through.", - "onboarding_lead_check_chapter__instructions": "Preserve all HTML tags (, ) and href attributes exactly. Only translate the visible text.", - "onboarding_lead_check_chapter": "First, check that your country doesn't already have a chapter: pauseai.info/communities.", - "onboarding_lead_what_you_do_title": "What you'll do", - "onboarding_lead_do_1": "Recruit and grow your local group by welcoming new volunteers and organising events together.", - "onboarding_lead_do_2": "Build relationships with local activist groups and journalists.", - "onboarding_lead_do_3": "Meet monthly with PauseAI's Global team to swap ideas and stay coordinated with the global strategy and quarterly campaigns.", - "onboarding_lead_do_4": "Share what your chapter is up to on social media and help promote events.", - "onboarding_lead_looking_for_title": "What we're looking for", - "onboarding_lead_looking_1": "Ability to plan and execute direct actions, including workshops and public events.", - "onboarding_lead_looking_2": "Excellent communication skills, eager and able to engage diverse communities.", - "onboarding_lead_looking_3": "Self-motivated, with the ability to work independently and as part of a team.", - "onboarding_lead_looking_4": "Passion for AI safety and alignment with PauseAI's mission.", - "onboarding_lead_looking_5": "Comfortable communicating in English", - "onboarding_lead_looking_6": "Adhere and practice to a non-violent, legal approach.", - "onboarding_lead_nice_to_have_title": "What would be nice to have", - "onboarding_lead_nice_1": "Strong organising skills and experience in grassroots activism.", - "onboarding_lead_nice_2": "Lobbying skills and policymaker engagement experience", - "onboarding_lead_nice_3": "Media and content writing experience", - "onboarding_lead_imperfect": "If you do not meet all of the requirements, we still want to hear from you. The commitment to AI Safety and the belief we can make an impact is the most important factor; we can provide training, resources and a community to lean on.", - "onboarding_lead_support_title": "Support you'll get", - "onboarding_lead_support": "You'll join a global network of organisers who are all figuring this out together. We meet monthly, share resources across our chat platforms and help each other troubleshoot. It's a great way to learn new skills, meet thoughtful people and be part of a fast-growing movement that's working on one of the most important issues of our time.", - "onboarding_lead_next_steps_title": "Next steps", - "onboarding_lead_next_steps__instructions": "Preserve the HTML tag and href attribute exactly. The email address inside the tag is a proper noun and must not be translated.", - "onboarding_lead_next_steps": "If you'd like to learn more, please contact our PauseAI Global organising director Irina Tavera at Irina@pauseai.info to schedule an informal exploratory chat.", - "onboarding_lead_email_btn": "✉️ Email our Organizing Director", - "onboarding_lead_please_mention": "Please let her know the following:", - "onboarding_lead_mention_1": "Your name", - "onboarding_lead_mention_2": "Your country of residence", - "onboarding_lead_mention_3": "A few sentences on why you'd like to become a lead", - "onboarding_discovery_pauseai_social": "PauseAI affiliated social media", - "onboarding_discovery_other_social": "Non-PauseAI affiliated social media", - "onboarding_discovery_referral": "Friend/Family referral", - "onboarding_discovery_news": "News article", - "onboarding_discovery_event": "Event/Presentation", - "onboarding_discovery_search": "Internet search", - "onboarding_discovery_other": "Other", - "onboarding_motivation_ai_safety": "AI Safety", - "onboarding_motivation_democratic_oversight": "Need for democratic oversight", - "onboarding_motivation_ethical_tech": "Ethical technology", - "onboarding_motivation_ai_governance": "AI Governance", - "onboarding_motivation_job_displacement": "Job Displacement", - "onboarding_motivation_misinformation": "Misinformation", - "onboarding_motivation_deepfakes": "Deepfake scams and harassment", - "onboarding_motivation_power": "Concentration of power", - "onboarding_motivation_privacy": "Privacy loss", - "onboarding_motivation_environment": "Environmental damage", - "onboarding_motivation_addiction": "Technology addiction", - "onboarding_motivation_weapons": "Autonomous weapons", - "onboarding_motivation_cyber": "Cyberattacks", - "onboarding_motivation_bioweapons": "Bioweapons", - "onboarding_motivation_other": "Other", - "onboarding_skill_software": "Software Development", - "onboarding_skill_video": "Video Creation", - "onboarding_skill_social_media": "Social Media Management", - "onboarding_skill_events": "Event Organization", - "onboarding_skill_speaking": "Public Speaking/ Presentation", - "onboarding_skill_writing": "Writing", - "onboarding_skill_design": "Graphic Design/ Visual Arts", - "onboarding_skill_research": "Research", - "onboarding_skill_comms": "Communications/ PR", - "onboarding_skill_fundraising": "Fundraising", - "onboarding_skill_community": "Community Organizing", - "onboarding_skill_advocacy": "Political Advocacy/ Lobbying", - "onboarding_skill_education": "Education/ Teaching", - "onboarding_skill_admin": "Administrative Support", - "onboarding_skill_legal": "Legal Knowledge", - "onboarding_skill_other": "Other", - "onboarding_hours_under_3": "Less than 3 hours", - "onboarding_hours_3_6": "3-6 hours", - "onboarding_hours_6_10": "6-10 hours", - "onboarding_hours_10_20": "10-20 hours", - "onboarding_hours_20_plus": "20+ hours", - "onboarding_action_ai_concerns_title": "Share your AI concerns", - "onboarding_action_ai_concerns_sub": "Tell your story and add your voice to the public record.", - "onboarding_action_email_reps_title": "Email your elected representatives", - "onboarding_action_email_reps_sub": "Use our Email Builder for a pre-drafted message you can personalize and send.", - "onboarding_action_lobby_title": "Contact your elected officials", - "onboarding_action_lobby_sub": "Our lobby tips walk you through what to say.", - "onboarding_action_talk_title": "Talk to someone in your life about AI safety", - "onboarding_action_talk_sub__instructions": "Preserve the HTML tag and href attribute exactly. Only translate the visible text.", - "onboarding_action_talk_sub": "A friend, neighbour, colleague, or family member. Our counterarguments guide can help you answer tough questions and encourage others to act.", - "onboarding_action_microcommit_title": "Microcommit — weekly bite-sized actions", - "onboarding_action_microcommit_sub": "Pick from a list of small ongoing actions and track your contribution over time.", - "onboarding_action_petitions_title": "Sign the petitions", - "onboarding_action_petitions_sub": "Add your name alongside thousands of others calling for safer AI.", - "onboarding_action_donate_title": "Donate or pick up some gear", - "onboarding_action_donate_sub__instructions": "Preserve both HTML tags and their href attributes exactly. Only translate the visible text inside and around the tags.", - "onboarding_action_donate_sub": "Make a donation or pick up some gear from our store.", - "onboarding_error_missing_fields": "Please fill in your name, email, country and city.", - "onboarding_error_invalid_email": "Please enter a valid email address.", - "onboarding_error_invalid_country": "Please select a country from the list.", - "onboarding_error_missing_intent": "Please choose what brings you here.", - "onboarding_error_missing_gdpr": "Please agree to the data processing consent to continue.", - "onboarding_error_missing_languages": "Please select at least one language you speak.", - "onboarding_error_missing_hours": "Please tell us how much time you can commit weekly.", - "onboarding_error_missing_agreements": "Please agree to the Volunteer Agreement and Code of Conduct.", - "onboarding_error_invalid_discovery": "Please select a valid option for how you found us.", - "onboarding_error_save_failed": "Sorry, we couldn't save your details. Please try again.", - "onboarding_error_generic": "Something went wrong. Please try again.", - "onboarding_error_unexpected": "An unexpected error occurred. Please try again." + "footer_info_about": "About Us" } diff --git a/src/lib/components/onboarding/ActionCards.svelte b/src/lib/components/onboarding/ActionCards.svelte index 961af2395..b1fae892b 100644 --- a/src/lib/components/onboarding/ActionCards.svelte +++ b/src/lib/components/onboarding/ActionCards.svelte @@ -1,37 +1,39 @@
    • -

      {m.onboarding_action_ai_concerns_title()}

      -

      {m.onboarding_action_ai_concerns_sub()}

      +

      {msgs.onboarding_action_ai_concerns_title}

      +

      {msgs.onboarding_action_ai_concerns_sub}

    • -

      {m.onboarding_action_email_reps_title()}

      -

      {m.onboarding_action_email_reps_sub()}

      +

      {msgs.onboarding_action_email_reps_title}

      +

      {msgs.onboarding_action_email_reps_sub}

    • -

      {m.onboarding_action_lobby_title()}

      -

      {m.onboarding_action_lobby_sub()}

      +

      {msgs.onboarding_action_lobby_title}

      +

      {msgs.onboarding_action_lobby_sub}

    • -

      {m.onboarding_action_talk_title()}

      -

      {@html safeHtml(m.onboarding_action_talk_sub())}

      +

      {msgs.onboarding_action_talk_title}

      +

      {@html safeHtml(msgs.onboarding_action_talk_sub)}

    • - {m.onboarding_action_microcommit_title()} + {msgs.onboarding_action_microcommit_title}

      -

      {m.onboarding_action_microcommit_sub()}

      +

      {msgs.onboarding_action_microcommit_sub}

    • -

      {m.onboarding_action_petitions_title()}

      -

      {m.onboarding_action_petitions_sub()}

      +

      {msgs.onboarding_action_petitions_title}

      +

      {msgs.onboarding_action_petitions_sub}

      • PauseAI Statement
      • Say No To Superintelligent AI
      • @@ -49,8 +51,8 @@
    • -

      {m.onboarding_action_donate_title()}

      -

      {@html safeHtml(m.onboarding_action_donate_sub())}

      +

      {msgs.onboarding_action_donate_title}

      +

      {@html safeHtml(msgs.onboarding_action_donate_sub)}

    diff --git a/src/lib/components/onboarding/OnboardingFlow.svelte b/src/lib/components/onboarding/OnboardingFlow.svelte index aad5e38f2..98b5cfb68 100644 --- a/src/lib/components/onboarding/OnboardingFlow.svelte +++ b/src/lib/components/onboarding/OnboardingFlow.svelte @@ -12,7 +12,7 @@ import Combobox from '$lib/components/Combobox.svelte' import ActionCards from './ActionCards.svelte' import Stepper from './Stepper.svelte' - import * as m from '$lib/paraglide/messages.js' + import { getMessages } from './i18n.svelte' import { COUNTRIES, COUNTRY_DIAL_CODES, @@ -33,7 +33,8 @@ lead: 'Lead' } - const intentOptions = $derived(getIntentOptions()) + const msgs = $derived(getMessages()) + const intentOptions = $derived(getIntentOptions(msgs)) let { initialEmail = '', @@ -148,14 +149,14 @@ const stepperLabels = $derived( intent === 'volunteer' ? [ - m.onboarding_step_about(), - m.onboarding_step_intent(), - m.onboarding_step_volunteer_form(), - m.onboarding_step_confirmed() + msgs.onboarding_step_about, + msgs.onboarding_step_intent, + msgs.onboarding_step_volunteer_form, + msgs.onboarding_step_confirmed ] : intent === 'lead' - ? [m.onboarding_step_about(), m.onboarding_step_intent(), m.onboarding_step_next_steps()] - : [m.onboarding_step_about(), m.onboarding_step_intent(), m.onboarding_step_confirmed()] + ? [msgs.onboarding_step_about, msgs.onboarding_step_intent, msgs.onboarding_step_next_steps] + : [msgs.onboarding_step_about, msgs.onboarding_step_intent, msgs.onboarding_step_confirmed] ) // Lead path: if the country already has a chapter, offer regional/city @@ -227,9 +228,9 @@ } onSuccess(result.data) } else if (result.type === 'failure') { - toast.error(String(result.data?.message ?? m.onboarding_error_generic())) + toast.error(String(result.data?.message ?? msgs.onboarding_error_generic)) } else { - toast.error(m.onboarding_error_unexpected()) + toast.error(msgs.onboarding_error_unexpected) } } } @@ -256,7 +257,7 @@ name="country" options={COUNTRIES} required - placeholder={m.onboarding_placeholder_country()} + placeholder={msgs.onboarding_placeholder_country} bind:value={basics.country} /> {/snippet} @@ -304,13 +305,13 @@ {#if keepInformed}
  • - {m.onboarding_confirm_keep_informed()} + {msgs.onboarding_confirm_keep_informed}
  • {/if} {#if basics.newsletter}
  • - {m.onboarding_confirm_newsletter()} + {msgs.onboarding_confirm_newsletter}
  • {/if} @@ -321,25 +322,25 @@ {/snippet} {#snippet nextStepBlock()}
    -

    {m.onboarding_next_step_title()}

    -

    {@html safeHtml(m.onboarding_next_step_p1())}

    -

    {@html safeHtml(m.onboarding_next_step_p2())}

    +

    {msgs.onboarding_next_step_title}

    +

    {@html safeHtml(msgs.onboarding_next_step_p1)}

    +

    {@html safeHtml(msgs.onboarding_next_step_p2)}

    {/snippet} {#snippet confirmationFooter()} {/snippet} @@ -347,7 +348,7 @@
    {#if mode === 'browse' && !browseSignedUp}
    - {m.onboarding_browse_banner()} + {msgs.onboarding_browse_banner}
    {/if} @@ -360,55 +361,55 @@
    - +
    {@render honeypotField('ob-nickname')}
    - + -

    {m.onboarding_helper_email_gmail()}

    +

    {msgs.onboarding_helper_email_gmail}

    - + {@render countrySelect('ob-country')}
    - +
    - +

    - {m.onboarding_browse_helper()} + {msgs.onboarding_browse_helper}

    @@ -433,7 +434,7 @@ {#if keepInformed} {/if} -

    {m.onboarding_step2_heading()}

    +

    {msgs.onboarding_step2_heading}

    - +
    {#each intentOptions as option (option.key)} {msgs.onboarding_btn_back} {:else if step === 3 && !intent}
    -

    {m.onboarding_confirm_a_title()}

    +

    {msgs.onboarding_confirm_a_title}

    {@render checkboxConfirmations()} {@render nextStepBlock()} {@render confirmationFooter()} @@ -522,25 +523,25 @@ {#if mode === 'contact'}
    -

    {m.onboarding_confirm_b_title()}

    -

    {m.onboarding_confirm_b_sub()}

    +

    {msgs.onboarding_confirm_b_title}

    +

    {msgs.onboarding_confirm_b_sub}

    {@render checkboxConfirmations()}
    {:else}
    -

    {m.onboarding_browse_header_title()}

    +

    {msgs.onboarding_browse_header_title}

    - {m.onboarding_browse_header_sub()} + {msgs.onboarding_browse_header_sub}

    {#if browseSignedUp}
    - {m.onboarding_browse_signed_up()} + {msgs.onboarding_browse_signed_up}
    {:else}
    -

    {m.onboarding_browse_keep_informed_title()}

    -

    {m.onboarding_browse_keep_informed_sub()}

    +

    {msgs.onboarding_browse_keep_informed_title}

    +

    {msgs.onboarding_browse_keep_informed_sub}

    - +
    - + @@ -577,35 +578,36 @@

    -->
    - + {@render countrySelect('loop-country')}
    - +
    {@render gdprConsentField()}
    {/if} - + {/if} {@render confirmationFooter()} {:else if step === 3 && intent === 'volunteer'} -

    {m.onboarding_volunteer_title()}

    -

    {m.onboarding_volunteer_intro()}

    +

    {msgs.onboarding_volunteer_title}

    +

    {msgs.onboarding_volunteer_intro}

    {@render checkboxConfirmations()}
    - + -

    {m.onboarding_helper_zip()}

    +

    {msgs.onboarding_helper_zip}

    {/if}
    - + -

    {@html safeHtml(m.onboarding_helper_discord())}

    +

    {@html safeHtml(msgs.onboarding_helper_discord)}

    - +
    (dialCodeEdited = true)} /> @@ -663,38 +665,38 @@ type="tel" id="vol-phone" class="phone-number" - placeholder={m.onboarding_placeholder_phone()} + placeholder={msgs.onboarding_placeholder_phone} bind:value={volunteer.phone} />
    - + {#if volunteer.languages.includes('Other')} {/if}
    - + @@ -702,7 +704,7 @@ @@ -710,40 +712,40 @@
    {m.onboarding_field_motivations()}{msgs.onboarding_field_motivations}
    - {@render selectCards('motivations', getMotivations(), volunteer.motivations)} + {@render selectCards('motivations', getMotivations(msgs), volunteer.motivations)}
    {#if volunteer.motivations.includes('Other')} {/if}
    - {m.onboarding_field_skills()} + {msgs.onboarding_field_skills}
    - {@render selectCards('skills', getSkills(), volunteer.skills)} + {@render selectCards('skills', getSkills(msgs), volunteer.skills)}
    {#if volunteer.skills.includes('Other')} {/if}
    - {m.onboarding_field_hours()} + {msgs.onboarding_field_hours}
    - {#each getWeeklyHours() as option (option.value)} + {#each getWeeklyHours(msgs) as option (option.value)} {msgs.onboarding_btn_back} {:else if step === 3 && intent === 'lead'} -

    {m.onboarding_lead_title({ role: leadRole })}

    -

    {m.onboarding_lead_meta()}

    +

    {msgs.onboarding_lead_title(leadRole)}

    +

    {msgs.onboarding_lead_meta}

    {@render checkboxConfirmations()}
    -

    {m.onboarding_lead_intro()}

    +

    {msgs.onboarding_lead_intro}

    {#if basics.country === 'United States'} -

    {@html safeHtml(m.onboarding_lead_us_branch())}

    +

    {@html safeHtml(msgs.onboarding_lead_us_branch)}

    {:else if countryHasChapter} -

    {@html safeHtml(m.onboarding_lead_has_chapter({ country: basics.country }))}

    +

    {@html safeHtml(msgs.onboarding_lead_has_chapter(basics.country))}

    {:else} -

    {@html safeHtml(m.onboarding_lead_check_chapter())}

    +

    {@html safeHtml(msgs.onboarding_lead_check_chapter)}

    {/if} -

    {m.onboarding_lead_what_you_do_title()}

    +

    {msgs.onboarding_lead_what_you_do_title}

      -
    • {m.onboarding_lead_do_1()}
    • -
    • {m.onboarding_lead_do_2()}
    • -
    • {m.onboarding_lead_do_3()}
    • -
    • {m.onboarding_lead_do_4()}
    • +
    • {msgs.onboarding_lead_do_1}
    • +
    • {msgs.onboarding_lead_do_2}
    • +
    • {msgs.onboarding_lead_do_3}
    • +
    • {msgs.onboarding_lead_do_4}
    -

    {m.onboarding_lead_looking_for_title()}

    +

    {msgs.onboarding_lead_looking_for_title}

      -
    • {m.onboarding_lead_looking_1()}
    • -
    • {m.onboarding_lead_looking_2()}
    • -
    • {m.onboarding_lead_looking_3()}
    • -
    • {m.onboarding_lead_looking_4()}
    • -
    • {m.onboarding_lead_looking_5()}
    • -
    • {m.onboarding_lead_looking_6()}
    • +
    • {msgs.onboarding_lead_looking_1}
    • +
    • {msgs.onboarding_lead_looking_2}
    • +
    • {msgs.onboarding_lead_looking_3}
    • +
    • {msgs.onboarding_lead_looking_4}
    • +
    • {msgs.onboarding_lead_looking_5}
    • +
    • {msgs.onboarding_lead_looking_6}
    -

    {m.onboarding_lead_nice_to_have_title()}

    +

    {msgs.onboarding_lead_nice_to_have_title}

      -
    • {m.onboarding_lead_nice_1()}
    • -
    • {m.onboarding_lead_nice_2()}
    • -
    • {m.onboarding_lead_nice_3()}
    • +
    • {msgs.onboarding_lead_nice_1}
    • +
    • {msgs.onboarding_lead_nice_2}
    • +
    • {msgs.onboarding_lead_nice_3}
    -

    {m.onboarding_lead_imperfect()}

    -

    {m.onboarding_lead_support_title()}

    -

    {m.onboarding_lead_support()}

    -

    {m.onboarding_lead_next_steps_title()}

    -

    {@html safeHtml(m.onboarding_lead_next_steps())}

    +

    {msgs.onboarding_lead_imperfect}

    +

    {msgs.onboarding_lead_support_title}

    +

    {msgs.onboarding_lead_support}

    +

    {msgs.onboarding_lead_next_steps_title}

    +

    {@html safeHtml(msgs.onboarding_lead_next_steps)}

    - {m.onboarding_lead_email_btn()} + {msgs.onboarding_lead_email_btn} -

    {m.onboarding_lead_please_mention()}

    +

    {msgs.onboarding_lead_please_mention}

      -
    • {m.onboarding_lead_mention_1()}
    • -
    • {m.onboarding_lead_mention_2()}
    • -
    • {m.onboarding_lead_mention_3()}
    • +
    • {msgs.onboarding_lead_mention_1}
    • +
    • {msgs.onboarding_lead_mention_2}
    • +
    • {msgs.onboarding_lead_mention_3}
    {@render confirmationFooter()} {:else if step === 4 && intent === 'volunteer'}
    -

    {m.onboarding_confirm_volunteer_title()}

    -

    {m.onboarding_confirm_volunteer_sub()}

    +

    {msgs.onboarding_confirm_volunteer_title}

    +

    {msgs.onboarding_confirm_volunteer_sub}

    {@render checkboxConfirmations()} {@render nextStepBlock()} {@render confirmationFooter()} diff --git a/src/lib/components/onboarding/i18n.svelte.ts b/src/lib/components/onboarding/i18n.svelte.ts new file mode 100644 index 000000000..cb0cc3842 --- /dev/null +++ b/src/lib/components/onboarding/i18n.svelte.ts @@ -0,0 +1,25 @@ +// Temporary localisation module for the onboarding embed. +// Replaces paraglide until the l10n cage (PauseAI/paraglide) is accessible +// and German translations can be compiled into the runtime via `pnpm l10n`. +// To add a new language, add a new locale object to messages.ts. +import { onboardingMessages, type OnboardingMessages } from './messages' + +export type OnboardingLocale = keyof typeof onboardingMessages + +let locale = $state('en') + +export function getLocale(): OnboardingLocale { + return locale +} + +export function setOnboardingLocale(l: string) { + if (l in onboardingMessages) locale = l as OnboardingLocale +} + +export function isOnboardingLocale(s: string): s is OnboardingLocale { + return s in onboardingMessages +} + +export function getMessages(): OnboardingMessages { + return onboardingMessages[locale] +} diff --git a/src/lib/components/onboarding/messages.ts b/src/lib/components/onboarding/messages.ts new file mode 100644 index 000000000..fbab3e56d --- /dev/null +++ b/src/lib/components/onboarding/messages.ts @@ -0,0 +1,591 @@ +// Hardcoded translations for the onboarding embed. +// This is a temporary local alternative to paraglide-based localisation, which +// requires the private l10n cage (PauseAI/paraglide) to compile translations +// into the runtime. Until that access is available, translations live here and +// are reviewable/shippable via normal PRs. +export interface OnboardingMessages { + onboarding_page_title: string + onboarding_page_description: string + onboarding_browse_banner: string + onboarding_field_full_name: string + onboarding_placeholder_full_name: string + onboarding_field_email: string + onboarding_placeholder_email: string + onboarding_helper_email_gmail: string + onboarding_field_country: string + onboarding_placeholder_country: string + onboarding_field_city: string + onboarding_placeholder_city: string + onboarding_btn_continue: string + onboarding_btn_browse: string + onboarding_browse_helper: string + onboarding_step2_heading: string + onboarding_intent_keep_informed_label: string + onboarding_intent_keep_informed_sub: string + onboarding_intent_newsletter_label: string + onboarding_intent_newsletter_sub: string + onboarding_intent_more_optional: string + onboarding_intent_act_now_label: string + onboarding_intent_act_now_sub: string + onboarding_intent_volunteer_label: string + onboarding_intent_volunteer_sub: string + onboarding_intent_lead_label: string + onboarding_intent_lead_sub: string + onboarding_gdpr_consent: string + onboarding_btn_submit: string + onboarding_btn_submitting: string + onboarding_btn_back: string + onboarding_confirm_keep_informed: string + onboarding_confirm_newsletter: string + onboarding_confirm_a_title: string + onboarding_confirm_b_title: string + onboarding_confirm_b_sub: string + onboarding_confirm_volunteer_title: string + onboarding_confirm_volunteer_sub: string + onboarding_browse_header_title: string + onboarding_browse_header_sub: string + onboarding_browse_signed_up: string + onboarding_browse_keep_informed_title: string + onboarding_browse_keep_informed_sub: string + onboarding_btn_sign_me_up: string + onboarding_btn_signing_up: string + onboarding_section_ways_to_help: string + onboarding_next_step_title: string + onboarding_next_step_p1: string + onboarding_next_step_p2: string + onboarding_footer_join_conversation: string + onboarding_footer_discord_btn: string + onboarding_footer_follow_us: string + onboarding_step_about: string + onboarding_step_intent: string + onboarding_step_volunteer_form: string + onboarding_step_confirmed: string + onboarding_step_next_steps: string + onboarding_volunteer_title: string + onboarding_volunteer_intro: string + onboarding_field_zip: string + onboarding_placeholder_zip: string + onboarding_helper_zip: string + onboarding_field_discord: string + onboarding_helper_discord: string + onboarding_field_phone: string + onboarding_placeholder_dial_code: string + onboarding_placeholder_phone: string + onboarding_field_languages: string + onboarding_placeholder_languages: string + onboarding_field_discovery: string + onboarding_placeholder_discovery: string + onboarding_field_motivations: string + onboarding_field_skills: string + onboarding_field_hours: string + onboarding_placeholder_specify: string + onboarding_agree_volunteer: string + onboarding_agree_conduct: string + onboarding_lead_title: (role: string) => string + onboarding_lead_meta: string + onboarding_lead_intro: string + onboarding_lead_us_branch: string + onboarding_lead_has_chapter: (country: string) => string + onboarding_lead_check_chapter: string + onboarding_lead_what_you_do_title: string + onboarding_lead_do_1: string + onboarding_lead_do_2: string + onboarding_lead_do_3: string + onboarding_lead_do_4: string + onboarding_lead_looking_for_title: string + onboarding_lead_looking_1: string + onboarding_lead_looking_2: string + onboarding_lead_looking_3: string + onboarding_lead_looking_4: string + onboarding_lead_looking_5: string + onboarding_lead_looking_6: string + onboarding_lead_nice_to_have_title: string + onboarding_lead_nice_1: string + onboarding_lead_nice_2: string + onboarding_lead_nice_3: string + onboarding_lead_imperfect: string + onboarding_lead_support_title: string + onboarding_lead_support: string + onboarding_lead_next_steps_title: string + onboarding_lead_next_steps: string + onboarding_lead_email_btn: string + onboarding_lead_please_mention: string + onboarding_lead_mention_1: string + onboarding_lead_mention_2: string + onboarding_lead_mention_3: string + onboarding_discovery_pauseai_social: string + onboarding_discovery_other_social: string + onboarding_discovery_referral: string + onboarding_discovery_news: string + onboarding_discovery_event: string + onboarding_discovery_search: string + onboarding_discovery_other: string + onboarding_motivation_ai_safety: string + onboarding_motivation_democratic_oversight: string + onboarding_motivation_ethical_tech: string + onboarding_motivation_ai_governance: string + onboarding_motivation_job_displacement: string + onboarding_motivation_misinformation: string + onboarding_motivation_deepfakes: string + onboarding_motivation_power: string + onboarding_motivation_privacy: string + onboarding_motivation_environment: string + onboarding_motivation_addiction: string + onboarding_motivation_weapons: string + onboarding_motivation_cyber: string + onboarding_motivation_bioweapons: string + onboarding_motivation_other: string + onboarding_skill_software: string + onboarding_skill_video: string + onboarding_skill_social_media: string + onboarding_skill_events: string + onboarding_skill_speaking: string + onboarding_skill_writing: string + onboarding_skill_design: string + onboarding_skill_research: string + onboarding_skill_comms: string + onboarding_skill_fundraising: string + onboarding_skill_community: string + onboarding_skill_advocacy: string + onboarding_skill_education: string + onboarding_skill_admin: string + onboarding_skill_legal: string + onboarding_skill_other: string + onboarding_hours_under_3: string + onboarding_hours_3_6: string + onboarding_hours_6_10: string + onboarding_hours_10_20: string + onboarding_hours_20_plus: string + onboarding_action_ai_concerns_title: string + onboarding_action_ai_concerns_sub: string + onboarding_action_email_reps_title: string + onboarding_action_email_reps_sub: string + onboarding_action_lobby_title: string + onboarding_action_lobby_sub: string + onboarding_action_talk_title: string + onboarding_action_talk_sub: string + onboarding_action_microcommit_title: string + onboarding_action_microcommit_sub: string + onboarding_action_petitions_title: string + onboarding_action_petitions_sub: string + onboarding_action_donate_title: string + onboarding_action_donate_sub: string + onboarding_error_generic: string + onboarding_error_unexpected: string +} + +const en: OnboardingMessages = { + onboarding_page_title: 'Get involved', + onboarding_page_description: + 'Find the highest-impact way for you to help pause the development of superhuman AI.', + onboarding_browse_banner: + "You're browsing without signing up, leave your email below so we can tell you when new opportunities go live.", + onboarding_field_full_name: 'Full name *', + onboarding_placeholder_full_name: 'Full name', + onboarding_field_email: 'Email *', + onboarding_placeholder_email: 'Email', + onboarding_helper_email_gmail: 'Preferably Gmail if you have one.', + onboarding_field_country: 'Country of residence *', + onboarding_placeholder_country: 'Select your country', + onboarding_field_city: 'City / town of residence *', + onboarding_placeholder_city: 'City / town', + onboarding_btn_continue: 'Continue →', + onboarding_btn_browse: '👀 I just want to take action now', + onboarding_browse_helper: 'Skip ahead to see the actions you can take today, no email required.', + onboarding_step2_heading: 'What brings you here?', + onboarding_intent_keep_informed_label: 'Keep me informed', + onboarding_intent_keep_informed_sub: + 'Connect me with my local PauseAI chapter, and keep me updated on global campaigns.', + onboarding_intent_newsletter_label: 'Subscribe to our Substack', + onboarding_intent_newsletter_sub: 'General news on AI, delivered via our Substack newsletter.', + onboarding_intent_more_optional: 'Want to do more? (optional)', + onboarding_intent_act_now_label: 'I want to take action now', + onboarding_intent_act_now_sub: 'Show me what I can do today.', + onboarding_intent_volunteer_label: 'I want to volunteer regularly', + onboarding_intent_volunteer_sub: 'Help me find a role that fits.', + onboarding_intent_lead_label: 'I want to lead', + onboarding_intent_lead_sub: "I'm ready to organize in my country or region.", + onboarding_gdpr_consent: + 'I agree to the Privacy Policy, including sharing my details with my local PauseAI chapter (which may be a separate entity to PauseAI Global) for local coordination. *', + onboarding_btn_submit: 'Submit →', + onboarding_btn_submitting: 'Submitting...', + onboarding_btn_back: '← Back', + onboarding_confirm_keep_informed: + "We'll connect you with your local PauseAI chapter and keep you updated on global campaigns.", + onboarding_confirm_newsletter: + "You're subscribed to our Substack newsletter for general news on AI.", + onboarding_confirm_a_title: "You're in.", + onboarding_confirm_b_title: "You're in, thanks for joining us.", + onboarding_confirm_b_sub: "You're all set. Here are a few ways to make a difference today.", + onboarding_confirm_volunteer_title: 'Welcome to the team.', + onboarding_confirm_volunteer_sub: "You're on the volunteer list. We'll be in touch soon.", + onboarding_browse_header_title: 'Take action right now.', + onboarding_browse_header_sub: + 'Below are some actions you can take right now. The best way to stay informed about new opportunities is to sign up below.', + onboarding_browse_signed_up: + "✓ You're in. We'll connect you with your local PauseAI chapter and keep you updated on global campaigns.", + onboarding_browse_keep_informed_title: 'Keep me informed', + onboarding_browse_keep_informed_sub: + 'Connect me with my local PauseAI chapter and keep me updated on global campaigns.', + onboarding_btn_sign_me_up: 'Sign me up →', + onboarding_btn_signing_up: 'Signing up...', + onboarding_section_ways_to_help: 'A few ways to help today', + onboarding_next_step_title: '★ Recommended next step', + onboarding_next_step_p1: + 'Join one of our Member Community Welcome Meetings, or a local social event, to find out more about PauseAI\'s community: see upcoming events.', + onboarding_next_step_p2: + 'Want to get kick-started into action straight away? Check out our list of actions.', + onboarding_footer_join_conversation: 'Join the conversation', + onboarding_footer_discord_btn: 'Join PauseAI on Discord', + onboarding_footer_follow_us: 'Follow us', + onboarding_step_about: 'About you', + onboarding_step_intent: 'Intent', + onboarding_step_volunteer_form: 'Volunteer form', + onboarding_step_confirmed: 'Confirmed', + onboarding_step_next_steps: 'Next steps', + onboarding_volunteer_title: 'Sign up to volunteer', + onboarding_volunteer_intro: 'Tell us a bit about yourself so we can find a role that fits.', + onboarding_field_zip: 'Zip code', + onboarding_placeholder_zip: 'e.g. 02134', + onboarding_helper_zip: 'Your 5-digit zip code is used to find your Local Group.', + onboarding_field_discord: 'Discord username', + onboarding_helper_discord: + 'If you don\'t have a Discord account, we encourage you to create one here.', + onboarding_field_phone: 'Phone number', + onboarding_placeholder_dial_code: '+1', + onboarding_placeholder_phone: '(555) 000-0000', + onboarding_field_languages: 'What languages do you speak? *', + onboarding_placeholder_languages: 'Select languages', + onboarding_field_discovery: 'How did you find out about PauseAI?', + onboarding_placeholder_discovery: 'Select an option', + onboarding_field_motivations: 'What motivated you to join?', + onboarding_field_skills: 'Skills & interests', + onboarding_field_hours: 'How much time can you commit weekly? *', + onboarding_placeholder_specify: 'Please specify', + onboarding_agree_volunteer: + 'I agree with the Volunteer Agreement *', + onboarding_agree_conduct: + 'I agree with the Code of Conduct *', + onboarding_lead_title: (role) => `${role} Volunteer Description`, + onboarding_lead_meta: 'Part-time volunteer role · 5–15 hours/week', + onboarding_lead_intro: + 'Pause AI Global is looking for leaders around the world to run local groups that lead organising efforts in their area. This is a part-time volunteer role requiring 5-15 hours per week. Each group leader will be responsible for planning direct actions, mobilising volunteers and coordinating with the Global PauseAI team.', + onboarding_lead_us_branch: + 'Since you\'re based in the United States, please apply through PauseAI US: PauseAI US application form.', + onboarding_lead_has_chapter: (country) => + `${country} already has a PauseAI chapter, so rather than founding a national group, you could lead a regional or city group within it. Find your chapter at pauseai.info/communities, or email our Organizing Director below to talk it through.`, + onboarding_lead_check_chapter: + 'First, check that your country doesn\'t already have a chapter: pauseai.info/communities.', + onboarding_lead_what_you_do_title: "What you'll do", + onboarding_lead_do_1: + 'Recruit and grow your local group by welcoming new volunteers and organising events together.', + onboarding_lead_do_2: 'Build relationships with local activist groups and journalists.', + onboarding_lead_do_3: + "Meet monthly with PauseAI's Global team to swap ideas and stay coordinated with the global strategy and quarterly campaigns.", + onboarding_lead_do_4: 'Share what your chapter is up to on social media and help promote events.', + onboarding_lead_looking_for_title: "What we're looking for", + onboarding_lead_looking_1: + 'Ability to plan and execute direct actions, including workshops and public events.', + onboarding_lead_looking_2: + 'Excellent communication skills, eager and able to engage diverse communities.', + onboarding_lead_looking_3: + 'Self-motivated, with the ability to work independently and as part of a team.', + onboarding_lead_looking_4: "Passion for AI safety and alignment with PauseAI's mission.", + onboarding_lead_looking_5: 'Comfortable communicating in English', + onboarding_lead_looking_6: 'Adhere and practice to a non-violent, legal approach.', + onboarding_lead_nice_to_have_title: 'What would be nice to have', + onboarding_lead_nice_1: 'Strong organising skills and experience in grassroots activism.', + onboarding_lead_nice_2: 'Lobbying skills and policymaker engagement experience', + onboarding_lead_nice_3: 'Media and content writing experience', + onboarding_lead_imperfect: + 'If you do not meet all of the requirements, we still want to hear from you. The commitment to AI Safety and the belief we can make an impact is the most important factor; we can provide training, resources and a community to lean on.', + onboarding_lead_support_title: "Support you'll get", + onboarding_lead_support: + "You'll join a global network of organisers who are all figuring this out together. We meet monthly, share resources across our chat platforms and help each other troubleshoot. It's a great way to learn new skills, meet thoughtful people and be part of a fast-growing movement that's working on one of the most important issues of our time.", + onboarding_lead_next_steps_title: 'Next steps', + onboarding_lead_next_steps: + 'If you\'d like to learn more, please contact our PauseAI Global organising director Irina Tavera at Irina@pauseai.info to schedule an informal exploratory chat.', + onboarding_lead_email_btn: '✉️ Email our Organizing Director', + onboarding_lead_please_mention: 'Please let her know the following:', + onboarding_lead_mention_1: 'Your name', + onboarding_lead_mention_2: 'Your country of residence', + onboarding_lead_mention_3: "A few sentences on why you'd like to become a lead", + onboarding_discovery_pauseai_social: 'PauseAI affiliated social media', + onboarding_discovery_other_social: 'Non-PauseAI affiliated social media', + onboarding_discovery_referral: 'Friend/Family referral', + onboarding_discovery_news: 'News article', + onboarding_discovery_event: 'Event/Presentation', + onboarding_discovery_search: 'Internet search', + onboarding_discovery_other: 'Other', + onboarding_motivation_ai_safety: 'AI Safety', + onboarding_motivation_democratic_oversight: 'Need for democratic oversight', + onboarding_motivation_ethical_tech: 'Ethical technology', + onboarding_motivation_ai_governance: 'AI Governance', + onboarding_motivation_job_displacement: 'Job Displacement', + onboarding_motivation_misinformation: 'Misinformation', + onboarding_motivation_deepfakes: 'Deepfake scams and harassment', + onboarding_motivation_power: 'Concentration of power', + onboarding_motivation_privacy: 'Privacy loss', + onboarding_motivation_environment: 'Environmental damage', + onboarding_motivation_addiction: 'Technology addiction', + onboarding_motivation_weapons: 'Autonomous weapons', + onboarding_motivation_cyber: 'Cyberattacks', + onboarding_motivation_bioweapons: 'Bioweapons', + onboarding_motivation_other: 'Other', + onboarding_skill_software: 'Software Development', + onboarding_skill_video: 'Video Creation', + onboarding_skill_social_media: 'Social Media Management', + onboarding_skill_events: 'Event Organization', + onboarding_skill_speaking: 'Public Speaking/ Presentation', + onboarding_skill_writing: 'Writing', + onboarding_skill_design: 'Graphic Design/ Visual Arts', + onboarding_skill_research: 'Research', + onboarding_skill_comms: 'Communications/ PR', + onboarding_skill_fundraising: 'Fundraising', + onboarding_skill_community: 'Community Organizing', + onboarding_skill_advocacy: 'Political Advocacy/ Lobbying', + onboarding_skill_education: 'Education/ Teaching', + onboarding_skill_admin: 'Administrative Support', + onboarding_skill_legal: 'Legal Knowledge', + onboarding_skill_other: 'Other', + onboarding_hours_under_3: 'Less than 3 hours', + onboarding_hours_3_6: '3-6 hours', + onboarding_hours_6_10: '6-10 hours', + onboarding_hours_10_20: '10-20 hours', + onboarding_hours_20_plus: '20+ hours', + onboarding_action_ai_concerns_title: 'Share your AI concerns', + onboarding_action_ai_concerns_sub: 'Tell your story and add your voice to the public record.', + onboarding_action_email_reps_title: 'Email your elected representatives', + onboarding_action_email_reps_sub: + 'Use our Email Builder for a pre-drafted message you can personalize and send.', + onboarding_action_lobby_title: 'Contact your elected officials', + onboarding_action_lobby_sub: 'Our lobby tips walk you through what to say.', + onboarding_action_talk_title: 'Talk to someone in your life about AI safety', + onboarding_action_talk_sub: + 'A friend, neighbour, colleague, or family member. Our counterarguments guide can help you answer tough questions and encourage others to act.', + onboarding_action_microcommit_title: 'Microcommit — weekly bite-sized actions', + onboarding_action_microcommit_sub: + 'Pick from a list of small ongoing actions and track your contribution over time.', + onboarding_action_petitions_title: 'Sign the petitions', + onboarding_action_petitions_sub: + 'Add your name alongside thousands of others calling for safer AI.', + onboarding_action_donate_title: 'Donate or pick up some gear', + onboarding_action_donate_sub: + 'Make a donation or pick up some gear from our store.', + onboarding_error_generic: 'Something went wrong. Please try again.', + onboarding_error_unexpected: 'An unexpected error occurred. Please try again.' +} + +const de: OnboardingMessages = { + onboarding_page_title: 'Mitmachen', + onboarding_page_description: + 'Finde den wirkungsvollsten Weg, um bei der Pause der KI-Entwicklung zu helfen.', + onboarding_browse_banner: + 'Du browst ohne dich anzumelden. Hinterlasse unten deine E-Mail, damit wir dich über neue Möglichkeiten informieren können.', + onboarding_field_full_name: 'Vollständiger Name *', + onboarding_placeholder_full_name: 'Vollständiger Name', + onboarding_field_email: 'E-Mail *', + onboarding_placeholder_email: 'E-Mail', + onboarding_helper_email_gmail: 'Falls vorhanden, bevorzugt Gmail.', + onboarding_field_country: 'Wohnsitzland *', + onboarding_placeholder_country: 'Land auswählen', + onboarding_field_city: 'Stadt / Gemeinde *', + onboarding_placeholder_city: 'Stadt / Gemeinde', + onboarding_btn_continue: 'Weiter →', + onboarding_btn_browse: '👀 Ich möchte nur jetzt handeln', + onboarding_browse_helper: 'Überspringe die Anmeldung und sieh, was du heute tun kannst.', + onboarding_step2_heading: 'Was bringt dich hierher?', + onboarding_intent_keep_informed_label: 'Auf dem Laufenden bleiben', + onboarding_intent_keep_informed_sub: + 'Verbinde mich mit meinem lokalen PauseAI-Kapitel und halte mich über globale Kampagnen auf dem Laufenden.', + onboarding_intent_newsletter_label: 'Unseren Substack abonnieren', + onboarding_intent_newsletter_sub: + 'Allgemeine Neuigkeiten zur KI über unseren Substack-Newsletter.', + onboarding_intent_more_optional: 'Möchtest du mehr tun? (optional)', + onboarding_intent_act_now_label: 'Ich möchte jetzt handeln', + onboarding_intent_act_now_sub: 'Zeig mir, was ich heute tun kann.', + onboarding_intent_volunteer_label: 'Ich möchte regelmäßig freiwillig tätig sein', + onboarding_intent_volunteer_sub: 'Hilf mir, eine passende Rolle zu finden.', + onboarding_intent_lead_label: 'Ich möchte leiten', + onboarding_intent_lead_sub: 'Ich bin bereit, in meinem Land oder meiner Region zu organisieren.', + onboarding_gdpr_consent: + 'Ich stimme der Datenschutzrichtlinie zu, einschließlich der Weitergabe meiner Daten an mein lokales PauseAI-Kapitel (das eine separate Einheit von PauseAI Global sein kann) zur lokalen Koordination. *', + onboarding_btn_submit: 'Absenden →', + onboarding_btn_submitting: 'Wird abgesendet...', + onboarding_btn_back: '← Zurück', + onboarding_confirm_keep_informed: + 'Wir werden dich mit deinem lokalen PauseAI-Kapitel verbinden und dich über globale Kampagnen informieren.', + onboarding_confirm_newsletter: + 'Du hast unseren Substack-Newsletter für allgemeine KI-Nachrichten abonniert.', + onboarding_confirm_a_title: 'Du bist dabei.', + onboarding_confirm_b_title: 'Du bist dabei – danke, dass du dich uns anschließt.', + onboarding_confirm_b_sub: + 'Alles ist bereit. Hier sind einige Möglichkeiten, heute etwas zu bewirken.', + onboarding_confirm_volunteer_title: 'Willkommen im Team.', + onboarding_confirm_volunteer_sub: + 'Du stehst auf der Freiwilligenliste. Wir melden uns bald bei dir.', + onboarding_browse_header_title: 'Jetzt handeln.', + onboarding_browse_header_sub: + 'Im Folgenden findest du Aktionen, die du sofort ergreifen kannst. Am besten bleibst du über neue Möglichkeiten informiert, indem du dich unten anmeldest.', + onboarding_browse_signed_up: + '✓ Du bist dabei. Wir verbinden dich mit deinem lokalen PauseAI-Kapitel und halten dich über globale Kampagnen auf dem Laufenden.', + onboarding_browse_keep_informed_title: 'Auf dem Laufenden bleiben', + onboarding_browse_keep_informed_sub: + 'Verbinde mich mit meinem lokalen PauseAI-Kapitel und halte mich über globale Kampagnen auf dem Laufenden.', + onboarding_btn_sign_me_up: 'Anmelden →', + onboarding_btn_signing_up: 'Wird angemeldet...', + onboarding_section_ways_to_help: 'Einige Möglichkeiten zu helfen', + onboarding_next_step_title: '★ Empfohlener nächster Schritt', + onboarding_next_step_p1: + 'Nimm an einem unserer Willkommensmeetings für Mitglieder oder an einem lokalen Event teil, um mehr über die PauseAI-Gemeinschaft zu erfahren: bevorstehende Events ansehen.', + onboarding_next_step_p2: + 'Möchtest du sofort aktiv werden? Sieh dir unsere Aktionsliste an.', + onboarding_footer_join_conversation: 'An der Unterhaltung teilnehmen', + onboarding_footer_discord_btn: 'PauseAI auf Discord beitreten', + onboarding_footer_follow_us: 'Folge uns', + onboarding_step_about: 'Über dich', + onboarding_step_intent: 'Absicht', + onboarding_step_volunteer_form: 'Freiwilligenformular', + onboarding_step_confirmed: 'Bestätigt', + onboarding_step_next_steps: 'Nächste Schritte', + onboarding_volunteer_title: 'Als Freiwillige/r anmelden', + onboarding_volunteer_intro: + 'Erzähl uns etwas über dich, damit wir eine passende Rolle finden können.', + onboarding_field_zip: 'Postleitzahl', + onboarding_placeholder_zip: 'z. B. 02134', + onboarding_helper_zip: + 'Deine 5-stellige Postleitzahl wird verwendet, um deine lokale Gruppe zu finden.', + onboarding_field_discord: 'Discord-Benutzername', + onboarding_helper_discord: + 'Wenn du noch kein Discord-Konto hast, empfehlen wir dir, hier eines zu erstellen.', + onboarding_field_phone: 'Telefonnummer', + onboarding_placeholder_dial_code: '+49', + onboarding_placeholder_phone: '170 0000000', + onboarding_field_languages: 'Welche Sprachen sprichst du? *', + onboarding_placeholder_languages: 'Sprachen auswählen', + onboarding_field_discovery: 'Wie hast du von PauseAI erfahren?', + onboarding_placeholder_discovery: 'Option auswählen', + onboarding_field_motivations: 'Was hat dich motiviert, beizutreten?', + onboarding_field_skills: 'Fähigkeiten & Interessen', + onboarding_field_hours: 'Wie viel Zeit kannst du wöchentlich einbringen? *', + onboarding_placeholder_specify: 'Bitte angeben', + onboarding_agree_volunteer: + 'Ich stimme der Freiwilligenvereinbarung zu *', + onboarding_agree_conduct: + 'Ich stimme dem Verhaltenskodex zu *', + onboarding_lead_title: (role) => `${role} – Freiwilligenbeschreibung`, + onboarding_lead_meta: 'Ehrenamtliche Teilzeitrolle · 5–15 Stunden/Woche', + onboarding_lead_intro: + 'PauseAI Global sucht Leitungspersonen auf der ganzen Welt, die lokale Gruppen führen und Organisierungsmaßnahmen in ihrer Region koordinieren. Es handelt sich um eine ehrenamtliche Teilzeitrolle mit einem Aufwand von 5–15 Stunden pro Woche. Jede Gruppenleitung ist verantwortlich für die Planung von Aktionen, die Mobilisierung von Freiwilligen und die Koordination mit dem globalen PauseAI-Team.', + onboarding_lead_us_branch: + 'Da du in den Vereinigten Staaten ansässig bist, bewirb dich bitte über PauseAI US: PauseAI US Bewerbungsformular.', + onboarding_lead_has_chapter: (country) => + `${country} hat bereits ein PauseAI-Kapitel, daher könntest du statt einer nationalen Gruppe eine regionale oder städtische Gruppe leiten. Finde dein Kapitel unter pauseai.info/communities, oder wende dich per E-Mail an unseren Organizing Director (siehe unten), um es zu besprechen.`, + onboarding_lead_check_chapter: + 'Überprüfe zunächst, ob dein Land bereits ein Kapitel hat: pauseai.info/communities.', + onboarding_lead_what_you_do_title: 'Was du tun wirst', + onboarding_lead_do_1: + 'Deine lokale Gruppe aufbauen und wachsen lassen, indem du neue Freiwillige willkommen heißt und gemeinsam Events organisierst.', + onboarding_lead_do_2: 'Beziehungen zu lokalen Aktivistgruppen und Journalist:innen aufbauen.', + onboarding_lead_do_3: + 'Monatlich mit dem globalen PauseAI-Team zusammenkommen, um Ideen auszutauschen und mit der globalen Strategie und vierteljährlichen Kampagnen koordiniert zu bleiben.', + onboarding_lead_do_4: + 'Die Aktivitäten deines Kapitels in sozialen Medien teilen und Events bekannt machen.', + onboarding_lead_looking_for_title: 'Was wir suchen', + onboarding_lead_looking_1: + 'Fähigkeit, direkte Aktionen zu planen und durchzuführen, einschließlich Workshops und öffentlicher Events.', + onboarding_lead_looking_2: + 'Ausgezeichnete Kommunikationsfähigkeiten und die Bereitschaft, vielfältige Gemeinschaften anzusprechen.', + onboarding_lead_looking_3: + 'Eigeninitiative und die Fähigkeit, sowohl selbstständig als auch im Team zu arbeiten.', + onboarding_lead_looking_4: + 'Leidenschaft für KI-Sicherheit und Übereinstimmung mit der Mission von PauseAI.', + onboarding_lead_looking_5: 'Kommunikation auf Englisch muss möglich sein.', + onboarding_lead_looking_6: 'Bekenntnis zu einem gewaltfreien und legalen Vorgehen.', + onboarding_lead_nice_to_have_title: 'Wünschenswert', + onboarding_lead_nice_1: 'Starke Organisationsfähigkeiten und Erfahrung in der Basisarbeit.', + onboarding_lead_nice_2: + 'Lobbying-Fähigkeiten und Erfahrung im Umgang mit politischen Entscheidungsträger:innen.', + onboarding_lead_nice_3: 'Erfahrung in Medien und Contentwriting.', + onboarding_lead_imperfect: + 'Wenn du nicht alle Anforderungen erfüllst, möchten wir dennoch von dir hören. Das Engagement für KI-Sicherheit und der Glaube, dass wir einen Unterschied machen können, sind das Wichtigste – wir bieten Schulungen, Ressourcen und eine Gemeinschaft, auf die du dich stützen kannst.', + onboarding_lead_support_title: 'Unterstützung, die du erhältst', + onboarding_lead_support: + 'Du trittst einem globalen Netzwerk von Organisator:innen bei, die gemeinsam an diesen Herausforderungen arbeiten. Wir treffen uns monatlich, teilen Ressourcen auf unseren Plattformen und helfen uns gegenseitig. Es ist eine großartige Möglichkeit, neue Fähigkeiten zu erlernen, nachdenkliche Menschen kennenzulernen und Teil einer schnell wachsenden Bewegung zu sein, die sich mit einem der wichtigsten Themen unserer Zeit befasst.', + onboarding_lead_next_steps_title: 'Nächste Schritte', + onboarding_lead_next_steps: + 'Wenn du mehr erfahren möchtest, wende dich an Irina Tavera, die globale Organizing Director von PauseAI, unter Irina@pauseai.info, um ein informelles Gespräch zu vereinbaren.', + onboarding_lead_email_btn: '✉️ E-Mail an unsere Organizing Director', + onboarding_lead_please_mention: 'Bitte teile ihr Folgendes mit:', + onboarding_lead_mention_1: 'Deinen Namen', + onboarding_lead_mention_2: 'Dein Wohnsitzland', + onboarding_lead_mention_3: + 'Ein paar Sätze darüber, warum du eine Leitungsrolle übernehmen möchtest', + onboarding_discovery_pauseai_social: 'PauseAI Social Media', + onboarding_discovery_other_social: 'Andere Social Media', + onboarding_discovery_referral: 'Empfehlung von Freund/Familie', + onboarding_discovery_news: 'Zeitungsartikel', + onboarding_discovery_event: 'Veranstaltung/Präsentation', + onboarding_discovery_search: 'Internetsuche', + onboarding_discovery_other: 'Sonstiges', + onboarding_motivation_ai_safety: 'KI-Sicherheit', + onboarding_motivation_democratic_oversight: 'Demokratische Kontrolle', + onboarding_motivation_ethical_tech: 'Ethische Technologie', + onboarding_motivation_ai_governance: 'KI-Regulierung', + onboarding_motivation_job_displacement: 'Jobverlust durch KI', + onboarding_motivation_misinformation: 'Fehlinformationen', + onboarding_motivation_deepfakes: 'Deepfake-Betrug und Belästigung', + onboarding_motivation_power: 'Machtkonzentration', + onboarding_motivation_privacy: 'Datenschutzverlust', + onboarding_motivation_environment: 'Umweltschäden', + onboarding_motivation_addiction: 'Technologieabhängigkeit', + onboarding_motivation_weapons: 'Autonome Waffen', + onboarding_motivation_cyber: 'Cyberangriffe', + onboarding_motivation_bioweapons: 'Biowaffen', + onboarding_motivation_other: 'Sonstiges', + onboarding_skill_software: 'Softwareentwicklung', + onboarding_skill_video: 'Videoerstellung', + onboarding_skill_social_media: 'Social-Media-Management', + onboarding_skill_events: 'Eventorganisation', + onboarding_skill_speaking: 'Öffentliches Sprechen / Präsentation', + onboarding_skill_writing: 'Schreiben', + onboarding_skill_design: 'Grafikdesign / Visuelle Künste', + onboarding_skill_research: 'Forschung', + onboarding_skill_comms: 'Kommunikation / PR', + onboarding_skill_fundraising: 'Fundraising', + onboarding_skill_community: 'Community-Aufbau', + onboarding_skill_advocacy: 'Politisches Engagement / Lobbying', + onboarding_skill_education: 'Bildung / Unterricht', + onboarding_skill_admin: 'Verwaltungsunterstützung', + onboarding_skill_legal: 'Rechtskenntnisse', + onboarding_skill_other: 'Sonstiges', + onboarding_hours_under_3: 'Weniger als 3 Stunden', + onboarding_hours_3_6: '3–6 Stunden', + onboarding_hours_6_10: '6–10 Stunden', + onboarding_hours_10_20: '10–20 Stunden', + onboarding_hours_20_plus: '20+ Stunden', + onboarding_action_ai_concerns_title: 'KI-Bedenken teilen', + onboarding_action_ai_concerns_sub: + 'Erzähl deine Geschichte und trage zum öffentlichen Diskurs bei.', + onboarding_action_email_reps_title: 'E-Mail an deine Abgeordneten senden', + onboarding_action_email_reps_sub: + 'Nutze unseren E-Mail-Builder für eine vorgefertigte Nachricht, die du personalisieren und versenden kannst.', + onboarding_action_lobby_title: 'Deine gewählten Vertreter:innen kontaktieren', + onboarding_action_lobby_sub: 'Unsere Lobbying-Tipps helfen dir, das Richtige zu sagen.', + onboarding_action_talk_title: 'Mit jemandem in deinem Umfeld über KI-Sicherheit sprechen', + onboarding_action_talk_sub: + 'Ein Freund, Nachbar, Kollege oder Familienmitglied. Unser Leitfaden für Gegenargumente hilft dir, schwierige Fragen zu beantworten und andere zum Handeln zu motivieren.', + onboarding_action_microcommit_title: 'Mikroengagement – wöchentlich kleine Aktionen', + onboarding_action_microcommit_sub: + 'Wähle aus einer Liste kleiner fortlaufender Aktionen und verfolge deinen Beitrag über Zeit.', + onboarding_action_petitions_title: 'Petitionen unterzeichnen', + onboarding_action_petitions_sub: + 'Füge deinen Namen zu Tausenden hinzu, die für eine sicherere KI eintreten.', + onboarding_action_donate_title: 'Spenden oder Merchandise kaufen', + onboarding_action_donate_sub: + 'Leiste eine Spende oder entdecke unser Sortiment in unserem Shop.', + onboarding_error_generic: 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.', + onboarding_error_unexpected: 'Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut.' +} + +export const onboardingMessages: Record = { en, de } diff --git a/src/lib/components/onboarding/options.ts b/src/lib/components/onboarding/options.ts index d55d1c037..9dc085392 100644 --- a/src/lib/components/onboarding/options.ts +++ b/src/lib/components/onboarding/options.ts @@ -1,6 +1,6 @@ // Controlled vocabularies for the onboarding pipeline form. -import * as m from '$lib/paraglide/messages.js' +import type { OnboardingMessages } from './messages' export const INTENTS = ['Keep informed', 'Act now', 'Volunteer', 'Lead'] as const export type Intent = (typeof INTENTS)[number] @@ -512,91 +512,93 @@ export const WEEKLY_HOURS = [ '20+ hours' ] -export function getDiscoveryOptions(): { value: string; label: string }[] { +export function getDiscoveryOptions(msgs: OnboardingMessages): { value: string; label: string }[] { return [ - { value: 'PauseAI affiliated social media', label: m.onboarding_discovery_pauseai_social() }, - { value: 'Non-PauseAI affiliated social media', label: m.onboarding_discovery_other_social() }, - { value: 'Friend/Family referral', label: m.onboarding_discovery_referral() }, - { value: 'News article', label: m.onboarding_discovery_news() }, - { value: 'Event/Presentation', label: m.onboarding_discovery_event() }, - { value: 'Internet search', label: m.onboarding_discovery_search() }, - { value: 'Other', label: m.onboarding_discovery_other() } + { value: 'PauseAI affiliated social media', label: msgs.onboarding_discovery_pauseai_social }, + { value: 'Non-PauseAI affiliated social media', label: msgs.onboarding_discovery_other_social }, + { value: 'Friend/Family referral', label: msgs.onboarding_discovery_referral }, + { value: 'News article', label: msgs.onboarding_discovery_news }, + { value: 'Event/Presentation', label: msgs.onboarding_discovery_event }, + { value: 'Internet search', label: msgs.onboarding_discovery_search }, + { value: 'Other', label: msgs.onboarding_discovery_other } ] } -export function getMotivations(): { value: string; label: string }[] { +export function getMotivations(msgs: OnboardingMessages): { value: string; label: string }[] { return [ - { value: 'AI Safety', label: m.onboarding_motivation_ai_safety() }, + { value: 'AI Safety', label: msgs.onboarding_motivation_ai_safety }, { value: 'Need for democratic oversight', - label: m.onboarding_motivation_democratic_oversight() + label: msgs.onboarding_motivation_democratic_oversight }, - { value: 'Ethical technology', label: m.onboarding_motivation_ethical_tech() }, - { value: 'AI Governance', label: m.onboarding_motivation_ai_governance() }, - { value: 'Job Displacement', label: m.onboarding_motivation_job_displacement() }, - { value: 'Misinformation', label: m.onboarding_motivation_misinformation() }, - { value: 'Deepfake scams and harassment', label: m.onboarding_motivation_deepfakes() }, - { value: 'Concentration of power', label: m.onboarding_motivation_power() }, - { value: 'Privacy loss', label: m.onboarding_motivation_privacy() }, - { value: 'Environmental damage', label: m.onboarding_motivation_environment() }, - { value: 'Technology addiction', label: m.onboarding_motivation_addiction() }, - { value: 'Autonomous weapons', label: m.onboarding_motivation_weapons() }, - { value: 'Cyberattacks', label: m.onboarding_motivation_cyber() }, - { value: 'Bioweapons', label: m.onboarding_motivation_bioweapons() }, - { value: 'Other', label: m.onboarding_motivation_other() } + { value: 'Ethical technology', label: msgs.onboarding_motivation_ethical_tech }, + { value: 'AI Governance', label: msgs.onboarding_motivation_ai_governance }, + { value: 'Job Displacement', label: msgs.onboarding_motivation_job_displacement }, + { value: 'Misinformation', label: msgs.onboarding_motivation_misinformation }, + { value: 'Deepfake scams and harassment', label: msgs.onboarding_motivation_deepfakes }, + { value: 'Concentration of power', label: msgs.onboarding_motivation_power }, + { value: 'Privacy loss', label: msgs.onboarding_motivation_privacy }, + { value: 'Environmental damage', label: msgs.onboarding_motivation_environment }, + { value: 'Technology addiction', label: msgs.onboarding_motivation_addiction }, + { value: 'Autonomous weapons', label: msgs.onboarding_motivation_weapons }, + { value: 'Cyberattacks', label: msgs.onboarding_motivation_cyber }, + { value: 'Bioweapons', label: msgs.onboarding_motivation_bioweapons }, + { value: 'Other', label: msgs.onboarding_motivation_other } ] } -export function getSkills(): { value: string; label: string }[] { +export function getSkills(msgs: OnboardingMessages): { value: string; label: string }[] { return [ - { value: 'Software Development', label: m.onboarding_skill_software() }, - { value: 'Video Creation', label: m.onboarding_skill_video() }, - { value: 'Social Media Management', label: m.onboarding_skill_social_media() }, - { value: 'Event Organization', label: m.onboarding_skill_events() }, - { value: 'Public Speaking/ Presentation', label: m.onboarding_skill_speaking() }, - { value: 'Writing', label: m.onboarding_skill_writing() }, - { value: 'Graphic Design/ Visual Arts', label: m.onboarding_skill_design() }, - { value: 'Research', label: m.onboarding_skill_research() }, - { value: 'Communications/ PR', label: m.onboarding_skill_comms() }, - { value: 'Fundraising', label: m.onboarding_skill_fundraising() }, - { value: 'Community Organizing', label: m.onboarding_skill_community() }, - { value: 'Political Advocacy/ Lobbying', label: m.onboarding_skill_advocacy() }, - { value: 'Education/ Teaching', label: m.onboarding_skill_education() }, - { value: 'Administrative Support', label: m.onboarding_skill_admin() }, - { value: 'Legal Knowledge', label: m.onboarding_skill_legal() }, - { value: 'Other', label: m.onboarding_skill_other() } + { value: 'Software Development', label: msgs.onboarding_skill_software }, + { value: 'Video Creation', label: msgs.onboarding_skill_video }, + { value: 'Social Media Management', label: msgs.onboarding_skill_social_media }, + { value: 'Event Organization', label: msgs.onboarding_skill_events }, + { value: 'Public Speaking/ Presentation', label: msgs.onboarding_skill_speaking }, + { value: 'Writing', label: msgs.onboarding_skill_writing }, + { value: 'Graphic Design/ Visual Arts', label: msgs.onboarding_skill_design }, + { value: 'Research', label: msgs.onboarding_skill_research }, + { value: 'Communications/ PR', label: msgs.onboarding_skill_comms }, + { value: 'Fundraising', label: msgs.onboarding_skill_fundraising }, + { value: 'Community Organizing', label: msgs.onboarding_skill_community }, + { value: 'Political Advocacy/ Lobbying', label: msgs.onboarding_skill_advocacy }, + { value: 'Education/ Teaching', label: msgs.onboarding_skill_education }, + { value: 'Administrative Support', label: msgs.onboarding_skill_admin }, + { value: 'Legal Knowledge', label: msgs.onboarding_skill_legal }, + { value: 'Other', label: msgs.onboarding_skill_other } ] } -export function getWeeklyHours(): { value: string; label: string }[] { +export function getWeeklyHours(msgs: OnboardingMessages): { value: string; label: string }[] { return [ - { value: 'Less than 3 hours', label: m.onboarding_hours_under_3() }, - { value: '3-6 hours', label: m.onboarding_hours_3_6() }, - { value: '6-10 hours', label: m.onboarding_hours_6_10() }, - { value: '10-20 hours', label: m.onboarding_hours_10_20() }, - { value: '20+ hours', label: m.onboarding_hours_20_plus() } + { value: 'Less than 3 hours', label: msgs.onboarding_hours_under_3 }, + { value: '3-6 hours', label: msgs.onboarding_hours_3_6 }, + { value: '6-10 hours', label: msgs.onboarding_hours_6_10 }, + { value: '10-20 hours', label: msgs.onboarding_hours_10_20 }, + { value: '20+ hours', label: msgs.onboarding_hours_20_plus } ] } -export function getIntentOptions(): { key: IntentKey; icon: string; label: string; sub: string }[] { +export function getIntentOptions( + msgs: OnboardingMessages +): { key: IntentKey; icon: string; label: string; sub: string }[] { return [ { key: 'act-now', icon: '✊', - label: m.onboarding_intent_act_now_label(), - sub: m.onboarding_intent_act_now_sub() + label: msgs.onboarding_intent_act_now_label, + sub: msgs.onboarding_intent_act_now_sub }, { key: 'volunteer', icon: '🤝', - label: m.onboarding_intent_volunteer_label(), - sub: m.onboarding_intent_volunteer_sub() + label: msgs.onboarding_intent_volunteer_label, + sub: msgs.onboarding_intent_volunteer_sub }, { key: 'lead', icon: '🚀', - label: m.onboarding_intent_lead_label(), - sub: m.onboarding_intent_lead_sub() + label: msgs.onboarding_intent_lead_label, + sub: msgs.onboarding_intent_lead_sub } ] } diff --git a/src/lib/env.ts b/src/lib/env.ts index ef20f03ab..61aefc29d 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -74,20 +74,13 @@ export function possiblyOverriddenLocales( if (inclusive) { const unknown = namedLocales.filter((l) => l !== 'all' && !defaults.locales.includes(l)) if (unknown.length > 0) { - if (isDev(env)) { - console.warn( - `Warning: ${source} requests "${unknown.join(', ')}" which are not in default-settings.js. Adding for local dev only.` - ) - result.push(...unknown) - } else { - console.error( - `Error: ${source} requests "${unknown.join(', ')}" but default-settings.js only has [${defaults.locales.join(', ')}].` - ) - console.error( - `Add the locale(s) to default-settings.js or fix your .env / PARAGLIDE_LOCALES.` - ) - process.exit(1) - } + console.error( + `Error: ${source} requests "${unknown.join(', ')}" but default-settings.js only has [${defaults.locales.join(', ')}].` + ) + console.error( + `Add the locale(s) to default-settings.js or fix your .env / PARAGLIDE_LOCALES.` + ) + process.exit(1) } } console.log(`Locale source: ${source} → [${result.join(', ')}]`) diff --git a/src/routes/embed/onboarding-form/+page.server.ts b/src/routes/embed/onboarding-form/+page.server.ts index 2d119fc22..69596790a 100644 --- a/src/routes/embed/onboarding-form/+page.server.ts +++ b/src/routes/embed/onboarding-form/+page.server.ts @@ -1,8 +1,6 @@ import { fail } from '@sveltejs/kit' -import * as m from '$lib/paraglide/messages.js' import type { FieldSet } from 'airtable' -import type { Actions, PageServerLoad } from './$types' -import { isLocale, setLocale } from '$lib/paraglide/runtime' +import type { Actions } from './$types' import type { NationalGroupsApiResponse } from '$api/national-groups/+server.js' import { createRecord, updateRecord } from '$lib/airtable' import { isOnboardingLive } from '$lib/server/onboarding' @@ -22,13 +20,6 @@ import { export const prerender = false -export const load: PageServerLoad = ({ url }) => { - const locale = url.searchParams.get('locale') - if (locale && isLocale(locale)) { - setLocale(locale) - } -} - // Write target per the Plan of Action: base "PauseAI Volunteers & Actions", // table "Members". const AIRTABLE_BASE_ID = 'appWPTGqZmUcs3NWu' @@ -99,21 +90,21 @@ export const actions: Actions = { const hasVolunteerDetails = data.get('volunteer_details') === 'on' if (!fullName || !email || !country || !city) { - return fail(400, { message: m.onboarding_error_missing_fields() }) + return fail(400, { message: 'Please fill in your name, email, country and city.' }) } if (!/^\S+@\S+\.\S+$/.test(email)) { - return fail(400, { message: m.onboarding_error_invalid_email() }) + return fail(400, { message: 'Please enter a valid email address.' }) } if (!COUNTRIES.includes(country)) { - return fail(400, { message: m.onboarding_error_invalid_country() }) + return fail(400, { message: 'Please select a country from the list.' }) } if (!isIntent(intent)) { - return fail(400, { message: m.onboarding_error_missing_intent() }) + return fail(400, { message: 'Please choose what brings you here.' }) } // Updates (volunteer step 3) carry an existing record id and already // consented at step 2; require consent only when creating a record. if (!existingRecordId && !gdprAgreed) { - return fail(400, { message: m.onboarding_error_missing_gdpr() }) + return fail(400, { message: 'Please agree to the data processing consent to continue.' }) } const fields: FieldSet = { @@ -138,18 +129,18 @@ export const actions: Actions = { const hours = getString(data, 'hours') if (!languages.length) { - return fail(400, { message: m.onboarding_error_missing_languages() }) + return fail(400, { message: 'Please select at least one language you speak.' }) } if (!WEEKLY_HOURS.includes(hours)) { - return fail(400, { message: m.onboarding_error_missing_hours() }) + return fail(400, { message: 'Please tell us how much time you can commit weekly.' }) } if (data.get('agree_volunteer') !== 'on' || data.get('agree_conduct') !== 'on') { return fail(400, { - message: m.onboarding_error_missing_agreements() + message: 'Please agree to the Volunteer Agreement and Code of Conduct.' }) } if (discovery && !DISCOVERY_OPTIONS.includes(discovery)) { - return fail(400, { message: m.onboarding_error_invalid_discovery() }) + return fail(400, { message: 'Please select a valid option for how you found us.' }) } fields['Discord Username'] = getString(data, 'discord_username') @@ -185,14 +176,14 @@ export const actions: Actions = { if (recordId) { const updated = await updateRecord(AIRTABLE_BASE_ID, MEMBERS_TABLE_ID, recordId, fields) if (!updated) { - return fail(502, { message: m.onboarding_error_save_failed() }) + return fail(502, { message: 'Sorry, we could not save your details. Please try again.' }) } } else { recordId = await createRecord(AIRTABLE_BASE_ID, MEMBERS_TABLE_ID, fields) // A failed write returns undefined; surface it instead of telling // the user they're signed up when no record was created. if (!recordId) { - return fail(502, { message: m.onboarding_error_save_failed() }) + return fail(502, { message: 'Sorry, we could not save your details. Please try again.' }) } // Subscription happens on the initial (step 2) submission only; // the volunteer-form update never re-subscribes. diff --git a/src/routes/embed/onboarding-form/+page.svelte b/src/routes/embed/onboarding-form/+page.svelte index 118f4e0ea..dc58def34 100644 --- a/src/routes/embed/onboarding-form/+page.svelte +++ b/src/routes/embed/onboarding-form/+page.svelte @@ -3,16 +3,20 @@ import { page } from '$app/state' import OnboardingFlow from '$lib/components/onboarding/OnboardingFlow.svelte' import PostMeta from '$lib/components/PostMeta.svelte' - import * as m from '$lib/paraglide/messages.js' - import { isLocale, setLocale } from '$lib/paraglide/runtime' + import { + isOnboardingLocale, + setOnboardingLocale, + getMessages + } from '$lib/components/onboarding/i18n.svelte' $effect(() => { const locale = page.url.searchParams.get('locale') - if (locale && isLocale(locale)) setLocale(locale) + if (locale && isOnboardingLocale(locale)) setOnboardingLocale(locale) }) - const title = $derived(m.onboarding_page_title()) - const description = $derived(m.onboarding_page_description()) + const msgs = $derived(getMessages()) + const title = $derived(msgs.onboarding_page_title) + const description = $derived(msgs.onboarding_page_description) const initialCountry = $derived(page.url.searchParams.get('country') ?? '') From c7bac3475a048609250a2a9bd611a5dd0b6756d3 Mon Sep 17 00:00:00 2001 From: Harry Turnbull <52940497+hturnbull93@users.noreply.github.com> Date: Thu, 2 Jul 2026 08:29:13 +0100 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20remove=20safeHtml=20sanitizer?= =?UTF-8?q?=20=E2=80=94=20translations=20are=20now=20hardcoded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With translations hardcoded in messages.ts rather than LLM-generated, {@html} content is fully under our control. The safeHtml allowlist sanitizer is no longer needed. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_015aKhZeP6x8pgBkDEraXGUs --- .../components/onboarding/ActionCards.svelte | 5 ++--- .../onboarding/OnboardingFlow.svelte | 21 +++++++++---------- src/lib/sanitize.ts | 15 ------------- 3 files changed, 12 insertions(+), 29 deletions(-) delete mode 100644 src/lib/sanitize.ts diff --git a/src/lib/components/onboarding/ActionCards.svelte b/src/lib/components/onboarding/ActionCards.svelte index b1fae892b..787d15a0e 100644 --- a/src/lib/components/onboarding/ActionCards.svelte +++ b/src/lib/components/onboarding/ActionCards.svelte @@ -1,7 +1,6 @@ @@ -21,7 +20,7 @@
  • {msgs.onboarding_action_talk_title}

    -

    {@html safeHtml(msgs.onboarding_action_talk_sub)}

    +

    {@html msgs.onboarding_action_talk_sub}

  • @@ -52,7 +51,7 @@

  • {msgs.onboarding_action_donate_title}

    -

    {@html safeHtml(msgs.onboarding_action_donate_sub)}

    +

    {@html msgs.onboarding_action_donate_sub}

  • diff --git a/src/lib/components/onboarding/OnboardingFlow.svelte b/src/lib/components/onboarding/OnboardingFlow.svelte index 98b5cfb68..69fac6875 100644 --- a/src/lib/components/onboarding/OnboardingFlow.svelte +++ b/src/lib/components/onboarding/OnboardingFlow.svelte @@ -5,7 +5,6 @@ import type { NationalGroupsApiResponse } from '$api/national-groups/+server.js' import type { OnboardingModeApiResponse } from '$api/onboarding-mode/+server.js' import { toast } from 'svelte-french-toast' - import { safeHtml } from '$lib/sanitize' import Link from '$lib/components/Link.svelte' import LinkWithoutIcon from '$lib/components/LinkWithoutIcon.svelte' import Socials from '$lib/components/Socials.svelte' @@ -322,15 +321,15 @@ {/snippet} {#snippet nextStepBlock()}

    {msgs.onboarding_next_step_title}

    -

    {@html safeHtml(msgs.onboarding_next_step_p1)}

    -

    {@html safeHtml(msgs.onboarding_next_step_p2)}

    +

    {@html msgs.onboarding_next_step_p1}

    +

    {@html msgs.onboarding_next_step_p2}

    {/snippet} @@ -648,7 +647,7 @@ name="discord_username" bind:value={volunteer.discordUsername} /> -

    {@html safeHtml(msgs.onboarding_helper_discord)}

    +

    {@html msgs.onboarding_helper_discord}

    @@ -771,12 +770,12 @@ bind:checked={agreements.volunteer} /> - {@html safeHtml(msgs.onboarding_agree_volunteer)} + {@html msgs.onboarding_agree_volunteer}
    @@ -770,11 +773,13 @@ bind:checked={agreements.volunteer} /> + {@html msgs.onboarding_agree_volunteer}