diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..79d38c5 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +PUBLIC_GA_ID= \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebeda96..514f6e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: pull_request: - branches: [main] + branches: [main, develop] jobs: build: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5b7325b..e0db204 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: Deploy to GitHub Pages on: push: - branches: [ main ] + branches: [main] workflow_dispatch: permissions: @@ -11,7 +11,7 @@ permissions: id-token: write concurrency: - group: "pages" + group: 'pages' cancel-in-progress: false jobs: @@ -20,18 +20,18 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - + - name: Setup Node uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' - + - name: Install dependencies run: npm install - + - name: Build with Astro run: npm run build - + - name: Upload artifact if: github.event_name != 'pull_request' uses: actions/upload-pages-artifact@v3 diff --git a/.gitignore b/.gitignore index c025635..a053b18 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,8 @@ dist/ .DS_Store # vscode settings -.vscode/ \ No newline at end of file +.vscode/ + +*.local +test-results/ +playwright-report/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2caa11f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,199 @@ +# AGENTS.md + +This document provides a detailed guide for agents interacting with this codebase, which is built on the Astro framework. Follow these standards to ensure consistency and maintainability. For further details on Astro, refer to the [official documentation](https://docs.astro.build/). + +--- + +## 1. Build, Lint, and Test Commands + +### Build Commands + +- **Development Server**: + + ```bash + pnpm dev + ``` + + Use this command to spin up a development server. It includes live-reloading. + +- **Build for Production**: + + ```bash + pnpm build + ``` + + This compiles the codebase into optimized production-ready files under the `dist/` directory. + +- **Preview Production Build**: + ```bash + pnpm preview + ``` + This command serves the production build locally for testing. + +### Linting and Formatting + +- **Run Prettier**: + ```bash + pnpm format + ``` + This formats all code under the `src` folder according to the Prettier settings. + +### Testing Commands + +This project uses **Vitest** for unit/logic testing and **Playwright** for E2E/smoke testing. + +- **Run all tests**: + ```bash + pnpm test + ``` + +- **Run unit tests only**: + ```bash + pnpm test:unit + ``` + +- **Run E2E tests only**: + ```bash + pnpm test:e2e + ``` + +Vitest tests should be located in `__tests__` directories relative to the code they test. +Playwright tests are located in the `e2e/` directory. + +--- + +## 2. Code Style Guidelines + +### General Formatting Rules + +This codebase uses **Prettier** for consistent formatting. Key settings include: + +- **Tab Width**: 2 spaces +- **Line Width**: 110 characters max +- **Semicolons**: Disabled +- **Quotes**: Single quotes preferred (`'example'`, not `"example"`) +- **Plugins**: Uses `prettier-plugin-astro` for `.astro` files. + +Run the formatter before committing code: + +```bash +pnpm format +``` + +### Import Conventions + +1. Place **external imports** above internal ones: + +```typescript +import { useState } from 'react' +import utils from '@/lib/utils' +``` + +2. **Do not use wildcard imports** (e.g., `import * as fs`). +3. Maintain alphabetical order where possible. + +### Code Organization + +1. Use TypeScript for all new files (`.ts`, `.tsx`). +2. Follow the directory structure: + - Components in `src/components` + - Pages in `src/pages` + - Utilities in `src/lib` + +### Naming Conventions + +#### Files/Folders + +- Use `kebab-case` for filenames (`example-file.ts`). +- Directories reflect their contents (`components`, `utils`). + +#### Variables and Functions + +Use `camelCase` for variables and methods: + +```javascript +const fetchData = () => {} +``` + +#### Types + +- Interface names should use `PascalCase`: + +```typescript +interface User { + id: string + name: string +} +``` + +### Error Handling + +1. Always check for edge cases in asynchronous operations: + +```typescript +try { + const response = await fetch('/api/data') + const data = response.json() +} catch (error) { + console.error('Error fetching data:', error) +} +``` + +2. Avoid silent failures. Log or handle errors appropriately. + +--- + +## 3. Guidelines for Agents + +Agents (like Copilot) must adhere to these coding standards to ensure consistency. + +1. **Pre-Execution** + - Ensure Prettier is configured. Adjust any settings, such as overriding `tabWidth` or `printWidth`, to align with this repository. + - Validate TypeScript definitions before proceeding. + +2. **During Execution** + - When generating test files, suggest using Jest or Vitest (if no tests are found). + - For updates, refactor to use modular imports and maintain concise separation of logic. + - **Follow SEO and i18n guidelines** defined in section 4 when creating or modifying pages. + +3. **Post-Execution** + - Always include a test or linting step before suggesting commits. + - Suggest meaningful commit messages, such as: + ```bash + chore: format code with Prettier + ``` + +--- + +## 4. SEO and Page Creation Guidelines + +Agents must ensure all new pages are optimized for search engines and follow the project's internationalization (i18n) structure. + +### Multi-language Pages +- All new pages must be placed in `src/pages/[lang]/`. +- Use `getStaticPaths()` to support all configured locales (`es`, `en`, `ca`). +- Example structure: + ```typescript + export function getStaticPaths() { + return [{ params: { lang: 'es' } }, { params: { lang: 'en' } }, { params: { lang: 'ca' } }] + } + ``` + +### Layout and Metadata +- Every page **must** use the `Layout` component from `src/layouts/Layout.astro`. +- Pass a unique and descriptive `title` and `description` (150-160 characters) to the `Layout` component. +- The `Layout` component automatically handles canonical URLs, social media tags (OG/Twitter), and `hreflang` tags. + +### Semantic HTML and Accessibility +- **H1 Tags**: Use exactly one `

` per page. +- **Headings**: Maintain a logical hierarchy (`h2`, `h3`, etc.). +- **Images**: All `` tags must include a descriptive `alt` attribute. +- **Links**: Use descriptive text for links. Avoid generic phrases like "click here". + +### Analytics and Monitoring +- Use the `PUBLIC_GA_ID` environment variable for Google Analytics. +- Do not hardcode tracking IDs. + +--- + +Adhering to these standards will ensure contributions are cohesive, minimizing review cycles and enhancing overall productivity. diff --git a/README.md b/README.md index d76a6c7..ba9e479 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,61 @@ pnpm build pnpm preview ``` +## Testing + +This project uses **Vitest** for unit/logic testing and **Playwright** for End-to-End (E2E) smoke testing. + +### Run all tests +```bash +pnpm test +``` + +### Run unit tests only +Used for verifying translations, utility functions, and business logic. +```bash +pnpm test:unit +``` + +### Run E2E tests only +Used for checking links, SEO metadata, and user flows across all supported browsers. +```bash +pnpm test:e2e +``` + +## SEO & New Pages + +To maintain good SEO and consistency as the project grows, follow these guidelines when adding new pages: + +### 1. Creating Multi-language Pages +New pages should be created in `src/pages/[lang]/` using `getStaticPaths`. +- Ensure you use the `` component. +- Always provide a unique `title` and `description` to the Layout. + +Example: +```astro +--- +import Layout from '../../layouts/Layout.astro' +// ... +--- + + + +``` + +### 2. SEO Best Practices +- **Semantic HTML**: Use only one `

` per page. Follow a logical heading hierarchy (`

`, `

`). +- **Image Alt Tags**: All `` tags MUST have descriptive `alt` attributes. +- **Internal Linking**: Use descriptive link text (avoid "click here"). + +### 3. Analytics +- Set the `PUBLIC_GA_ID` environment variable in your `.env` file to enable Google Analytics. + ```text + PUBLIC_GA_ID=G-XXXXXXXXXX + ``` + +### 4. Structured Data +- The main event structured data (JSON-LD) is globally included in `Layout.astro`. +- For specific pages (like "Sponsors" or "Talks"), consider adding additional [schema.org](https://schema.org) types locally if necessary. + +### 5. Sitemap +- The sitemap is automatically generated on every build. No manual action is required. diff --git a/astro.config.mjs b/astro.config.mjs index 6f86018..4835849 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,9 +1,11 @@ import { defineConfig } from 'astro/config' import tailwindcss from '@tailwindcss/vite' +import sitemap from '@astrojs/sitemap' export default defineConfig({ site: 'https://2026.es.pycon.org', base: '/', + integrations: [sitemap()], vite: { plugins: [tailwindcss()], }, @@ -12,7 +14,7 @@ export default defineConfig({ locales: ['es', 'en', 'ca'], routing: { prefixDefaultLocale: true, - redirectToDefaultLocale: false + redirectToDefaultLocale: false, }, }, }) diff --git a/e2e/accessibility.spec.ts b/e2e/accessibility.spec.ts new file mode 100644 index 0000000..33dfa56 --- /dev/null +++ b/e2e/accessibility.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from '@playwright/test' +import AxeBuilder from '@axe-core/playwright' + +const languages = ['es', 'en', 'ca'] +const pages = [ + '', // Home + 'code-of-conduct', + 'location', + 'sponsors' +] + +test.describe('Accessibility Audit', () => { + for (const lang of languages) { + for (const pagePath of pages) { + const url = `/${lang}/${pagePath}` + + test(`page "${url}" should not have accessibility violations`, async ({ page }) => { + await page.goto(url) + + // Wait for page to be ready + await page.waitForLoadState('networkidle') + + // Run scan + const accessibilityScanResults = await new AxeBuilder({ page }) + .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) + .analyze() + + // If it fails, Playwright report will detailed the issues + expect(accessibilityScanResults.violations).toEqual([]) + }) + } + } +}) diff --git a/e2e/language-picker.spec.ts b/e2e/language-picker.spec.ts new file mode 100644 index 0000000..f91c13d --- /dev/null +++ b/e2e/language-picker.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@playwright/test' + +const languages = ['es', 'en', 'ca'] + +test.describe('Language Selection', () => { + test('Desktop: should allow switching languages', async ({ page, isMobile }) => { + test.skip(isMobile, 'This test is for desktop only') + + await page.goto('/es/') + + // Switch to English + const enLink = page.locator('.desktop-menu + div .lang-picker a').filter({ + hasText: /^en$/i + }) + await expect(enLink).toBeVisible() + await enLink.click() + + // Match /en or /en/ + await expect(page).toHaveURL(/\/en\/?$/) + + // Switch to Catalan + const caLink = page.locator('.desktop-menu + div .lang-picker a').filter({ + hasText: /^ca$/i + }) + await expect(caLink).toBeVisible() + await caLink.click() + await expect(page).toHaveURL(/\/ca\/?$/) + }) + + test('Mobile: should allow switching languages via mobile menu', async ({ page, isMobile }) => { + test.skip(!isMobile, 'This test is for mobile only') + + await page.goto('/es/') + + // Open mobile menu + await page.locator('.responsive-toggle').click() + + // Switch to English + const enLink = page.locator('.mobile-menu .lang-picker-mobile a').filter({ + hasText: /^en$/i + }) + await expect(enLink).toBeVisible() + await enLink.click() + + await expect(page).toHaveURL(/\/en\/?$/) + + // Re-open menu to switch to Catalan + await page.locator('.responsive-toggle').click() + const caLink = page.locator('.mobile-menu .lang-picker-mobile a').filter({ + hasText: /^ca$/i + }) + await expect(caLink).toBeVisible() + await caLink.click() + await expect(page).toHaveURL(/\/ca\/?$/) + }) +}) diff --git a/e2e/mobile-nav.spec.ts b/e2e/mobile-nav.spec.ts new file mode 100644 index 0000000..a7f95c8 --- /dev/null +++ b/e2e/mobile-nav.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test' + +test.describe('Mobile Navigation', () => { + test.use({ viewport: { width: 375, height: 667 } }) // Mobile viewport + + test('should toggle mobile menu when clicking the burger button', async ({ page }) => { + await page.goto('/es/') + + const toggle = page.locator('.responsive-toggle') + const mobileMenu = page.locator('.mobile-menu') + + // Initially hidden + await expect(mobileMenu).not.toHaveClass(/show/) + + // Toggle ON + await toggle.click() + await expect(mobileMenu).toHaveClass(/show/) + + // Toggle OFF + await toggle.click() + await expect(mobileMenu).not.toHaveClass(/show/) + }) + + test('should close mobile menu on Escape key', async ({ page }) => { + await page.goto('/es/') + + const toggle = page.locator('.responsive-toggle') + const mobileMenu = page.locator('.mobile-menu') + + await toggle.click() + await expect(mobileMenu).toHaveClass(/show/) + + await page.keyboard.press('Escape') + await expect(mobileMenu).not.toHaveClass(/show/) + }) +}) diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts new file mode 100644 index 0000000..ce5389f --- /dev/null +++ b/e2e/smoke.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test' + +const languages = ['es', 'en', 'ca'] + +for (const lang of languages) { + test.describe(`Language: ${lang}`, () => { + test('homepage should load and have SEO metadata', async ({ page }) => { + await page.goto(`/${lang}/`) + + // Check Title + await expect(page).toHaveTitle(/PyConES 2026/i) + + // Check for Meta Description (standard for SEO) + const description = await page.getAttribute('meta[name="description"]', 'content') + expect(description).toBeTruthy() + expect(description?.length).toBeGreaterThan(50) + + // Check for viewport meta (responsive) + const viewport = await page.getAttribute('meta[name="viewport"]', 'content') + expect(viewport).toContain('width=device-width') + }) + + test('should not have broken internal links', async ({ page }) => { + await page.goto(`/${lang}/`) + + const links = await page.getByRole('link').all() + for (const link of links) { + const href = await link.getAttribute('href') + if (href && href.startsWith('/') && !href.startsWith('//')) { + const response = await page.request.get(href) + expect(response.status()).toBe(200) + } + } + }) + }) +} diff --git a/e2e/visual.spec.ts b/e2e/visual.spec.ts new file mode 100644 index 0000000..0532483 --- /dev/null +++ b/e2e/visual.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '@playwright/test' + +test.describe('Visual Regression', () => { + test('should match homepage snapshot (Desktop)', async ({ page, isMobile }) => { + test.skip(isMobile, 'Only run on Desktop for visual consistency') + + await page.goto('/es/') + + // Wait for dynamic content and animations to settle + await page.waitForLoadState('networkidle') + await page.waitForTimeout(1000) + + await expect(page).toHaveScreenshot('homepage-es-desktop.png', { + fullPage: true, + maxDiffPixelRatio: 0.1, + }) + }) + + test('should match navigation snapshot', async ({ page }) => { + await page.goto('/es/') + + // Use the navigation bar ID, as the
tag might have 0 height due to fixed positioning + const nav = page.locator('#main-navigation') + + await expect(nav).toBeVisible() + await expect(nav).toHaveScreenshot('navigation-es.png') + }) +}) diff --git a/e2e/visual.spec.ts-snapshots/homepage-es-desktop-chromium-darwin.png b/e2e/visual.spec.ts-snapshots/homepage-es-desktop-chromium-darwin.png new file mode 100644 index 0000000..e7f4cfb Binary files /dev/null and b/e2e/visual.spec.ts-snapshots/homepage-es-desktop-chromium-darwin.png differ diff --git a/e2e/visual.spec.ts-snapshots/navigation-es-chromium-darwin.png b/e2e/visual.spec.ts-snapshots/navigation-es-chromium-darwin.png new file mode 100644 index 0000000..177958b Binary files /dev/null and b/e2e/visual.spec.ts-snapshots/navigation-es-chromium-darwin.png differ diff --git a/package.json b/package.json index 639f78c..dc9c785 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,14 @@ "name": "2026.es.pycon.org", "version": "1.0.0", "description": "", + "type": "module", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "pnpm test:unit && pnpm test:e2e", + "test:unit": "vitest run", + "test:e2e": "playwright test", + "test:visual": "playwright test visual --update-snapshots", + "test:a11y": "playwright test accessibility", "dev": "astro dev", "build": "astro build", "preview": "astro preview", @@ -15,6 +20,7 @@ "license": "ISC", "packageManager": "pnpm@10.10.0", "dependencies": { + "@astrojs/sitemap": "^3.7.1", "@fontsource-variable/jetbrains-mono": "^5.2.8", "@fontsource-variable/outfit": "^5.2.8", "@tailwindcss/vite": "^4.1.18", @@ -24,9 +30,12 @@ }, "devDependencies": { "@astrojs/check": "^0.9.6", + "@axe-core/playwright": "^4.11.1", + "@playwright/test": "^1.58.2", "prettier": "^3.7.4", "prettier-plugin-astro": "^0.14.1", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.1.0" }, "prettier": { "tabWidth": 2, diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..80c8528 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,29 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:4321', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'mobile', + use: { ...devices['iPhone 13'] }, + }, + ], + webServer: { + command: 'pnpm dev', + url: 'http://localhost:4321', + reuseExistingServer: !process.env.CI, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5fc6b3..c306020 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@astrojs/sitemap': + specifier: ^3.7.1 + version: 3.7.1 '@fontsource-variable/jetbrains-mono': specifier: ^5.2.8 version: 5.2.8 @@ -16,13 +19,13 @@ importers: version: 5.2.8 '@tailwindcss/vite': specifier: ^4.1.18 - version: 4.1.18(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + version: 4.1.18(vite@6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) animejs: specifier: ^4.2.2 version: 4.2.2 astro: specifier: ^5.16.8 - version: 5.16.8(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.55.1)(typescript@5.9.3)(yaml@2.8.2) + version: 5.16.8(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.55.1)(typescript@5.9.3)(yaml@2.8.2) tailwindcss: specifier: ^4.1.18 version: 4.1.18 @@ -30,6 +33,12 @@ importers: '@astrojs/check': specifier: ^0.9.6 version: 0.9.6(prettier-plugin-astro@0.14.1)(prettier@3.7.4)(typescript@5.9.3) + '@axe-core/playwright': + specifier: ^4.11.1 + version: 4.11.1(playwright-core@1.58.2) + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 prettier: specifier: ^3.7.4 version: 3.7.4 @@ -39,6 +48,9 @@ importers: typescript: specifier: ^5.9.3 version: 5.9.3 + vitest: + specifier: ^4.1.0 + version: 4.1.0(@types/node@24.12.0)(vite@6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) packages: @@ -73,6 +85,9 @@ packages: resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} + '@astrojs/sitemap@3.7.1': + resolution: {integrity: sha512-IzQqdTeskaMX+QDZCzMuJIp8A8C1vgzMBp/NmHNnadepHYNHcxQdGLQZYfkbd2EbRXUfOS+UDIKx8sKg0oWVdw==} + '@astrojs/telemetry@3.3.0': resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} @@ -80,6 +95,11 @@ packages: '@astrojs/yaml2ts@0.2.2': resolution: {integrity: sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ==} + '@axe-core/playwright@4.11.1': + resolution: {integrity: sha512-mKEfoUIB1MkVTht0BGZFXtSAEKXMJoDkyV5YZ9jbBmZCcWDz71tegNsdTkIN8zc/yMi5Gm2kx7Z5YQ9PfWNAWw==} + peerDependencies: + playwright-core: '>= 1.0.0' + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -443,6 +463,11 @@ packages: '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -598,6 +623,9 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tailwindcss/node@4.1.18': resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} @@ -688,9 +716,15 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -706,12 +740,47 @@ packages: '@types/nlcst@2.0.3': resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + '@types/node@24.12.0': + resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} + + '@types/sax@1.2.7': + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitest/expect@4.1.0': + resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} + + '@vitest/mocker@4.1.0': + resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.0': + resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} + + '@vitest/runner@4.1.0': + resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} + + '@vitest/snapshot@4.1.0': + resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} + + '@vitest/spy@4.1.0': + resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} + + '@vitest/utils@4.1.0': + resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + '@volar/kit@2.4.27': resolution: {integrity: sha512-ilZoQDMLzqmSsImJRWx4YiZ4FcvvPrPnFVmL6hSsIWB6Bn3qc7k88J9yP32dagrs5Y8EXIlvvD/mAFaiuEOACQ==} peerDependencies: @@ -780,6 +849,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -790,11 +862,19 @@ packages: array-iterate@2.0.1: resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + astro@5.16.8: resolution: {integrity: sha512-gzZE+epuCrNuxOa8/F1dzkllDOFvxWhGeobQKeBRIAef5sUpUKMHZo/8clse+02rYnKJCgwXBgjW4uTu9mqUUw==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true + axe-core@4.11.1: + resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} + engines: {node: '>=4'} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -819,6 +899,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -869,6 +953,9 @@ packages: common-ancestor-path@1.0.1: resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} @@ -987,6 +1074,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -1009,6 +1099,10 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -1038,6 +1132,11 @@ packages: resolution: {integrity: sha512-b0RdzQeztiiUFWEDzq6Ka26qkNVNLCehoRtifOIGNbQ4CfxyYRh73fyWaQX/JshPVcueITOEeoSWPy5XQv8FUg==} engines: {node: '>=20'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1404,6 +1503,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} @@ -1440,6 +1542,9 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + piccolore@0.1.3: resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} @@ -1454,6 +1559,16 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -1575,9 +1690,17 @@ packages: shiki@3.21.0: resolution: {integrity: sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + sitemap@9.0.1: + resolution: {integrity: sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==} + engines: {node: '>=20.19.5', npm: '>=10.8.2'} + hasBin: true + smol-toml@1.6.0: resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} engines: {node: '>= 18'} @@ -1589,6 +1712,15 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + + stream-replace-string@2.0.0: + resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1626,6 +1758,9 @@ packages: tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -1634,6 +1769,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -1677,6 +1816,9 @@ packages: uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -1829,6 +1971,41 @@ packages: vite: optional: true + vitest@4.1.0: + resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.0 + '@vitest/browser-preview': 4.1.0 + '@vitest/browser-webdriverio': 4.1.0 + '@vitest/ui': 4.1.0 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + volar-service-css@0.0.68: resolution: {integrity: sha512-lJSMh6f3QzZ1tdLOZOzovLX0xzAadPhx8EKwraDLPxBndLCYfoTvnNuiFFV8FARrpAlW5C0WkH+TstPaCxr00Q==} peerDependencies: @@ -1928,6 +2105,11 @@ packages: resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} engines: {node: '>=4'} + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + widest-line@5.0.0: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} @@ -1995,6 +2177,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -2071,6 +2256,12 @@ snapshots: dependencies: prismjs: 1.30.0 + '@astrojs/sitemap@3.7.1': + dependencies: + sitemap: 9.0.1 + stream-replace-string: 2.0.0 + zod: 4.3.6 + '@astrojs/telemetry@3.3.0': dependencies: ci-info: 4.3.1 @@ -2087,6 +2278,11 @@ snapshots: dependencies: yaml: 2.8.2 + '@axe-core/playwright@4.11.1(playwright-core@1.58.2)': + dependencies: + axe-core: 4.11.1 + playwright-core: 1.58.2 + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -2332,6 +2528,10 @@ snapshots: '@oslojs/encoding@1.1.0': {} + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + '@rollup/pluginutils@5.3.0(rollup@4.55.1)': dependencies: '@types/estree': 1.0.8 @@ -2448,6 +2648,8 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@standard-schema/spec@1.1.0': {} + '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 @@ -2509,17 +2711,24 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - '@tailwindcss/vite@4.1.18(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))': + '@tailwindcss/vite@4.1.18(vite@6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))': dependencies: '@tailwindcss/node': 4.1.18 '@tailwindcss/oxide': 4.1.18 tailwindcss: 4.1.18 - vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vite: 6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/hast@3.0.4': @@ -2536,10 +2745,59 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/node@24.12.0': + dependencies: + undici-types: 7.16.0 + + '@types/sax@1.2.7': + dependencies: + '@types/node': 24.12.0 + '@types/unist@3.0.3': {} '@ungap/structured-clone@1.3.0': {} + '@vitest/expect@4.1.0': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.0(vite@6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.1.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + + '@vitest/pretty-format@4.1.0': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.0': + dependencies: + '@vitest/utils': 4.1.0 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + '@vitest/utils': 4.1.0 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.0': {} + + '@vitest/utils@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@volar/kit@2.4.27(typescript@5.9.3)': dependencies: '@volar/language-service': 2.4.27 @@ -2624,13 +2882,17 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + arg@5.0.2: {} + argparse@2.0.1: {} aria-query@5.3.2: {} array-iterate@2.0.1: {} - astro@5.16.8(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.55.1)(typescript@5.9.3)(yaml@2.8.2): + assertion-error@2.0.1: {} + + astro@5.16.8(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.55.1)(typescript@5.9.3)(yaml@2.8.2): dependencies: '@astrojs/compiler': 2.13.0 '@astrojs/internal-helpers': 0.7.5 @@ -2687,8 +2949,8 @@ snapshots: unist-util-visit: 5.0.0 unstorage: 1.17.3 vfile: 6.0.3 - vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) - vitefu: 1.1.1(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + vite: 6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vitefu: 1.1.1(vite@6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) xxhash-wasm: 1.1.0 yargs-parser: 21.1.1 yocto-spinner: 0.2.3 @@ -2732,6 +2994,8 @@ snapshots: - uploadthing - yaml + axe-core@4.11.1: {} + axobject-query@4.1.0: {} bail@2.0.2: {} @@ -2755,6 +3019,8 @@ snapshots: ccount@2.0.1: {} + chai@6.2.2: {} + chalk@5.6.2: {} character-entities-html4@2.1.0: {} @@ -2791,6 +3057,8 @@ snapshots: common-ancestor-path@1.0.1: {} + convert-source-map@2.0.0: {} + cookie-es@1.2.2: {} cookie@1.1.1: {} @@ -2895,6 +3163,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -2936,6 +3206,8 @@ snapshots: eventemitter3@5.0.1: {} + expect-type@1.3.0: {} + extend@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -2956,6 +3228,9 @@ snapshots: dependencies: tiny-inflate: 1.0.3 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -3512,6 +3787,8 @@ snapshots: dependencies: boolbase: 1.0.0 + obug@2.1.1: {} + ofetch@1.5.1: dependencies: destr: 2.0.5 @@ -3556,6 +3833,8 @@ snapshots: path-browserify@1.0.1: {} + pathe@2.0.3: {} + piccolore@0.1.3: {} picocolors@1.1.1: {} @@ -3564,6 +3843,14 @@ snapshots: picomatch@4.0.3: {} + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -3784,14 +4071,29 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + siginfo@2.0.0: {} + sisteransi@1.0.5: {} + sitemap@9.0.1: + dependencies: + '@types/node': 24.12.0 + '@types/sax': 1.2.7 + arg: 5.0.2 + sax: 1.4.4 + smol-toml@1.6.0: {} source-map-js@1.2.1: {} space-separated-tokens@2.0.2: {} + stackback@0.0.2: {} + + std-env@4.0.0: {} + + stream-replace-string@2.0.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -3837,6 +4139,8 @@ snapshots: tiny-inflate@1.0.3: {} + tinybench@2.9.0: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -3844,6 +4148,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyrainbow@3.1.0: {} + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -3871,6 +4177,8 @@ snapshots: uncrypto@0.1.3: {} + undici-types@7.16.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -3955,7 +4263,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2): + vite@6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -3964,14 +4272,42 @@ snapshots: rollup: 4.55.1 tinyglobby: 0.2.15 optionalDependencies: + '@types/node': 24.12.0 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 yaml: 2.8.2 - vitefu@1.1.1(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): + vitefu@1.1.1(vite@6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): optionalDependencies: - vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vite: 6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + + vitest@4.1.0(@types/node@24.12.0)(vite@6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): + dependencies: + '@vitest/expect': 4.1.0 + '@vitest/mocker': 4.1.0(vite@6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.0 + '@vitest/runner': 4.1.0 + '@vitest/snapshot': 4.1.0 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.12.0 + transitivePeerDependencies: + - msw volar-service-css@0.0.68(@volar/language-service@2.4.27): dependencies: @@ -4074,6 +4410,11 @@ snapshots: which-pm-runs@1.1.0: {} + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + widest-line@5.0.0: dependencies: string-width: 7.2.0 @@ -4144,4 +4485,6 @@ snapshots: zod@3.25.76: {} + zod@4.3.6: {} + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5d3f68b..ddc79a3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,4 +3,4 @@ onlyBuiltDependencies: - sharp packages: - - src \ No newline at end of file + - src diff --git a/public/collaborators/europython_logo.png b/public/collaborators/europython_logo.png new file mode 100644 index 0000000..d76c888 Binary files /dev/null and b/public/collaborators/europython_logo.png differ diff --git a/public/collaborators/pyes_logo.png b/public/collaborators/pyes_logo.png new file mode 100644 index 0000000..cf7ecd9 Binary files /dev/null and b/public/collaborators/pyes_logo.png differ diff --git a/public/collaborators/python_logo.png b/public/collaborators/python_logo.png new file mode 100644 index 0000000..d5dfdc7 Binary files /dev/null and b/public/collaborators/python_logo.png differ diff --git a/public/collaborators/ub_white.png b/public/collaborators/ub_white.png new file mode 100644 index 0000000..45a0d43 Binary files /dev/null and b/public/collaborators/ub_white.png differ diff --git a/public/favicon/site.webmanifest b/public/favicon/site.webmanifest index 88a4b40..dc9b96d 100644 --- a/public/favicon/site.webmanifest +++ b/public/favicon/site.webmanifest @@ -1,19 +1,19 @@ { - "name": "2026.es.pycon.org", - "short_name": "PyConES 2026", - "icons": [ - { - "src": "/favicon/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/favicon/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} \ No newline at end of file + "name": "2026.es.pycon.org", + "short_name": "PyConES 2026", + "icons": [ + { + "src": "/favicon/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/favicon/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#1d1d1b", + "background_color": "#1d1d1b", + "display": "standalone" +} diff --git a/public/images/Barcelona/seuUB.jpg b/public/images/Barcelona/seuUB.jpg new file mode 100644 index 0000000..f2be3ae Binary files /dev/null and b/public/images/Barcelona/seuUB.jpg differ diff --git a/public/images/Barcelona/ub_map.png b/public/images/Barcelona/ub_map.png new file mode 100644 index 0000000..ab5fe3f Binary files /dev/null and b/public/images/Barcelona/ub_map.png differ diff --git a/public/robots.txt b/public/robots.txt index f58dbcb..c02d945 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,4 +1,4 @@ -# Example: Allow all bots to scan and index your site. -# Full syntax: https://developers.google.com/search/docs/advanced/robots/create-robots-txt User-agent: * Allow: / + +Sitemap: https://2026.es.pycon.org/sitemap-index.xml diff --git a/src/components/AccommodationPage.astro b/src/components/AccommodationPage.astro new file mode 100644 index 0000000..f3dc022 --- /dev/null +++ b/src/components/AccommodationPage.astro @@ -0,0 +1,142 @@ +--- +import { accommodationTexts } from '../i18n/accommodation' + +interface Props { + lang: string +} + +const { lang } = Astro.props +const t = accommodationTexts[lang as keyof typeof accommodationTexts] +--- + +
+
+

+ {t['accommodation.hero.title']} +

+

+ {t['accommodation.hero.subtitle']} +

+
+ +
+

+ {t['accommodation.intro']} +

+
+ + +
+
+
+

{t['accommodation.hotels.title']}

+
+
+ +
+ +
+
+ +
+ 🏗️ +

{t['accommodation.hotels.subtitle']}

+

+ {t['accommodation.hotels.disclaimer']} +

+
+
+
+ + +
+

+ + {t['accommodation.areas.title']} +

+
+
+

+ {t['accommodation.areas.eixample.name']} +

+

{t['accommodation.areas.eixample.desc']}

+
+
+

+ {t['accommodation.areas.gothic.name']} +

+

{t['accommodation.areas.gothic.desc']}

+
+
+

+ {t['accommodation.areas.poblenou.name']} +

+

{t['accommodation.areas.poblenou.desc']}

+
+
+
+ + +
+
+
+

+ {t['accommodation.apartments.title']} +

+

+ {t['accommodation.apartments.text']} +

+ + {t['accommodation.apartments.link']} + + +
+
+ 🏙️ +
+
+
+
+ + diff --git a/src/components/Button.astro b/src/components/Button.astro new file mode 100644 index 0000000..b12697e --- /dev/null +++ b/src/components/Button.astro @@ -0,0 +1,34 @@ +--- +interface Props { + variant?: 'primary' | 'secondary' | 'outline' | 'ghost' + size?: 'sm' | 'md' | 'lg' + href?: string + class?: string + [x: string]: any +} + +const { variant = 'primary', size = 'md', href, class: className, ...rest } = Astro.props + +const variants = { + primary: 'bg-orange-500 text-white hover:bg-orange-600', + secondary: 'bg-amber-400 text-stone-900 hover:bg-amber-500', + outline: 'outline outline-2 outline-offset-[-2px] outline-orange-500 text-orange-500 hover:bg-orange-50', + ghost: 'text-stone-900 text-white hover:bg-gray-100 hover:bg-gray-800' +}; + +// Map sizes based on exact Figma sizes +const sizes = { + sm: 'h-8 px-3 py-1.5 text-sm leading-5', + md: 'h-10 px-4 py-2 text-base leading-6', // Uses h-10 based on size specs, though some variants export h-11 + lg: 'h-12 px-6 py-3 text-lg leading-7', +} + +const baseClasses = + "inline-flex items-center justify-center font-['Outfit'] font-medium rounded-lg text-center transition-colors duration-200 cursor-pointer" + +const Element = href ? 'a' : 'button' +--- + + + + diff --git a/src/components/CenteredPanel.astro b/src/components/CenteredPanel.astro new file mode 100644 index 0000000..a400954 --- /dev/null +++ b/src/components/CenteredPanel.astro @@ -0,0 +1,13 @@ +--- +interface Props { + text: string +} + +const { text } = Astro.props +--- + +
+

+ {text} +

+
diff --git a/src/components/LanguagePicker.astro b/src/components/LanguagePicker.astro deleted file mode 100644 index 6ece2a0..0000000 --- a/src/components/LanguagePicker.astro +++ /dev/null @@ -1,54 +0,0 @@ ---- -import { i18n } from 'astro:config/client' -import { texts } from '../i18n/components/LanguagePicker' - -// Idiomas disponibles -const languages = i18n.locales as string[] - -interface Props { - lang: string -} - -const { lang = i18n.defaultLocale } = Astro.params -const t = texts[lang as keyof typeof texts] - -// Idioma actual detectado desde la URL -const pathname = Astro.url.pathname -const currentLang = pathname.split('/')[1] || i18n.defaultLocale - -// Función para construir la URL destino -const getPathForLang = (lang) => { - const parts = pathname.split('/').filter(Boolean) - - // Si ya hay idioma, lo reemplaza - if (languages.some((l) => l === parts[0])) { - parts[0] = lang - } else { - parts.unshift(lang) - } - - return '/' + parts.join('/') -} ---- - - diff --git a/src/components/LocationPage.astro b/src/components/LocationPage.astro new file mode 100644 index 0000000..5a8e7c6 --- /dev/null +++ b/src/components/LocationPage.astro @@ -0,0 +1,337 @@ +--- +import { locationTexts } from '../i18n/location' + +interface Props { + lang: string +} + +const { lang } = Astro.props +const t = locationTexts[lang as keyof typeof locationTexts] + +const transportIcons = { + metro: 'M12 2L4.5 20.29L5.21 21L12 18L18.79 21L19.5 20.29L12 2Z', + train: + 'M12 2c-4 0-8 .5-8 4v9.5C4 17.43 5.57 19 7.5 19L6 20.5v.5h12v-.5L16.5 19c1.93 0 3.5-1.57 3.5-3.5V6c0-3.5-4-4-8-4zM7.5 17c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM18 11H6V6h12v5z', + car: 'M18.92 6.01C18.72 5.42 18.16 5 17.5 5h-11c-.66 0-1.21.42-1.42.99L3 13v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-6.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z', + bus: 'M4 16c0 .88.39 1.67 1 2.22V20c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h8v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1.78c.61-.55 1-1.34 1-2.22V6c0-3.5-3.58-4-8-4s-8 .5-8 4v10zm3.5 1c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM17 11H7V6h10v5z', +} +--- + +
+ +
+ {t['location.hero.title']} +
+
+

+ {t['location.hero.title']} +

+

+ {t['location.hero.subtitle']} +

+
+
+
+ + +
+
+
+

+ + {t['location.location.title']} +

+

+ {t['location.hero.text']} +

+
+ + + + + {t['location.location.text']} +
+
+ + +
+

+ + {t['location.rooms.title']} +

+
+
+
+

{t['location.rooms.paranimf.title']}

+ 400 PAX +
+

{t['location.rooms.paranimf.desc']}

+
+
+
+

{t['location.rooms.aulamagna.title']}

+ 160 PAX +
+

{t['location.rooms.aulamagna.desc']}

+
+
+
+

{t['location.rooms.teaching.title']}

+ 90-120 PAX +
+

{t['location.rooms.teaching.desc']}

+
+
+
+
+ + +
+
+

Google Maps

+

Gran Via de les Corts Catalanes, 585, 08007 Barcelona

+ + Ver en el mapa + +
+ +
+

+ + + + {t['location.spaces.title']} +

+
    +
  • + {t['location.spaces.gallery.title']} + {t['location.spaces.gallery.desc']} +
  • +
  • + {t['location.spaces.hall.title']} + {t['location.spaces.hall.desc']} +
  • +
  • + {t['location.spaces.cloister.title']} + {t['location.spaces.cloister.desc']} +
  • +
  • + {t['location.spaces.garden.title']} + {t['location.spaces.garden.desc']} +
  • +
+
+
+
+ + +
+
+ +
+
+ +

+ {t['location.city.title']} +

+ +
+
+

+ {t['location.city.intro']} +

+

+ {t['location.city.heritage']} +

+
+

+ {t['location.city.climate.title']} +

+

+ {t['location.city.climate.text']} +

+
+
+ +
+
+

+ + {t['location.city.tech.title']} +

+

+ {t['location.city.tech.text']} +

+
+ +
+

+ + {t['location.city.connections.title']} +

+

+ {t['location.city.connections.text']} +

+
+
+
+
+
+ + +
+

+ {t['location.transport.title']} +

+
+ +
+
+ +
+

{t['location.transport.metro.title']}

+

{t['location.transport.metro.items']}

+
+ +
+
+ +
+

{t['location.transport.renfe.title']}

+

{t['location.transport.renfe.items']}

+
+ +
+
+ +
+

{t['location.transport.long.title']}

+

{t['location.transport.long.items']}

+
+ +
+
+ +
+

{t['location.transport.car.title']}

+

{t['location.transport.car.desc']}

+
+
+
+
+ + diff --git a/src/components/SectionTitle.astro b/src/components/SectionTitle.astro new file mode 100644 index 0000000..d6c3fe1 --- /dev/null +++ b/src/components/SectionTitle.astro @@ -0,0 +1,9 @@ +--- +interface Props { + title: string +} + +const { title } = Astro.props +--- + +

{title}

diff --git a/src/components/home/SectionMain.astro b/src/components/home/SectionMain.astro new file mode 100644 index 0000000..2602d3f --- /dev/null +++ b/src/components/home/SectionMain.astro @@ -0,0 +1,100 @@ +--- +import { texts } from '@/i18n/home' +import CenteredPanel from '../CenteredPanel.astro' + +interface Props { + lang: string +} + +const { lang } = Astro.props +const t = texts[lang as keyof typeof texts] +--- + +
+
+

+ PyconES Barcelona 2026 +

+ +

+ + + {t['index.initializing']} + + +

+
+ +
+ + diff --git a/src/components/home/SectionSponsors.astro b/src/components/home/SectionSponsors.astro new file mode 100644 index 0000000..ef58492 --- /dev/null +++ b/src/components/home/SectionSponsors.astro @@ -0,0 +1,71 @@ +--- +import { texts } from '../../i18n/home' +import SectionTitle from '../SectionTitle.astro' +import CenteredPanel from '../CenteredPanel.astro' +import type { ISponsor } from '../../types/sponsors' +import SponsorsGroup from './sponsors/SponsorsGroup.astro' + +const sponsors = Object.values(import.meta.glob('../../data/sponsors/*.md', { eager: true })) as { + frontmatter: ISponsor +}[] + +interface Props { + lang: string +} + +const { lang } = Astro.props +const t = texts[lang as keyof typeof texts] + +function filterSponsorsByTier(tier: string): ISponsor[] { + return sponsors.filter((s) => s.frontmatter.tier === tier).map((s) => s.frontmatter) +} + +const bronze = filterSponsorsByTier('bronze') +const silver = filterSponsorsByTier('silver') +const gold = filterSponsorsByTier('gold') +const platinum = filterSponsorsByTier('platinum') +const main = filterSponsorsByTier('main') +--- + +
+ + + +
+ + + + + +
+
diff --git a/src/components/home/sponsors/SponsorsGroup.astro b/src/components/home/sponsors/SponsorsGroup.astro new file mode 100644 index 0000000..37ec937 --- /dev/null +++ b/src/components/home/sponsors/SponsorsGroup.astro @@ -0,0 +1,51 @@ +--- +import type { ISponsor } from '../../../types/sponsors' +import { texts } from '../../../i18n/home' + +interface Props { + lang: string + title: string + sponsors: ISponsor[] + tierColor: string + size: number +} + +const { title, sponsors, lang, tierColor, size } = Astro.props +const t = texts[lang as keyof typeof texts] +--- + +{ + sponsors.length > 0 ? ( +
+
+
+

{title}

+
+
+
+ {sponsors.map((sponsor) => ( + + {t['sponsors.altlogo'].replace('{name}', + + ))} +
+
+ ) : null +} diff --git a/src/components/index.astro b/src/components/index.astro index 8ffe31d..14b19b7 100644 --- a/src/components/index.astro +++ b/src/components/index.astro @@ -1,127 +1,20 @@ --- -import Layout from '../layouts/Layout.astro' -import '@fontsource-variable/jetbrains-mono' -import { texts } from '../i18n/home' -import { getRelativeLocaleUrl } from 'astro:i18n' +import Layout from '@/layouts/Layout.astro' +import SectionMain from './home/SectionMain.astro' +import SectionSponsors from './home/SectionSponsors.astro' +import { texts } from '@/i18n/home' interface Props { - lang: string + lang: string | any } const { lang } = Astro.props -const t = texts[lang as keyof typeof texts] +const t = texts[lang as keyof typeof texts] || texts.es --- - -
-
-

- PyconES Barcelona 2026 -

- -

- - {t['index.initializing']} - -

- - -
-
+ +
+ + +
- - diff --git a/src/pages/constants.ts b/src/constants.ts similarity index 50% rename from src/pages/constants.ts rename to src/constants.ts index 6857489..efb50d5 100644 --- a/src/pages/constants.ts +++ b/src/constants.ts @@ -1 +1,2 @@ export const SPONSORS_EMAIL = 'sponsors@2026.es.pycon.org' +export const CONTACT_EMAIL = 'contacto@2026.es.pycon.org' diff --git a/src/data/sponsors/gisce.md b/src/data/sponsors/gisce.md new file mode 100644 index 0000000..e6ef0f6 --- /dev/null +++ b/src/data/sponsors/gisce.md @@ -0,0 +1,7 @@ +--- +name: 'GISCE' +website: 'https://gisce.net/' +tier: 'bronze' +logobg: '#ffffff' +logo: '/sponsors/gisce.png' +--- diff --git a/src/data/sponsors/nagarro.md b/src/data/sponsors/nagarro.md new file mode 100644 index 0000000..9568ced --- /dev/null +++ b/src/data/sponsors/nagarro.md @@ -0,0 +1,7 @@ +--- +name: 'Nagarro' +website: 'https://www.nagarro.com/es/' +tier: 'platinum' +logobg: '#ffffff' +logo: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjIuMywgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMDI0IDc2OCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTAyNCA3Njg7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojMDYwNDFGO30KCS5zdDF7ZmlsbDojNDdEN0FDO30KPC9zdHlsZT4KPHBhdGggY2xhc3M9InN0MCIgZD0iTTI2MC4xLDU3OS4ydi02Ni4yaDE2LjR2MTAuOGMyLjUtMy45LDYuMi03LDEwLjUtOC45YzUtMi40LDEwLjUtMy42LDE2LjEtMy41YzEzLjUsMCwyMS42LDQuNywyNC40LDE0LjEKCWMxLDMuNSwxLjYsOC4zLDEuNiwxNC41djM5LjJoLTE2LjZWNTQ0YzAtNS4zLTAuNi05LjEtMS45LTExLjJjLTItNC02LjUtNi0xMy4zLTZjLTQsMC04LDEtMTEuNywyLjhjLTMuNSwxLjUtNi41LDQtOC42LDcuMnY0Mi40CglIMjYwLjF6Ii8+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik00MDIuMyw1ODEuM2MtOC4zLDAtMTMuMy0yLjgtMTUuMi04LjVjLTUuMSw2LjEtMTMuMSw5LjItMjQuMSw5LjJjLTguNCwwLTE1LTEuOS0xOS42LTUuNwoJYy00LjUtMy42LTcuMS05LjEtNi45LTE0LjljMC05LjcsNS0xNiwxNC45LTE4LjljNi4zLTEuOCwxNC4yLTIuNywyMy42LTIuN2MzLjMsMCw2LjUsMC4zLDkuNywwLjh2LTIuMWMwLjEtMi44LTAuOS01LjYtMi43LTcuNwoJYy0yLjQtMi41LTYuOS0zLjctMTMuNi0zLjdjLTEwLjUsMC0xOS4xLDIuNi0yNS44LDcuOWgtMC41di0xNWM4LjEtNS4yLDE4LjEtNy45LDMwLTcuOWM5LjYsMCwxNi44LDEuOCwyMS42LDUuNQoJYzIuNiwyLDQuNiw0LjYsNS44LDcuN2MxLjIsMy4xLDEuOCw3LjIsMS44LDEyLjVjMCwxLjYsMCw0LjUtMC4xLDguNnMtMC4xLDctMC4xLDguN2MwLDQuOSwwLjIsNy44LDAuNyw4LjgKCWMwLjcsMS45LDIuNSwyLjgsNS4zLDIuOGMxLjgsMCwzLjYtMC4yLDUuMy0wLjh2MTMuNkM0MTAsNTgwLjcsNDA2LjYsNTgxLjMsNDAyLjMsNTgxLjN6IE0zODQuOCw1NTIuOGMtMy4yLTAuNS02LjQtMC43LTkuNi0wLjcKCWMtNC43LTAuMS05LjMsMC4zLTEzLjksMS4zYy01LjIsMS4xLTcuOSwzLjctNy45LDcuN2MwLDUuMyw0LjQsOCwxMyw4YzguMiwwLDE0LjMtMi40LDE4LjQtNy4zQzM4NC44LDU2MC4xLDM4NC44LDU1NywzODQuOCw1NTIuOHoKCSIvPgo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNNDIyLjcsNTg0LjZoMC41YzcuNyw1LjcsMTcuNiw4LjUsMjkuNiw4LjVjOS4zLDAsMTYtMi4xLDE5LjktNi4zYzIuNy0yLjgsNC03LjQsNC0xMy45di00LjkKCWMtMS45LDMuMS01LjEsNS43LTkuNyw3LjljLTUsMi4yLTEwLjMsMy4zLTE1LjgsMy4yYy0xMC42LDAtMTkuMy0yLjctMjYtOC4xYy02LjgtNS40LTEwLjItMTMuMy0xMC4yLTIzLjcKCWMtMC4xLTUuNiwxLTExLjIsMy40LTE2LjNjMi00LjQsNS4xLTguMiw5LTExLjFjMy42LTIuNiw3LjctNC42LDExLjktNS45YzQuMi0xLjMsOC41LTEuOSwxMi45LTEuOWM1LjMtMC4xLDEwLjUsMC45LDE1LjUsMi45CgljNC42LDEuOSw3LjcsNC4yLDkuMiw3LjF2LThoMTZ2NTguMWMwLDkuNC0xLjcsMTYuNy01LjIsMjEuOGMtNi4yLDkuMS0xNy45LDEzLjctMzQuOSwxMy43Yy02LDAuMS0xMi0wLjYtMTcuOS0yCgljLTUuMy0xLjMtOS40LTMtMTIuMy00LjlMNDIyLjcsNTg0LjZ6IE00NzYuMSw1MzYuNmMtMS44LTMuMy00LjYtNi04LjEtNy41Yy00LTEuOC04LjQtMi43LTEyLjktMi43Yy01LjktMC4yLTExLjYsMS42LTE2LjQsNS4xCgljLTQuNSwzLjQtNi44LDguNS02LjgsMTUuM2MwLDUuOSwyLjIsMTAuMyw2LjcsMTMuM2M0LjgsMy4xLDEwLjUsNC43LDE2LjIsNC42YzQuNCwwLDguOC0wLjksMTIuOC0yLjdjMy41LTEuNCw2LjQtNCw4LjQtNy4yCglMNDc2LjEsNTM2LjZ6Ii8+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik01NjYuMiw1ODEuM2MtOC4zLDAtMTMuMy0yLjgtMTUuMi04LjVjLTUuMSw2LjEtMTMuMSw5LjItMjQuMSw5LjJjLTguNCwwLTE1LTEuOS0xOS42LTUuNwoJYy00LjUtMy42LTcuMS05LjEtNi45LTE0LjljMC05LjcsNS0xNiwxNC45LTE4LjljNi4zLTEuOCwxNC4yLTIuNywyMy42LTIuN2MzLjMsMCw2LjUsMC4zLDkuNywwLjh2LTIuMWMwLjEtMi44LTAuOC01LjYtMi43LTcuNwoJYy0yLjQtMi41LTYuOS0zLjctMTMuNi0zLjdjLTEwLjUsMC0xOS4xLDIuNi0yNS45LDcuOUg1MDZ2LTE1YzguMS01LjIsMTguMS03LjksMzAtNy45YzkuNiwwLDE2LjgsMS44LDIxLjYsNS41CgljMi42LDIsNC42LDQuNiw1LjgsNy43YzEuMiwzLjEsMS44LDcuMiwxLjgsMTIuNWMwLDEuNiwwLDQuNS0wLjEsOC42cy0wLjEsNy0wLjEsOC43YzAsNC45LDAuMiw3LjgsMC43LDguOAoJYzAuNywxLjksMi41LDIuOCw1LjMsMi44YzEuOCwwLDMuNi0wLjIsNS4zLTAuOHYxMy42QzU3My45LDU4MC43LDU3MC41LDU4MS4zLDU2Ni4yLDU4MS4zeiBNNTQ4LjYsNTUyLjhjLTMuMi0wLjUtNi40LTAuNy05LjYtMC43CgljLTQuNy0wLjEtOS4zLDAuMy0xMy44LDEuM2MtNS4yLDEuMS03LjksMy43LTcuOSw3LjdjMCw1LjMsNC4zLDgsMTMsOGM4LjIsMCwxNC4zLTIuNCwxOC40LTcuM0M1NDguNyw1NjAuMSw1NDguNiw1NTcsNTQ4LjYsNTUyLjgKCUw1NDguNiw1NTIuOHoiLz4KPHBhdGggY2xhc3M9InN0MCIgZD0iTTU4My43LDU4MC4yVjUxNGgxNi40djExLjJjMS4yLTMuMywzLjgtNi4zLDcuNi04LjljNC4xLTIuOCw5LTQuMiwxNC00YzMuOSwwLDYuOSwwLjYsOS4xLDEuOXYxNi43aC0wLjUKCWMtMy4yLTEuNC02LjYtMi4xLTEwLjEtMmMtNC40LTAuMS04LjcsMC43LTEyLjcsMi42Yy0zLjEsMS40LTUuNiw0LTYuOSw3LjF2NDEuN0w1ODMuNyw1ODAuMnoiLz4KPHBhdGggY2xhc3M9InN0MCIgZD0iTTYzOC40LDU4MC4yVjUxNGgxNi40djExLjJjMS4zLTMuMywzLjgtNi4zLDcuNi04LjljNC4xLTIuOCw5LTQuMiwxNC00YzMuOSwwLDYuOSwwLjYsOS4xLDEuOXYxNi43aC0wLjUKCWMtMy4yLTEuNC02LjYtMi4xLTEwLjEtMmMtNC40LTAuMS04LjcsMC43LTEyLjcsMi42Yy0zLjEsMS40LTUuNiw0LTYuOSw3LjF2NDEuN0w2MzguNCw1ODAuMnoiLz4KPHBhdGggY2xhc3M9InN0MCIgZD0iTTc1My4xLDU3Mi4xYy03LjIsNi42LTE2LjUsOS45LTI4LDkuOWMtMTEuNSwwLTIwLjgtMy4yLTI3LjktOS43Yy03LjEtNi41LTEwLjYtMTQuOS0xMC42LTI1LjIKCWMwLTEwLjMsMy42LTE4LjcsMTAuOS0yNS4xYzcuMi02LjUsMTYuNi05LjgsMjgtOS44YzExLjgsMCwyMS4xLDMuMiwyOCw5LjZzMTAuNCwxNC44LDEwLjMsMjUuMwoJQzc2My45LDU1Ny4yLDc2MC4zLDU2NS41LDc1My4xLDU3Mi4xeiBNNzQxLjIsNTMyLjVjLTQtMy44LTkuMS01LjctMTUuOC01LjdzLTExLjgsMS45LTE2LDUuN2MtNC4xLDMuNy02LjQsOS02LjIsMTQuNQoJYy0wLjIsNS42LDIuMSwxMC45LDYuMywxNC42YzQuMiwzLjcsOS40LDUuNSwxNS44LDUuNWM2LjYsMCwxMS43LTEuOSwxNS44LTUuN2M0LTMuNyw2LjItOSw2LTE0LjQKCUM3NDcuMyw1NDEuNiw3NDUuMSw1MzYuMyw3NDEuMiw1MzIuNXoiLz4KPHBhdGggY2xhc3M9InN0MSIgZD0iTTU5OS4xLDQ0Mi40Yy0zNi4zLDAtODIuNS0yMy41LTEyMi43LTYzYy0xLjgsNS0zLjksOS45LTYuMywxNC43Yy0xMy45LDI4LjItMzMuOCw0My44LTU2LjIsNDMuOAoJcy00Mi41LTE1LjItNTYuNi00Mi44Yy0xMi45LTI1LjItMTkuOS01OS0xOS45LTk1LjFjMC01OC45LDE5LjUtMTA4LjYsNTAuOC0xMjkuN2MyMS43LTE0LjYsNDkuMS0xNi40LDc5LTUuNAoJYzI1LjUsOS40LDUyLjgsMjguMyw3OS44LDU1YzIuNC02LjQsNS4zLTEyLjcsOC43LTE4LjdjMTQuMS0yNSwzMy44LTM4LjgsNTUuNS0zOC44YzQzLjEsMCw3NS41LDU5LjEsNzUuNSwxMzcuNgoJYzAsMzQtNi4yLDY1LjMtMTgsOTAuNGMtNS44LDEyLjMtMTIuNywyMi43LTIwLjYsMzAuOWMtOC4zLDguNi0xNy41LDE0LjctMjcuMywxNy45QzYxMy43LDQ0MS40LDYwNi40LDQ0Mi41LDU5OS4xLDQ0Mi40egoJIE00ODIsMzYxLjJjNTAuNiw1MywxMDQuOSw3MS41LDEzMy41LDYyYzE0LjctNC45LDI4LjItMTkuMSwzOC00MGMxMC43LTIyLjksMTYuNC01MS42LDE2LjQtODMuMmMwLTMzLjQtNi40LTY0LjQtMTguMS04Ny4zCgljLTExLTIxLjYtMjUuNC0zMy40LTQwLjctMzMuNGMtMjEuOCwwLTQwLjksMjEuOS01MSw1NC4zYzQwLjgsNDQuOCw2Ny41LDk5LjksNjUuMiwxMzUuMWMtMC44LDEyLjYtNS40LDIyLjctMTMuMiwyOS4xbC0wLjcsMC41CgljLTguNyw1LjgtMTguOSw2LjEtMjguOCwwLjhjLTI1LjktMTMuOS00Ny02NC4zLTQ3LTExMi40Yy0wLjEtMTYuMywxLjktMzIuNiw1LjgtNDguNWMtNTUuMS01Ny4zLTEwOC43LTc3LjUtMTQzLjgtNTMuOQoJYy0yNi40LDE3LjgtNDMuNCw2My4yLTQzLjQsMTE1LjhjMCw2Ny45LDI2LjIsMTIxLjEsNTkuOCwxMjEuMWMyMC45LDAsMzktMjEuOCw0OS40LTU1LjRjLTI3LTMwLTQ3LjQtNjMuNi01Ny41LTk0LjgKCWMtOS45LTMwLjUtOC44LTU1LDMuMS02Ny40YzcuNy04LDE4LjktOS45LDMwLjgtNS4xYzI0LjcsOS45LDQ5LjcsNDcuNCw0OS43LDk5LjdDNDg5LjYsMzE5LjUsNDg3LjEsMzQwLjYsNDgyLDM2MS4yeiBNNTU1LjMsMjUzLjQKCWMtMS45LDEwLjktMi45LDIyLTIuOSwzMy4xYzAsNDUuNSwyMC4xLDg3LjksMzguMSw5Ny42YzUuNSwyLjksOC44LDEuOCwxMS4yLDAuM2M0LTMuNSw2LjMtOS4yLDYuOC0xNwoJQzYxMC40LDMzOSw1ODguNCwyOTIuOCw1NTUuMywyNTMuNEw1NTUuMywyNTMuNHogTTQyNy4zLDIxMi43Yy0yLjMtMC4xLTQuNiwwLjgtNi4yLDIuNWMtNS40LDUuNi04LjQsMjIuMiwwLjgsNTAuNgoJYzguNCwyNiwyNC43LDU0LDQ2LjQsODBjMy0xNS43LDQuNS0zMS42LDQuNC00Ny42YzAtNDkuOC0yNC03OC4xLTM5LjItODQuMkM0MzEuNSwyMTMuMiw0MjkuNCwyMTIuOCw0MjcuMywyMTIuN0w0MjcuMywyMTIuN3oiLz4KPC9zdmc+Cg==' +--- diff --git a/src/data/sponsors/perk.md b/src/data/sponsors/perk.md new file mode 100644 index 0000000..69a8fee --- /dev/null +++ b/src/data/sponsors/perk.md @@ -0,0 +1,7 @@ +--- +name: 'Perk' +website: 'https://www.travelperk.com/' +tier: 'main' +logobg: '#ffffff' +logo: '/sponsors/perk.svg' +--- diff --git a/src/i18n/__tests__/translations.test.ts b/src/i18n/__tests__/translations.test.ts new file mode 100644 index 0000000..9f154ad --- /dev/null +++ b/src/i18n/__tests__/translations.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest' +import { texts as homeTexts } from '../home' +import { menuTexts } from '../menu/index' +import { accommodationTexts } from '../accommodation/index' +import { texts as cocTexts } from '../code-of-conduct/index' +import { locationTexts } from '../location/index' +import { texts as sponsorsTexts } from '../sponsors/index' +import { texts as lpTexts } from '../components/LanguagePicker' + +describe('Internationalization Keys Consistency', () => { + const testConsistency = (moduleName: string, moduleTexts: any) => { + it(`${moduleName} translations should have consistent keys across all languages`, () => { + const locales = ['es', 'en', 'ca'] + const esKeys = Object.keys(moduleTexts.es).sort() + + for (const locale of locales) { + const currentKeys = Object.keys(moduleTexts[locale]).sort() + expect(currentKeys, `${locale} keys for ${moduleName} do not match Spanish keys`).toEqual(esKeys) + } + }) + } + + testConsistency('home', homeTexts) + testConsistency('menu', menuTexts) + testConsistency('accommodation', accommodationTexts) + testConsistency('code-of-conduct', cocTexts) + testConsistency('location', locationTexts) + testConsistency('sponsors', sponsorsTexts) + testConsistency('LanguagePicker', lpTexts) +}) diff --git a/src/i18n/accommodation/ca.ts b/src/i18n/accommodation/ca.ts new file mode 100644 index 0000000..22d95a7 --- /dev/null +++ b/src/i18n/accommodation/ca.ts @@ -0,0 +1,25 @@ +export const ca = { + 'accommodation.title': 'Allotjament', + 'accommodation.hero.title': 'On allotjar-se', + 'accommodation.hero.subtitle': 'Recomanacions per a la teva estada a Barcelona', + 'accommodation.intro': + "Barcelona és una de les ciutats amb més oferta hotelera d'Europa. Per a la PyConES 2026, recomanem buscar allotjament al districte de l'Eixample o a Ciutat Vella per estar a pocs minuts a peu de la seu (UB Edifici Històric).", + 'accommodation.hotels.title': 'Hotels Recomanats', + 'accommodation.hotels.subtitle': 'Convenis i opcions properes', + 'accommodation.hotels.disclaimer': + 'Estem treballant per tancar convenis exclusius amb descomptes per als assistents. Torna aviat per veure els codis promocionals!', + 'accommodation.areas.title': 'Millors Zones', + 'accommodation.areas.eixample.name': "L'Eixample", + 'accommodation.areas.eixample.desc': + 'La zona més segura i elegant, amb arquitectura modernista i a un pas de la seu.', + 'accommodation.areas.gothic.name': 'Barri Gòtic', + 'accommodation.areas.gothic.desc': + 'El centre històric, amb carrers màgics i molta vida nocturna, a 10 min de la UB.', + 'accommodation.areas.poblenou.name': 'Poblenou / 22@', + 'accommodation.areas.poblenou.desc': + 'El hub tecnològic. Un poc més allunyat però molt ben connectat per metro (L1).', + 'accommodation.apartments.title': 'Apartaments Turístics', + 'accommodation.apartments.text': + "Si prefereixes un apartament, assegura't que compti amb llicència turística oficial (HUTB). Barcelona és molt estricta amb la regulació d'allotjaments.", + 'accommodation.apartments.link': "Veure guia d'allotjament segur a Barcelona", +} as const diff --git a/src/i18n/accommodation/en.ts b/src/i18n/accommodation/en.ts new file mode 100644 index 0000000..7baa419 --- /dev/null +++ b/src/i18n/accommodation/en.ts @@ -0,0 +1,25 @@ +export const en = { + 'accommodation.title': 'Accommodation', + 'accommodation.hero.title': 'Where to stay', + 'accommodation.hero.subtitle': 'Recommendations for your stay in Barcelona', + 'accommodation.intro': + 'Barcelona offers one of the largest hotel selections in Europe. For PyConES 2026, we recommend looking for accommodation in the Eixample district or Ciutat Vella to be within walking distance of the venue (UB Historic Building).', + 'accommodation.hotels.title': 'Recommended Hotels', + 'accommodation.hotels.subtitle': 'Agreements and nearby options', + 'accommodation.hotels.disclaimer': + 'We are working on exclusive discounts for attendees. Check back soon for promo codes!', + 'accommodation.areas.title': 'Best Areas', + 'accommodation.areas.eixample.name': "L'Eixample", + 'accommodation.areas.eixample.desc': + 'The safest and most elegant area, with modernist architecture and very close to the venue.', + 'accommodation.areas.gothic.name': 'Gothic Quarter', + 'accommodation.areas.gothic.desc': + 'The historical center, with magical streets and vibrant nightlife, 10 min from UB.', + 'accommodation.areas.poblenou.name': 'Poblenou / 22@', + 'accommodation.areas.poblenou.desc': + 'The tech hub. Slightly further away but very well connected by metro (L1).', + 'accommodation.apartments.title': 'Tourist Apartments', + 'accommodation.apartments.text': + 'If you prefer an apartment, make sure it has an official tourist license (HUTB). Barcelona is very strict regarding accommodation regulations.', + 'accommodation.apartments.link': 'View safe accommodation guide in Barcelona', +} as const diff --git a/src/i18n/accommodation/es.ts b/src/i18n/accommodation/es.ts new file mode 100644 index 0000000..e215478 --- /dev/null +++ b/src/i18n/accommodation/es.ts @@ -0,0 +1,25 @@ +export const es = { + 'accommodation.title': 'Alojamiento', + 'accommodation.hero.title': 'Dónde alojarse', + 'accommodation.hero.subtitle': 'Recomendaciones para tu estancia en Barcelona', + 'accommodation.intro': + "Barcelona es una de las ciudades con mayor oferta hotelera de Europa. Para la PyConES 2026, recomendamos buscar alojamiento en el distrito de l'Eixample o en Ciutat Vella para estar a pocos minutos a pie de la sede (UB Edifici Històric).", + 'accommodation.hotels.title': 'Hoteles Recomendados', + 'accommodation.hotels.subtitle': 'Convenios y opciones cercanas', + 'accommodation.hotels.disclaimer': + 'Estamos trabajando para cerrar convenios exclusivos con descuentos para los asistentes. ¡Vuelve pronto para ver los códigos promocionales!', + 'accommodation.areas.title': 'Mejores Zonas', + 'accommodation.areas.eixample.name': "L'Eixample", + 'accommodation.areas.eixample.desc': + 'La zona más segura y elegante, con arquitectura modernista y a un paso de la sede.', + 'accommodation.areas.gothic.name': 'Barrio Gótico', + 'accommodation.areas.gothic.desc': + 'El centro histórico, con calles mágicas y mucha vida nocturna, a 10 min de la UB.', + 'accommodation.areas.poblenou.name': 'Poblenou / 22@', + 'accommodation.areas.poblenou.desc': + 'El hub tecnológico. Un poco más alejado pero muy bien conectado por metro (L1).', + 'accommodation.apartments.title': 'Apartamentos Turísticos', + 'accommodation.apartments.text': + 'Si prefieres un apartamento, asegúrate de que cuente con licencia turística oficial (HUTB). Barcelona es muy estricta con la regulación de alojamientos.', + 'accommodation.apartments.link': 'Ver guía de alojamiento seguro en Barcelona', +} as const diff --git a/src/i18n/accommodation/index.ts b/src/i18n/accommodation/index.ts new file mode 100644 index 0000000..93fc10f --- /dev/null +++ b/src/i18n/accommodation/index.ts @@ -0,0 +1,9 @@ +import { es } from './es' +import { en } from './en' +import { ca } from './ca' + +export const accommodationTexts = { + es, + en, + ca, +} as const diff --git a/src/i18n/code-of-conduct/ca.ts b/src/i18n/code-of-conduct/ca.ts new file mode 100644 index 0000000..6ccff8a --- /dev/null +++ b/src/i18n/code-of-conduct/ca.ts @@ -0,0 +1,75 @@ +export const ca = { + title: 'Codi de conducta - PyConES 2026', + heading: 'Codi de conducta 🛑', + intro: + 'Python España, com a associació al voltant de la qual s\'organitzen esdeveniments de diferents tipus, vol assegurar que totes les persones que participin en aquests esdeveniments o comunicacions tinguin una experiència professional i positiva d\'aprenentatge, col·laboració o oci. Per això, s\'espera que qui participi en la comunitat mostri respecte i cortesia envers la resta.', + commitment: + 'En participar en la comunitat de Python España, et compromets a fomentar una experiència lliure d\'assetjament per a tothom, independentment de l\'edat, dimensió corporal, discapacitat visible o invisible, etnicitat, característiques sexuals, identitat i expressió de gènere, nivell d\'experiència, educació, nivell socioeconòmic, nacionalitat, aparença personal, raça, religió, o identitat o orientació sexual.', + detailIntro: + 'Aquest Codi de Conducta detalla quins comportaments s\'esperen, quins es rebutgen i quins mecanismes hi ha per ajudar una persona que estigui sent objecte de comportaments inadequats.', + why: { + title: 'Per què un codi de conducta?', + intro: + 'Seguint amb el zen de Python, explícit millor que implícit. Explicitar què s\'espera de l\'ambient en qualsevol esdeveniment de Python España:', + reasons: [ + 'Afavoreix que més persones sàpiguen que són benvingudes.', + 'Evita ambigüitats.', + 'Construeix un clima de confiança, on si algú vol reportar un incident, sabrà que no començarem per qüestionar-lo (victim blaming).', + ], + }, + scope: { + title: 'Abast', + body: 'Aquest codi de conducta és aplicable a totes les persones que participin en espais de la comunitat de Python España, ja siguin en línia o presencials. També s\'aplica a espais públics on una persona estigui en representació de la comunitat. Exemples d\'això últim inclouen l\'ús del compte oficial de correu electrònic, publicacions a través de les xarxes socials oficials, o presentacions amb persones designades en esdeveniments en línia o no.', + }, + standards: { + title: 'Els nostres estàndards', + positiveTitle: 'Exemples de comportament que contribueixen a crear un ambient positiu per a la nostra comunitat:', + positiveItems: [ + 'Demostrar empatia i amabilitat envers altres persones. No insultis ni humiliïs altres assistents. Recorda que les bromes sexistes, racistes o discriminatòries no són apropiades. Mai ho són.', + 'Respectar les diferents opinions, punts de vista i experiències.', + 'Donar i acceptar adequadament crítiques constructives.', + 'Acceptar la responsabilitat i disculpar-se davant de qui es vegi afectat pels nostres errors, aprenent de l\'experiència.', + 'Centrar-se en el que sigui millor no només per a nosaltres com a individus, sinó per a la comunitat en general.', + 'Utilitzar un llenguatge inclusiu i que doni cabuda a una audiència diversa.', + 'Prestar especial atenció a les persones que acaben d\'arribar a la comunitat.', + 'Presentar-te amb els teus pronoms i preguntar-li a una altra persona els seus perquè existeixi una comunicació clara i sense biaix.', + ], + negativeTitle: 'Exemples de comportament inacceptable:', + negativeItems: [ + 'L\'ús de llenguatge o imatges sexualitzades, i aproximacions o atencions sexuals de qualsevol tipus.', + 'Comentaris despectius, trolling, insultants o derogatoris, i atacs personals o polítics.', + 'Bromes racistes, sexistes o excloents.', + 'L\'assetjament en públic o privat.', + 'Publicar informació privada d\'altres persones, com ara adreces físiques o de correu electrònic, sense el seu permís explícit.', + 'Altres conductes que puguin ser raonablement considerades com a inapropiades en un entorn professional.', + ], + harassmentNote: + 'Per assetjament s\'entén comentaris ofensius relacionats amb gènere, orientació sexual, discapacitat, aparença física, mida corporal, ètnia o religió, pornografia en espais públics, intimidació deliberada, assetjament, persecució, assetjament per fotografies o gravacions, constant interrupció de xerrades o altres esdeveniments, contacte físic inapropiat i atenció sexual no desitjada.', + }, + enforcement: { + title: 'Compliment', + body: 'L\'administració de la comunitat és responsable d\'aclarir i fer complir aquest codi de conducta; en cas que es determini un comportament inadequat, prendrà les accions que consideri oportunes. Aquestes van des d\'exigir el cessament del comportament, fins a l\'expulsió d\'una persona d\'un esdeveniment o de l\'Associació, sense dret a reemborsament. L\'administració de la comunitat tindrà el dret i la responsabilitat d\'eliminar, editar o rebutjar missatges, comentaris, codi, edicions de pàgines de wiki, tickets i altres contribucions que no s\'alineïn amb aquest codi de conducta, i comunicarà les raons de les seves decisions de moderació quan sigui apropiat.', + }, + reporting: { + title: 'Denúncia i informació de contacte', + intro: + 'Els casos de comportament abusiu, assetjador o inacceptable d\'una altra manera podran ser denunciats a les persones administradores de la comunitat responsables del compliment:', + channels: [ + 'Si és un esdeveniment presencial, posa\'t en contacte directament amb les persones organitzadores de l\'esdeveniment. És molt probable que hagin publicat un codi de conducta específic de l\'esdeveniment amb instruccions de a qui acudir; et proporcionaran un espai segur per ajudar-te.', + 'Si es tracta d\'un espai en línia, posa\'t en contacte amb les persones moderadores d\'aquest espai.', + 'Al fòrum de Discourse pots denunciar publicacions individuals o contactar amb el grup de moderadors.', + 'A Telegram, a la persona propietària del grup o altres administradores.', + 'Per a altres espais o de forma alternativa, posa\'t en contacte amb la Junta Directiva a contacto@es.python.org.', + ], + privacy: + 'Totes les persones administradores de la comunitat estan obligades a respectar la privacitat i la seguretat de qui denunciï incidents.', + }, + attribution: { + title: 'Atribució', + intro: 'Aquest codi de conducta estén l\'existent amb aportacions d\'altres codis:', + sources: [ + 'La versió en espanyol del Contributor Covenant 2.0.', + 'El codi de conducta del DjangoCon Europe 2020.', + ], + }, +} as const diff --git a/src/i18n/code-of-conduct/en.ts b/src/i18n/code-of-conduct/en.ts new file mode 100644 index 0000000..bdd7f8d --- /dev/null +++ b/src/i18n/code-of-conduct/en.ts @@ -0,0 +1,75 @@ +export const en = { + title: 'Code of Conduct - PyConES 2026', + heading: 'Code of Conduct 🛑', + intro: + 'Python España, as an association around which different types of events are organised, wants to ensure that all people who participate in such events or communications have a professional and positive experience of learning, collaboration, or leisure. To this end, everyone participating in the community is expected to show respect and courtesy towards others.', + commitment: + 'By participating in the Python España community, you commit to fostering a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity or orientation.', + detailIntro: + 'This Code of Conduct details what behaviours are expected, which are rejected, and what mechanisms exist to help a person who is the subject of inappropriate behaviour.', + why: { + title: 'Why a code of conduct?', + intro: + 'Following the Zen of Python, explicit is better than implicit. Making explicit what is expected of the environment at any Python España event:', + reasons: [ + 'Helps more people know they are welcome.', + 'Avoids ambiguities.', + 'Builds a climate of trust, where if someone wants to report an incident, they will know that we will not start by questioning them (victim blaming).', + ], + }, + scope: { + title: 'Scope', + body: 'This code of conduct applies to all people participating in Python España community spaces, whether online or in person. It also applies to public spaces where a person is representing the community. Examples of the latter include the use of the official email account, posts through official social media, or presentations by designated persons at online or offline events.', + }, + standards: { + title: 'Our standards', + positiveTitle: 'Examples of behaviour that contributes to creating a positive environment for our community:', + positiveItems: [ + 'Demonstrating empathy and kindness towards other people. Do not insult or humiliate other attendees. Remember that sexist, racist, or discriminatory jokes are not appropriate. They never are.', + 'Respecting differing opinions, viewpoints, and experiences.', + 'Giving and gracefully accepting constructive criticism.', + 'Accepting responsibility and apologising to those affected by our mistakes, and learning from the experience.', + 'Focusing on what is best not only for us as individuals, but for the community at large.', + 'Using inclusive language that accommodates a diverse audience.', + 'Paying special attention to newcomers to the community.', + 'Introducing yourself with your pronouns and asking others for theirs so that communication is clear and unbiased.', + ], + negativeTitle: 'Examples of unacceptable behaviour:', + negativeItems: [ + 'The use of sexualised language or imagery, and sexual attention or advances of any kind.', + 'Derogatory comments, trolling, insulting or derogatory remarks, and personal or political attacks.', + 'Racist, sexist, or exclusionary jokes.', + 'Public or private harassment.', + 'Publishing others\' private information, such as physical or email addresses, without their explicit permission.', + 'Other conduct which could reasonably be considered inappropriate in a professional setting.', + ], + harassmentNote: + 'Harassment is understood as offensive comments related to gender, sexual orientation, disability, physical appearance, body size, ethnicity or religion, pornography in public spaces, deliberate intimidation, stalking, following, harassment through photography or recording, sustained disruption of talks or other events, inappropriate physical contact, and unwelcome sexual attention.', + }, + enforcement: { + title: 'Enforcement', + body: 'Community administrators are responsible for clarifying and enforcing this code of conduct; if inappropriate behaviour is determined, they will take whatever action they consider appropriate. These range from demanding that the behaviour cease, to the expulsion of a person from an event or the Association, without the right to a refund. Community administrators will have the right and responsibility to remove, edit, or reject messages, comments, code, wiki edits, tickets, and other contributions that do not align with this code of conduct, and will communicate the reasons for their moderation decisions when appropriate.', + }, + reporting: { + title: 'Reporting and contact information', + intro: + 'Cases of abusive, harassing, or otherwise unacceptable behaviour may be reported to the community administrators responsible for enforcement:', + channels: [ + 'If it is an in-person event, contact the event organisers directly. They will most likely have published an event-specific code of conduct with instructions on whom to contact; they will provide you with a safe space to help you.', + 'If it is an online space, contact the moderators of that space.', + 'On the Discourse forum, you can report individual posts or contact the moderator group.', + 'On Telegram, contact the group owner or other administrators.', + 'For other spaces or alternatively, contact the Board of Directors at contacto@es.python.org.', + ], + privacy: + 'All community administrators are obligated to respect the privacy and security of those who report incidents.', + }, + attribution: { + title: 'Attribution', + intro: 'This code of conduct extends the existing one with contributions from other codes:', + sources: [ + 'The Spanish version of the Contributor Covenant 2.0.', + 'The DjangoCon Europe 2020 code of conduct.', + ], + }, +} as const diff --git a/src/i18n/code-of-conduct/es.ts b/src/i18n/code-of-conduct/es.ts new file mode 100644 index 0000000..952d9d7 --- /dev/null +++ b/src/i18n/code-of-conduct/es.ts @@ -0,0 +1,75 @@ +export const es = { + title: 'Código de conducta - PyConES 2026', + heading: 'Código de conducta 🛑', + intro: + 'Python España, como asociación en torno a la que se organizan eventos de distintos tipos, quiere asegurar que todas las personas que participen en dichos eventos o comunicaciones tengan una experiencia profesional y positiva de aprendizaje, colaboración u ocio. Para ello, se espera que quien participe en la comunidad muestre respeto y cortesía hacia el resto.', + commitment: + 'Al participar en la comunidad de Python España, te comprometes a fomentar una experiencia libre de acoso para todo el mundo, independientemente de la edad, dimensión corporal, discapacidad visible o invisible, etnicidad, características sexuales, identidad y expresión de género, nivel de experiencia, educación, nivel socio-económico, nacionalidad, apariencia personal, raza, religión, o identidad u orientación sexual.', + detailIntro: + 'Este Código de Conducta detalla qué comportamientos se esperan, cuáles se rechazan y qué mecanismos hay para ayudar a una persona que esté siendo objeto de comportamientos inadecuados.', + why: { + title: '¿Por qué un código de conducta?', + intro: + 'Siguiendo con el zen de Python, explícito mejor que implícito. Explicitar qué se espera del ambiente en cualquier evento de Python España:', + reasons: [ + 'Favorece que más personas sepan que son bienvenidas.', + 'Evita ambigüedades.', + 'Construye un clima de confianza, donde si alguien quiere reportar un incidente, sabrá que no empezaremos por cuestionarle (victim blaming).', + ], + }, + scope: { + title: 'Alcance', + body: 'Este código de conducta es aplicable a todas las personas que participen en espacios de la comunidad de Python España, ya sean en línea o presenciales. También se aplica a espacios públicos donde una persona esté en representación de la comunidad. Ejemplos de esto último incluyen el uso de la cuenta oficial de correo electrónico, publicaciones a través de las redes sociales oficiales, o presentaciones con personas designadas en eventos en línea o no.', + }, + standards: { + title: 'Nuestros estándares', + positiveTitle: 'Ejemplos de comportamiento que contribuyen a crear un ambiente positivo para nuestra comunidad:', + positiveItems: [ + 'Demostrar empatía y amabilidad ante otras personas. No insultes o humilles a otros asistentes. Recuerda que las bromas sexistas, racistas o discriminatorias no son apropiadas. Nunca lo son.', + 'Respetar las diferentes opiniones, puntos de vista y experiencias.', + 'Dar y aceptar adecuadamente críticas constructivas.', + 'Aceptar la responsabilidad y disculparse ante quienes se vean afectados por nuestros errores, aprendiendo de la experiencia.', + 'Centrarse en lo que sea mejor no sólo para nosotros como individuos, sino para la comunidad en general.', + 'Usar un lenguaje inclusivo y que dé cabida a una audiencia diversa.', + 'Prestar especial atención a las personas que recién llegan a la comunidad.', + 'Presentarte con tus pronombres y preguntarle a otra persona los suyos para que exista una comunicación clara y sin sesgo.', + ], + negativeTitle: 'Ejemplos de comportamiento inaceptable:', + negativeItems: [ + 'El uso de lenguaje o imágenes sexualizadas, y aproximaciones o atenciones sexuales de cualquier tipo.', + 'Comentarios despectivos, trolling, insultantes o derogatorios, y ataques personales o políticos.', + 'Bromas racistas, sexistas o excluyentes.', + 'El acoso en público o privado.', + 'Publicar información privada de otras personas, tales como direcciones físicas o de correo electrónico, sin su permiso explícito.', + 'Otras conductas que puedan ser razonablemente consideradas como inapropiadas en un entorno profesional.', + ], + harassmentNote: + 'Por acoso se entiende comentarios ofensivos relacionados con género, orientación sexual, discapacidad, apariencia física, tamaño corporal, etnia o religión, pornografía en espacios públicos, intimidación deliberada, acecho, persecución, acoso por fotografías o grabaciones, constante interrupción de charlas u otros eventos, contacto físico inapropiado y atención sexual no deseada.', + }, + enforcement: { + title: 'Cumplimiento', + body: 'La administración de la comunidad es responsable de aclarar y hacer cumplir este código de conducta; en caso de que se determine un comportamiento inadecuado, tomará las acciones que considere oportunas. Éstas van desde exigir el cese del comportamiento, hasta la expulsión de una persona de un evento o de la Asociación, sin derecho a reembolso. La administración de la comunidad tendrá el derecho y la responsabilidad de eliminar, editar o rechazar mensajes, comentarios, código, ediciones de páginas de wiki, tickets y otras contribuciones que no se alineen con este código de conducta, y comunicará las razones para sus decisiones de moderación cuando sea apropiado.', + }, + reporting: { + title: 'Denuncia e información de contacto', + intro: + 'Los casos de comportamiento abusivo, acosador o inaceptable de otro modo podrán ser denunciados a las personas administradoras de la comunidad responsables del cumplimiento:', + channels: [ + 'Si es un evento presencial, ponte en contacto directamente con las personas organizadoras del evento. Es muy probable que hayan publicado un código de conducta específico del evento con instrucciones de a quién acudir; te proporcionarán un espacio seguro para ayudarte.', + 'Si se trata de un espacio en línea, ponte en contacto con las personas moderadoras de ese espacio.', + 'En el foro de Discourse puedes denunciar publicaciones individuales o contactar con el grupo de moderadores.', + 'En Telegram, a la persona propietaria del grupo u otras administradoras.', + 'Para otros espacios o de forma alternativa, ponte en contacto con la Junta Directiva en contacto@es.python.org.', + ], + privacy: + 'Todas las personas administradoras de la comunidad están obligadas a respetar la privacidad y la seguridad de quienes denuncien incidentes.', + }, + attribution: { + title: 'Atribución', + intro: 'Este código de conducta extiende el ya existente con aportaciones de otros códigos:', + sources: [ + 'La versión en español del Contributor Covenant 2.0.', + 'El código de conducta de DjangoCon Europe 2020.', + ], + }, +} as const diff --git a/src/i18n/code-of-conduct/index.ts b/src/i18n/code-of-conduct/index.ts new file mode 100644 index 0000000..2411ea8 --- /dev/null +++ b/src/i18n/code-of-conduct/index.ts @@ -0,0 +1,9 @@ +import { es } from './es' +import { en } from './en' +import { ca } from './ca' + +export const texts = { + es, + en, + ca, +} as const diff --git a/src/i18n/components/footer/ca.ts b/src/i18n/components/footer/ca.ts new file mode 100644 index 0000000..c1e193c --- /dev/null +++ b/src/i18n/components/footer/ca.ts @@ -0,0 +1,10 @@ +export const ca = { + copyright: 'Copyright © Python España & PyConES 2026 Org', + links: [{ label: 'Codi de conducta', href: '/ca/code-of-conduct' }], + social: { + title: 'Segueix-nos', + }, + collaborators: { + title: 'Col·laboren', + }, +} as const diff --git a/src/i18n/components/footer/en.ts b/src/i18n/components/footer/en.ts new file mode 100644 index 0000000..dcb250d --- /dev/null +++ b/src/i18n/components/footer/en.ts @@ -0,0 +1,10 @@ +export const en = { + copyright: 'Copyright © Python España & PyConES 2026 Org', + links: [{ label: 'Code of Conduct', href: '/en/code-of-conduct' }], + social: { + title: 'Follow us', + }, + collaborators: { + title: 'Collaborators', + }, +} as const diff --git a/src/i18n/components/footer/es.ts b/src/i18n/components/footer/es.ts new file mode 100644 index 0000000..1745a26 --- /dev/null +++ b/src/i18n/components/footer/es.ts @@ -0,0 +1,10 @@ +export const es = { + copyright: 'Copyright © Python España & PyConES 2026 Org', + links: [{ label: 'Código de conducta', href: '/es/code-of-conduct' }], + social: { + title: 'Síguenos en redes', + }, + collaborators: { + title: 'Colaboran', + }, +} as const diff --git a/src/i18n/components/footer/index.ts b/src/i18n/components/footer/index.ts new file mode 100644 index 0000000..98f8310 --- /dev/null +++ b/src/i18n/components/footer/index.ts @@ -0,0 +1,9 @@ +import { es } from './es' +import { en } from './en' +import { ca } from './ca' + +export const footerTexts = { + es, + en, + ca, +} as const diff --git a/src/i18n/home.ts b/src/i18n/home.ts index e981ddd..3ad2135 100644 --- a/src/i18n/home.ts +++ b/src/i18n/home.ts @@ -2,16 +2,49 @@ export const texts = { es: { 'index.initializing': 'Inicializando sistema...', 'index.subtitle': 'Sede UB Barcelona | 6-8 Nov 2026', - 'index.sponsor_btn': 'PATROCINA', + 'index.message': + 'Forma parte de la mayor conferencia nacional de Python, donde cientos de entusiastas, profesionales y empresas se reúnen para compartir conocimientos, experiencias y oportunidades en el mundo de Python. ¡No te pierdas esta oportunidad única de aprender, conectar y crecer en la comunidad Python!', + 'sponsors.title': 'Patrocinios', + 'sponsors.description': + 'Gracias a las empresas que colaboran con la PyConES podemos ofrecer el mejor evento y experiencia posible. Somos una conferencia con un bajo coste de entrada capaz de ofrecer una experiencia de 3 días incluyendo regalos, almuerzos, comidas y meriendas. Además contamos con servicio de guardería, becas y traducción en directo a lenguaje de signos para que nadie se quede fuera. Con la ayuda de estas empresas conseguimos hacer un evento diverso e inclusivo enfocado en cuidar la comunidad de Python.', + 'sponsors.main': 'Patrocinador Principal', + 'sponsors.platinum': 'Patrocinador Platino', + 'sponsors.gold': 'Patrocinador Oro', + 'sponsors.silver': 'Patrocinador Plata', + 'sponsors.bronze': 'Patrocinador Bronce', + 'sponsors.none': 'No hay patrocinadores en este nivel', + 'sponsors.altlogo': 'Logo de {name}', }, en: { 'index.initializing': 'Initialising system...', 'index.subtitle': 'UB Barcelona Venue | Nov 6-8, 2026', - 'index.sponsor_btn': 'BECOME A SPONSOR', + 'index.message': + "Join the largest national Python conference, where hundreds of enthusiasts, professionals, and companies come together to share knowledge, experiences, and opportunities in the world of Python. Don't miss this unique opportunity to learn, connect, and grow in the Python community!", + 'sponsors.title': 'Sponsorships', + 'sponsors.description': + 'Thanks to the companies that collaborate with PyConES, we can offer the best possible event and experience. We are a conference with a low entry cost capable of offering a 3-day experience including gifts, lunches, meals, and snacks. Additionally, we have childcare services, scholarships, and live sign language translation to ensure that no one is left out. With the help of these companies, we manage to create a diverse and inclusive event focused on caring for the Python community.', + 'sponsors.main': 'Main Sponsor', + 'sponsors.platinum': 'Platinum Sponsor', + 'sponsors.gold': 'Gold Sponsor', + 'sponsors.silver': 'Silver Sponsor', + 'sponsors.bronze': 'Bronze Sponsor', + 'sponsors.none': 'No sponsors in this tier', + 'sponsors.altlogo': '{name} logo', }, ca: { 'index.initializing': 'Inicialitzant sistema...', 'index.subtitle': 'Seu UB Barcelona | 6-8 Nov 2026', - 'index.sponsor_btn': 'PATROCINA', + 'index.message': + 'Forma part de la major conferència nacional de Python, on centenars d’entusiastes, professionals i empreses es reuneixen per compartir coneixements, experiències i oportunitats en el món de Python. No et perdis aquesta oportunitat única d’aprendre, connectar i créixer en la comunitat Python!', + 'sponsors.title': 'Patrocinis', + 'sponsors.description': + 'Gràcies a les empreses que col·laboren amb la PyConES podem oferir el millor esdeveniment i experiència possible. Som una conferència amb un baix cost d’entrada capaç d’oferir una experiència de 3 dies incloent regals, àpats, menjars i berenars. A més comptem amb servei de guarderia, beques i traducció en directe a llenguatge de signes perquè ningú es quedi fora. Amb l’ajuda d’aquestes empreses aconseguim fer un esdeveniment divers i inclusiu enfocat en cuidar la comunitat de Python.', + 'sponsors.main': 'Patrocinador Principal', + 'sponsors.platinum': 'Patrocinador Platí', + 'sponsors.gold': 'Patrocinador Or', + 'sponsors.silver': 'Patrocinador Plata', + 'sponsors.bronze': 'Patrocinador Bronze', + 'sponsors.none': 'No hi ha patrocinadors en aquest nivell', + 'sponsors.altlogo': 'Logo de {name}', }, } as const diff --git a/src/i18n/location/ca.ts b/src/i18n/location/ca.ts new file mode 100644 index 0000000..4fd9f7b --- /dev/null +++ b/src/i18n/location/ca.ts @@ -0,0 +1,58 @@ +export const ca = { + 'location.title': 'Localització', + 'location.hero.title': 'Seu: Universitat de Barcelona', + 'location.hero.subtitle': 'Edifici Històric de la Gran Via', + 'location.hero.text': + "Després d'una recerca exhaustiva de possibles seus per a la PyConES 2026, vam concloure que la Universitat de Barcelona (UB) oferia un dels espais més emblemàtics de la ciutat. Com a universitat més gran i antiga de Catalunya, les seves instal·lacions històriques compleixen totes les necessitats del nostre esdeveniment.", + 'location.location.title': 'Ubicació', + 'location.location.text': + 'Situada al bell mig de Barcelona, al costat de la Plaça Universidat i la Plaça Catalunya.', + 'location.rooms.title': 'Sales i Espais', + 'location.rooms.paranimf.title': 'Paranimf', + 'location.rooms.paranimf.desc': + 'Sala on es reuneix el Claustre de la universitat, amb capacitat per a 400 persones i un projector de gran canó.', + 'location.rooms.aulamagna.title': 'Aula Magna', + 'location.rooms.aulamagna.desc': + 'Segona sala més gran, polivalent, per a 160 persones. Pot transmetre la xerrada del Paranimf.', + 'location.rooms.teaching.title': 'Aules Docents', + 'location.rooms.teaching.desc': + 'Almenys 4 sales recentment restaurades amb capacitat per a 90-120 persones, equipades amb projector.', + 'location.spaces.title': 'Espais Oberts', + 'location.spaces.gallery.title': 'Galeria del Paranimf', + 'location.spaces.gallery.desc': 'Ideal per a acreditacions i pòsters acadèmics (250 persones).', + 'location.spaces.hall.title': 'Vestíbul Principal', + 'location.spaces.hall.desc': + 'Espai impressionant per a la fira de sponsors i connexió amb el jardí (500 personas).', + 'location.spaces.cloister.title': 'Claustre de Matemàtiques', + 'location.spaces.cloister.desc': + 'Pati central amb balcons, ideal per al descans entre sessions (350 persones total).', + 'location.spaces.garden.title': 'Jardí Central', + 'location.spaces.garden.desc': 'Espai exterior per a caterings i esdeveniments socials (300 persones).', + 'location.transport.title': 'Com arribar-hi', + 'location.transport.metro.title': 'Metro i FGC', + 'location.transport.metro.items': + 'Universitat (L1, L2) - 1 min; Catalunya (L1, L3, L6, L7) - 10 min; Passeig de Gràcia (L2, L4) - 5 min', + 'location.transport.renfe.title': 'Renfe', + 'location.transport.renfe.items': + 'Catalunya (R1, R3, R4) - 10 min; Passeig de Gràcia (R2, R2 Sud, R2 Nord) - 15 min', + 'location.transport.long.title': 'Llarga Distància', + 'location.transport.long.items': + 'Aeroport El Prat (35 min Aerobús); Sants Estació (20 min Metro); Estació del Nord (10 min Metro)', + 'location.transport.car.title': 'Cotxe i Pàrquing', + 'location.transport.car.desc': + 'Es desaconsella el cotxe (sense pàrquing propi i dins de la ZBE). Pàrquings propers: Romara, Roma 4, Baldisa.', + 'location.city.title': 'Barcelona: Seu per a la PyConES 2026', + 'location.city.intro': + 'Barcelona s’alça com una elecció immillorable per allotjar la PyConES 2026, la ciutat fusiona el seu inconfusible encant cultural amb un dinamisme tecnològic d’avantguarda.', + 'location.city.heritage': + 'La seva rica herència cultural, plasmada en obres com la Sagrada Família o el Park Güell, crea un ambient que fomenta la creativitat. És un gresol d’idees on la innovació prospera naturalment.', + 'location.city.climate.title': 'L’escenari perfecte', + 'location.city.climate.text': + 'Barcelona garanteix allotjament per a tots els gustos amb més de 500 hotels i un clima ideal durant la tardor, amb temperatures entre 18-24 graus.', + 'location.city.tech.title': 'Centre tecnològic a Espanya', + 'location.city.tech.text': + 'El 22@Barcelona és la prova de com la ciutat ha transformat zones industrials en un epicentre de coneixement, situant-se al top 5 d’Europa en noves empreses tecnològiques.', + 'location.city.connections.title': 'Connexions i mobilitat', + 'location.city.connections.text': + 'Sants és un node clau de l’AVE, connectant amb Madrid en 2.5h. L’Aeroport del Prat i una xarxa de mobilitat sostenible completen una accessibilitat excepcional.', +} as const diff --git a/src/i18n/location/en.ts b/src/i18n/location/en.ts new file mode 100644 index 0000000..4a0914d --- /dev/null +++ b/src/i18n/location/en.ts @@ -0,0 +1,56 @@ +export const en = { + 'location.title': 'Location', + 'location.hero.title': 'Venue: University of Barcelona', + 'location.hero.subtitle': 'Historic Building at Gran Via', + 'location.hero.text': + "After an exhaustive search for possible venues for PyConES 2026, we concluded that the University of Barcelona (UB) offered one of the city's most iconic spaces. As Catalonia's largest and oldest university, its historic facilities meet all our event's needs.", + 'location.location.title': 'Location', + 'location.location.text': + 'Located in the heart of Barcelona, right by Plaza Universidad and Plaza Cataluña.', + 'location.rooms.title': 'Rooms and Spaces', + 'location.rooms.paranimf.title': 'Paranimf', + 'location.rooms.paranimf.desc': + 'The University Senate room, with a capacity of 400 people and a large-scale projector.', + 'location.rooms.aulamagna.title': 'Aula Magna', + 'location.rooms.aulamagna.desc': + 'The second largest room, multipurpose, for 160 people. It can stream talks from the Paranimf.', + 'location.rooms.teaching.title': 'Teaching Classrooms', + 'location.rooms.teaching.desc': + 'At least 4 recently restored rooms with capacity for 90-120 people, fully equipped.', + 'location.spaces.title': 'Open Spaces', + 'location.spaces.gallery.title': 'Paranimf Gallery', + 'location.spaces.gallery.desc': 'Perfect for registrations and academic posters (250 people).', + 'location.spaces.hall.title': 'Main Hall', + 'location.spaces.hall.desc': 'An impressive space for the sponsor fair and garden access (500 people).', + 'location.spaces.cloister.title': 'Mathematics Cloister', + 'location.spaces.cloister.desc': 'Central courtyard with balconies, ideal for breaks (350 people total).', + 'location.spaces.garden.title': 'Central Garden', + 'location.spaces.garden.desc': 'Outdoor space for catering and social events (300 people).', + 'location.transport.title': 'How to Get There', + 'location.transport.metro.title': 'Metro and FGC', + 'location.transport.metro.items': + 'Universitat (L1, L2) - 1 min; Catalunya (L1, L3, L6, L7) - 10 min; Passeig de Gràcia (L2, L4) - 5 min', + 'location.transport.renfe.title': 'Renfe', + 'location.transport.renfe.items': + 'Catalunya (R1, R3, R4) - 10 min; Passeig de Gràcia (R2, R2 Sud, R2 Nord) - 15 min', + 'location.transport.long.title': 'Long Distance', + 'location.transport.long.items': + 'El Prat Airport (35 min Aerobús); Sants Station (20 min Metro); Estació del Nord (10 min Metro)', + 'location.transport.car.title': 'Car and Parking', + 'location.transport.car.desc': + 'Car travel is discouraged (no private parking and within ZBE). Nearby parking: Romara, Roma 4, Baldisa.', + 'location.city.title': 'Barcelona: Host City for PyConES 2026', + 'location.city.intro': + 'Barcelona stands as an unbeatable choice for PyConES 2026, blending its unique cultural charm with cutting-edge technological dynamism and world-class infrastructure.', + 'location.city.heritage': + 'Its rich heritage, from Sagrada Familia to Park Güell, fosters creativity. Ranked among the top 25 global cities, it is a melting pot where innovation thrives naturally.', + 'location.city.climate.title': 'The Perfect Setting', + 'location.city.climate.text': + 'With over 500 hotels and a mild autumn climate (18-24°C), Barcelona ensures a comfortable stay for all attendees.', + 'location.city.tech.title': 'Spain’s Tech Hub', + 'location.city.tech.text': + 'The 22@Barcelona innovation district transforms industrial zones into a knowledge epicenter, placing the city in the European top 5 for tech startups.', + 'location.city.connections.title': 'Connections and Mobility', + 'location.city.connections.text': + 'Sants station connects to Madrid via high-speed AVE in 2.5h. El Prat Airport and extensive public transport offer seamless national and international access.', +} as const diff --git a/src/i18n/location/es.ts b/src/i18n/location/es.ts new file mode 100644 index 0000000..43b72a9 --- /dev/null +++ b/src/i18n/location/es.ts @@ -0,0 +1,58 @@ +export const es = { + 'location.title': 'Localización', + 'location.hero.title': 'Sede: Universidad de Barcelona', + 'location.hero.subtitle': 'Edificio Histórico de la Gran Vía', + 'location.hero.text': + 'Tras una búsqueda exhaustiva de posibles sedes para el evento de PyConES 2026, concluimos que la Universidad de Barcelona (UB) ofrecía uno de los espacios más emblemáticos de la ciudad. Siendo la UB la universidad más grande y antigua de Cataluña, las equipaciones de este edificio cumplen con todas las necesidades de nuestro evento.', + 'location.location.title': 'Ubicación', + 'location.location.text': + 'Situada en el mismísimo centro de Barcelona, junto a Plaza Universidad y Plaza Cataluña.', + 'location.rooms.title': 'Salas y Espacios', + 'location.rooms.paranimf.title': 'Paranimf', + 'location.rooms.paranimf.desc': + 'Sala donde se reúne el Claustro de la universidad, con capacidad para 400 personas y un proyector de gran cañón.', + 'location.rooms.aulamagna.title': 'Aula Magna', + 'location.rooms.aulamagna.desc': + 'Segunda sala más grande, polivalente, para 160 personas. Puede transmitir la charla del Paranimf.', + 'location.rooms.teaching.title': 'Aulas Docentes', + 'location.rooms.teaching.desc': + 'Al menos 4 salas recién restauradas con capacidad para 90-120 personas, equipadas con proyector.', + 'location.spaces.title': 'Espacios Abiertos', + 'location.spaces.gallery.title': 'Galería del Paranimf', + 'location.spaces.gallery.desc': 'Ideal para acreditaciones y pósters académicos (250 personas).', + 'location.spaces.hall.title': 'Vestíbulo Principal', + 'location.spaces.hall.desc': + 'Espacio impresionante para la feria de sponsors y conexión con el jardín (500 personas).', + 'location.spaces.cloister.title': 'Claustro de Matemáticas', + 'location.spaces.cloister.desc': + 'Patio central con balcones, ideal para el descanso entre sesiones (350 personas total).', + 'location.spaces.garden.title': 'Jardín Central', + 'location.spaces.garden.desc': 'Espacio exterior para caterings y eventos sociales (300 personas).', + 'location.transport.title': 'Cómo llegar', + 'location.transport.metro.title': 'Metro y FGC', + 'location.transport.metro.items': + 'Universitat (L1, L2) - 1 min; Catalunya (L1, L3, L6, L7) - 10 min; Passeig de Gràcia (L2, L4) - 5 min', + 'location.transport.renfe.title': 'Renfe', + 'location.transport.renfe.items': + 'Catalunya (R1, R3, R4) - 10 min; Passeig de Gràcia (R2, R2 Sud, R2 Nord) - 15 min', + 'location.transport.long.title': 'Larga Distancia', + 'location.transport.long.items': + 'Aeropuerto El Prat (35 min Aerobús); Sants Estació (20 min Metro); Estació del Nord (10 min Metro)', + 'location.transport.car.title': 'Coche y Parking', + 'location.transport.car.desc': + 'Se desaconseja el coche (sin parking propio y dentro de ZBE). Parkings cercanos: Romara, Roma 4, Baldisa.', + 'location.city.title': 'Barcelona: Sede para la PyConES 2026', + 'location.city.intro': + 'Barcelona se alza como una elección inmejorable para albergar la PyConES 2026, la ciudad fusiona su inconfundible encanto cultural con un dinamismo tecnológico de vanguardia. La ciudad posee una infraestructura de primer nivel que garantiza una experiencia memorable para todos.', + 'location.city.heritage': + 'Su rica herencia cultural, plasmada en maravillas como la Sagrada Familia, el Park Güell o la Casa Batlló, crea un ambiente que fomenta la creatividad. Clasificada entre las 25 ciudades más potentes del mundo, es un crisol donde la innovación prospera naturalmente.', + 'location.city.climate.title': 'El escenario perfecto', + 'location.city.climate.text': + 'Barcelona garantiza alojamiento para todos los gustos con más de 500 hoteles y un clima ideal durante otoño, con temperaturas entre 18-24 grados.', + 'location.city.tech.title': 'Centro tecnológico en España', + 'location.city.tech.text': + 'El 22@Barcelona es la prueba de cómo la ciudad ha transformado zonas industriales en un epicentro de conocimiento, situándose en el top 5 de Europa en número de nuevas empresas tecnológicas.', + 'location.city.connections.title': 'Conexiones y movilidad', + 'location.city.connections.text': + 'Sants es un nodo clave del AVE, conectando con Madrid en 2.5h. El Aeropuerto de El Prat y la Estació del Nord completan una red de acceso excepcional, apoyada internamente por una movilidad sostenible de vanguardia.', +} as const diff --git a/src/i18n/location/index.ts b/src/i18n/location/index.ts new file mode 100644 index 0000000..3811b89 --- /dev/null +++ b/src/i18n/location/index.ts @@ -0,0 +1,9 @@ +import { es } from './es' +import { en } from './en' +import { ca } from './ca' + +export const locationTexts = { + es, + en, + ca, +} as const diff --git a/src/i18n/menu/ca.ts b/src/i18n/menu/ca.ts new file mode 100644 index 0000000..30318d9 --- /dev/null +++ b/src/i18n/menu/ca.ts @@ -0,0 +1,63 @@ +export const ca = { + items: [ + { + label: 'Inici', + href: '/', + }, + { + label: 'Seu', + href: '/location', + }, + { + label: 'On allotjar-se', + href: '/accommodation', + }, + { + label: 'Diversitat i Inclusió', + children: [ + { + label: 'Codi de conducta', + href: '/code-of-conduct', + }, + ], + }, + { + label: 'Patrocinis', + children: [ + { + label: 'Sobre la PyConES', + href: '/sponsors#about', + }, + { + label: 'En números', + href: '/sponsors#stats', + }, + { + label: 'Lloc', + href: '/sponsors#location', + }, + { + label: 'Opinions', + href: '/sponsors#testimonials', + }, + { + label: 'Paquets de patrocini', + href: '/sponsors#tiers', + }, + ], + }, + { + label: 'Edicions Anteriors', + children: [ + { + label: '2025 (Sevilla)', + href: 'https://2025.es.pycon.org', + }, + { + label: '2024 (Vigo)', + href: 'https://2024.es.pycon.org', + }, + ], + }, + ], +} as const diff --git a/src/i18n/menu/en.ts b/src/i18n/menu/en.ts new file mode 100644 index 0000000..20a9e40 --- /dev/null +++ b/src/i18n/menu/en.ts @@ -0,0 +1,63 @@ +export const en = { + items: [ + { + label: 'Home', + href: '/', + }, + { + label: 'Venue', + href: '/location', + }, + { + label: 'Where to stay', + href: '/accommodation', + }, + { + label: 'Diversity and Inclusion', + children: [ + { + label: 'Code of Conduct', + href: '/code-of-conduct', + }, + ], + }, + { + label: 'Sponsorship', + children: [ + { + label: 'About', + href: '/sponsors#about', + }, + { + label: 'Stats', + href: '/sponsors#stats', + }, + { + label: 'Location', + href: '/sponsors#location', + }, + { + label: 'Testimonials', + href: '/sponsors#testimonials', + }, + { + label: 'Sponsorship Packages', + href: '/sponsors#tiers', + }, + ], + }, + { + label: 'Past Editions', + children: [ + { + label: '2025 (Seville)', + href: 'https://2025.es.pycon.org', + }, + { + label: '2024 (Vigo)', + href: 'https://2024.es.pycon.org', + }, + ], + }, + ], +} as const diff --git a/src/i18n/menu/es.ts b/src/i18n/menu/es.ts new file mode 100644 index 0000000..6a13fa1 --- /dev/null +++ b/src/i18n/menu/es.ts @@ -0,0 +1,63 @@ +export const es = { + items: [ + { + label: 'Inicio', + href: '/', + }, + { + label: 'Sede', + href: '/location', + }, + { + label: 'Dónde alojarse', + href: '/accommodation', + }, + { + label: 'Diversidad e Inclusión', + children: [ + { + label: 'Código de conducta', + href: '/code-of-conduct', + }, + ], + }, + { + label: 'Patrocinios', + children: [ + { + label: 'Sobre la PyConES', + href: '/sponsors#about', + }, + { + label: 'En números', + href: '/sponsors#stats', + }, + { + label: 'Lugar', + href: '/sponsors#location', + }, + { + label: 'Testimonios', + href: '/sponsors#testimonials', + }, + { + label: 'Paquetes de patrocinio', + href: '/sponsors#tiers', + }, + ], + }, + { + label: 'Ediciones Anteriores', + children: [ + { + label: '2025 (Sevilla)', + href: 'https://2025.es.pycon.org', + }, + { + label: '2024 (Vigo)', + href: 'https://2024.es.pycon.org', + }, + ], + }, + ], +} as const diff --git a/src/i18n/menu/index.ts b/src/i18n/menu/index.ts new file mode 100644 index 0000000..302dfca --- /dev/null +++ b/src/i18n/menu/index.ts @@ -0,0 +1,9 @@ +import { es } from './es' +import { en } from './en' +import { ca } from './ca' + +export const menuTexts = { + es, + en, + ca, +} as const diff --git a/src/i18n/sponsors/ca.ts b/src/i18n/sponsors/ca.ts index 82d7257..ce55902 100644 --- a/src/i18n/sponsors/ca.ts +++ b/src/i18n/sponsors/ca.ts @@ -206,43 +206,43 @@ export const ca = { no_taxes: 'sense IVA', items: [ { + color: 'text-tier-bronze', name: 'Bronze', emoji: '🟤', price: 'Preu 1.000€', limit: 'Il·limitat', - color: '#d97706', bg: 'rgba(180, 83, 9, 0.1)', }, { + color: 'text-tier-silver', name: 'Plata', emoji: '⚪', price: 'Preu 3.000€', limit: '10 disp.', - color: '#9ca3af', bg: 'rgba(107, 114, 128, 0.1)', }, { + color: 'text-tier-gold', name: 'Or', emoji: '🌟', price: 'Preu 6.000€', limit: '5 disp.', - color: '#facc15', bg: 'rgba(234, 179, 8, 0.1)', }, { + color: 'text-tier-platinium', name: 'Platí', emoji: '🏆', price: 'Preu 8.000€', limit: '2 disp.', - color: '#4ade80', bg: 'rgba(34, 197, 94, 0.1)', }, { + color: 'text-tier-main', name: 'Principal', emoji: '🏰', price: 'Preu Personalitzat', limit: '1 disp.', - color: '#c084fc', bg: 'rgba(168, 85, 247, 0.1)', }, ], @@ -409,16 +409,6 @@ export const ca = { }, ], }, - socialLinks: { - title: 'Segueix-nos a les xarxes', - items: [ - { icon: '🦋', label: 'Bluesky', url: 'https://bsky.app/profile/es.pycon.org' }, - { icon: '🐙', label: 'GitHub', url: 'https://github.com/python-spain' }, - { icon: '𝕏', label: '', url: 'https://x.com/PyConES' }, - { icon: '💼', label: 'LinkedIn', url: 'https://www.linkedin.com/company/pycones' }, - { icon: '📸', label: 'Instagram', url: 'https://www.instagram.com/pycon_es' }, - ], - }, contact: { title: 'T’hi apuntes?', body: 'T’ho posem fàcil. Escriu-nos explicant-nos quin nivell de patrocini t’interessa o quin pressupost teniu al cap. Nosaltres et guiarem en el procés, resoldrem els teus dubtes i veurem com encaixar la teva marca de la millor forma possible.', diff --git a/src/i18n/sponsors/en.ts b/src/i18n/sponsors/en.ts index c6b1988..bd8e5a2 100644 --- a/src/i18n/sponsors/en.ts +++ b/src/i18n/sponsors/en.ts @@ -206,43 +206,43 @@ export const en = { no_taxes: 'VAT not included', items: [ { + color: 'text-tier-bronze', name: 'Bronze', emoji: '🟤', price: 'Price €1,000', limit: 'Unlimited', - color: '#d97706', bg: 'rgba(180, 83, 9, 0.1)', }, { + color: 'text-tier-silver', name: 'Silver', emoji: '⚪', price: 'Price €3,000', limit: '10 avail.', - color: '#9ca3af', bg: 'rgba(107, 114, 128, 0.1)', }, { + color: 'text-tier-gold', name: 'Gold', emoji: '🌟', price: 'Price €6,000', limit: '5 avail.', - color: '#facc15', bg: 'rgba(234, 179, 8, 0.1)', }, { + color: 'text-tier-platinum', name: 'Platinum', emoji: '🏆', price: 'Price €8,000', limit: '2 avail.', - color: '#4ade80', bg: 'rgba(34, 197, 94, 0.1)', }, { + color: 'text-tier-main', name: 'Main', emoji: '🏰', price: 'Price Custom', limit: '1 avail.', - color: '#c084fc', bg: 'rgba(168, 85, 247, 0.1)', }, ], @@ -409,16 +409,7 @@ export const en = { }, ], }, - socialLinks: { - title: 'Follow us', - items: [ - { icon: '🦋', label: 'Bluesky', url: 'https://bsky.app/profile/es.pycon.org' }, - { icon: '🐙', label: 'GitHub', url: 'https://github.com/python-spain' }, - { icon: '𝕏', label: '', url: 'https://x.com/PyConES' }, - { icon: '💼', label: 'LinkedIn', url: 'https://www.linkedin.com/company/pycones' }, - { icon: '📸', label: 'Instagram', url: 'https://www.instagram.com/pycon_es' }, - ], - }, + contact: { title: 'Interested?', body: 'We make it easy. Write to us mentioning which sponsorship level interests you or what budget you have in mind. We will guide you through the process and help your brand fit in the best possible way.', diff --git a/src/i18n/sponsors/es.ts b/src/i18n/sponsors/es.ts index 79978d6..41897e1 100644 --- a/src/i18n/sponsors/es.ts +++ b/src/i18n/sponsors/es.ts @@ -30,6 +30,7 @@ export const es = { sunday: 'Domingo', sundayBody: 'Más charlas, "charlas relámpago" y la despedida final.', }, + stats: { title: 'La PyConES en números', items: [ @@ -206,43 +207,43 @@ export const es = { no_taxes: 'sin IVA', items: [ { + color: 'text-tier-bronze', name: 'Bronce', emoji: '🟤', price: 'Precio 1.000€', limit: 'Ilimitado', - color: '#d97706', bg: 'rgba(180, 83, 9, 0.1)', }, { + color: 'text-tier-silver', name: 'Plata', emoji: '⚪', price: 'Precio 3.000€', limit: '10 disp.', - color: '#9ca3af', bg: 'rgba(107, 114, 128, 0.1)', }, { + color: 'text-tier-gold', name: 'Oro', emoji: '🌟', price: 'Precio 6.000€', limit: '5 disp.', - color: '#facc15', bg: 'rgba(234, 179, 8, 0.1)', }, { + color: 'text-tier-platinum', name: 'Platino', emoji: '🏆', price: 'Precio 8.000€', limit: '2 disp.', - color: '#4ade80', bg: 'rgba(34, 197, 94, 0.1)', }, { + color: 'text-tier-main', name: 'Principal', emoji: '🏰', price: 'Precio Personalizado', limit: '1 disp.', - color: '#c084fc', bg: 'rgba(168, 85, 247, 0.1)', }, ], @@ -409,16 +410,7 @@ export const es = { }, ], }, - socialLinks: { - title: 'Síguenos en redes', - items: [ - { icon: '🦋', label: 'Bluesky', url: 'https://bsky.app/profile/es.pycon.org' }, - { icon: '🐙', label: 'GitHub', url: 'https://github.com/python-spain' }, - { icon: '𝕏', label: '', url: 'https://x.com/PyConES' }, - { icon: '💼', label: 'LinkedIn', url: 'https://www.linkedin.com/company/pycones' }, - { icon: '📸', label: 'Instagram', url: 'https://www.instagram.com/pycon_es' }, - ], - }, + contact: { title: '¿Te apuntas?', body: 'Te lo ponemos fácil. Escríbenos contándonos qué nivel de patrocinio te interesa o qué presupuesto tenéis en mente. Nosotros te guiaremos en el proceso, resolveremos tus dudas y veremos cómo encajar tu marca de la mejor forma posible.', diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 07d9bbe..c8c0613 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -1,6 +1,7 @@ --- -import LanguagePicker from '../components/LanguagePicker.astro' -import '../style/global.css' +import Header from './components/Header/Header.astro' +import Footer from './components/Footer.astro' +import '@/style/global.css' import '@fontsource-variable/jetbrains-mono' import '@fontsource-variable/outfit' import { ClientRouter } from 'astro:transitions' @@ -9,15 +10,44 @@ interface Props { title: string description?: string // Optional (?) } -const { lang } = Astro.params || { lang: 'es' } +const { lang = 'es' } = Astro.params const { title, description = 'PyconES 2026' } = Astro.props + +const siteUrl = Astro.site ? Astro.site.origin : 'https://2026.es.pycon.org' +const canonicalURL = new URL(Astro.url.pathname, siteUrl) +const socialImageURL = new URL('/images/logo-vertical-alt-color-dark.png', siteUrl) // I should check if PNG exists or generate it later + +const alternates = [ + { href: new URL('/es/', siteUrl), hreflang: 'es' }, + { href: new URL('/en/', siteUrl), hreflang: 'en' }, + { href: new URL('/ca/', siteUrl), hreflang: 'ca' }, +] --- + + { + import.meta.env.PUBLIC_GA_ID && ( + <> + + + ) + } - @@ -25,23 +55,100 @@ const { title, description = 'PyconES 2026' } = Astro.props + {title} + + + + + + { + alternates.map(({ href, hreflang }) => ( + + )) + } + + + + + + + + + + + + + + + + + + + + - +
- +
-
+
+