From 88962d5aa1dccfb500b737dc55b2b584f2766481 Mon Sep 17 00:00:00 2001 From: Pep Villalonga Date: Tue, 14 Jul 2026 13:30:43 +0200 Subject: [PATCH 01/47] fix: charges selected projects total --- src/components/admin/Contributions.svelte | 163 ++++++++++++++++++++-- src/components/home/Campaigns.astro | 2 +- src/components/home/CampaignsTabs.astro | 2 +- src/i18n/locales/es.json | 1 + 4 files changed, 158 insertions(+), 10 deletions(-) diff --git a/src/components/admin/Contributions.svelte b/src/components/admin/Contributions.svelte index 736b0a88..680dcf47 100644 --- a/src/components/admin/Contributions.svelte +++ b/src/components/admin/Contributions.svelte @@ -9,6 +9,7 @@ import Table, { type ExtendedCharge } from "./Table.svelte"; import { session } from "../../auth/store"; import { t } from "../../i18n/store"; + import { client } from "../../openapi/client/client.gen"; import { apiAccountingsIdGet, apiGatewayChargesGetCollection, @@ -19,6 +20,7 @@ apiUsersIdOrHandleGet, type Accounting, type ApiGatewayChargesGetCollectionData, + type ApiProjectsGetCollectionData, type GatewayCharge, type GatewayCheckout, type Project, @@ -61,6 +63,7 @@ let isFirstLoad = $state(true); let selectedSort = $state("date-desc"); let totalTips = $state("—"); + let selectedProjectsCount = $state("—"); async function loadTotalTips() { if (!isEnabled || !$session) return; @@ -79,6 +82,135 @@ } } + // Preferred source: the /v4/gateway_charges/stats aggregate (may not be deployed yet). + // Fallback: scan the filtered collection page by page (same approach as ExportCsv) + // and deduplicate targets — only viable for small result sets, hence the cap. + // Results are cached per filter set for the session. + const CHARGES_SCAN_PAGE_SIZE = 100; + const CHARGES_SCAN_CONCURRENCY = 3; + const MAX_SCANNABLE_CHARGES = 2000; + const PROJECTS_LOOKUP_CHUNK_SIZE = 50; + const projectsCountCache = new Map(); + let projectsCountRequestId = 0; + + async function fetchDistinctProjectsFromStats( + chargeFilters: ApiGatewayChargesGetCollectionData["query"], + headers: HeadersInit, + ): Promise { + try { + const { data, response } = await client.get({ + url: "/v4/gateway_charges/stats", + query: chargeFilters as Record, + headers, + }); + if (!response.ok || !data || typeof data !== "object") return null; + + const count = (data as Record).distinctProjectCount; + return typeof count === "number" ? count : null; + } catch { + return null; + } + } + + async function loadSelectedProjectsCount( + chargeFilters: ApiGatewayChargesGetCollectionData["query"], + ) { + const requestId = ++projectsCountRequestId; + const cacheKey = JSON.stringify(chargeFilters ?? {}); + const cached = projectsCountCache.get(cacheKey); + if (cached !== undefined) { + selectedProjectsCount = cached; + return; + } + + selectedProjectsCount = "—"; + + const headers = { + Accept: "application/ld+json", + ...($session?.token.asHttpHeaders ?? {}), + }; + + const fetchChargesPage = async (page: number) => { + const { data, error } = await apiGatewayChargesGetCollection({ + query: { ...chargeFilters, page, itemsPerPage: CHARGES_SCAN_PAGE_SIZE }, + headers, + }); + if (error) throw error; + return data; + }; + + try { + const statsCount = await fetchDistinctProjectsFromStats(chargeFilters, headers); + if (requestId !== projectsCountRequestId) return; + if (statsCount !== null) { + projectsCountCache.set(cacheKey, statsCount); + selectedProjectsCount = statsCount; + return; + } + + const targets = new Set(); + const collectTargets = (collection: unknown) => { + for (const charge of toCollectionItems(collection)) { + if (charge.target) targets.add(charge.target); + } + }; + + const firstPage = await fetchChargesPage(1); + collectTargets(firstPage); + + // With a single target filter every charge shares it: page 1 is enough. + if (!chargeFilters?.target) { + const totalCharges = getCollectionTotalItems(firstPage); + + // Without the stats endpoint, big result sets would take minutes to + // scan and hammer the API — leave the card empty instead. + if (totalCharges > MAX_SCANNABLE_CHARGES) return; + + const totalPages = Math.ceil(totalCharges / CHARGES_SCAN_PAGE_SIZE); + + for (let page = 2; page <= totalPages; page += CHARGES_SCAN_CONCURRENCY) { + if (requestId !== projectsCountRequestId) return; + + const batch = []; + for ( + let p = page; + p < page + CHARGES_SCAN_CONCURRENCY && p <= totalPages; + p++ + ) { + batch.push(fetchChargesPage(p)); + } + (await Promise.all(batch)).forEach(collectTargets); + } + } + + if (requestId !== projectsCountRequestId) return; + + // Targets can also be user or tipjar accountings; keep only projects. + const targetList = [...targets]; + let projectCount = 0; + + for (let i = 0; i < targetList.length; i += PROJECTS_LOOKUP_CHUNK_SIZE) { + const query: ApiProjectsGetCollectionData["query"] = { + "accounting[]": targetList.slice(i, i + PROJECTS_LOOKUP_CHUNK_SIZE), + itemsPerPage: 1, + }; + const { data, error } = await apiProjectsGetCollection({ + query, + headers: { Accept: "application/ld+json" }, + }); + if (error) throw error; + projectCount += getCollectionTotalItems(data); + } + + if (requestId !== projectsCountRequestId) return; + + projectsCountCache.set(cacheKey, projectCount); + selectedProjectsCount = projectCount; + } catch (error) { + console.error("Failed to count projects for filtered charges:", error); + } + } + function buildChargesQuery( filters: ApiGatewayChargesGetCollectionData["query"], page: number, @@ -375,6 +507,24 @@ reloadCharges(); }); + // Plain (non-reactive) bookkeeping: avoids re-triggering the effect below on write. + let prevProjectsCountKey: string | undefined; + + $effect(() => { + if (pendingSearch) return; + const currentFilters = Object.fromEntries( + Object.entries(filters ?? {}).filter( + ([, value]) => value !== undefined && value !== "", + ), + ) as ApiGatewayChargesGetCollectionData["query"]; + const key = JSON.stringify(currentFilters); + + if (key === prevProjectsCountKey) return; + + prevProjectsCountKey = key; + loadSelectedProjectsCount(currentFilters); + }); + let prevItemsPerPage = $state($itemsPerPage); $effect(() => { @@ -386,10 +536,10 @@ }); let chargeSlides = $derived([ - { title: $t("admin.charges.totalizers.selected"), amount: $totalItems }, - { title: $t("admin.charges.totalizers.totalCharges"), amount: "—" }, - { title: $t("admin.charges.totalizers.totalTips"), amount: totalTips }, - { title: $t("admin.charges.totalizers.totalFees"), amount: "—" }, + { title: $t("domain.charges.totalizers.selected"), amount: selectedProjectsCount }, + { title: $t("domain.charges.totalizers.totalCharges"), amount: "—" }, + { title: $t("domain.charges.totalizers.totalTips"), amount: totalTips }, + { title: $t("domain.charges.totalizers.totalFees"), amount: "—" }, ]); onMount(async () => { @@ -397,10 +547,7 @@ paymentMethodOptions = [ ["all", $t("pages.admin.charges.filters.paymentMethod.options.all")], - ...(paymentGateways ?? []).map((g): [string, string] => [ - g.name!, - $t(`pages.admin.charges.filters.paymentMethod.options.${g.name}`), - ]), + ...(paymentGateways ?? []).map((g): [string, string] => [g.id!, g.name]), ]; chargeStatusOptions = Object.entries( diff --git a/src/components/home/Campaigns.astro b/src/components/home/Campaigns.astro index 8f77081d..9058cf4f 100644 --- a/src/components/home/Campaigns.astro +++ b/src/components/home/Campaigns.astro @@ -28,7 +28,7 @@ const { data: projects } = await apiProjectsGetCollection({ }); const campaigns: Campaign[] = await Promise.all( - projects!.map(async (project, index) => { + (projects ?? []).map(async (project, index) => { const { data: accounting } = (await client.get({ url: client.buildUrl({ url: project.accounting! }), })) as { data: Accounting }; diff --git a/src/components/home/CampaignsTabs.astro b/src/components/home/CampaignsTabs.astro index f3cc521b..996eb0f7 100644 --- a/src/components/home/CampaignsTabs.astro +++ b/src/components/home/CampaignsTabs.astro @@ -31,7 +31,7 @@ const { data: projects } = await apiProjectsGetCollection({ }); const campaigns: Campaign[] = await Promise.all( - projects!.map(async (project) => { + (projects ?? []).map(async (project) => { const { data: accounting } = (await client.get({ url: client.buildUrl({ url: project.accounting! }), })) as { data: Accounting }; diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 4eab314d..c6035f19 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -109,6 +109,7 @@ }, "lastContributions": "Últimos aportes:", "totalizers": { + "selected": "Proyectos seleccionados:", "totalCharges": "Total aportes:", "totalTips": "Total propinas:", "totalFees": "Comisiones totales:" From cfa8dc53fc89f04724a5cd6df5fa6694ee998a3d Mon Sep 17 00:00:00 2001 From: Dayv1dDev Date: Tue, 14 Jul 2026 14:11:54 +0200 Subject: [PATCH 02/47] feat: add FilterSubject registry and createFilterRow serializer --- src/utils/filterComposer.ts | 146 ++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 src/utils/filterComposer.ts diff --git a/src/utils/filterComposer.ts b/src/utils/filterComposer.ts new file mode 100644 index 00000000..9ade1bed --- /dev/null +++ b/src/utils/filterComposer.ts @@ -0,0 +1,146 @@ +export interface FilterRow { + id: string; + subject: FilterSubject; + operator: FilterOperator; + referent: string | string[]; + serialize: () => Record; +} + +export type FilterOperator = + | "equals" + | "is_any_of" + | "before" + | "after" + | "strictly_before" + | "strictly_after" + | "gte" + | "gt" + | "lte" + | "lt"; + +export interface FilterSubject { + key: string; + type: "string" | "date" | "number"; + compatibleOperators: FilterOperator[]; +} + +const filterSubjects: Record = { + title: { + key: "title", + type: "string", + compatibleOperators: ["equals"], + }, + subtitle: { + key: "subtitle", + type: "string", + compatibleOperators: ["equals"], + }, + description: { + key: "description", + type: "string", + compatibleOperators: ["equals"], + }, + status: { + key: "status", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + }, + categories: { + key: "categories", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + }, + owner: { + key: "owner", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + }, + slug: { + key: "slug", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + }, + accounting: { + key: "accounting", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + }, + territoryCountry: { + key: "territory.country", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + }, + territorySubLvl1: { + key: "territory.subLvl1", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + }, + territorySubLvl2: { + key: "territory.subLvl2", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + }, + releaseDate: { + key: "releaseDate", + type: "date", + compatibleOperators: ["before", "after", "strictly_before", "strictly_after"], + }, + dateUpdated: { + key: "dateUpdated", + type: "date", + compatibleOperators: ["before", "after", "strictly_before", "strictly_after"], + }, + budgetAmount: { + key: "budgetAmount", + type: "number", + compatibleOperators: ["gte", "gt", "lte", "lt"], + }, +}; + +export function createFilterRow( + subject: FilterSubject, + operator: FilterOperator, + referent: string | string[], +): FilterRow { + const id = crypto.randomUUID(); + + return { + id, + subject, + operator, + referent, + serialize: () => { + const key = subject.key; + + switch (operator) { + case "equals": + return { [key]: referent as string }; + case "is_any_of": + return { [`${key}[]`]: referent as string[] }; + case "before": + case "after": + case "strictly_before": + case "strictly_after": { + const apiKey = key === "releaseDate" ? "dateCreated" : key; + const dateStr = new Date(referent as string).toISOString(); + return { [`${apiKey}[${operator}]`]: dateStr }; + } + case "gte": + case "gt": + case "lte": + case "lt": + return { [`${key}[${operator}]`]: referent as string }; + default: + return { [key]: referent as string }; + } + }, + }; +} + +export function getFilterSubject(key: string): FilterSubject | undefined { + return filterSubjects[key]; +} + +export function getAllFilterSubjects(): FilterSubject[] { + return Object.values(filterSubjects); +} From 87e9841b65e07d1e57d9df60776f3e1fe02775cd Mon Sep 17 00:00:00 2001 From: Pep Villalonga Date: Thu, 16 Jul 2026 11:35:23 +0200 Subject: [PATCH 03/47] feat: add configurable campaign end date limit --- .env.example | 5 ++ .../project/edit/ConfigurationStep.svelte | 33 ++++++++++- .../project/edit/RoundSelector.svelte | 52 +++++++++++------ src/i18n/locales/es.json | 6 +- .../create/CreateProjectForm.svelte | 2 + src/pages/[...locale]/create/validation.ts | 17 ++++++ src/utils/campaign.ts | 58 +++++++++++++++++++ 7 files changed, 153 insertions(+), 20 deletions(-) create mode 100644 src/utils/campaign.ts diff --git a/.env.example b/.env.example index 08938382..0c186249 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,11 @@ PUBLIC_TIPPING_TIPJAR_ID="" PUBLIC_TIPPING_DEFAULT_AMOUNT="" PUBLIC_TIPPING_DEFAULT_CHECKED="false" +# Campaign limit +# Set a date (YYYY-MM-DD) to prevent projects from being in campaign past it +# Leave the value empty to disable the limit +PUBLIC_CAMPAIGN_MAX_END_DATE="" + # Facebook's share dialog # https://developers.facebook.com/docs/sharing/reference/share-dialog PUBLIC_FACEBOOK_APP_ID="" diff --git a/src/components/project/edit/ConfigurationStep.svelte b/src/components/project/edit/ConfigurationStep.svelte index 9099c649..75cfee53 100644 --- a/src/components/project/edit/ConfigurationStep.svelte +++ b/src/components/project/edit/ConfigurationStep.svelte @@ -24,6 +24,8 @@ updateConfiguration, updateProject, } from "../../../stores/drafts/projectDraft"; + import { getMaxReleaseDate, isEnabled, maxEndDate } from "../../../utils/campaign"; + import { formatDate } from "../../../utils/dates"; import { toCollectionItems } from "../../../utils/hydra"; import Button from "../../library/buttons/Button.svelte"; @@ -52,6 +54,26 @@ $currentDraft?.wizardForm.configuration.projectDeadline ?? "minimum", ); + // Campaign start date: server-computed calendar first, draft as fallback + let releaseDate = $derived.by(() => { + const release = project?.calendar?.release ?? $currentDraft?.createProject.release; + return release ? new Date(release) : undefined; + }); + + // The 2-rounds (optimum) campaign lasts 80 days; block it when it would + // extend past the platform-wide cutoff (PUBLIC_CAMPAIGN_MAX_END_DATE) + let optimumDisabled = $derived.by(() => { + if (!isEnabled || !releaseDate) return false; + const maxRelease = getMaxReleaseDate("optimum"); + return maxRelease !== undefined && releaseDate > maxRelease; + }); + + $effect(() => { + if (optimumDisabled && projectDeadline === "optimum") { + updateConfiguration({ projectDeadline: "minimum" }); + } + }); + /** * Handle Continue button * Simple navigation to next step (2) - validation happens on save/submit @@ -122,7 +144,16 @@ {$t("pages.project.edit.configuration.rounds.description")}

- + diff --git a/src/components/project/edit/RoundSelector.svelte b/src/components/project/edit/RoundSelector.svelte index c1cc5de7..9cdb2520 100644 --- a/src/components/project/edit/RoundSelector.svelte +++ b/src/components/project/edit/RoundSelector.svelte @@ -21,9 +21,16 @@ interface RoundSelectorProps { deadline: Project["deadline"]; onChange: (selectedDeadline: "minimum" | "optimum") => void; + optimumDisabled?: boolean; + optimumDisabledReason?: string; } - let { deadline = $bindable("minimum"), onChange }: RoundSelectorProps = $props(); + let { + deadline = $bindable("minimum"), + onChange, + optimumDisabled = false, + optimumDisabledReason, + }: RoundSelectorProps = $props(); /** * Handle round selection change @@ -33,22 +40,31 @@ } -
- - handleChange("minimum")} - label={$t("pages.project.edit.configuration.rounds.option1")} - /> +
+
+ + handleChange("minimum")} + label={$t("pages.project.edit.configuration.rounds.option1")} + /> - - handleChange("optimum")} - label={$t("pages.project.edit.configuration.rounds.option2")} - /> + +
+ handleChange("optimum")} + label={$t("pages.project.edit.configuration.rounds.option2")} + /> +
+
+ + {#if optimumDisabled && optimumDisabledReason} +

{optimumDisabledReason}

+ {/if}
diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 968362de..c376d61d 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -26,6 +26,9 @@ "project": { "terms": { "required": "Debes aceptar las condiciones del impulsor y las bases de la convocatoria" + }, + "release": { + "max": "La fecha de inicio es demasiado tardía: la campaña no puede extenderse más allá de la fecha límite de la plataforma" } }, "errors": { @@ -478,7 +481,8 @@ "title": "Elige las rondas que tendrá tu campaña", "description": "La configuración predeterminada son 40 días cada ronda, puedes solicitar modificaciones a tu asesor si tu proyecto es seleccionado", "option1": "1 Ronda", - "option2": "2 Rondas" + "option2": "2 Rondas", + "optimumDisabled": "La opción de 2 rondas no está disponible: la campaña superaría la fecha límite del {{ date }}" }, "continue": "Continuar con info de campaña", "geography": { diff --git a/src/pages/[...locale]/create/CreateProjectForm.svelte b/src/pages/[...locale]/create/CreateProjectForm.svelte index 65c60c99..f736c472 100644 --- a/src/pages/[...locale]/create/CreateProjectForm.svelte +++ b/src/pages/[...locale]/create/CreateProjectForm.svelte @@ -29,6 +29,7 @@ project, validationErrors, } from "../../../stores/drafts/projectDraft"; + import { getMaxReleaseDate } from "../../../utils/campaign"; import { formatCurrency } from "../../../utils/currencies"; let releaseDate = $state($project.release ? new Date($project.release) : new Date()); @@ -340,6 +341,7 @@ name="release" bind:value={releaseDate} min={getMinDate()} + max={getMaxReleaseDate("minimum")} error={shouldShowError("release") ? $t($validationErrors.release) : undefined} onBlur={() => handleFieldBlur("release")} onInput={(date) => handleFieldChange("release", date)} diff --git a/src/pages/[...locale]/create/validation.ts b/src/pages/[...locale]/create/validation.ts index ecc52d4c..d5dfc02e 100644 --- a/src/pages/[...locale]/create/validation.ts +++ b/src/pages/[...locale]/create/validation.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { getMaxReleaseDate } from "../../../utils/campaign"; + /** * Validation schema for project creation form. * Mirrors backend validation rules from Project.ProjectCreationDto. @@ -58,6 +60,21 @@ export const projectCreationSchema = z.object({ }, { message: "system.validation.project.release.min" }, ) + .refine( + (date) => { + // Latest release so the campaign cannot extend past the + // platform-wide cutoff (PUBLIC_CAMPAIGN_MAX_END_DATE) + const maxDate = getMaxReleaseDate("minimum"); + if (!maxDate) return true; + + // Create a copy of the input date to avoid mutation + const normalizedDate = new Date(date); + normalizedDate.setHours(0, 0, 0, 0); + + return normalizedDate <= maxDate; + }, + { message: "system.validation.project.release.max" }, + ) .default(() => { const defaultDate = new Date(); defaultDate.setDate(defaultDate.getDate() + 28); diff --git a/src/utils/campaign.ts b/src/utils/campaign.ts new file mode 100644 index 00000000..a5af16c7 --- /dev/null +++ b/src/utils/campaign.ts @@ -0,0 +1,58 @@ +/** + * Campaign duration in days for each funding round choice. + * Mirrors the server-side calendar computation: the minimum deadline is + * 40 days after release, the optimum deadline 40 days after the minimum. + */ +export const CAMPAIGN_DURATION_DAYS: Record<"minimum" | "optimum", number> = { + minimum: 40, + optimum: 80, +}; + +/** + * Parses a YYYY-MM-DD string into a local-midnight Date. + * Returns undefined for empty or malformed values. + */ +export function parseMaxEndDate(raw: string | undefined): Date | undefined { + if (!raw || raw === "") return undefined; + + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw); + if (!match) { + console.warn(`Invalid PUBLIC_CAMPAIGN_MAX_END_DATE value: ${raw}`); + return undefined; + } + + return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])); +} + +/** + * Latest allowed release date so a campaign with the given round choice + * ends on or before the cutoff date. + */ +export function computeMaxReleaseDate( + cutoff: Date, + deadline: "minimum" | "optimum" = "minimum", +): Date { + const maxRelease = new Date(cutoff); + maxRelease.setDate(maxRelease.getDate() - CAMPAIGN_DURATION_DAYS[deadline]); + return maxRelease; +} + +/** + * Last date a project may be in campaign, as configured by the platform owner. + * Leave PUBLIC_CAMPAIGN_MAX_END_DATE empty to disable the limit. + */ +export const maxEndDate = parseMaxEndDate(import.meta.env.PUBLIC_CAMPAIGN_MAX_END_DATE); + +/** + * Is a campaign end limit configured? + */ +export const isEnabled = maxEndDate !== undefined; + +/** + * Latest allowed release date for the given round choice, + * or undefined when no limit is configured. + */ +export function getMaxReleaseDate(deadline: "minimum" | "optimum" = "minimum"): Date | undefined { + if (!maxEndDate) return undefined; + return computeMaxReleaseDate(maxEndDate, deadline); +} From 7c88eef6a6ed3bd248bacf1f47a69410c5a64a52 Mon Sep 17 00:00:00 2001 From: Pep Villalonga Date: Thu, 16 Jul 2026 12:21:14 +0200 Subject: [PATCH 04/47] fix: crowdin resolve errors --- .github/workflows/crowdin-upload.yml | 16 +++++++++++----- crowdin.yml | 2 -- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/crowdin-upload.yml b/.github/workflows/crowdin-upload.yml index e2e87ea7..49534405 100644 --- a/.github/workflows/crowdin-upload.yml +++ b/.github/workflows/crowdin-upload.yml @@ -25,7 +25,7 @@ jobs: download_translations: false push_translations: false create_pull_request: false - crowdin_branch_name: develop + crowdin_branch_name: ${{ github.ref_name }} env: CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} @@ -33,20 +33,26 @@ jobs: - name: Resolve Crowdin branch and file IDs id: ids + env: + CROWDIN_BRANCH: ${{ github.ref_name }} run: | api="https://api.crowdin.com/api/v2/projects/${CROWDIN_PROJECT_ID}" auth="Authorization: Bearer ${CROWDIN_PERSONAL_TOKEN}" - branch_id=$(curl -fsS -H "$auth" "$api/branches?name=develop" \ + # Crowdin normalizes branch names: \ / : * ? " < > | become "." + branch_name=$(printf '%s' "$CROWDIN_BRANCH" | sed 's#[\\/:*?"<>|]#.#g') + + branch_id=$(curl -fsS -G -H "$auth" "$api/branches" \ + --data-urlencode "name=${branch_name}" \ | jq -r '.data[0].data.id') if [ -z "$branch_id" ] || [ "$branch_id" = "null" ]; then - echo "Branch 'develop' not found in Crowdin"; exit 1 + echo "Branch '${branch_name}' not found in Crowdin"; exit 1 fi - file_id=$(curl -fsS -H "$auth" "$api/files?branchId=${branch_id}&limit=500" \ + file_id=$(curl -fsS -H "$auth" "$api/files?branchId=${branch_id}&recursion=1&limit=500" \ | jq -r '.data[] | select(.data.name=="es.json") | .data.id' | head -n1) if [ -z "$file_id" ] || [ "$file_id" = "null" ]; then - echo "es.json not found in Crowdin branch 'develop'"; exit 1 + echo "es.json not found in Crowdin branch '${branch_name}'"; exit 1 fi echo "file_id=$file_id" >> "$GITHUB_OUTPUT" diff --git a/crowdin.yml b/crowdin.yml index 523daeb9..1859c175 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -7,5 +7,3 @@ preserve_hierarchy: true files: - source: /src/i18n/locales/es.json translation: /src/i18n/locales/%two_letters_code%.json - ignore: - - /src/i18n/locales/es.json From d53812310a2f6e25089f26233eee426073cf3923 Mon Sep 17 00:00:00 2001 From: OlivierSchulbaum Date: Thu, 16 Jul 2026 12:23:35 +0200 Subject: [PATCH 05/47] New translations es.json (Catalan) [ci skip] --- src/i18n/locales/ca.json | 2373 +++++++++++++++++++++----------------- 1 file changed, 1315 insertions(+), 1058 deletions(-) diff --git a/src/i18n/locales/ca.json b/src/i18n/locales/ca.json index 421688da..6c85878c 100644 --- a/src/i18n/locales/ca.json +++ b/src/i18n/locales/ca.json @@ -1,23 +1,50 @@ { - "common": { - "_": "Traduccions de la interfície global (botons, etiquetes, etc.)", - "greeting": "Hola", - "farewell": "Adéu", - "donate": "Donar", - "donateAmount": "Dona {{ amount }}", - "save": "Desa", - "edit": "Editar", - "delete": "Esborrar", - "continue": "Continuar", - "cancel": "Cancel·lar", - "publish": "Publicar", - "preview": "Previsualitzar", - "select": "Escull una opció", - "remove": "Eliminar", - "textPlaceholder": "Escriu aquí" + "system": { + "_": "Missatges del sistema (errors, notificacions, validacions, etc.)", + "loading": "Carregant...", + "error": { + "unknown": "Ha ocurrido un error desconocido", + "payment": { + "missingMethod": "Se debe seleccionar un método de pago", + "missingCartData": "La cesta está vacía", + "unprocessableCartData": "No se puede procesar la cesta, por favor vacíala y vuelve a intentarlo", + "badAPIResponse": "Ha ocurrido un error al comunicarse con la pasarela de pago" + }, + "file": { + "sizeTooLarge": "El fitxer {{ file }} supera la mida màxima ({{ max }})", + "unsupportedType": "Tipus de fitxer no permès: {{ file }}" + } + }, + "constraint": { + "file": { + "maxAllowedSize": "Mida màxima permesa 10MB", + "supportedTypes": "Arxius compatibles: .png, .jpg" + } + }, + "validation": { + "missingRequiredFields": "Un o més camps obligatoris estan buits", + "project": { + "terms": { + "required": "Debes aceptar las condiciones del impulsor y las bases de la convocatoria" + } + }, + "errors": { + "summary": { + "title": "Corrige los siguientes errores:" + } + } + } }, "domain": { "_": "Conceptes empresarials (projecte, recompensa, contribució, etc.)", + "header": { + "categories": "Proyectos" + }, + "search": { + "bar": { + "placeholder": "Buscar..." + } + }, "project": { "campaign": { "minimumReached": "¡Mínim aconseguit!", @@ -37,16 +64,395 @@ }, "reward": { "byAtLeast": "Per {{ amount }}", + "byAtLeastOrMore": "Por {{ amount }} o más", "unitsTotal": { "single": "Hi ha una unitat", "multiple": "Hi ha {{ units }} unitats" }, + "unitsAvailable": "Quedan {{ units }} unidades", + "donators": "{{ donators }} donantes", + "noUnits": "No quedan unidades", "isInfinite": "Sense límit d'unitats" + }, + "status": { + "all": "Todos los estados", + "in_campaign": "En campaña", + "funded": "Financiados", + "archived": "Archivados" + } + }, + "gatewayCheckout": { + "paymentMethod": "Método" + }, + "charges": { + "status": { + "to_charge": "Por cobrar", + "in_charge": "Cobrado", + "to_refund": "Por reembolsar", + "refunded": "Reembolsado", + "to_wallet": "Por reembolsar a monedero", + "walleted": "Reembolsado a monedero" + }, + "refund": { + "to_wallet": "Sí", + "to_gateway": "No" + }, + "entityLabels": { + "projects": "Proyectos", + "users": "Usuarios", + "tipjars": "Botes de propina", + "tipjarId": "ID del bote de propina" + } + }, + "review": { + "risks": { + "low": "Riesgo bajo", + "medium": "Riesgo medio", + "high": "Riesgo alto" + } + }, + "tipping": { + "title": "Deja una propina" + }, + "navigation": { + "search": { + "placeholder": "Busca tu campaña" } } }, + "common": { + "_": "Traduccions de la interfície global (botons, etiquetes, etc.)", + "dateInput": { + "apply": "Aplicar", + "previousMonth": "Mes anterior", + "nextMonth": "Mes siguiente", + "close": "Cerrar" + }, + "greeting": "Hola", + "farewell": "Adéu", + "donate": "Donar", + "donateAmount": "Dona {{ amount }}", + "save": "Desa", + "edit": "Editar", + "delete": "Esborrar", + "continue": "Continuar", + "back": "Volver", + "cancel": "Cancel·lar", + "publish": "Publicar", + "preview": "Previsualitzar", + "select": "Escull una opció", + "remove": "Eliminar", + "textPlaceholder": "Escriu aquí", + "remember": "Recordar", + "search": "Buscar", + "countdown": { + "remaining": "Quedan", + "expired": "Campaña finalizada" + }, + "login": "Iniciar Sesión", + "goHome": "Volver a inicio", + "viewAll": "Ver todo", + "viewMore": "Ver más", + "viewLess": "Ver menos", + "share": "Compartir", + "noData": "No hay datos disponibles", + "tooltip": { + "copied": "¡Copiado!" + }, + "header": { + "search": "Buscar", + "createProject": "Crea tu proyecto", + "goToProfile": "Ir a Perfil" + }, + "footer": { + "funding": { + "title": "Financia:", + "partOf": "Somos parte de:" + }, + "navigation": { + "collaborate": { + "title": "Colabora", + "createProject": "Crea un proyecto", + "matchfunding": "Matchfunding", + "workshops": "Talleres" + }, + "help": { + "title": "Ayuda", + "faqs": "FAQs", + "contact": "Contacto", + "taxCalculator": "Calculadora fiscal" + }, + "aboutGoteo": { + "title": "Sobre Goteo", + "whoWeAre": "¿Quiénes somos?", + "goteoBenefits": "Ventajas de Goteo", + "foundationImpact": "Impacto de la fundación", + "platoniqFoundation": "Fundación Platoniq" + }, + "forUsers": { + "title": "Para usuarias", + "rechargeWallet": "Recarga tu monedero", + "downloadCertificate": "Descarga certificado", + "accessComplementaryFunds": "Accede a fondos complementarios", + "designFundingProgram": "Diseña un programa de financiación" + } + }, + "legal": { + "termsOfUse": "Condiciones de uso", + "privacyPolicy": "Política de privacidad", + "licenses": "Licencias", + "stats": "Stats" + }, + "platoniq": { + "foundation": "Fundación" + } + }, + "saved": "Guardado" + }, "pages": { + "403": { + "title": "No tienes permisos suficientes para continuar", + "subtitle": "Por favor, vuelve por donde has venido.", + "meta": { + "title": "Acceso prohibido", + "description": "Sin permisos suficientes." + } + }, + "404": { + "title": "No queda ni gota de la página que estás buscando", + "subtitle": "Por favor, vuelve al inicio.", + "meta": { + "title": "Página no encontrada", + "description": "La página que buscas no existe o ha sido movida." + } + }, + "500": { + "title": "Algo ha salido terriblemente mal", + "subtitle": "Por favor, espera unos segundos antes de volver a intentarlo.", + "meta": { + "title": "Condición inesperada para la solicitud", + "description": "Error interno del servidor." + } + }, "_": "Contingut especifíc de la pàgina (/home, /search, /project/[id], etc.)", + "home": { + "banner": { + "title": "¡Renovamos Goteo!", + "description": "Goteo ha construido una historia única en el mundo del micromecenazgo. ¿Quieres seguir formando parte de ella?", + "ctaText": "Súmate a nuestra historia", + "closeAriaLabel": "Cerrar banner" + }, + "campaigns": { + "title": "Campañas más recientes", + "tabsTitle": "Campañas en Goteo", + "tabs": { + "endingSoon": "Acaban pronto", + "mostSuccessful": "Más exitosas", + "withMatchfunding": "Con Matchfunding" + }, + "viewAll": "Ver todas", + "obtained": "Obtenido", + "minimum": "Mínimo", + "optimum": "Óptimo", + "daysRemaining": "Quedan {{days}} días", + "matchfunding": "Matchfunding", + "status": { + "minimumReached": "¡Mínimo conseguido!", + "goForMinimum": "¡A por el mínimo!" + }, + "userDonations": "Tus donaciones:" + }, + "blog": { + "title": "Se avecinan grandes cambios… ¡no te pierdas nada!", + "excerpt": "Un proceso que implica una importante migración técnica de la plataforma actual, y para hacerlo con todas las garantías de seguridad y sin pérdida de información, necesitamos llevar a cabo una transición ordenada, con fechas clave para la publicación de campañas y la realización de donaciones…", + "readMore": "Seguir leyendo" + }, + "matchfunding": { + "title": "Matchfunding", + "description": "El efecto match sólo lo verás en Goteo. Desde el 2013 buscamos organizaciones que quieran multiplicar el impacto del crowdfunding. Por cada euro que una persona aporta a una campaña de Goteo, una institución colaboradora (fundación, administración pública, empresa…) aporta la misma cantidad, o incluso más.", + "actions": { + "learnMore": "Quiero saber más", + "viewCalls": "Ver convocatorias activas" + }, + "impact": { + "title": "Datos de impacto", + "description": "El matchfunding no solo multiplica recursos económicos, sino que genera un impacto social tangible. En esencia, el matchfunding transforma microdonaciones en impacto macro, validando proyectos con apoyo ciudadano y optimizando recursos públicos/privados.", + "matchers": "Aportado en Matchfunding:", + "successfulProjects": "Proyectos beneficiados:", + "successRate": "Porcentaje de éxito:" + }, + "cards": { + "grant": "Dotación convocatoria", + "inscription": "Fecha de inscripción", + "participants": "Proyectos participantes" + } + }, + "stats": { + "title": "Estadísticas de Goteo", + "theoryToAction": { + "title": "De la teoría a la acción", + "subtitle": "Las cifras que revelan nuestra huella real. Datos a fecha de diciembre de 2024.", + "projectsReceived": { + "value": "10.281", + "label": "Proyectos recibidos" + }, + "projectsPublished": { + "value": "2.813", + "label": "Proyectos publicados" + }, + "successRate": { + "value": "83%", + "label": "Tasa de éxito" + }, + "button": "Ver Stats de Goteo" + }, + "whyGoteo": { + "title": "¿Por qué Goteo?", + "subtitle": "Tenemos valores y los datos nos avalan. Datos a fecha de diciembre de 2024.", + "community": { + "value": "384.341", + "label": "Comunidad" + }, + "averageDonors": { + "value": "152", + "label": "Media de donantes por proyecto" + }, + "totalRaised": { + "value": "23M €", + "label": "Recaudación total" + }, + "button": "Quiero saber más" + } + }, + "stories": { + "title": "Goteo estrena nueva web!", + "subtitle": "Encantados de compartirlo contigo", + "description": "¡Goteo tiene una nueva cara! Navega por nuestra web renovada y descubre una experiencia de crowdfunding social más intuitiva y atractiva. Explora proyectos innovadores, conoce a los creadores detrás de ellos y únete a nuestra comunidad.\n¡Conviértete en donante y haz realidad tus ideas!", + "button": "Conviértete en donante" + }, + "featuredPromoters": { + "title": "Impulsores destacados", + "viewAll": "Ver todes", + "projectsPromoted": "Proyectos impulados", + "capitalRaised": "Capital recaudado", + "promoterImage": "Foto del impulsor", + "shareProfile": "Compartir perfil de {{name}}" + }, + "resources": { + "title": "Recursos: Combustible para Proyectos Creativos", + "viewAll": "Ver más recursos", + "readMore": "Leer más", + "articles": { + "firstCampaign": { + "title": "¿Cómo hago mi primera campaña?", + "description": "Tu proyecto merece despegar: Domina el crowdfunding social con nuestra CAJA DE HERRAMIENTAS GRATUITA. Guía práctica + plantillas descargables para conquistar a tu audiencia." + }, + "testimonials": { + "title": "Testimonios de nuestros impulsores", + "description": "¿Quién puede contarte mejor como es impulsar campañas exitosas? Nuestros impulsores nos cuentan de primera mano como es gestionar una campaña con Goteo." + } + } + } + }, + "index": { + "title": "Goteo" + }, + "checkout": { + "metadata": { + "title": "Checkout", + "description": "Página de checkout" + }, + "subtitle": "Te mostramos las que has seleccionado, pulsa el botón \"continuar\" para pagar.", + "reward": { + "claimed": "{{ units }} personas ya han reclamado esta recompensa", + "unclaimed": "Aún nadie ha reclamado esta recompensa. ¡Hazlo tú primero!" + }, + "tipjar": { + "community": "Quiero apoyar a la Fundación Platoniq", + "checkboxLabel": "Impulsar una sociedad más justa y solidaria", + "input": "Ingrese monto libre", + "error": { + "invalidAmount": "La cantidad mínima es de 1€" + } + }, + "changeAddressLabel": "Enviar esta recompensa a una dirección distinta a la mia.", + "title": "Caja", + "summary": { + "toRecipient": "{{ amount }} a {{ name }}", + "total": { + "title": "Total que vas a donar" + }, + "deduct": "Puedes desgravarte hasta 100€ en tu declaración de la renta si realizas esta donación.", + "showDetails": "Ver detalle", + "hideDetails": "Ocultar desglose", + "showMore": "Ver más", + "hide": "Ocultar" + }, + "steps": { + "reward": { + "title": "Recompensa" + }, + "data": { + "title": "Tus datos" + }, + "payment": { + "title": "Método de pago" + }, + "confirmation": { + "title": "Confirmar" + } + }, + "payment": { + "title": "Indícanos tus datos de pago", + "description": "Ahora necesitamos que nos digas cómo quieres pagar.", + "selectMethod": "Selecciona método de pago:" + }, + "wallet": { + "title": "Tu monedero virtual", + "description": "Bien, vas a pagar con el saldo acumulado en tu monedero. Recuerda que si tienes saldo y en dos años no lo gastas, se transferirá a la fundación.", + "balanceLabel": "Saldo: ", + "currentBalance": "Saldo actual", + "amountToUse": "Cuánto vas a usar", + "remainingBalance": "Saldo que te quedará" + }, + "verify": { + "approved": { + "title": "Tu medio de pago se ha procesado con un éxito apabullante", + "description": "¡Bien! Estás a un paso de terminar y de hacer el mundo un poco mejor.", + "formGoal": { + "title": "Si la campaña no logra su objetivo", + "options": { + "1": "Ingresar en mi monedero de Goteo para poder destinarlo a otros proyectos. Saber más", + "2": "Devolverlo a mi medio de pago" + } + }, + "formReview": { + "title": "Deja un mensaje en los proyectos a los que has apoyado", + "description": "Tu donación se verá reflejada en los proyectos que has apoyado. Puedes dejar un mensaje o permanecer en el anonimato. " + } + }, + "error": { + "title": "Algo ha fallado... Vuelve a intentarlo para poder donar.", + "description": "Vaya, quizá sea error nuestro, del medio de pago o simplemente un malvado duende informático. Vamos a probar de nuevo.", + "selectMethod": "Selecciona de nuevo un método de pago:" + } + }, + "postPayment": { + "title": "¡Enhorabuena! Gracias por tu donación", + "emailSent": "Te hemos enviado un resumen a:", + "certificate": { + "title": "¿Cómo solicito mi certificado?", + "description": "El certificado se genera de forma automática al final del año fiscal (diciembre). Te mandaremos un correo una vez hayamos presentado la documentación a Hacienda. En caso de que encontremos algún problema te contactaremos por correo." + }, + "donatedProjects": { + "title": "Has donado a los siguientes proyectos" + }, + "suggestedProjects": { + "title": "Otros proyectos que requieren de tu ayuda", + "empty": "No hay proyectos disponibles en este momento" + } + } + }, "project": { "edit": { "title": "Editant projecte: {{ project }}", @@ -74,7 +480,19 @@ "option1": "1 Ronda", "option2": "2 Rondes" }, - "continue": "Continuar amb informació de campanya" + "continue": "Continuar amb informació de campanya", + "geography": { + "scopeLabel": "Ámbito de actuación", + "scopePlaceholder": "Selecciona el ámbito", + "options": { + "local": "Local", + "estatal": "Estatal", + "internacional": "Internacional" + }, + "localitiesLabel": "Localidades", + "localitiesPlaceholder": "Selecciona las localidades", + "localitiesHelper": "Indica las localidades donde se desarrolla el proyecto" + } }, "campaignInfo": { "title": "Informació de campanya", @@ -83,7 +501,18 @@ "title": "Imatge i vídeo de presentació", "description": "Afegeix una imatge o vídeo que representi el teu projecte. Serà el primer que veuran els futurs donants de la teva campanya", "addImage": "Afegir imatge", - "addVideo": "Afegir vídeo" + "addVideo": "Afegir vídeo", + "uploading": "Subiendo…", + "processing": "Procesando…", + "maxImagesReached": "Has alcanzado el máximo de imágenes", + "removeVideo": "Eliminar vídeo", + "videoPlaceholder": "Pega aquí la URL del vídeo", + "validation": { + "image_too_large": "La imagen supera el tamaño máximo permitido", + "invalid_image": "Formato de imagen no válido", + "max_images": "Número máximo de imágenes alcanzado", + "upload_failed": "Error al subir el archivo" + } }, "goals": { "title": "Objectius i propòsit de la teva campanya", @@ -101,7 +530,12 @@ "title": "Amb quin equip comptes?", "description": "Descriu amb quin equip comptes i si heu tingut experiències prèvies realitzant projectes com aquest" }, - "continue": "Continuar amb recompenses" + "continue": "Continuar amb recompenses", + "video": { + "validation": { + "invalid_url": "La URL del vídeo no es válida" + } + } }, "rewards": { "title": "Recompenses", @@ -121,6 +555,11 @@ "description": "Descripció de la recompensa", "moneyAmount": "Preu", "files": "Fes clic o arrossega aquí el teu fitxer per pujar-lo" + }, + "validation": { + "title": "El título no puede estar vacío", + "description": "La descripción no puede estar vacía", + "amount": "El importe debe ser mayor que cero" } }, "deleteModal": { @@ -167,7 +606,17 @@ "add": { "title": "Afegeix una partida", "description": "Pots indicar quin és el mínim i quin és l'òptim que necessites per dur a terme el teu projecte.", - "button": "Afegir partida" + "button": "Afegir partida", + "minimum": { + "title": "Partida mínima", + "description": "Define los gastos básicos e imprescindibles para que tu proyecto despegue.", + "button": "Añadir mínima" + }, + "optimum": { + "title": "Partida óptima", + "description": "Da un salto de calidad con recursos extra que harían tu proyecto aún más completo.", + "button": "Añadir óptima" + } }, "modal": { "title": "Afegir partides del pressupost", @@ -184,6 +633,8 @@ "title": "Borraràs aquesta partida", "description": "Aquesta és una acció permanent i no es pot desfer" }, + "minimumSubtitle": "Partidas imprescindibles para realizar tu proyecto", + "optimumSubtitle": "Partidas adicionales para mejoras y ampliaciones", "validation": { "title": "El títol no pot estar buit", "description": "La descripció no pot estar buida", @@ -191,1090 +642,896 @@ "amountMinimum": "El pressupost mínim ha de ser major que zero", "currency": "L'import ha d'especificar una divisa", "type": "El tipus de partida s'ha d'especificar", - "class": "La classe de partida s'ha d'especificar" + "class": "La classe de partida s'ha d'especificar", + "minimumRequiredFirst": "Debes crear al menos una partida de presupuesto mínimo antes de añadir partidas óptimas" + } + }, + "aboutYou": { + "title": "Más sobre ti", + "subtitle": "Descríbete al impulsor de la campaña.", + "prefill": "Seleccione datos guardados", + "legalEntity": "Forma jurídica", + "legalEntityHelper": "Indica la forma jurídica de quien impulsa esta campaña.", + "individual": "Persona física", + "organization": "Persona jurídica", + "name": "Indica el nombre del impulsor", + "nameHelper": "Dinos cómo denominaremos el impulsor. Esta denominación aparecerá en la parte pública de la campaña.", + "namePlaceholder": "Nombre del impulsor", + "taxId": "Indica el NIF del impulsor", + "taxIdHelper": "Solo lo verá el impulsor. Esta información no será pública, solo se usará para formalizar los acuerdos y condiciones de la campaña.", + "taxIdPlaceholder": "Indica el CIF", + "location": "Lugar de actividad", + "locationHelper": "Indica cuál es el ámbito geográfico de actuación del impulsor o grupo impulsor.", + "territory": "Territorio de actuación", + "country": "País", + "teamDescription": "Descripción del equipo impulsor", + "teamDescriptionHelper": "Cuéntanos a qué se dedica la entidad impulsora, qué tipo de actividades lleva a cabo en el día a día y cuáles son sus objetivos.", + "teamDescriptionPlaceholder": "Describe brevemente la actividad del impulsor", + "privateContacts": "Datos de contacto privados", + "privateContactsHelper": "Completa al menos un dato de contacto. La información no será pública, solo lo podrá usar Goteo para comunicarse sobre usos relacionados con la campaña que estás preparando.", + "email": "Mail", + "phone": "Teléfono", + "addContact": "Añadir contacto", + "preferred": "Preferido", + "publicContacts": "Datos de contacto públicos", + "publicContactsHelper": "Indicaremos qué datos de contacto/públicos quieres que aparezcan en tu campaña y en tu perfil impulsor.", + "placeholders": { + "website": "Inserta tu web", + "email": "Inserta tu cuenta de correo", + "instagram": "Inserta tu cuenta de Instagram", + "facebook": "Inserta tu cuenta de Facebook", + "twitter": "Inserta tu cuenta de X/Twitter", + "linkedin": "Inserta tu cuenta de LinkedIn" + }, + "paymentData": { + "title": "Datos de pago", + "subtitle": "Indica los datos bancarios necesarios para gestionar los pagos de la campaña.", + "iban": "IBAN", + "ibanHelper": "Introduce el IBAN de la cuenta bancaria donde recibirás los fondos recaudados.", + "ibanPlaceholder": "ES00 0000 0000 0000 0000 0000", + "bankCertificate": "Certificado de titularidad de cuenta", + "bankCertificateHelper": "Adjunta el documento que acredita la titularidad de la cuenta bancaria.", + "bankCertificateAriaLabel": "Subir certificado de titularidad", + "ibanInvalid": "El formato del IBAN no es válido" + }, + "continue": "Siguiente paso" + }, + "errors": { + "session": { + "title": "Sesión expirada" + }, + "storage": { + "title": "Error de almacenamiento", + "save_failed": "No se pudieron guardar los cambios", + "quota_exceeded": "Almacenamiento local lleno", + "close": "Cerrar" } } }, + "create": { + "title": "Propón una campaña", + "subtitle": "Describe brevemete el proyecto y la campaña de crowdfunding.", + "budgetPreview": "Dinero necesario", + "description": { + "title": "Describe brevemente tu propuesta", + "titlePrompt": "Indica el nombre de tu campaña", + "titlePlaceholder": "Nombre de campaña", + "subtitle": "Indica el nombre de tu campaña y describe brevemente el proyecto que realizarás si consigues la financiación.", + "subtitlePrompt": "Indica brevemente la descripción de tu proyecto.", + "subtitlePlaceholder": "Sin descripción" + }, + "categories": { + "title": "¿Qué categoría se ajusta a tu campaña?", + "subtitle": "(Puedes elegir un máximo de 2)" + }, + "budget": { + "title": "¿Cuánto dinero necesitas?", + "subtitle": "Selecciona la cantidad que necesites para llevar a cabo tu proyecto." + }, + "release": { + "title": "Fecha salida de campaña", + "subtitle": "(Ten en cuenta que las campañas de crowdfunding tienen una duración mínima de 40 días)" + }, + "terms": { + "label": "Acepto las condiciones del impulsor y las bases de la convocatoria" + }, + "submit": "Proponer campaña", + "submitting": "Enviando…", + "submitSuccess": "¡Campaña propuesta correctamente!" + }, "view": { - "owner": "Campanya de crowdfunding impulsada per {{ owner }}" - } - } - }, - "system": { - "_": "Missatges del sistema (errors, notificacions, validacions, etc.)", - "loading": "Carregant...", - "notImplemented": "Aquesta funcionalitat encara no està implementada. Estem treballant-hi, gràcies per la teva paciència!", - "error": { - "file": { - "sizeTooLarge": "El fitxer {{ file }} supera la mida màxima ({{ max }})", - "unsupportedType": "Tipus de fitxer no permès: {{ file }}" + "owner": "Campanya de crowdfunding impulsada per {{ owner }}", + "tabs": { + "project": "Información de campaña", + "rewards": "Recompensas", + "budget": { + "title": "Presupuesto", + "headline": "Conoce las necesidades clave de esta campaña y descubre cómo tu apoyo puede hacer realidad este proyecto.", + "minimum": "Mínimo", + "optimal": "Óptimo", + "raised": "Obtenido", + "total": "Total", + "chartTitle": "Este es el presupuesto para esta campaña. Descubre cómo tu apoyo puede llevarnos al éxito.", + "chart": { + "secondRound": "2ª Ronda", + "start": "Inicio" + } + }, + "updates": { + "title": "Actualizaciones", + "modalTitle": "Nueva actualización", + "by": "Por:", + "content": { + "title": "No pierdas detalle de los avances de esta campaña", + "empty": "No existen más actualizaciones previstas.", + "btn": { + "readMore": "Seguir leyendo", + "share": "Compartir" + } + } + }, + "community": { + "title": "Comunidad", + "content": { + "title": "Donaciones y mensajes de apoyo de la comunidad" + }, + "contribution": "Aporte", + "matchfunding": { + "contribution": "Suma de los aportes", + "aported": "Aportados", + "upTo": "de hasta" + }, + "role": { + "sponsor": "Patrocinador/a", + "matchfunding": "Matchfunding" + }, + "anonymous": { + "length": "aportes", + "title": "Donaciones anónimas", + "description": "No todas las personas heróicas llevan capa ni tampoco sienten la necesidad de mostrar su identidad." + }, + "loadMore": "Ver más" + } + }, + "rewards": { + "trending": "Recompensas más populares", + "showAll": "Ver todas", + "title": "Selecciona tus recompensas", + "headline": "Selecciona tu recompensa", + "thanksMessage": "Gracias por cooperar", + "ivaMessage": "¡Recuerda que cada aportación tiene una desgravación fiscal entre el 35 y 80%! Calcula aquí: {{link}}", + "addToCart": "Añadir al carro y continuar", + "unavailable": "No hay recompensas disponibles.", + "loading": "Cargando recompensas...", + "donationFree": { + "title": "Renuncio a una recompensa individual, solo quiero ayudar al proyecto", + "description": "En este espacio puedes donar la cantidad que consideres sin obtener recompensa a cambio. Cada aportación, grande o pequeña, marca la diferencia y nos acerca a la meta.", + "placeholder": "Seleccione el aporte Ej.20€" + }, + "error": { + "amount": "Por favor, ingresa una monto válido", + "invalidAmount": "Por favor, ingresa una cantidad válida mayor o igual al monto de la recompensa" + } + }, + "banner": { + "donator": { + "description": "Goteo pertenece a la Fundación Platoniq. Al colaborar en una campaña tu dinero será custodiado por Platoniq y donado a {{owner}} en caso de llegar a los objetivos de éxito marcados.", + "learnMore": "Saber más." + }, + "certificate": { + "description": "Al colaborar con un proyecto impulsado en Goteo puedes solicitar un certificado de donación. Así, te deduces hasta un 80% de lo que hayas aportado en tu próxima declaración de la renta.", + "learnMore": "Saber más." + } + }, + "langNotAvailable": "El idioma seleccionado \"{{lang}}\" no está disponible para este proyecto. Mostrando contenido en el idioma disponible.", + "outOfCampaign": "Este proyecto está fuera de campaña y ya no admite pagos.", + "share": { + "modal": { + "title": "Difunde este proyecto por el mundo", + "description": "Puedes copiar el enlace para enviarlo y también compartirla en Facebook o X. Incluso copiar un fragmento de código, para insertar en otra web un banner de la campaña. ¡Haz que llegue más lejos!" + }, + "iframe-modal": { + "title": "Así es como se verá en tu plataforma", + "description": "Copia este fragmento de código y pégalo en el código de tu página. Así conseguirás que la campaña llegue a más gente", + "btnCopy": "Copiar código", + "btnCopied": "¡Código copiado!" + } + }, + "video": { + "title": "Vídeo de la campaña", + "poster": "Imagen del vídeo", + "not-found": "Disculpa, esta campaña no tiene vídeo de presentación", + "not-supported": "No ha sido posible cargar el vídeo. Por favor, revisa el enlace o contacta con el administrador de la campaña." + } } }, - "constraint": { - "file": { - "maxAllowedSize": "Mida màxima permesa 10MB", - "supportedTypes": "Arxius compatibles: .png, .jpg" - } - }, - "validation": { - "missingRequiredFields": "Un o més camps obligatoris estan buits" - } - }, - "reward": { - "donors": "donatius", - "units": "Queden {{units}} unitats", - "noUnits": "No queden unitats", - "trending": "Recompenses més populars", - "headline": "Selecciona la teva recompensa", - "addAndContinue": "Afegeix al carro i continua", - "input": "Seleccioneu l'aportació Ej.20€", - "btnFreeDonationLabel": "Donació lliure", - "showAll": "Veure totes" - }, - "budget": { - "headline": "Coneix les necessitats clau d'aquesta campanya i descobreix com el teu suport pot fer realitat aquest projecte.", - "chart": { - "minimum": "Mínim", - "optimal": "Òptim", - "progress": "Estat de la campanya", - "secondRound": "2a Ronda", - "start": "Inici" - } - }, - "project": { - "tabs": { - "project": "Informació de campanya", - "budget": { - "chart-title": "Aquest és el pressupost per a aquesta campanya. Descobreix com el teu suport pot portar-nos a l'èxit.", - "title": "Pressupost", - "minimum": "Mínim", - "optimal": "Òptim", - "raised": "Obtingut", - "total": "Total", - "campaingState": "Estat de la campanya" + "about": { + "title": "Sobre Goteo", + "hero": { + "title": "Conoce nuestra manera de hacer crowdfunding", + "description": "En Goteo creemos que la financiación colectiva es más que dinero: es confianza, comunidad y compromiso social. Nuestro modelo de crowdfunding se basa en el impulso de proyectos que generan impacto positivo y en el acompañamiento cercano a las personas y organizaciones que los hacen posibles.", + "cta": "Impulsa tu proyecto" }, - "rewards": "Recompenses", - "updates": { - "title": "Actualitzacions", - "modal-title": "Nova actualització", - "by": "Per:", - "content": { - "title": "No perdis detall dels avenços d'aquesta campanya", - "empty": "No hi ha més actualitzacions previstes.", - "btn": { - "read-more": "Seguir llegint", - "share": "Compartir" + "whatIs": { + "title": "Qué es crowdfunding", + "subtitle": "Un impacto social de verdad.", + "description": "El crowdfunding es una forma de financiación que une a muchas personas que creen en una idea y deciden apoyarla con pequeñas (o grandes) aportaciones. En Goteo lo entendemos como un espacio donde no solo se busca recaudar fondos, sino también crear redes de apoyo, visibilidad y aprendizaje." + }, + "advisory": { + "title": "Asesoría y acompañamiento de nuestro equipo", + "description": "En Goteo no estás sola. Nuestro equipo te acompaña paso a paso, desde la definición de tu proyecto hasta la gestión de la campaña. Ofrecemos asesoría gratuita que te ayudará a preparar tu proyecto. Si necesitas más ayuda puedes solicitar una asesoría personalizada premium, con materiales de apoyo y una metodología probada para aumentar tus posibilidades de éxito." + }, + "phases": { + "title": "Entiende las fases y mecánica", + "items": { + "preparation": { + "title": "Preparación", + "description": "Define objetivos, presupuesto, duración y plan de comunicación. Tu proyecto podrá publicarse si cumple estos criterios." + }, + "launch": { + "title": "Lanzamiento", + "description": "Activamos la recaudación y te apoyamos en la difusión." + }, + "fundsReceived": { + "title": "Fondos recibidos", + "description": "Al alcanzar el objetivo, completa un formulario y envía documentación para la transferencia. Asegura la transparencia y explica a la comunidad el proyecto." + }, + "followUp": { + "title": "Seguimiento", + "description": "Tras la campaña, comparte el avance del proyecto con tu comunidad de donantes y cierra el ciclo con transparencia." } } }, - "community": { - "title": "Comunitat", - "owner-anonymous": "Anònim", - "content": { - "title": "Donacions i missatges de suport de la comunitat" - }, - "contribution": "Aportació", - "matchfunding": { - "contribution": "Suma de les aportacions", - "aported": "Aportats", - "up_to": "fins a" - }, - "anonymous": { - "length": "anònims", - "title": "Donacions anònimes", - "description": "No totes les persones heroiques porten capa ni tampoc senten la necessitat de mostrar la seva identitat." + "events": { + "title": "Apúntate a nuestras formaciones y eventos", + "description": "Periódicamente organizamos talleres, webinars y encuentros donde compartimos aprendizajes, tendencias y experiencias reales de crowdfunding. Son espacios para formarte, inspirarte y conectar con otras personas que, como tú, quieren transformar ideas en realidades. Suscríbete para recibir información actualizada desde nuestro boletín." + }, + "advantages": { + "title": "Ventajas de Goteo", + "items": { + "taxDeductions": { + "title": "Desgravaciones fiscales", + "description": "Las donaciones en Goteo tienen ventajas fiscales tanto para particulares como para empresas, gracias a que gestionamos las aportaciones a través de una fundación. Esto es algo que la mayoría de plataformas no ofrecen." + }, + "allOrNothing": { + "title": "Todo o nada", + "description": "Los proyectos reciben el apoyo económico únicamente si demuestran ser viables y alcanzan el presupuesto mínimo. Si no, las donaciones regresan a los donantes para seguir apoyando nuevas iniciativas." + }, + "twoRounds": { + "title": "Dos rondas de cofinanciación en 40+40 días", + "description": "Por defecto se establecen dos rondas de cofinanciación, de 40 días cada una. La primera, a \"todo o nada\", ligada al presupuesto mínimo imprescindible; y la segunda a un importe óptimo, para realizar mejoras adicionales en la que todo entra." + }, + "personalizedSupport": { + "title": "Acompañamiento personalizado", + "description": "No dejamos a los impulsores solos. Nuestro equipo asesora antes, durante y después de la campaña, con recomendaciones estratégicas y apoyo técnico. Otras plataformas se limitan a ser un escaparate de proyectos." + }, + "loyalCommunity": { + "title": "Comunidad fidelizada", + "description": "Contamos con una base de donantes recurrentes que confían en Goteo y vuelven a apoyar campañas porque valoran el impacto social y cultural. Esto da a los proyectos más oportunidades de alcanzar sus metas." + }, + "matchfunding": { + "title": "Cofinanciación institucional (matchfunding)", + "description": "A través de alianzas con instituciones, multiplicamos las aportaciones ciudadanas con fondos públicos o privados. Este modelo híbrido es único en Goteo y no existe en la mayoría de plataformas." + }, + "socialImpact": { + "title": "Foco en impacto social transformador", + "description": "No todos los proyectos son aceptados. En Goteo priorizamos aquellos alineados con la Economía Social y los valores democráticos, ya sea en forma de conocimiento abierto, mejoras comunitarias o impacto ambiental y social." + }, + "transparency": { + "title": "Transparencia y confianza", + "description": "Las campañas pasan por un proceso de validación y asesoría. La comunidad sabe que los proyectos publicados cumplen unos mínimos de calidad y coherencia, lo que aumenta la credibilidad." + }, + "provenMethodology": { + "title": "Metodología probada", + "description": "Más de una década de experiencia, con cientos de proyectos financiados, nos permite ofrecer una metodología contrastada que incrementa las posibilidades de éxito." + } } } }, - "actions": { - "share": "Compartir", - "remember": "Recordar", - "viewAll": "Veure totes", - "viewMore": "Veure més", - "viewLess": "Veure menys" - }, - "impact": { - "description": "Quan col·labores amb un projecte impulsat a Goteo pots sol·licitar un certificat de donació. Així, et dedueixes fins a un 80% del que hagis aportat a la teva pròxima declaració de la renda.", - "learnMore": "Saber més." - }, - "banner": { - "donator": { - "description": "Goteo pertany a la Fundació Platoniq. Quan col·labores en una campanya els teus diners seran custodiats per Platoniq i donats a {{owner}} en cas que s'assoleixin els objectius d'èxit marcats.", - "learnMore": "Saber més." + "review": { + "btns": { + "seeChat": "Ver conversación", + "changeRisk": "Cambiar riesgo" }, - "certificate": { - "description": "Quan col·labores amb un projecte impulsat a Goteo pots sol·licitar un certificat de donació. Així, et dedueixes fins a un 80% del que hagis aportat a la teva pròxima declaració de la renda.", - "learnMore": "Saber més." + "chat": { + "placeholder": "Escribe un mensaje…" } }, - "share": { - "modal": { - "title": "Difon aquest projecte al món", - "description": "Pots copiar l'enllaç per enviar-lo i també compartir-lo a Facebook o X. Fins i tot copiar un fragment de codi, per inserir un bàner de la campanya en una altra web. Fes que arribi més lluny!" - }, - "iframe-modal": { - "title": "Així és com es veurà a la teva plataforma", - "description": "Copia aquest fragment de codi i enganxa'l al codi de la teva pàgina. Així aconseguiràs que la campanya arribi a més gent", - "btnCopy": "Copiar codi", - "btnCopied": "Codi copiat!" - } - }, - "video": { - "title": "Vídeo de la campanya", - "description": "Aquí pots veure el vídeo de presentació de la campanya. Dona-li al play!", - "poster": "Imatge del vídeo", - "not-found": "Disculpa, aquesta campanya no té vídeo de presentació", - "not-supported": "No ha estat possible carregar el vídeo. Si us plau, revisa l'enllaç o contacta amb l'administrador de la campanya." - } - }, - "navigation": { "search": { - "placeholder": "Busca la teva campanya" - }, - "categories": "Categories" - }, - "search": { - "title": "Buscar campanyes - Goteo", - "description": "Explora el nostre catàleg de campanyes i troba la que més t'inspiri per fer la teva donació", - "heading": "Troba la campanya que busques", - "subtitle": "Explora el nostre catàleg de campanyes i tria la que més t'inspiri per fer la teva donació", - "topResults": "Campanyes més buscades", - "showing_top_results": "Mostrant les {{count}} campanyes més populars", - "placeholder": "Cerca", - "label": "Cercar campanyes", - "clearSearch": "Netejar cerca", - "searchButton": "Cercar", - "closeFilters": "Tancar filtres", - "showFilters": "Mostrar filtres", - "hideFilters": "Ocultar filtres", - "applyFilters": "Aplicar filtres", - "categoryLabel": "Tria una categoria", - "selectCategory": "Seleccionar categoria", - "deselectCategory": "Deseleccionar categoria", - "selectedCategories": "{{count}} categories seleccionades", - "clearFilters": "Netejar filtres", - "loading": "Cercant campanyes...", - "results": { - "count": "{{count}} resultats trobats", - "found": "{{count}} resultats trobats", - "noResults": "No s'han trobat resultats", - "regionLabel": "Resultats de cerca", - "showing": "Mostrant {{count}} resultats", + "title": "Buscar campañas - Goteo", + "description": "Explora nuestro catálogo de campañas y encuentra la que más te inspire para hacer tu donación", + "heading": "Encuentra la campaña que buscas", + "subtitle": "Explora nuestro catálogo de campañas y elige la que más te inspire para hacer tu donación", + "topResults": "Campañas más buscadas", + "showingTopResults": "Mostrando las {{count}} campañas más populares", + "loading": "Buscando campañas...", + "input": { + "placeholder": "Buscar", + "label": "Buscar campañas", + "clear": "Limpiar búsqueda", + "button": "Buscar" + }, + "filters": { + "show": "Mostrar filtros", + "close": "Cerrar filtros", + "apply": "Aplicar filtros", + "categoryLabel": "Elige una categoría", + "territoryLabel": "Territorio", + "selectCategory": "Seleccionar categoría", + "deselectCategory": "Deseleccionar categoría", + "selectedCategories": "{{count}} categorías seleccionadas", + "status": { + "label": "Estado de la campaña" + }, + "territory": { + "label": "Ámbito de actuación" + } + }, "empty": { - "title": "No s'han trobat resultats", - "description": "No hi ha campanyes que coincideixin amb la teva cerca actual.", - "clearFilters": "Netejar filtres" + "title": "No se encontraron campañas", + "description": "No hay campañas que coincidan con tu búsqueda actual. Verifica la ortografía o prueba con palabras clave diferentes o términos más generales.", + "clearFilters": "Limpiar filtros" + }, + "initial": { + "title": "Busca tu campaña ideal", + "description": "Utiliza los filtros de arriba para encontrar campañas que te inspiren." + }, + "results": { + "count": "{{count}} resultados encontrados", + "found": "{{count}} resultados encontrados", + "regionLabel": "Resultados de búsqueda", + "showing": "Mostrando {{count}} resultados", + "empty": { + "title": "No se encontraron resultados", + "description": "No hay campañas que coincidan con tu búsqueda actual.", + "clearFilters": "Limpiar filtros" + } + }, + "error": { + "title": "Error al buscar", + "retry": "Intentar de nuevo", + "network": { + "title": "Sin conexión a internet", + "description": "No se puede conectar con el servidor. Verifica tu conexión a internet e inténtalo de nuevo." + }, + "timeout": { + "title": "La búsqueda está tardando demasiado", + "description": "El servidor no responde. Inténtalo de nuevo o usa los datos locales." + }, + "unknown": { + "title": "Error inesperado", + "description": "Ha ocurrido un error inesperado. Inténtalo de nuevo." + }, + "actions": { + "retry": "Reintentar", + "showHelp": "Mostrar ayuda" + } + }, + "errors": { + "searchFailed": "No se pudo realizar la búsqueda. Intenta de nuevo.", + "api": { + "networkError": "Error de conexión. Verifica tu conexión a internet.", + "serverError": "Error del servidor. Por favor, inténtalo de nuevo más tarde." + } + }, + "accessibility": { + "resultsFound": "Se encontraron {{count}} campañas" + }, + "loadMore": { + "button": "Cargar más", + "loading": "Cargando más resultados...", + "announcement": "Se cargaron {{count}} resultados más. Mostrando {{loaded}} resultados en total." + }, + "pagination": { + "page": "Página {{page}}", + "currentPage": "Página {{page}}", + "previous": "Anterior", + "next": "Siguiente", + "loading": "Cargando..." } }, - "initial": { - "title": "Cerca la teva campanya ideal", - "description": "Utilitza els filtres de dalt per trobar campanyes que t'inspirin." - }, - "filters": { - "clear": "Netejar filtres" - }, - "errors": { - "searchFailed": "No s'ha pogut realitzar la cerca. Torna a intentar-ho.", - "api": { - "invalidToken": "Autenticació fallida. Si us plau, verifica les teves credencials.", - "expiredToken": "La teva sessió ha expirat. Si us plau, inicia sessió de nou.", - "missingToken": "Es requereix autenticació. Si us plau, inicia sessió.", - "networkError": "Error de connexió. Verifica la teva connexió a internet.", - "serverError": "Error del servidor. Si us plau, prova-ho de nou més tard." + "register": { + "title": "Registro", + "page": { + "title": "Indícanos tus datos personales", + "description": "Necesitamos saber quién eres antes de pagar.", + "registerOr": "Regístrate o", + "loginBtnLabel": "Entra con tu cuenta" + }, + "form": { + "userType": { + "individual": "Persona física", + "organization": "Persona jurídica (empresas)" + }, + "btnSubmit": "Registrarse", + "termsCheckbox": "Acepto las condiciones legales", + "policiesCheckbox": "Al registrarme confirmo mi aceptación de vuestros términos de uso, política de privacidad y política de cookies.", + "validation": { + "password": { + "minLength": "*Requisitos de la contraseña: debe tener 8 caracteres" + } + } + }, + "individual": { + "firstName": "Nombre", + "lastName": "Apellidos", + "email": "Correo electrónico", + "password": "Contraseña", + "taxIdCheckbox": "Deseo desgravar mis donaciones. Es necesario indicar el número de DNI para comunicárselo a Hacienda.", + "taxId": { + "label": "DNI*", + "msgDeduction": "Deseo desgravar mis donaciones. Es necesario indicar el número de DNI para comunicárselo a Hacienda." + } + }, + "organization": { + "legalName": "Razón social", + "email": "Correo electrónico", + "password": "Contraseña", + "taxId": "CIF*", + "taxIdDescription": "*Con el CIF avisaremos a la agencia tributaria para la tu declaración de la renta.", + "representativeTitle": "Datos del representante de la empresa.", + "firstName": "Nombre", + "lastName": "Apellidos" + }, + "error": { + "incompletePersonFields": "Por favor, completa todos los campos obligatorios para persona física.", + "incompleteOrgFields": "Por favor, completa todos los campos obligatorios para persona jurídica.", + "unexpectedRegistration": "Error al registrarse. Por favor, inténtalo de nuevo más tarde." } }, - "empty": { - "title": "No s'han trobat campanyes", - "description": "No hi ha campanyes que coincideixin amb la teva cerca actual. Verifica l'ortografia o prova amb paraules clau diferents o termes més generals.", - "suggestions": { - "title": "Suggeriments per millorar la teva cerca:", - "checkSpelling": "Verifica l'ortografia de les teves paraules clau", - "tryDifferentKeywords": "Prova amb paraules clau diferents", - "removeFilters": "Treu alguns filtres per ampliar la cerca", - "broadenSearch": "Usa termes més generals" - } - }, - "accessibility": { - "resultsFound": "S'han trobat {{count}} campanyes" - }, - "loadMore": { - "button": "Carregar més", - "loading": "Carregant més resultats...", - "announcement": "S'han carregat {{count}} resultats més. Mostrant {{loaded}} resultats en total." - }, - "pagination": { - "summary": "Pàgina {{page}} de {{totalPages}} ({{totalCount}} resultats en total)", - "ariaLabel": "Navegació de pàgines de resultats", - "page": "Pàgina {{page}}", - "currentPage": "Pàgina {{page}}", - "previous": "Anterior", - "next": "Següent", - "loading": "Carregant...", - "loadMore": "Carregar més resultats", - "noMoreResults": "No hi ha més resultats" - }, - "error": { - "title": "Error en cercar", - "retry": "Torni a intentar-ho", - "network": { - "title": "Sense connexió a internet", - "description": "No es pot connectar amb el servidor. Verifica la teva connexió a internet i torna a intentar-ho." + "admin": { + "charges": { + "title": "Aportes", + "description": "Aquí tienes las últimas contribuciones a campañas, puedes ordenarlas y filtrarlas como quieras.", + "headers": { + "target": "Destino", + "amount": "Importe", + "origin": "Origen", + "paymentMethod": "Método", + "date": "Fecha", + "chargeStatus": "Estado de la operación", + "refundToWallet": "A Monedero" + }, + "noData": "No hay datos disponibles", + "details": { + "operationTime": "Hora de la operación", + "trackingCodes": { + "title": "Códigos de seguimiento", + "headers": { + "title": "Título", + "trackingCode": "Código de seguimiento" + } + }, + "toWallet": "A monedero", + "platformLinks": { + "title": "Enlace a la plataforma", + "headers": { + "type": "Tipo de Enlace", + "rel": "Relación", + "href": "Url" + } + }, + "estimatedFee": "Comisión estimada", + "concept": "Concepto" + }, + "actions": { + "refundToWallet": "Devolución a monedero", + "refundToUser": "Devolución a usuario", + "changeUser": "Cambiar de usuario", + "manageReward": "Gestionar recompensa" + }, + "filters": { + "order": { + "title": "Ordenar por:", + "options": { + "date-desc": "Fecha (más reciente primero)", + "date-asc": "Fecha (más antigua primero)", + "amount-desc": "Importe (mayor a menor)", + "amount-asc": "Importe (menor a mayor)" + } + }, + "itemsPerPage": { + "title": "Líneas por página:", + "options": { + "10": "10", + "20": "20", + "30": "30", + "50": "50" + } + }, + "search": { + "placeholder": "Buscar...", + "resultsFound": "{{totalItems}} resultados para \"{{query}}\"", + "noResults": "No se han encontrado resultados" + }, + "paymentMethod": { + "title": "Método de pago", + "options": { + "all": "Todos", + "paypal": "Paypal", + "stripe": "Stripe", + "wallet": "Monedero" + } + }, + "rangeAmount": { + "title": "Selecciona importe", + "options": { + "500000": "+ 5.000€", + "all": "Todos", + "000..10000": "0€ - 100€", + "10000..50000": "100€ - 500€", + "50000..100000": "500€ - 1.000€", + "100000..500000": "1.000€ - 5.000€" + } + }, + "dateRange": { + "initDate": "Fecha Inicio", + "endDate": "Fecha Fin", + "errors": { + "invalidRange": "La fecha de inicio no puede ser mayor que la fecha fin." + } + }, + "chargeStatus": { + "title": "Estado de Aporte", + "options": { + "all": "Todos", + "to_charge": "Por cobrar", + "in_charge": "Cobrado", + "to_refund": "Por reembolsar", + "refunded": "Reembolsado", + "to_wallet": "Por reembolsar a monedero", + "walleted": "Reembolsado a monedero" + } + }, + "btns": { + "openFilters": "Filtros", + "closeFilters": "Cerrar filtros", + "apply": "Aplicar filtros" + } + }, + "pagination": { + "showing": "Mostrando {{items}} de {{total}}", + "unloaded": "Mostrando...", + "noResults": "No hay resultados" + }, + "export": { + "csv": "Exportar CSV", + "exporting": "Exportando...", + "error": "Error al exportar el archivo CSV. Por favor, inténtalo de nuevo.", + "filename": "gateway-charges", + "itemsText": "items", + "noData": "No se encontraron datos para exportar" + }, + "downloadButtons": { + "certificates": "Descarga certificados", + "pdf": "Descarga PDF" + }, + "lastEdited": "Última modificación: {{date}} {{time}}", + "annotations": "{{annotations}} Anotaciones", + "lastContributions": "Últimos aportes:", + "totalizers": { + "selected": "Aportes seleccionados:", + "totalCharges": "Total aportes:", + "totalTips": "Total propinas:", + "totalFees": "Comisiones totales:" + } }, - "timeout": { - "title": "La cerca està trigant massa", - "description": "El servidor no respon. Intenti-ho de nou o usa les dades locals." - }, - "server": { - "title": "Error del servidor", - "description": "Hi ha un problema amb el servidor. Si us plau, intenta-ho més tard o usa les dades locals." - }, - "unknown": { - "title": "Error inesperat", - "description": "S'ha produït un error inesperat. Torna a provar-ho." - }, - "actions": { - "retry": "Reintentar", - "showHelp": "Mostrar ajuda" - }, - "technicalDetails": "Detalls tècnics" - } - }, - "filters": { - "selectOption": "Seleccionar opció", - "timePeriod": "Període de temps", - "campaignStatus": "Estat de la campanya", - "location": "Ubicació", - "time": { - "all": "Qualsevol moment", - "week": "Última setmana", - "month": "Últim mes", - "year": "Últim any" - }, - "status": { - "all": "Tots els estats", - "funding": "En finançament", - "successful": "Eixides", - "completed": "Completades" - }, - "locationOptions": { - "all": "Qualsevol ubicació", - "spain": "Espanya", - "europe": "Europa", - "global": "Global" - } - }, - "categories": { - "culture": "Cultura", - "rights": "Drets", - "education": "Educació", - "journalism": "Periodisme independent", - "health": "Salut", - "ecological_transition": "Transició ecològica", - "social_economy": "Economia social", - "rural": "Rural", - "science": "Ciència" - }, - "ShareButton": { - "title": "Difon aquest projecte pel món", - "description": "Pots copiar l'enllaç per enviar-lo i també compartir-la a Facebook o X. Fins i tot copiar un fragment de codi, per inserir en una altra web un banner de la campanya. Fes que arribi més lluny!", - "global": "compartir global", - "facebook": "compartir a facebook", - "twitter": "compartir a X", - "code": "compartir com a codi", - "copied": "Enllaç copiat al porta-retalls!" - }, - "register": { - "page": { - "title": "Indica'ns les teves dades personals", - "description": "Necessitem saber qui ets abans de pagar.", - "registerOr": "Registra't o", - "loginBtnLabel": "Entra amb el teu compte", - "goteoUserInfo": "Si ja ets usuari de Goteo no necessites registrar-te de nou.", - "otherAccessMethods": "També pots accedir a través de:" - }, - "form": { - "userType": { - "individual": "Persona física", - "organization": "Persona jurídica (empreses)" - }, - "btnSubmit": "Registrar-se", - "termsCheckbox": "Accepto les condicions legals", - "policiesCheckbox": "En registrar-me confirmo la meva acceptació dels vostres termes d'ús, política de privacitat i política de cookies.", - "zod": { - "taxId.custom": "El CIF/DNI ha de tenir almenys 8 caràcters", - "legalName.custom": "La raó social és obligatòria per a empreses" - }, - "validation": { - "password": { - "minLength": "*Requisits de la contrasenya: ha de tenir 8 caràcters" + "projects": { + "title": "Proyectos", + "description": "Busca proyectos y gestiónalos de la manera más rápida posible gracias al buscador", + "lastProjects": "Listado de campañas:", + "export": { + "csv": "Exportar .csv" + }, + "totalizers": { + "selected": "Campañas seleccionadas:", + "totalEarned": "Total conseguido:", + "totalPaid": "Total pagado:", + "totalUnpaid": "Falta por pagar:" + }, + "filters": { + "search": { + "placeholder": "Búsqueda", + "btn": "Buscar", + "minLength": "Es necesario meter más de 4 caracteres" + }, + "btns": { + "openFilters": "Filtros", + "closeFilters": "Cerrar filtros", + "apply": "Aplicar filtros" + }, + "campaignStatus": { + "title": "Estado de campaña", + "options": { + "completed": "Completada", + "active": "Activa", + "failed": "Fallida" + } + }, + "status": { + "title": "Estado de proyecto", + "options": { + "in_draft": "Borrador", + "to_campaign_review": "Pendiente revisión campaña", + "in_campaign_review": "En revisión campaña", + "to_campaign": "Pendiente campaña", + "in_campaign": "En campaña", + "campaign_failed": "Campaña fallida", + "to_funding_review": "Pendiente revisión cobro", + "in_funding_review": "En revisión cobro", + "to_funding": "Pendiente cobro", + "in_funding": "En cobro", + "funding_paid": "Cobro pagado" + } + }, + "dateRange": { + "initDate": "Fecha de inicio", + "endDate": "Fecha fin" + }, + "order": { + "title": "Ordenar por", + "options": { + "date-desc": "Fecha (más reciente primero)", + "date-asc": "Fecha (más antiguo primero)", + "title-asc": "Título A–Z", + "title-desc": "Título Z–A" + } + }, + "itemsPerPage": { + "title": "Líneas por página", + "options": { + "10": "10", + "25": "25", + "50": "50" + } + } + }, + "table": { + "headers": { + "name": "Proyecto", + "promoter": "Impulsor", + "contractNumber": "Número de contrato", + "achieved": "Conseguido", + "paid": "Pagado", + "process": "Estado" + }, + "rows": { + "status": { + "in_draft": "Borrador", + "to_campaign_review": "Pendiente revisión campaña", + "in_campaign_review": "En revisión campaña", + "in_campaign_review_request_change": "Revisión campaña: cambios", + "campaign_review_rejected": "Campaña rechazada", + "to_campaign": "Pendiente campaña", + "in_campaign": "En campaña", + "campaign_failed": "Campaña fallida", + "to_funding_review": "Pendiente revisión cobro", + "in_funding_review": "En revisión cobro", + "in_funding_review_request_change": "Revisión cobro: cambios", + "funding_review_rejected": "Cobro rechazado", + "to_funding": "Pendiente cobro", + "in_funding": "En cobro", + "funding_paid": "Cobro pagado" + }, + "details": { + "datePublished": "Publicado", + "dateEnd1": "Fin primera ronda", + "dateEnd2": "Fin segunda ronda", + "minOptim": "Mínimo - Óptimo", + "email": "Email", + "phone": "Teléfono", + "contractExpiry": "Vencimiento del contrato", + "remaining": "Falta por pagar", + "btns": { + "changeStatus": "Cambiar cuentas", + "createContract": "Formulario de contrato", + "promoter": "Impulsor", + "contributions": "Aportes", + "donors": "Donantes de campaña", + "financialReport": "Informe financiación", + "certificates": "Certificados", + "contractPdf": "Contrato PDF", + "annotations": "Anotaciones" + } + }, + "noData": "No hay datos disponibles" + } + }, + "modals": { + "paid": { + "title": "Pagado", + "description": "Esta cantidad refleja el total de lo aportado a la campaña por todos los medios de pago posibles.", + "input": "Pago", + "matchfunding": "Pago matchfunding", + "save": "Guardar" + }, + "annotations": { + "title": "Anotaciones", + "placeholder": "Escribe tu anotación", + "empty": "Sin registro", + "add": "Añadir anotación", + "modify": "Modificar anotación" + } + }, + "informe": { + "title": "Informe de financiación del proyecto", + "subtitle": "Revisa los datos antes de actuar", + "description": "Edite lo necesario, descárguelo en formato PDF o envíe el informe final.", + "fields": { + "invoiceNumber": "Número de factura", + "transferDate": "Fecha de transferencia", + "platformFee": "Comisión de plataforma", + "email": "Correo para envío" + }, + "btns": { + "edit": "Editar datos", + "pdf": "Descargar PDF", + "send": "Enviar informe" + }, + "modal": { + "title": "Edita los datos antes de enviar", + "fields": { + "invoiceNumber": "Número de factura", + "transferDate": "Fecha de nueva transferencia", + "platformFee": "Comisión de plataforma", + "email": "Correo destinatario" + }, + "save": "Guardar" + } + }, + "pagination": { + "showing": "Mostrando {{ items }} de {{ total }}", + "itemsPerPage": "{{ count }} por página" } } }, - "individual": { - "firstName": "Nom", - "lastName": "Cognoms", - "email": "Correu electrònic", - "password": "Contrasenya", - "taxIdCheckbox": "Desitjo desgravar les meves donacions. És necessari indicar el número de DNI per comunicar-ho a Hisenda.", - "taxId": { - "label": "DNI*", - "msgDeduction": "Desitjo desgravar les meves donacions. És necessari indicar el número de DNI per comunicar-ho a Hisenda." - } + "static": { + "title": "Goteo" }, - "organization": { - "legalName": "Raó social", - "email": "Correu electrònic", - "password": "Contrasenya", - "taxId": "CIF*", - "taxIdDescription": "*Amb el CIF avisarem a l'agència tributària per la vostra declaració de la renda.", - "representativeTitle": "Dades del representant de l'empresa.", - "firstName": "Nom", - "lastName": "Cognoms" + "profile": { + "share": "Compartir", + "shareModal": { + "title": "Comparte este perfil" + }, + "subscribe": "Subscríbete", + "tabs": { + "about": "Sobre el perfil", + "projects": "Proyectos impulsados", + "donorType": "Tipo de donante" + }, + "noProjects": "No hay proyectos impulsados aún.", + "noBio": "Todavía no hay información sobre este perfil.", + "donorType": { + "fan": { + "title": "Fan", + "description": "Este donante ha invertido más de 25€", + "projectsDonated": "Proyectos donados", + "moneyDonated": "Dinero donado" + }, + "nextLevel": { + "title": "Próximo nivel", + "description": "Sigue donando para subir de nivel" + } + }, + "title": "Goteo", + "notFound": "Usuario no encontrado" }, - "error": { - "incompletePersonFields ": "Si us plau, completi tots els camps obligatoris per a persona física.", - "incompleteOrgFields ": "Si us plau, completi tots els camps obligatoris per a persona jurídica.", - "unexpectedRegistration": "Error al registrar-se. Si us plau, intenti-ho de nou més tard." - } - }, - "login": { - "page": { - "title": "Indica'ns les teves dades personals", - "description": "Necessitem saber qui ets abans de pagar.", - "registerOr": "Entra al teu compte o", - "registerBtnLabel": "Registra't", - "otherAccessMethods": "També pots accedir a través de:", - "forgotPassword": "Has oblidat la teva contrasenya?", - "welcome": { - "title": "Benvingut/da al nou Goteo.", - "subtitle": "Estem estrenant una nova etapa i volem compartir-la amb tu des del començament. La plataforma creixerà poc a poc: activarem seccions i funcionalitats a mesura que estiguin llestes i validades amb la nostra comunitat. De moment pots accedir amb el mateix correu i contrasenya i explorar l'arxiu de projectes. Gràcies per acompanyar-nos en aquest camí." - } - }, - "form": { - "email": "Correu electrònic o nom d'usuari", - "password": "Contrasenya", - "btnSubmit": "Iniciar sessió" - }, - "error": { - "invalidCredentials": "Correu electrònic o contrasenya invàlids.", - "unexpectedLogin": "Error en iniciar sessió. Si us plau, intenta-ho de nou més tard.", - "invalidEmail": "El correu electrònic no és vàlid.", - "maxCharacters": "El correu electrònic no pot excedir els 255 caràcters." - } - }, - "logout": { - "label": "Tancar sessió", - "error": { - "cookieNotFound": "No s'ha trobat la cookie de sessió.", - "invalidToken": "La sessió no és vàlida. Si us plau, torna a iniciar sessió.", - "logoutSuccess": "Has tancat sessió correctament.", - "unexpectedLogout": "Error en tancar sessió. Si us plau, intenta-ho de nou més tard." - } - }, - "zod": { - "required": "Aquest camp és obligatori", - "invalid_type": "S'esperava {expected} però s'ha rebut {received}", - "string": { - "too_small": "Ha de tenir almenys {min} caràcter(s)", - "too_big": "No ha de excedir els {max} caràcters", - "invalid": "Format de text no vàlid" - }, - "number": { - "too_small": "Ha de ser almenys {min}", - "too_big": "No ha de excedir {max}" - }, - "too_small": "Ha de ser almenys {min}", - "too_big": "No ha de excedir {max}", - "email": { - "invalid": "Correu electrònic no vàlid" - }, - "url": { - "invalid": "Format de URL no vàlid" - }, - "enum": { - "invalid": "Opció no vàlida. Les opcions vàlides són: {options}" - }, - "invalid": "Entrada no vàlida" - }, - "countdown": { - "expired": "Campanya finalitzada", - "remaining": "Queden", - "error": { - "not-found": "No s'ha trobat la data de finalització de la campanya." - } - }, - "rewards": { - "title": "Selecciona les teves recompenses", - "unavailable": "No hi ha recompenses disponibles.", - "donators": "{{donators}} donants", - "units-available": "Queden {{units}} unitats", - "donation-free": { - "title": "Renuncio a una recompensa individual, només vull ajudar el projecte", - "description": "En aquest espai pots donar la quantitat que consideris sense obtenir recompensa a canvi. Cada aportació, gran o petita, fa la diferència i ens acosta a la meta.", - "placeholder": "Seleccioneu el aporti Ex.20€" - }, - "by-amount-or-more": "Per {{amount}} o més", - "thanks-message": "Gràcies per cooperar", - "reward-iva-message": "Recorda que cada aportació té una desgravació fiscal entre el 35 i 80%! Calcula aquí: {{link}}", - "reward-donate-close": "Afegir al carro i continuar", - "error-invalid-amount": "Si us plau, ingreseu una quantitat vàlida major o igual a l'import de la recompensa", - "error-amount": "Si us plau, ingreseu un import vàlid", - "loading": "Carregant recompenses..." - }, - "updates": { - "heading": "No perdis detall dels avenços d'aquesta campanya", - "empty": "No hi ha més actualitzacions previstes." - }, - "updateCard": { - "readMore": "Seguir llegint" - }, - "metadata": { - "title": "Inici", - "description": "Pàgina d'inici" - }, - "hero": { - "title": "Finança el canvi que el món necessita", - "description": "Comencem una nova etapa amb el mateix rumb que ens ha guiat des del principi: impulsar projectes que transformin el món des de la justícia social, la participació democràtica i la defensa dels drets humans i dels ecosistemes. Uneix-te a una comunitat que transforma realitats. Sigues part de la solució!" - }, - "renovationBanner": { - "title": "Renovem Goteo!", - "description": "Goteo ha construit una història única en el món del micromecenatge. Vols seguir formant-ne part?", - "ctaButton": "Uneix-te a la nostra història" - }, - "title": "Hola!", - "header": { - "login": "Iniciar Sessió", - "btnCart": "Anar al checkout", - "search": "Cercar", - "createProject": "Crea el teu projecte", - "goToProfile": "Anar a Perfil" - }, - "checkout": { - "title": "Resum de les teves donacions seleccionades", - "subtitle": "Et mostrem les que has seleccionat, prem el botó \"continuar\" per pagar.", - "reward": { - "claimed": "persones ja han reclamat aquesta recompensa", - "unclaimed": "Encara ningú ha reclamat aquesta recompensa. Fes-ho tu primer!" - }, - "tipjar": { - "community": "Vull donar suport a la Fundació Platoniq", - "checkboxLabel": "Impulsar una societat més justa i solidària", - "input": "Ingresseu quantitat lliure", + "login": { + "page": { + "title": "Indícanos tus datos personales", + "description": "Necesitamos saber quién eres antes de pagar.", + "registerOr": "Entra en tu cuenta o", + "registerBtnLabel": "Regístrate", + "otherAccessMethods": "También puedes acceder a través de:", + "forgotPassword": "¿Olvidaste tu contraseña?", + "welcome": { + "title": "Bienvenid@ al nuevo Goteo.", + "subtitle": "Estamos estrenando una nueva etapa y queremos compartirla contigo desde el principio. La plataforma irá creciendo poco a poco: activaremos secciones y funcionalidades a medida que estén listas y validadas con nuestra comunidad. De momento puedes acceder con tu mismo correo y contraseña y explorar el archivo de proyectos. Gracias por acompañarnos en este camino." + } + }, + "form": { + "email": "Email o nombre de usuario", + "password": "Contraseña" + }, "error": { - "invalidAmount": "La quantitat mínima és de 1€" + "invalidCredentials": "Correo electrónico o contraseña inválidos.", + "unexpectedLogin": "Error al iniciar sesión. Por favor, inténtalo de nuevo más tarde.", + "invalidEmail": "El correo electrónico no es válido." } }, - "changeAddressLabel": "Enviar aquesta recompensa a una direcció diferent de la meva.", - "summary": { - "total": { - "title": "Total que vas a donar", - "description": "Descripció del total" - }, - "deduct": "Pots desgravar-te fins a 100€ en la teva declaració de la renda si fas aquesta donació.", - "resume": { - "donationsPrefix": "de donacions", - "foundationPrefix": "de contribució a la fundació" - }, - "show_details": "Veure detall", - "hide_details": "Ocultar detall", - "show_more": "Veure més", - "hide": "Ocultar", - "donations": "Donacions", - "foundation": "Fundació Platoniq" + "logout": { + "label": "Cerrar sesión" }, - "cart": { - "freeDonation": { - "title": "Donació lliure" + "metadata": { + "description": "Página de inicio" + }, + "hero": { + "title": "Financia el cambio que el mundo necesita", + "description": "Arrancamos una nueva etapa con la misma brújula que nos ha guiado desde el principio: impulsar proyectos que transformen el mundo desde la justicia social, la participación democrática y la defensa de los derechos humanos y de los ecosistemas. Únete a una comunidad que transforma realidades. ¡Sé parte de la solución!", + "buttons": { + "discover": "Descubre campañas", + "impulse": "Impulsa un proyecto" } }, - "steps": { - "reward": { - "title": "Recompensa" + "me": { + "page": { + "title": "Actividad", + "subtitle": "Consulta tu actividad en Goteo" }, - "data": { - "title": "Les teves dades" + "period": { + "label": "Periodo", + "year": "Año {{year}}" }, - "payment": { - "title": "Mètode de pagament" + "donations": { + "title": "Mis donaciones", + "empty": "Aún no has donado a ningún proyecto", + "explore": "Explora proyectos", + "count": "Donaciones", + "total": "Total donado", + "recent": "Últimas donaciones", + "viewAll": "Ver todas", + "certificate": "Solicitar certificado" }, - "confirmation": { - "title": "Confirmar" - } - }, - "btnContinue": { - "label": "Continuar" - } - }, - "payment": { - "postPayment": { - "title": "Enhorabona! Gràcies per la teva donació", - "emailSent": "T'hem enviat un resum a:", - "certificate": { - "title": "Com sol·licito el meu certificat?", - "description": "El certificat es genera de forma automàtica al final de l'any fiscal (desembre). T'enviarem un correu un cop haguem presentat la documentació a Hisenda. En cas que trobem algun problema et contactarem per correu." + "projects": { + "title": "Proyectos impulsados", + "empty": "Aún no has impulsado ningún proyecto", + "create": "Impulsa tu proyecto", + "count": "Proyectos", + "raised": "Recaudado", + "recent": "Últimos proyectos impulsados", + "viewAll": "Ver todos", + "createNew": "Crear un proyecto" + }, + "ownedProjects": { + "title": "Tus Proyectos Activos", + "messageToDonatorsButton": "Mensaje a donantes", + "uploadNewsButton": "Subir novedad" }, "donatedProjects": { - "title": "Has donat als següents projectes", - "empty": "No s'han trobat projectes donats" + "title": "Proyectos activos donde donaste" }, - "suggestedProjects": { - "title": "Altres projectes que requereixen de la teva ajuda", - "empty": "No hi ha projectes disponibles en aquest moment" - } - }, - "page": { - "title": "Indica'ns les teves dades de pagament", - "description": "Ara necessitem que ens diguis com vols pagar.", - "selectMethod": "Selecciona mètode de pagament:" - }, - "page-approved": { - "title": "El teu mitjà de pagament s'ha processat amb un èxit aclaparador", - "description": "Molt bé! Estàs a un pas d'acabar i de fer el món una mica millor.", - "form-goal": { - "title": "Si la campanya no aconsegueix el seu objectiu", - "options": { - "1": "Ingressar al meu moneder de Goteo per poder destinar-lo a altres projectes. Saber més", - "2": "Tornar-ho al meu mitjà de pagament" + "cta": { + "discover": { + "title": "Descubre, conecta y dona", + "description": "Imagina un lugar donde cientos de proyectos sociales esperan tu apoyo. En nuestra plataforma, explorar es fácil: navega por causas comunitarias, ambientales o educativas. Porque aquí, tú decides qué historias se escriben.", + "button": "Dona a un proyecto" + }, + "matchfunding": { + "title": "Donaciones que brillan. Proyectos que inspiran.", + "description": "Goteo es pionera en matchfunding. Ya seas un donante que buscas potenciar tu contribución a proyectos, un impulsor que quiere aumentar su presupuesto o una entidad que busca apoyar a proyectos e impulsores a través de refuerzos en el donativo... nuestro programa de matchfunding te permite multiplicar el impulso para el cambio", + "learnMore": "Descubre cómo funciona el matchfunding", + "becomeMatcher": "Hazte Matcher" } }, - "form-review": { - "title": "Deixa un missatge en els projectes que has donat suport", - "description": "La teva donació es veurà reflectida en els projectes que has donat suport. Pots deixar un missatge o romandre en l'anonimat. ", - "labels": { - "name": "Etiqueta", - "anonymous": "Deixar un missatge de forma anònima", - "leave-a-message": "Deixar un missatge de forma pública" - } - } - }, - "page-error": { - "title": "Alguna cosa ha fallat... Torna-ho a intentar per poder donar.", - "description": "Vaja, potser és error nostre, del mitjà de pagament o simplement un follet informàtic malvat. Anem a provar de nou.", - "selectMethod": "Selecciona de nou un mètode de pagament:", - "btnContinue": "Torna-ho a intentar" - }, - "wallet-confirmation": { - "title": "El teu moneder virtual", - "description": "Bé, vas a pagar amb el saldo acumulat en el teu moneder. Recorda que si tens saldo i en dos anys no ho gastes, es transferirà a la fundació.", - "currentBalance": "Saldo actual", - "amountToUse": "Quant vas a usar", - "remainingBalance": "Saldo que et quedarà" - }, - "wallet": { - "balanceLabel": "Saldo: " - }, - "error": { - "unexpectedPayment": "Error al processar el pagament. Si us plau, intenta-ho de nou més tard.", - "paymentFailed": "El pagament ha fallat. Si us plau, revisa les dades introduïdes i torna-ho a intentar.", - "paymentCancelled": "El pagament ha estat cancel·lat. Si us plau, torna-ho a intentar.", - "missingAccountingId": "No s'han trobat totes les dades. Si us plau, torna a iniciar la sessió." - } - }, - "global": { - "btn": { - "continue": "Continuar", - "back": "Tornar", - "send": "Enviar" - } - }, - "contributions": { - "title": "Aportacions", - "description": "Aquí tens les últimes contribucions a campanyes, pots ordenar-les i filtrar-les com vulguis.", - "table": { - "headers": { - "target": "Destí", - "amount": "Import", - "origin": "Origen", - "paymentMethod": "Mètode", - "date": "Data", - "chargeStatus": "Estat de l'operació", - "refundToWallet": "Al Moneder" - }, - "rows": { - "payments": { - "tpv": "Targeta", - "paypal": "Paypal", - "stripe": "Stripe", - "wallet": "Moneder" - }, - "status": { - "to_charge": "Per cobrar", - "in_charge": "Cobrat", - "to_refund": "Per reemborsar", - "refunded": "Reemborsat", - "to_wallet": "Per reemborsar a moneder", - "walleted": "Reemborsat a moneder" - }, - "refund": { - "to_wallet": "Sí", - "to_gateway": "No" - }, - "noData": "No hi ha dades disponibles" - } - }, - "grid": { - "details": { - "operationTime": "Hora de l'operació", - "trackingCodes": { - "title": "Codis de seguiment", - "headers": { - "title": "Títol", - "trackingCode": "Codi de seguiment" - } - }, - "toWallet": "Al moneder", - "platformLinks": { - "title": "Enllaç a la plataforma", - "headers": { - "type": "Tipus d'enllaç", - "rel": "Relació", - "href": "Url" - } - }, - "estimatedFee": "Comissió estimada", - "concept": "Concepte" - }, - "actions": { - "refundToWallet": "Reemborsament a moneder", - "refundToUser": "Reemborsament a l'usuari", - "changeUser": "Canviar d'usuari", - "manageReward": "Gestionar recompensa" - }, - "content-footer": { - "lastEdited": "Última modificació: {{date}} {{time}}", - "downloadDetails": "Descarrega el detall", - "downloadCertificates": "Descarrega certificats", - "downloadPdf": "Descarrega PDF", - "annotations": "{{annotations}} Anotacions" - } - }, - "filters": { - "order": { - "title": "Ordenar per:", - "options": { - "date-desc": "Data (més recent primer)", - "date-asc": "Data (més antiga primer)", - "amount-desc": "Import (de major a menor)", - "amount-asc": "Import (de menor a major)", - "status-desc": "Estat (Z-A)", - "status-asc": "Estat (A-Z)" - } - }, - "itemsPerPage": { - "title": "Línies per pàgina:", - "options": { - "10": "10", - "20": "20", - "30": "30", - "50": "50" - } - }, - "search": { - "placeholder": "Cercar...", - "btnLabel": "Cerca", - "resultsFound": "{{totalItems}} resultats per \"{{query}}\"", - "noResults": "No s'han trobat resultats", - "labels": { - "projects": "Projectes", - "users": "Usuaris", - "tipjars": "Botes de propina", - "tipjar-id": "ID del bot de propina" - } - }, - "paymentMethod": { - "title": "Mètode de pagament", - "options": { - "all": "Tots", - "paypal": "Paypal", - "stripe": "Stripe", - "wallet": "Moneder" - } - }, - "rangeAmount": { - "title": "Selecciona import", - "options": { - "all": "Tots", - "000..10000": "0€ - 100€", - "10000..50000": "100€ - 500€", - "50000..100000": "500 € - 1.000 €", - "100000..500000": "1.000 € - 5.000 €", - "500000": "+ 5.000 €" - } - }, - "dateRange": { - "initDate": "Data Inici", - "endDate": "Data Fi", - "from": "Des de", - "to": "Fins a", - "errors": { - "invalidDate": "Data no vàlida", - "invalidRange": "La data d'inici no pot ser major que la data de fi." - } - }, - "chargeStatus": { - "title": "Estat d'Aportació", - "options": { - "all": "Tots", - "to_charge": "Per cobrar", - "in_charge": "Cobrat", - "to_refund": "Per reemborsar", - "refunded": "Reemborsat", - "to_wallet": "Per reemborsar a moneder", - "walleted": "Reemborsat a moneder" - } - }, - "projectStatus": { - "title": "Estat de la campanya", - "options": { - "all": "Totes", - "active": "Actives", - "finished": "Finalitzades", - "cancelled": "Cancel·lades" - } - }, - "btns": { - "openFilters": "Filtres", - "closeFilters": "Tanca filtres", - "reset": "Reinicia filtres", - "apply": "Aplica filtres" - } - }, - "pagination": { - "showing": "Mostrant {{items}} de {{total}}", - "unloaded": "Mostrant...", - "noResults": "No hi ha resultats" - }, - "tootip": { - "copied": "Copiat!" - }, - "export": { - "csv": "Exportar CSV", - "exporting": "Exportant...", - "error": "Error a l'exportar l'arxiu CSV. Si us plau, intenta-ho de nou.", - "errorAuth": "Error d'autenticació. Si us plau, inicia sessió de nou.", - "filename": "gateway-charges", - "itemsText": "elements", - "noData": "No es van trobar dades per exportar" - } - }, - "home": { - "campaigns": { - "title": "Campanyes més recents", - "tabsTitle": "Campanyes a Goteo", - "tabs": { - "endingSoon": "Acaben aviat", - "mostSuccessful": "Més exitoses", - "withMatchfunding": "Amb Matchfunding" - }, - "viewAll": "Veure totes", - "obtained": "Obtingut", - "minimum": "Mínim", - "optimum": "Òptim", - "daysRemaining": "Queden {{days}} dies", - "matchfunding": "Matchfunding", - "status": { - "minimumReached": "Mínim aconseguit!", - "goForMinimum": "A per al mínim!" - }, - "remaining": { - "toOptimum": "Falta per òptim", - "toMinimum": "Falta per mínim" - }, - "userDonations": "Les teves donacions:" - }, - "blog": { - "title": "S'acosten grans canvis… no et perdis res!", - "excerpt": "Un procés que implica una important migració tècnica de la plataforma actual, i per fer-ho amb totes les garanties de seguretat i sense pèrdua d'informació, necessitem portar a terme una transició ordenada, amb dates clau per a la publicació de campanyes i la realització de donacions…", - "readMore": "Segueix llegint" - }, - "matchfunding": { - "title": "Matchfunding", - "description": "L'efecte match només el veuràs en Goteo. Des del 2013 busquem organitzacions que vulguin multiplicar l'impacte del micromecenatge. Per cada euro que una persona aporta a una campanya de Goteo, una institució col·laboradora (fundació, administració pública, empresa…) aporta la mateixa quantitat, o fins i tot més.", - "actions": { - "learnMore": "Vull saber més", - "viewCalls": "Veure convocatòries actives" - }, - "impact": { - "title": "Dades d'impacte", - "description": "El matchfunding no només multiplica recursos econòmics, sinó que genera un impacte social tangible. En essència, el matchfunding transforma microdonacions en impacte macro, validant projectes amb suport ciutadà i optimitzant recursos públics/privats.", - "matchers": "Aportat en Matchfunding:", - "successfulProjects": "Projectes beneficiats:", - "successRate": "Percentatge d'èxit:" - }, - "cards": { - "grant": "Dotació convocatòria", - "inscription": "Data d'inscripció", - "participants": "Projectes participants", - "apply": "Inscriu-te en la convocatòria", - "viewCampaigns": "Veure les campanyes de la convocatòria", - "finished": "Finalitzada {{date}}" - } - }, - "stats": { - "title": "Estadístiques de Goteo", - "theoryToAction": { - "title": "De la teoria a l'acció", - "subtitle": "Les xifres que revelen la nostra petjada real. Dades a data de desembre de 2024.", - "projectsReceived": { - "value": "10.281", - "label": "Projectes rebuts" - }, - "projectsPublished": { - "value": "2.813", - "label": "Projectes publicats" - }, - "successRate": { - "value": "83%", - "label": "Taxa d'èxit" - }, - "button": "Veure Estadístiques de Goteo" - }, - "whyGoteo": { - "title": "Per què Goteo?", - "subtitle": "Tenim valors i les dades ens avalen. Dades a data de desembre de 2024.", - "community": { - "value": "384.341", - "label": "Comunitat" - }, - "averageDonors": { - "value": "152", - "label": "Mitjana de donants per projecte" - }, - "totalRaised": { - "value": "23M €", - "label": "Recaptació total" - }, - "button": "Vull saber-ne més" - } - }, - "stories": { - "title": "Goteo estrena nova web!", - "subtitle": "Encantats de compartir-ho amb tu", - "description": "Goteo té una nova cara! Navega pel nostre web renovat i descobreix una experiència de crowdfunding social més intuïtiva i atractiva. Explora projectes innovadors, coneix els creadors darrere ells i uneix-te a la nostra comunitat.\nFes-te donant i fes realitat les teves idees!", - "button": "Fes-te donant" - }, - "featuredPromoters": { - "title": "Impulsors destacats", - "viewAll": "Veure totes", - "projectsPromoted": "Projectes impulsats", - "capitalRaised": "Capital recaptat", - "promoterImage": "Foto de l'impulsor", - "shareProfile": "Compartir perfil de {{name}}" - }, - "resources": { - "title": "Recursos: Combustible per a Projectes Creatius", - "viewAll": "Veure més recursos", - "readMore": "Llegir més", - "articles": { - "firstCampaign": { - "title": "Com faig la meva primera campanya?", - "description": "El teu projecte mereix enlairar-se: Domina el crowdfunding social amb la nostra CAIXA D’EINES GRATUÏTA. Guia pràctica + plantilles descarregables per conquistar la teva audiència." - }, - "testimonials": { - "title": "Testimonis dels nostres impulsors", - "description": "Qui pot explicar-te millor com és impulsar campanyes exitoses? Els nostres impulsors ens expliquen de primera mà com és gestionar una campanya amb Goteo." - } - } - } - }, - "admin": { - "projects": { - "totalizers": { - "selectedCampaigns": "Campanyes seleccionades:", - "totalEarned": "Total aconseguit:", - "totalPaid": "Total pagat:", - "totalUnpaid": "Falta per pagar:" - } - }, - "charges": { - "lastContributions": "Últimes aportacions:", - "totalizers": { - "totalCharges": "Total aportacions:", - "totalTips": "Total propines:", - "totalFees": "Comissions totals:" - } - } - }, - "footer": { - "funding": { - "title": "Finança:", - "partOf": "Som part de:" - }, - "navigation": { - "collaborate": { - "title": "Col·labora", - "createProject": "Crea un projecte", - "matchfunding": "Matchfunding", - "workshops": "Tallers" - }, - "help": { - "title": "Ajuda", - "faqs": "FAQs", - "contact": "Contacte", - "taxCalculator": "Calculadora fiscal" - }, - "aboutGoteo": { - "title": "Sobre Goteo", - "whoWeAre": "Qui som?", - "goteoBenefits": "Avantatges de Goteo", - "foundationImpact": "Impacte de la fundació", - "platoniqFoundation": "Fundació Platoniq" - }, - "forUsers": { - "title": "Per a usuaris", - "rechargeWallet": "Recàrrega el teu moneder", - "downloadCertificate": "Descarrega certificat", - "accessComplementaryFunds": "Accedeix a fons complementaris", - "designFundingProgram": "Dissenya un programa de finançament" - } - }, - "legal": { - "termsOfUse": "Condicions d'ús", - "privacyPolicy": "Política de privacitat", - "licenses": "Llicències", - "stats": "Estadístiques" - }, - "platoniq": { - "foundation": "Fundació", - "name": "Platoniq" - } - }, - "create": { - "project": { - "title": "Proposa una campanya", - "subtitle": "Descriu breument el projecte i la campanya de crowdfunding.", - "budgetPreview": "Diners necessaris", - "description": { - "title": "Descriu breument la teva proposta", - "titlePrompt": "Indica el nom de la teva campanya", - "titlePlaceholder": "Nom de campanya", - "subtitle": "Indica el nom de la teva campanya i descriu breument el projecte que realitzaràs si aconsegueixes el finançament.", - "subtitlePrompt": "Indica breument la descripció del teu projecte.", - "subtitlePlaceholder": "Sense descripció" - }, - "categories": { - "title": "Quina categoria s'ajusta a la teva campanya?", - "subtitle": "(Pots triar un màxim de 2)" - }, - "budget": { - "title": "Quants diners necessites?", - "subtitle": "Selecciona la quantitat que necessitis per dur a terme el teu projecte.", - "amountPrompt": "Indica la quantitat que necessites" - }, - "release": { - "title": "Data sortida de campanya", - "subtitle": "(Ten en compte que les campanyes de crowdfunding tenen una durada mínima de 40 dies)" - }, - "submit": "Proposa una campanya" - } - }, - "profile": { - "share": "Compartir", - "subscribe": "Subscriu-te", - "tabs": { - "about": "Sobre el perfil", - "projects": "Projectes impulsats", - "donorType": "Tipus de donador" - }, - "notFound": "Usuari no trobat", - "location": "Ubicació", - "userIntro": "Membre actiu de la comunitat Goteo", - "organizationIntro": "Organització compromesa amb el canvi social", - "bioSample": "Amb una trajectòria dedicada a l'ecosistema de micromecenatge i finançament col·lectiu, {{name}} s'ha consolidat com un referent en la creació de projectes col·laboratius i la mobilització de recursos. La seva visió es centra a potenciar iniciatives que transformen realitats locals, combinant estratègies innovadores amb un profund compromís per la participació ciutadana i l'equitat social.", - "noProjects": "No hi ha projectes impulsats encara.", - "donorType": { - "fan": { - "title": "Fan", - "description": "Aquest donant ha invertit més de 25€", - "projectsDonated": "Projectes donats", - "moneyDonated": "Diners donats" - }, - "nextLevel": { - "title": "Pròxim nivell", - "description": "Segueix donant per pujar de nivell" - } - } - }, - "me": { - "page": { - "title": "Activitat", - "subtitle": "Consulta la teva activitat a Goteo" - }, - "period": { - "label": "Període", - "year": "Any {{year}}" - }, - "donations": { - "title": "Les meves donacions", - "empty": "Encara no has donat a cap projecte", - "explore": "Explora projectes", - "illustration": "/images/profile/ilustration-donations.png", - "ctaLink": "/projects", - "count": "Donacions", - "total": "Total donat", - "recent": "Últimes donacions", - "viewAll": "Veure totes", - "certificate": "Sol·licitar certificat" - }, - "projects": { - "title": "Projectes impulsats", - "empty": "Encara no has impulsat cap projecte", - "create": "Impulsa el teu projecte", - "illustration": "/images/profile/ilustration-project.png", - "ctaLink": "/projects/new", - "count": "Projectes", - "raised": "Recaptat", - "recent": "Últims projectes impulsats", - "viewAll": "Veure tots", - "createNew": "Crear un projecte" - }, - "ownedProjects": { - "title": "Els teus Projectes Actius", - "messageToDonatorsButton": "Missatge a donants", - "uploadNewsButton": "Puja novetat" - }, - "donatedProjects": { - "title": "Projectes actius on vas donar" - }, - "cta": { - "discover": { - "title": "Descobreix, connecta i dona", - "description": "Imagina un lloc on centenars de projectes socials esperen el teu suport. A la nostra plataforma, explorar és fàcil: navega per causes comunitàries, ambientals o educatives. Perquè aquí, tu decideixes quines històries s'escriuen.", - "button": "Dona a un projecte" - }, "matchfunding": { - "title": "Donacions que brillen. Projectes que inspiren.", - "description": "Goteo és pionera en matchfunding. Ja siguis un donant que busca potenciar la teva contribució a projectes, un impulsor que vol augmentar el seu pressupost o una entitat que busca donar suport a projectes i impulsors a través de reforços en el donatiu... el nostre programa de matchfunding et permet multiplicar l'impuls pel canvi", - "learnMore": "Descobreix com funciona el matchfunding", - "becomeMatcher": "Fes-te Matcher" + "card": { + "title": "Matchfunding", + "calls": "Convocatorias", + "donated": "Donado", + "recent": "Últimas convocatorias", + "viewAll": "Ver convocatorias", + "create": "Crear convocatoria" + }, + "section": { + "title": "Tus convocatorias de Matchfunding activas", + "error": "Error al cargar las convocatorias" + }, + "callCard": { + "donationCall": "Dotación convocatoria", + "participatingProjects": "Proyectos participantes", + "successfulProjects": "Proyectos acogidos con éxito", + "projects": "proyectos", + "viewCall": "Ir a la convocatoria" + } } }, - "matchfunding": { - "card": { - "title": "Matchfunding", - "calls": "Convocatòries", - "donated": "Donat", - "recent": "Últimes convocatòries", - "viewAll": "Veure convocatòries", - "create": "Crear convocatòria" - }, - "section": { - "title": "Les teves convocatòries de Matchfunding actives", - "empty": "No tens convocatòries actives", - "error": "Error al carregar les convocatòries" - }, - "callCard": { - "donationCall": "Dotació convocatòria", - "participatingProjects": "Projectes participants", - "successfulProjects": "Projectes acollits amb èxit", - "projects": "projectes", - "viewCall": "Anar a la convocatòria" + "blog": { + "share": { + "modal": { + "title": "Difunde este blog por el mundo", + "description": "Puedes copiar el enlace para enviarlo y también compartirla en Facebook o X." + } } } - }, - "lang": { - "error": { - "notAvailable": "L'idioma seleccionat \"{{lang}}\" no està disponible per a aquest projecte. Mostrant contingut en l'idioma disponible." - } } } From 0f0ffe527e3bdd8ddf1cf90aef2da7f1b0c57f66 Mon Sep 17 00:00:00 2001 From: OlivierSchulbaum Date: Thu, 16 Jul 2026 12:23:36 +0200 Subject: [PATCH 06/47] New translations es.json (English) [ci skip] --- src/i18n/locales/en.json | 2373 +++++++++++++++++++++----------------- 1 file changed, 1315 insertions(+), 1058 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index dea1f438..46e7626a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1,23 +1,50 @@ { - "common": { - "_": "Global UI translations (buttons, labels, etc.)", - "greeting": "Hello", - "farewell": "Goodbye", - "donate": "Donate", - "donateAmount": "Donate {{ amount }}", - "save": "Save", - "edit": "Edit", - "delete": "Delete", - "continue": "Continue", - "cancel": "Cancel", - "publish": "Publish", - "preview": "Preview", - "select": "Choose an option", - "remove": "Remove", - "textPlaceholder": "Type here" + "system": { + "_": "System messages (errors, notifications, validation, etc.)", + "loading": "Loading...", + "error": { + "unknown": "Ha ocurrido un error desconocido", + "payment": { + "missingMethod": "Se debe seleccionar un método de pago", + "missingCartData": "La cesta está vacía", + "unprocessableCartData": "No se puede procesar la cesta, por favor vacíala y vuelve a intentarlo", + "badAPIResponse": "Ha ocurrido un error al comunicarse con la pasarela de pago" + }, + "file": { + "sizeTooLarge": "{{ file }} exceeds the maximum size ({{ max }})", + "unsupportedType": "Unsupported file type: {{ file }}" + } + }, + "constraint": { + "file": { + "maxAllowedSize": "Maximum allowed size 10MB", + "supportedTypes": "Supported files: .png, .jpg" + } + }, + "validation": { + "missingRequiredFields": "One or more required fields are empty", + "project": { + "terms": { + "required": "Debes aceptar las condiciones del impulsor y las bases de la convocatoria" + } + }, + "errors": { + "summary": { + "title": "Corrige los siguientes errores:" + } + } + } }, "domain": { "_": "Business concepts (project, reward, contribution, etc.)", + "header": { + "categories": "Proyectos" + }, + "search": { + "bar": { + "placeholder": "Buscar..." + } + }, "project": { "campaign": { "minimumReached": "¡Minimum achieved!", @@ -37,16 +64,395 @@ }, "reward": { "byAtLeast": "By {{ amount }}", + "byAtLeastOrMore": "Por {{ amount }} o más", "unitsTotal": { "single": "There is 1 unit", "multiple": "There are {{ units }} units" }, + "unitsAvailable": "Quedan {{ units }} unidades", + "donators": "{{ donators }} donantes", + "noUnits": "No quedan unidades", "isInfinite": "No unit limit" + }, + "status": { + "all": "Todos los estados", + "in_campaign": "En campaña", + "funded": "Financiados", + "archived": "Archivados" + } + }, + "gatewayCheckout": { + "paymentMethod": "Método" + }, + "charges": { + "status": { + "to_charge": "Por cobrar", + "in_charge": "Cobrado", + "to_refund": "Por reembolsar", + "refunded": "Reembolsado", + "to_wallet": "Por reembolsar a monedero", + "walleted": "Reembolsado a monedero" + }, + "refund": { + "to_wallet": "Sí", + "to_gateway": "No" + }, + "entityLabels": { + "projects": "Proyectos", + "users": "Usuarios", + "tipjars": "Botes de propina", + "tipjarId": "ID del bote de propina" + } + }, + "review": { + "risks": { + "low": "Riesgo bajo", + "medium": "Riesgo medio", + "high": "Riesgo alto" + } + }, + "tipping": { + "title": "Deja una propina" + }, + "navigation": { + "search": { + "placeholder": "Busca tu campaña" } } }, + "common": { + "_": "Global UI translations (buttons, labels, etc.)", + "dateInput": { + "apply": "Aplicar", + "previousMonth": "Mes anterior", + "nextMonth": "Mes siguiente", + "close": "Cerrar" + }, + "greeting": "Hello", + "farewell": "Goodbye", + "donate": "Donate", + "donateAmount": "Donate {{ amount }}", + "save": "Save", + "edit": "Edit", + "delete": "Delete", + "continue": "Continue", + "back": "Volver", + "cancel": "Cancel", + "publish": "Publish", + "preview": "Preview", + "select": "Choose an option", + "remove": "Remove", + "textPlaceholder": "Type here", + "remember": "Recordar", + "search": "Buscar", + "countdown": { + "remaining": "Quedan", + "expired": "Campaña finalizada" + }, + "login": "Iniciar Sesión", + "goHome": "Volver a inicio", + "viewAll": "Ver todo", + "viewMore": "Ver más", + "viewLess": "Ver menos", + "share": "Compartir", + "noData": "No hay datos disponibles", + "tooltip": { + "copied": "¡Copiado!" + }, + "header": { + "search": "Buscar", + "createProject": "Crea tu proyecto", + "goToProfile": "Ir a Perfil" + }, + "footer": { + "funding": { + "title": "Financia:", + "partOf": "Somos parte de:" + }, + "navigation": { + "collaborate": { + "title": "Colabora", + "createProject": "Crea un proyecto", + "matchfunding": "Matchfunding", + "workshops": "Talleres" + }, + "help": { + "title": "Ayuda", + "faqs": "FAQs", + "contact": "Contacto", + "taxCalculator": "Calculadora fiscal" + }, + "aboutGoteo": { + "title": "Sobre Goteo", + "whoWeAre": "¿Quiénes somos?", + "goteoBenefits": "Ventajas de Goteo", + "foundationImpact": "Impacto de la fundación", + "platoniqFoundation": "Fundación Platoniq" + }, + "forUsers": { + "title": "Para usuarias", + "rechargeWallet": "Recarga tu monedero", + "downloadCertificate": "Descarga certificado", + "accessComplementaryFunds": "Accede a fondos complementarios", + "designFundingProgram": "Diseña un programa de financiación" + } + }, + "legal": { + "termsOfUse": "Condiciones de uso", + "privacyPolicy": "Política de privacidad", + "licenses": "Licencias", + "stats": "Stats" + }, + "platoniq": { + "foundation": "Fundación" + } + }, + "saved": "Guardado" + }, "pages": { + "403": { + "title": "No tienes permisos suficientes para continuar", + "subtitle": "Por favor, vuelve por donde has venido.", + "meta": { + "title": "Acceso prohibido", + "description": "Sin permisos suficientes." + } + }, + "404": { + "title": "No queda ni gota de la página que estás buscando", + "subtitle": "Por favor, vuelve al inicio.", + "meta": { + "title": "Página no encontrada", + "description": "La página que buscas no existe o ha sido movida." + } + }, + "500": { + "title": "Algo ha salido terriblemente mal", + "subtitle": "Por favor, espera unos segundos antes de volver a intentarlo.", + "meta": { + "title": "Condición inesperada para la solicitud", + "description": "Error interno del servidor." + } + }, "_": "Page-specific content (/home, /search, /project/[id], etc.)", + "home": { + "banner": { + "title": "¡Renovamos Goteo!", + "description": "Goteo ha construido una historia única en el mundo del micromecenazgo. ¿Quieres seguir formando parte de ella?", + "ctaText": "Súmate a nuestra historia", + "closeAriaLabel": "Cerrar banner" + }, + "campaigns": { + "title": "Campañas más recientes", + "tabsTitle": "Campañas en Goteo", + "tabs": { + "endingSoon": "Acaban pronto", + "mostSuccessful": "Más exitosas", + "withMatchfunding": "Con Matchfunding" + }, + "viewAll": "Ver todas", + "obtained": "Obtenido", + "minimum": "Mínimo", + "optimum": "Óptimo", + "daysRemaining": "Quedan {{days}} días", + "matchfunding": "Matchfunding", + "status": { + "minimumReached": "¡Mínimo conseguido!", + "goForMinimum": "¡A por el mínimo!" + }, + "userDonations": "Tus donaciones:" + }, + "blog": { + "title": "Se avecinan grandes cambios… ¡no te pierdas nada!", + "excerpt": "Un proceso que implica una importante migración técnica de la plataforma actual, y para hacerlo con todas las garantías de seguridad y sin pérdida de información, necesitamos llevar a cabo una transición ordenada, con fechas clave para la publicación de campañas y la realización de donaciones…", + "readMore": "Seguir leyendo" + }, + "matchfunding": { + "title": "Matchfunding", + "description": "El efecto match sólo lo verás en Goteo. Desde el 2013 buscamos organizaciones que quieran multiplicar el impacto del crowdfunding. Por cada euro que una persona aporta a una campaña de Goteo, una institución colaboradora (fundación, administración pública, empresa…) aporta la misma cantidad, o incluso más.", + "actions": { + "learnMore": "Quiero saber más", + "viewCalls": "Ver convocatorias activas" + }, + "impact": { + "title": "Datos de impacto", + "description": "El matchfunding no solo multiplica recursos económicos, sino que genera un impacto social tangible. En esencia, el matchfunding transforma microdonaciones en impacto macro, validando proyectos con apoyo ciudadano y optimizando recursos públicos/privados.", + "matchers": "Aportado en Matchfunding:", + "successfulProjects": "Proyectos beneficiados:", + "successRate": "Porcentaje de éxito:" + }, + "cards": { + "grant": "Dotación convocatoria", + "inscription": "Fecha de inscripción", + "participants": "Proyectos participantes" + } + }, + "stats": { + "title": "Estadísticas de Goteo", + "theoryToAction": { + "title": "De la teoría a la acción", + "subtitle": "Las cifras que revelan nuestra huella real. Datos a fecha de diciembre de 2024.", + "projectsReceived": { + "value": "10.281", + "label": "Proyectos recibidos" + }, + "projectsPublished": { + "value": "2.813", + "label": "Proyectos publicados" + }, + "successRate": { + "value": "83%", + "label": "Tasa de éxito" + }, + "button": "Ver Stats de Goteo" + }, + "whyGoteo": { + "title": "¿Por qué Goteo?", + "subtitle": "Tenemos valores y los datos nos avalan. Datos a fecha de diciembre de 2024.", + "community": { + "value": "384.341", + "label": "Comunidad" + }, + "averageDonors": { + "value": "152", + "label": "Media de donantes por proyecto" + }, + "totalRaised": { + "value": "23M €", + "label": "Recaudación total" + }, + "button": "Quiero saber más" + } + }, + "stories": { + "title": "Goteo estrena nueva web!", + "subtitle": "Encantados de compartirlo contigo", + "description": "¡Goteo tiene una nueva cara! Navega por nuestra web renovada y descubre una experiencia de crowdfunding social más intuitiva y atractiva. Explora proyectos innovadores, conoce a los creadores detrás de ellos y únete a nuestra comunidad.\n¡Conviértete en donante y haz realidad tus ideas!", + "button": "Conviértete en donante" + }, + "featuredPromoters": { + "title": "Impulsores destacados", + "viewAll": "Ver todes", + "projectsPromoted": "Proyectos impulados", + "capitalRaised": "Capital recaudado", + "promoterImage": "Foto del impulsor", + "shareProfile": "Compartir perfil de {{name}}" + }, + "resources": { + "title": "Recursos: Combustible para Proyectos Creativos", + "viewAll": "Ver más recursos", + "readMore": "Leer más", + "articles": { + "firstCampaign": { + "title": "¿Cómo hago mi primera campaña?", + "description": "Tu proyecto merece despegar: Domina el crowdfunding social con nuestra CAJA DE HERRAMIENTAS GRATUITA. Guía práctica + plantillas descargables para conquistar a tu audiencia." + }, + "testimonials": { + "title": "Testimonios de nuestros impulsores", + "description": "¿Quién puede contarte mejor como es impulsar campañas exitosas? Nuestros impulsores nos cuentan de primera mano como es gestionar una campaña con Goteo." + } + } + } + }, + "index": { + "title": "Goteo" + }, + "checkout": { + "metadata": { + "title": "Checkout", + "description": "Página de checkout" + }, + "subtitle": "Te mostramos las que has seleccionado, pulsa el botón \"continuar\" para pagar.", + "reward": { + "claimed": "{{ units }} personas ya han reclamado esta recompensa", + "unclaimed": "Aún nadie ha reclamado esta recompensa. ¡Hazlo tú primero!" + }, + "tipjar": { + "community": "Quiero apoyar a la Fundación Platoniq", + "checkboxLabel": "Impulsar una sociedad más justa y solidaria", + "input": "Ingrese monto libre", + "error": { + "invalidAmount": "La cantidad mínima es de 1€" + } + }, + "changeAddressLabel": "Enviar esta recompensa a una dirección distinta a la mia.", + "title": "Caja", + "summary": { + "toRecipient": "{{ amount }} a {{ name }}", + "total": { + "title": "Total que vas a donar" + }, + "deduct": "Puedes desgravarte hasta 100€ en tu declaración de la renta si realizas esta donación.", + "showDetails": "Ver detalle", + "hideDetails": "Ocultar desglose", + "showMore": "Ver más", + "hide": "Ocultar" + }, + "steps": { + "reward": { + "title": "Recompensa" + }, + "data": { + "title": "Tus datos" + }, + "payment": { + "title": "Método de pago" + }, + "confirmation": { + "title": "Confirmar" + } + }, + "payment": { + "title": "Indícanos tus datos de pago", + "description": "Ahora necesitamos que nos digas cómo quieres pagar.", + "selectMethod": "Selecciona método de pago:" + }, + "wallet": { + "title": "Tu monedero virtual", + "description": "Bien, vas a pagar con el saldo acumulado en tu monedero. Recuerda que si tienes saldo y en dos años no lo gastas, se transferirá a la fundación.", + "balanceLabel": "Saldo: ", + "currentBalance": "Saldo actual", + "amountToUse": "Cuánto vas a usar", + "remainingBalance": "Saldo que te quedará" + }, + "verify": { + "approved": { + "title": "Tu medio de pago se ha procesado con un éxito apabullante", + "description": "¡Bien! Estás a un paso de terminar y de hacer el mundo un poco mejor.", + "formGoal": { + "title": "Si la campaña no logra su objetivo", + "options": { + "1": "Ingresar en mi monedero de Goteo para poder destinarlo a otros proyectos. Saber más", + "2": "Devolverlo a mi medio de pago" + } + }, + "formReview": { + "title": "Deja un mensaje en los proyectos a los que has apoyado", + "description": "Tu donación se verá reflejada en los proyectos que has apoyado. Puedes dejar un mensaje o permanecer en el anonimato. " + } + }, + "error": { + "title": "Algo ha fallado... Vuelve a intentarlo para poder donar.", + "description": "Vaya, quizá sea error nuestro, del medio de pago o simplemente un malvado duende informático. Vamos a probar de nuevo.", + "selectMethod": "Selecciona de nuevo un método de pago:" + } + }, + "postPayment": { + "title": "¡Enhorabuena! Gracias por tu donación", + "emailSent": "Te hemos enviado un resumen a:", + "certificate": { + "title": "¿Cómo solicito mi certificado?", + "description": "El certificado se genera de forma automática al final del año fiscal (diciembre). Te mandaremos un correo una vez hayamos presentado la documentación a Hacienda. En caso de que encontremos algún problema te contactaremos por correo." + }, + "donatedProjects": { + "title": "Has donado a los siguientes proyectos" + }, + "suggestedProjects": { + "title": "Otros proyectos que requieren de tu ayuda", + "empty": "No hay proyectos disponibles en este momento" + } + } + }, "project": { "edit": { "title": "Editing project: {{ project }}", @@ -74,7 +480,19 @@ "option1": "1 Round", "option2": "2 Rounds" }, - "continue": "Continue with campaign info" + "continue": "Continue with campaign info", + "geography": { + "scopeLabel": "Ámbito de actuación", + "scopePlaceholder": "Selecciona el ámbito", + "options": { + "local": "Local", + "estatal": "Estatal", + "internacional": "Internacional" + }, + "localitiesLabel": "Localidades", + "localitiesPlaceholder": "Selecciona las localidades", + "localitiesHelper": "Indica las localidades donde se desarrolla el proyecto" + } }, "campaignInfo": { "title": "Campaign information", @@ -83,7 +501,18 @@ "title": "Presentation image and video", "description": "Add an image or video representing your project. This is the first impression for potential donors", "addImage": "Add image", - "addVideo": "Add video" + "addVideo": "Add video", + "uploading": "Subiendo…", + "processing": "Procesando…", + "maxImagesReached": "Has alcanzado el máximo de imágenes", + "removeVideo": "Eliminar vídeo", + "videoPlaceholder": "Pega aquí la URL del vídeo", + "validation": { + "image_too_large": "La imagen supera el tamaño máximo permitido", + "invalid_image": "Formato de imagen no válido", + "max_images": "Número máximo de imágenes alcanzado", + "upload_failed": "Error al subir el archivo" + } }, "goals": { "title": "Objectives and purposes of your campaign", @@ -101,7 +530,12 @@ "title": "What team do you have?", "description": "Describe your team and any previous experiences with similar projects" }, - "continue": "Continue with rewards" + "continue": "Continue with rewards", + "video": { + "validation": { + "invalid_url": "La URL del vídeo no es válida" + } + } }, "rewards": { "title": "Rewards", @@ -121,6 +555,11 @@ "description": "Reward description", "moneyAmount": "Price", "files": "Click or drag your file here to upload" + }, + "validation": { + "title": "El título no puede estar vacío", + "description": "La descripción no puede estar vacía", + "amount": "El importe debe ser mayor que cero" } }, "deleteModal": { @@ -167,7 +606,17 @@ "add": { "title": "Add an item", "description": "You can indicate what the minimum and optimal requirements are for your project.", - "button": "Add item" + "button": "Add item", + "minimum": { + "title": "Partida mínima", + "description": "Define los gastos básicos e imprescindibles para que tu proyecto despegue.", + "button": "Añadir mínima" + }, + "optimum": { + "title": "Partida óptima", + "description": "Da un salto de calidad con recursos extra que harían tu proyecto aún más completo.", + "button": "Añadir óptima" + } }, "modal": { "title": "Add budget items", @@ -184,6 +633,8 @@ "title": "You're about to delete this item", "description": "This action is permanent and cannot be undone" }, + "minimumSubtitle": "Partidas imprescindibles para realizar tu proyecto", + "optimumSubtitle": "Partidas adicionales para mejoras y ampliaciones", "validation": { "title": "The title cannot be empty", "description": "The description cannot be empty", @@ -191,1090 +642,896 @@ "amountMinimum": "The minimum budget must be greater than zero", "currency": "The amount must specify a currency", "type": "The item type must be specified", - "class": "The item class must be specified" + "class": "The item class must be specified", + "minimumRequiredFirst": "Debes crear al menos una partida de presupuesto mínimo antes de añadir partidas óptimas" + } + }, + "aboutYou": { + "title": "Más sobre ti", + "subtitle": "Descríbete al impulsor de la campaña.", + "prefill": "Seleccione datos guardados", + "legalEntity": "Forma jurídica", + "legalEntityHelper": "Indica la forma jurídica de quien impulsa esta campaña.", + "individual": "Persona física", + "organization": "Persona jurídica", + "name": "Indica el nombre del impulsor", + "nameHelper": "Dinos cómo denominaremos el impulsor. Esta denominación aparecerá en la parte pública de la campaña.", + "namePlaceholder": "Nombre del impulsor", + "taxId": "Indica el NIF del impulsor", + "taxIdHelper": "Solo lo verá el impulsor. Esta información no será pública, solo se usará para formalizar los acuerdos y condiciones de la campaña.", + "taxIdPlaceholder": "Indica el CIF", + "location": "Lugar de actividad", + "locationHelper": "Indica cuál es el ámbito geográfico de actuación del impulsor o grupo impulsor.", + "territory": "Territorio de actuación", + "country": "País", + "teamDescription": "Descripción del equipo impulsor", + "teamDescriptionHelper": "Cuéntanos a qué se dedica la entidad impulsora, qué tipo de actividades lleva a cabo en el día a día y cuáles son sus objetivos.", + "teamDescriptionPlaceholder": "Describe brevemente la actividad del impulsor", + "privateContacts": "Datos de contacto privados", + "privateContactsHelper": "Completa al menos un dato de contacto. La información no será pública, solo lo podrá usar Goteo para comunicarse sobre usos relacionados con la campaña que estás preparando.", + "email": "Mail", + "phone": "Teléfono", + "addContact": "Añadir contacto", + "preferred": "Preferido", + "publicContacts": "Datos de contacto públicos", + "publicContactsHelper": "Indicaremos qué datos de contacto/públicos quieres que aparezcan en tu campaña y en tu perfil impulsor.", + "placeholders": { + "website": "Inserta tu web", + "email": "Inserta tu cuenta de correo", + "instagram": "Inserta tu cuenta de Instagram", + "facebook": "Inserta tu cuenta de Facebook", + "twitter": "Inserta tu cuenta de X/Twitter", + "linkedin": "Inserta tu cuenta de LinkedIn" + }, + "paymentData": { + "title": "Datos de pago", + "subtitle": "Indica los datos bancarios necesarios para gestionar los pagos de la campaña.", + "iban": "IBAN", + "ibanHelper": "Introduce el IBAN de la cuenta bancaria donde recibirás los fondos recaudados.", + "ibanPlaceholder": "ES00 0000 0000 0000 0000 0000", + "bankCertificate": "Certificado de titularidad de cuenta", + "bankCertificateHelper": "Adjunta el documento que acredita la titularidad de la cuenta bancaria.", + "bankCertificateAriaLabel": "Subir certificado de titularidad", + "ibanInvalid": "El formato del IBAN no es válido" + }, + "continue": "Siguiente paso" + }, + "errors": { + "session": { + "title": "Sesión expirada" + }, + "storage": { + "title": "Error de almacenamiento", + "save_failed": "No se pudieron guardar los cambios", + "quota_exceeded": "Almacenamiento local lleno", + "close": "Cerrar" } } }, + "create": { + "title": "Propón una campaña", + "subtitle": "Describe brevemete el proyecto y la campaña de crowdfunding.", + "budgetPreview": "Dinero necesario", + "description": { + "title": "Describe brevemente tu propuesta", + "titlePrompt": "Indica el nombre de tu campaña", + "titlePlaceholder": "Nombre de campaña", + "subtitle": "Indica el nombre de tu campaña y describe brevemente el proyecto que realizarás si consigues la financiación.", + "subtitlePrompt": "Indica brevemente la descripción de tu proyecto.", + "subtitlePlaceholder": "Sin descripción" + }, + "categories": { + "title": "¿Qué categoría se ajusta a tu campaña?", + "subtitle": "(Puedes elegir un máximo de 2)" + }, + "budget": { + "title": "¿Cuánto dinero necesitas?", + "subtitle": "Selecciona la cantidad que necesites para llevar a cabo tu proyecto." + }, + "release": { + "title": "Fecha salida de campaña", + "subtitle": "(Ten en cuenta que las campañas de crowdfunding tienen una duración mínima de 40 días)" + }, + "terms": { + "label": "Acepto las condiciones del impulsor y las bases de la convocatoria" + }, + "submit": "Proponer campaña", + "submitting": "Enviando…", + "submitSuccess": "¡Campaña propuesta correctamente!" + }, "view": { - "owner": "Crowdfunding campaign by {{ owner }}" - } - } - }, - "system": { - "_": "System messages (errors, notifications, validation, etc.)", - "loading": "Loading...", - "notImplemented": "This functionality is not yet implemented. We're working on it, thank you for your patience!", - "error": { - "file": { - "sizeTooLarge": "{{ file }} exceeds the maximum size ({{ max }})", - "unsupportedType": "Unsupported file type: {{ file }}" + "owner": "Crowdfunding campaign by {{ owner }}", + "tabs": { + "project": "Información de campaña", + "rewards": "Recompensas", + "budget": { + "title": "Presupuesto", + "headline": "Conoce las necesidades clave de esta campaña y descubre cómo tu apoyo puede hacer realidad este proyecto.", + "minimum": "Mínimo", + "optimal": "Óptimo", + "raised": "Obtenido", + "total": "Total", + "chartTitle": "Este es el presupuesto para esta campaña. Descubre cómo tu apoyo puede llevarnos al éxito.", + "chart": { + "secondRound": "2ª Ronda", + "start": "Inicio" + } + }, + "updates": { + "title": "Actualizaciones", + "modalTitle": "Nueva actualización", + "by": "Por:", + "content": { + "title": "No pierdas detalle de los avances de esta campaña", + "empty": "No existen más actualizaciones previstas.", + "btn": { + "readMore": "Seguir leyendo", + "share": "Compartir" + } + } + }, + "community": { + "title": "Comunidad", + "content": { + "title": "Donaciones y mensajes de apoyo de la comunidad" + }, + "contribution": "Aporte", + "matchfunding": { + "contribution": "Suma de los aportes", + "aported": "Aportados", + "upTo": "de hasta" + }, + "role": { + "sponsor": "Patrocinador/a", + "matchfunding": "Matchfunding" + }, + "anonymous": { + "length": "aportes", + "title": "Donaciones anónimas", + "description": "No todas las personas heróicas llevan capa ni tampoco sienten la necesidad de mostrar su identidad." + }, + "loadMore": "Ver más" + } + }, + "rewards": { + "trending": "Recompensas más populares", + "showAll": "Ver todas", + "title": "Selecciona tus recompensas", + "headline": "Selecciona tu recompensa", + "thanksMessage": "Gracias por cooperar", + "ivaMessage": "¡Recuerda que cada aportación tiene una desgravación fiscal entre el 35 y 80%! Calcula aquí: {{link}}", + "addToCart": "Añadir al carro y continuar", + "unavailable": "No hay recompensas disponibles.", + "loading": "Cargando recompensas...", + "donationFree": { + "title": "Renuncio a una recompensa individual, solo quiero ayudar al proyecto", + "description": "En este espacio puedes donar la cantidad que consideres sin obtener recompensa a cambio. Cada aportación, grande o pequeña, marca la diferencia y nos acerca a la meta.", + "placeholder": "Seleccione el aporte Ej.20€" + }, + "error": { + "amount": "Por favor, ingresa una monto válido", + "invalidAmount": "Por favor, ingresa una cantidad válida mayor o igual al monto de la recompensa" + } + }, + "banner": { + "donator": { + "description": "Goteo pertenece a la Fundación Platoniq. Al colaborar en una campaña tu dinero será custodiado por Platoniq y donado a {{owner}} en caso de llegar a los objetivos de éxito marcados.", + "learnMore": "Saber más." + }, + "certificate": { + "description": "Al colaborar con un proyecto impulsado en Goteo puedes solicitar un certificado de donación. Así, te deduces hasta un 80% de lo que hayas aportado en tu próxima declaración de la renta.", + "learnMore": "Saber más." + } + }, + "langNotAvailable": "El idioma seleccionado \"{{lang}}\" no está disponible para este proyecto. Mostrando contenido en el idioma disponible.", + "outOfCampaign": "Este proyecto está fuera de campaña y ya no admite pagos.", + "share": { + "modal": { + "title": "Difunde este proyecto por el mundo", + "description": "Puedes copiar el enlace para enviarlo y también compartirla en Facebook o X. Incluso copiar un fragmento de código, para insertar en otra web un banner de la campaña. ¡Haz que llegue más lejos!" + }, + "iframe-modal": { + "title": "Así es como se verá en tu plataforma", + "description": "Copia este fragmento de código y pégalo en el código de tu página. Así conseguirás que la campaña llegue a más gente", + "btnCopy": "Copiar código", + "btnCopied": "¡Código copiado!" + } + }, + "video": { + "title": "Vídeo de la campaña", + "poster": "Imagen del vídeo", + "not-found": "Disculpa, esta campaña no tiene vídeo de presentación", + "not-supported": "No ha sido posible cargar el vídeo. Por favor, revisa el enlace o contacta con el administrador de la campaña." + } } }, - "constraint": { - "file": { - "maxAllowedSize": "Maximum allowed size 10MB", - "supportedTypes": "Supported files: .png, .jpg" - } - }, - "validation": { - "missingRequiredFields": "One or more required fields are empty" - } - }, - "reward": { - "donors": "Donors", - "units": "{{units}} units remain", - "noUnits": "No units remain", - "trending": "Most popular rewards", - "headline": "Select your reward", - "addAndContinue": "Add to cart and continue", - "input": "Select the contribution e.g. €20", - "btnFreeDonationLabel": "Free donation", - "showAll": "See all" - }, - "budget": { - "headline": "Know the key needs of this campaign and discover how your support can make this project come true.", - "chart": { - "minimum": "Minimum", - "optimal": "Optimum", - "progress": "Campaign status", - "secondRound": "2nd Round", - "start": "Home" - } - }, - "project": { - "tabs": { - "project": "Campaign information", - "budget": { - "chart-title": "This is the necessary budget to make this campaign come true and discover how your support can lead us to success.", - "title": "Budget", - "minimum": "Minimum", - "optimal": "Optimum", - "raised": "Raised", - "total": "Total", - "campaingState": "Campaign status" + "about": { + "title": "Sobre Goteo", + "hero": { + "title": "Conoce nuestra manera de hacer crowdfunding", + "description": "En Goteo creemos que la financiación colectiva es más que dinero: es confianza, comunidad y compromiso social. Nuestro modelo de crowdfunding se basa en el impulso de proyectos que generan impacto positivo y en el acompañamiento cercano a las personas y organizaciones que los hacen posibles.", + "cta": "Impulsa tu proyecto" }, - "rewards": "Rewards", - "updates": { - "title": "Updates", - "modal-title": "New update", - "by": "By:", - "content": { - "title": "Do not lose detail of the advances of this campaign", - "empty": "There are no more planned updates.", - "btn": { - "read-more": "Continue reading", - "share": "Share" + "whatIs": { + "title": "Qué es crowdfunding", + "subtitle": "Un impacto social de verdad.", + "description": "El crowdfunding es una forma de financiación que une a muchas personas que creen en una idea y deciden apoyarla con pequeñas (o grandes) aportaciones. En Goteo lo entendemos como un espacio donde no solo se busca recaudar fondos, sino también crear redes de apoyo, visibilidad y aprendizaje." + }, + "advisory": { + "title": "Asesoría y acompañamiento de nuestro equipo", + "description": "En Goteo no estás sola. Nuestro equipo te acompaña paso a paso, desde la definición de tu proyecto hasta la gestión de la campaña. Ofrecemos asesoría gratuita que te ayudará a preparar tu proyecto. Si necesitas más ayuda puedes solicitar una asesoría personalizada premium, con materiales de apoyo y una metodología probada para aumentar tus posibilidades de éxito." + }, + "phases": { + "title": "Entiende las fases y mecánica", + "items": { + "preparation": { + "title": "Preparación", + "description": "Define objetivos, presupuesto, duración y plan de comunicación. Tu proyecto podrá publicarse si cumple estos criterios." + }, + "launch": { + "title": "Lanzamiento", + "description": "Activamos la recaudación y te apoyamos en la difusión." + }, + "fundsReceived": { + "title": "Fondos recibidos", + "description": "Al alcanzar el objetivo, completa un formulario y envía documentación para la transferencia. Asegura la transparencia y explica a la comunidad el proyecto." + }, + "followUp": { + "title": "Seguimiento", + "description": "Tras la campaña, comparte el avance del proyecto con tu comunidad de donantes y cierra el ciclo con transparencia." } } }, - "community": { - "title": "Community", - "owner-anonymous": "Anonymous", - "content": { - "title": "Community support donations and messages" - }, - "contribution": "Input", - "matchfunding": { - "contribution": "Sum of contributions", - "aported": "Provided", - "up_to": "up to" - }, - "anonymous": { - "length": "anonymous", - "title": "Anonymous donations", - "description": "Not all heroes wear capes nor feel the need to reveal their identity." + "events": { + "title": "Apúntate a nuestras formaciones y eventos", + "description": "Periódicamente organizamos talleres, webinars y encuentros donde compartimos aprendizajes, tendencias y experiencias reales de crowdfunding. Son espacios para formarte, inspirarte y conectar con otras personas que, como tú, quieren transformar ideas en realidades. Suscríbete para recibir información actualizada desde nuestro boletín." + }, + "advantages": { + "title": "Ventajas de Goteo", + "items": { + "taxDeductions": { + "title": "Desgravaciones fiscales", + "description": "Las donaciones en Goteo tienen ventajas fiscales tanto para particulares como para empresas, gracias a que gestionamos las aportaciones a través de una fundación. Esto es algo que la mayoría de plataformas no ofrecen." + }, + "allOrNothing": { + "title": "Todo o nada", + "description": "Los proyectos reciben el apoyo económico únicamente si demuestran ser viables y alcanzan el presupuesto mínimo. Si no, las donaciones regresan a los donantes para seguir apoyando nuevas iniciativas." + }, + "twoRounds": { + "title": "Dos rondas de cofinanciación en 40+40 días", + "description": "Por defecto se establecen dos rondas de cofinanciación, de 40 días cada una. La primera, a \"todo o nada\", ligada al presupuesto mínimo imprescindible; y la segunda a un importe óptimo, para realizar mejoras adicionales en la que todo entra." + }, + "personalizedSupport": { + "title": "Acompañamiento personalizado", + "description": "No dejamos a los impulsores solos. Nuestro equipo asesora antes, durante y después de la campaña, con recomendaciones estratégicas y apoyo técnico. Otras plataformas se limitan a ser un escaparate de proyectos." + }, + "loyalCommunity": { + "title": "Comunidad fidelizada", + "description": "Contamos con una base de donantes recurrentes que confían en Goteo y vuelven a apoyar campañas porque valoran el impacto social y cultural. Esto da a los proyectos más oportunidades de alcanzar sus metas." + }, + "matchfunding": { + "title": "Cofinanciación institucional (matchfunding)", + "description": "A través de alianzas con instituciones, multiplicamos las aportaciones ciudadanas con fondos públicos o privados. Este modelo híbrido es único en Goteo y no existe en la mayoría de plataformas." + }, + "socialImpact": { + "title": "Foco en impacto social transformador", + "description": "No todos los proyectos son aceptados. En Goteo priorizamos aquellos alineados con la Economía Social y los valores democráticos, ya sea en forma de conocimiento abierto, mejoras comunitarias o impacto ambiental y social." + }, + "transparency": { + "title": "Transparencia y confianza", + "description": "Las campañas pasan por un proceso de validación y asesoría. La comunidad sabe que los proyectos publicados cumplen unos mínimos de calidad y coherencia, lo que aumenta la credibilidad." + }, + "provenMethodology": { + "title": "Metodología probada", + "description": "Más de una década de experiencia, con cientos de proyectos financiados, nos permite ofrecer una metodología contrastada que incrementa las posibilidades de éxito." + } } } }, - "actions": { - "share": "Share", - "remember": "Remember", - "viewAll": "See all", - "viewMore": "See more", - "viewLess": "See less" - }, - "impact": { - "description": "By collaborating with a Goteo-promoted project you can request a donation certificate. You can deduct up to 80% of what you've contributed in your next tax return.", - "learnMore": "Know more." - }, - "banner": { - "donator": { - "description": "Goteo belongs to the Platoniq Foundation. {{owner}}, in case of reaching the successful objectives set.", - "learnMore": "Know more." + "review": { + "btns": { + "seeChat": "Ver conversación", + "changeRisk": "Cambiar riesgo" }, - "certificate": { - "description": "By collaborating with a Goteo-promoted project you can request a donation certificate. You can deduct up to 80% of what you've contributed in your next tax return.", - "learnMore": "Know more." + "chat": { + "placeholder": "Escribe un mensaje…" } }, - "share": { - "modal": { - "title": "Spread this project around the world", - "description": "You can copy the link to send it and also share it on Facebook or X. You can even copy a code snippet to insert a campaign banner on another website. Make it go further!" - }, - "iframe-modal": { - "title": "This is how it will look on your platform", - "description": "Copy this code snippet and paste it into your page's code. This will help spread the campaign", - "btnCopy": "Copy code", - "btnCopied": "Code copied!" - } - }, - "video": { - "title": "Campaign video", - "description": "Here you can see the campaign presentation video. Play it!", - "poster": "Video image", - "not-found": "Sorry, this campaign doesn't have a presentation video", - "not-supported": "It has not been possible to load the video. Please check the link or contact the campaign administrator." - } - }, - "navigation": { "search": { - "placeholder": "Search for your campaign" - }, - "categories": "Categories" - }, - "search": { - "title": "Search campaigns - Goteo", - "description": "Explore our campaign catalog to find the one that inspires you to donate", - "heading": "Find the campaign you are looking for", - "subtitle": "Explore our campaign catalog and choose the one that inspires you the most to make your donation", - "topResults": "Most Searched Campaigns", - "showing_top_results": "Showing the {{count}} most popular campaigns", - "placeholder": "Search", - "label": "Search campaigns", - "clearSearch": "Clear Search", - "searchButton": "Search", - "closeFilters": "Close Filters", - "showFilters": "Show Filters", - "hideFilters": "Hide Filters", - "applyFilters": "Apply Filters", - "categoryLabel": "Choose a category", - "selectCategory": "Select category", - "deselectCategory": "Unselect category", - "selectedCategories": "{{count}} categories selected", - "clearFilters": "Clear Filters", - "loading": "Searching campaigns...", - "results": { - "count": "{{count}} results found", - "found": "{{count}} results found", - "noResults": "No results found", - "regionLabel": "Search Results", - "showing": "Showing {{count}} results", + "title": "Buscar campañas - Goteo", + "description": "Explora nuestro catálogo de campañas y encuentra la que más te inspire para hacer tu donación", + "heading": "Encuentra la campaña que buscas", + "subtitle": "Explora nuestro catálogo de campañas y elige la que más te inspire para hacer tu donación", + "topResults": "Campañas más buscadas", + "showingTopResults": "Mostrando las {{count}} campañas más populares", + "loading": "Buscando campañas...", + "input": { + "placeholder": "Buscar", + "label": "Buscar campañas", + "clear": "Limpiar búsqueda", + "button": "Buscar" + }, + "filters": { + "show": "Mostrar filtros", + "close": "Cerrar filtros", + "apply": "Aplicar filtros", + "categoryLabel": "Elige una categoría", + "territoryLabel": "Territorio", + "selectCategory": "Seleccionar categoría", + "deselectCategory": "Deseleccionar categoría", + "selectedCategories": "{{count}} categorías seleccionadas", + "status": { + "label": "Estado de la campaña" + }, + "territory": { + "label": "Ámbito de actuación" + } + }, "empty": { - "title": "No results found", - "description": "There are no campaigns matching your current search.", - "clearFilters": "Clear Filters" + "title": "No se encontraron campañas", + "description": "No hay campañas que coincidan con tu búsqueda actual. Verifica la ortografía o prueba con palabras clave diferentes o términos más generales.", + "clearFilters": "Limpiar filtros" + }, + "initial": { + "title": "Busca tu campaña ideal", + "description": "Utiliza los filtros de arriba para encontrar campañas que te inspiren." + }, + "results": { + "count": "{{count}} resultados encontrados", + "found": "{{count}} resultados encontrados", + "regionLabel": "Resultados de búsqueda", + "showing": "Mostrando {{count}} resultados", + "empty": { + "title": "No se encontraron resultados", + "description": "No hay campañas que coincidan con tu búsqueda actual.", + "clearFilters": "Limpiar filtros" + } + }, + "error": { + "title": "Error al buscar", + "retry": "Intentar de nuevo", + "network": { + "title": "Sin conexión a internet", + "description": "No se puede conectar con el servidor. Verifica tu conexión a internet e inténtalo de nuevo." + }, + "timeout": { + "title": "La búsqueda está tardando demasiado", + "description": "El servidor no responde. Inténtalo de nuevo o usa los datos locales." + }, + "unknown": { + "title": "Error inesperado", + "description": "Ha ocurrido un error inesperado. Inténtalo de nuevo." + }, + "actions": { + "retry": "Reintentar", + "showHelp": "Mostrar ayuda" + } + }, + "errors": { + "searchFailed": "No se pudo realizar la búsqueda. Intenta de nuevo.", + "api": { + "networkError": "Error de conexión. Verifica tu conexión a internet.", + "serverError": "Error del servidor. Por favor, inténtalo de nuevo más tarde." + } + }, + "accessibility": { + "resultsFound": "Se encontraron {{count}} campañas" + }, + "loadMore": { + "button": "Cargar más", + "loading": "Cargando más resultados...", + "announcement": "Se cargaron {{count}} resultados más. Mostrando {{loaded}} resultados en total." + }, + "pagination": { + "page": "Página {{page}}", + "currentPage": "Página {{page}}", + "previous": "Anterior", + "next": "Siguiente", + "loading": "Cargando..." } }, - "initial": { - "title": "Search for your ideal campaign", - "description": "Use the filters above to find campaigns that inspire you." - }, - "filters": { - "clear": "Reset filters" - }, - "errors": { - "searchFailed": "Could not perform the search. Try again.", - "api": { - "invalidToken": "Authentication failed. Please verify your credentials.", - "expiredToken": "Your session has expired. Please log in again.", - "missingToken": "Authentication required. Please log in.", - "networkError": "Connection error. Check your internet connection.", - "serverError": "Server error. Please try again later." + "register": { + "title": "Registro", + "page": { + "title": "Indícanos tus datos personales", + "description": "Necesitamos saber quién eres antes de pagar.", + "registerOr": "Regístrate o", + "loginBtnLabel": "Entra con tu cuenta" + }, + "form": { + "userType": { + "individual": "Persona física", + "organization": "Persona jurídica (empresas)" + }, + "btnSubmit": "Registrarse", + "termsCheckbox": "Acepto las condiciones legales", + "policiesCheckbox": "Al registrarme confirmo mi aceptación de vuestros términos de uso, política de privacidad y política de cookies.", + "validation": { + "password": { + "minLength": "*Requisitos de la contraseña: debe tener 8 caracteres" + } + } + }, + "individual": { + "firstName": "Nombre", + "lastName": "Apellidos", + "email": "Correo electrónico", + "password": "Contraseña", + "taxIdCheckbox": "Deseo desgravar mis donaciones. Es necesario indicar el número de DNI para comunicárselo a Hacienda.", + "taxId": { + "label": "DNI*", + "msgDeduction": "Deseo desgravar mis donaciones. Es necesario indicar el número de DNI para comunicárselo a Hacienda." + } + }, + "organization": { + "legalName": "Razón social", + "email": "Correo electrónico", + "password": "Contraseña", + "taxId": "CIF*", + "taxIdDescription": "*Con el CIF avisaremos a la agencia tributaria para la tu declaración de la renta.", + "representativeTitle": "Datos del representante de la empresa.", + "firstName": "Nombre", + "lastName": "Apellidos" + }, + "error": { + "incompletePersonFields": "Por favor, completa todos los campos obligatorios para persona física.", + "incompleteOrgFields": "Por favor, completa todos los campos obligatorios para persona jurídica.", + "unexpectedRegistration": "Error al registrarse. Por favor, inténtalo de nuevo más tarde." } }, - "empty": { - "title": "No campaigns found", - "description": "There are no campaigns matching your current search. Check the spelling or try different or more general keywords.", - "suggestions": { - "title": "Suggestions to improve your search:", - "checkSpelling": "Check the spelling of your keywords", - "tryDifferentKeywords": "Try different keywords", - "removeFilters": "Remove some filters to broaden the search", - "broadenSearch": "Use more general terms" - } - }, - "accessibility": { - "resultsFound": "{{count}} campaigns found" - }, - "loadMore": { - "button": "Load More", - "loading": "Loading more results...", - "announcement": "{{count}} more results loaded. Showing {{loaded}} results in total." - }, - "pagination": { - "summary": "Page {{page}} of {{totalPages}} ({{totalCount}} total results)", - "ariaLabel": "Results page navigation", - "page": "Page {{page}}", - "currentPage": "Page {{page}}", - "previous": "Previous", - "next": "Next", - "loading": "Loading...", - "loadMore": "Load more results", - "noMoreResults": "No more results" - }, - "error": { - "title": "Search Error", - "retry": "Try again", - "network": { - "title": "No internet connection", - "description": "Cannot connect to the server. Verify your internet connection and try again." + "admin": { + "charges": { + "title": "Aportes", + "description": "Aquí tienes las últimas contribuciones a campañas, puedes ordenarlas y filtrarlas como quieras.", + "headers": { + "target": "Destino", + "amount": "Importe", + "origin": "Origen", + "paymentMethod": "Método", + "date": "Fecha", + "chargeStatus": "Estado de la operación", + "refundToWallet": "A Monedero" + }, + "noData": "No hay datos disponibles", + "details": { + "operationTime": "Hora de la operación", + "trackingCodes": { + "title": "Códigos de seguimiento", + "headers": { + "title": "Título", + "trackingCode": "Código de seguimiento" + } + }, + "toWallet": "A monedero", + "platformLinks": { + "title": "Enlace a la plataforma", + "headers": { + "type": "Tipo de Enlace", + "rel": "Relación", + "href": "Url" + } + }, + "estimatedFee": "Comisión estimada", + "concept": "Concepto" + }, + "actions": { + "refundToWallet": "Devolución a monedero", + "refundToUser": "Devolución a usuario", + "changeUser": "Cambiar de usuario", + "manageReward": "Gestionar recompensa" + }, + "filters": { + "order": { + "title": "Ordenar por:", + "options": { + "date-desc": "Fecha (más reciente primero)", + "date-asc": "Fecha (más antigua primero)", + "amount-desc": "Importe (mayor a menor)", + "amount-asc": "Importe (menor a mayor)" + } + }, + "itemsPerPage": { + "title": "Líneas por página:", + "options": { + "10": "10", + "20": "20", + "30": "30", + "50": "50" + } + }, + "search": { + "placeholder": "Buscar...", + "resultsFound": "{{totalItems}} resultados para \"{{query}}\"", + "noResults": "No se han encontrado resultados" + }, + "paymentMethod": { + "title": "Método de pago", + "options": { + "all": "Todos", + "paypal": "Paypal", + "stripe": "Stripe", + "wallet": "Monedero" + } + }, + "rangeAmount": { + "title": "Selecciona importe", + "options": { + "500000": "+ 5.000€", + "all": "Todos", + "000..10000": "0€ - 100€", + "10000..50000": "100€ - 500€", + "50000..100000": "500€ - 1.000€", + "100000..500000": "1.000€ - 5.000€" + } + }, + "dateRange": { + "initDate": "Fecha Inicio", + "endDate": "Fecha Fin", + "errors": { + "invalidRange": "La fecha de inicio no puede ser mayor que la fecha fin." + } + }, + "chargeStatus": { + "title": "Estado de Aporte", + "options": { + "all": "Todos", + "to_charge": "Por cobrar", + "in_charge": "Cobrado", + "to_refund": "Por reembolsar", + "refunded": "Reembolsado", + "to_wallet": "Por reembolsar a monedero", + "walleted": "Reembolsado a monedero" + } + }, + "btns": { + "openFilters": "Filtros", + "closeFilters": "Cerrar filtros", + "apply": "Aplicar filtros" + } + }, + "pagination": { + "showing": "Mostrando {{items}} de {{total}}", + "unloaded": "Mostrando...", + "noResults": "No hay resultados" + }, + "export": { + "csv": "Exportar CSV", + "exporting": "Exportando...", + "error": "Error al exportar el archivo CSV. Por favor, inténtalo de nuevo.", + "filename": "gateway-charges", + "itemsText": "items", + "noData": "No se encontraron datos para exportar" + }, + "downloadButtons": { + "certificates": "Descarga certificados", + "pdf": "Descarga PDF" + }, + "lastEdited": "Última modificación: {{date}} {{time}}", + "annotations": "{{annotations}} Anotaciones", + "lastContributions": "Últimos aportes:", + "totalizers": { + "selected": "Aportes seleccionados:", + "totalCharges": "Total aportes:", + "totalTips": "Total propinas:", + "totalFees": "Comisiones totales:" + } }, - "timeout": { - "title": "Search is taking too long", - "description": "Server is not responding. Try again or use local data." - }, - "server": { - "title": "Server Error", - "description": "There is a problem with the server. Try later or use local data." - }, - "unknown": { - "title": "Unexpected Error", - "description": "An unexpected error occurred. Try again." - }, - "actions": { - "retry": "Retry", - "showHelp": "Show Help" - }, - "technicalDetails": "Technical Details" - } - }, - "filters": { - "selectOption": "Select Option", - "timePeriod": "Time Period", - "campaignStatus": "Campaign Status", - "location": "Location", - "time": { - "all": "Anytime", - "week": "Last week", - "month": "Last month", - "year": "Last year" - }, - "status": { - "all": "All statuses", - "funding": "In funding", - "successful": "Successful", - "completed": "Completed" - }, - "locationOptions": { - "all": "Any location", - "spain": "Spain", - "europe": "Europe", - "global": "Global" - } - }, - "categories": { - "culture": "Culture", - "rights": "Rights", - "education": "Education", - "journalism": "Independent Journalism", - "health": "Health", - "ecological_transition": "Ecological Transition", - "social_economy": "Social Economy", - "rural": "Rural", - "science": "Science" - }, - "ShareButton": { - "title": "Spread this project around the world", - "description": "You can copy the link to send it and also share it on Facebook or X. You can even copy a code snippet to insert a campaign banner on another website. Make it go further!", - "global": "share globally", - "facebook": "share on Facebook", - "twitter": "share on X", - "code": "share as code", - "copied": "Link copied to clipboard!" - }, - "register": { - "page": { - "title": "Tell us your personal data", - "description": "We need to know who you are before paying.", - "registerOr": "Register or", - "loginBtnLabel": "Enter your account", - "goteoUserInfo": "If you are already a Goteo user, you do not need to register again.", - "otherAccessMethods": "You can also access through:" - }, - "form": { - "userType": { - "individual": "Natural person", - "organization": "Legal person (companies)" - }, - "btnSubmit": "Register", - "termsCheckbox": "I accept legal conditions", - "policiesCheckbox": "When I register I confirm my acceptance of your terms of use, privacy policy and cookies policy.", - "zod": { - "taxId.custom": "ID must have at least 8 characters", - "legalName.custom": "The corporate name is mandatory for companies" - }, - "validation": { - "password": { - "minLength": "*Password must be at least 8 characters long" + "projects": { + "title": "Proyectos", + "description": "Busca proyectos y gestiónalos de la manera más rápida posible gracias al buscador", + "lastProjects": "Listado de campañas:", + "export": { + "csv": "Exportar .csv" + }, + "totalizers": { + "selected": "Campañas seleccionadas:", + "totalEarned": "Total conseguido:", + "totalPaid": "Total pagado:", + "totalUnpaid": "Falta por pagar:" + }, + "filters": { + "search": { + "placeholder": "Búsqueda", + "btn": "Buscar", + "minLength": "Es necesario meter más de 4 caracteres" + }, + "btns": { + "openFilters": "Filtros", + "closeFilters": "Cerrar filtros", + "apply": "Aplicar filtros" + }, + "campaignStatus": { + "title": "Estado de campaña", + "options": { + "completed": "Completada", + "active": "Activa", + "failed": "Fallida" + } + }, + "status": { + "title": "Estado de proyecto", + "options": { + "in_draft": "Borrador", + "to_campaign_review": "Pendiente revisión campaña", + "in_campaign_review": "En revisión campaña", + "to_campaign": "Pendiente campaña", + "in_campaign": "En campaña", + "campaign_failed": "Campaña fallida", + "to_funding_review": "Pendiente revisión cobro", + "in_funding_review": "En revisión cobro", + "to_funding": "Pendiente cobro", + "in_funding": "En cobro", + "funding_paid": "Cobro pagado" + } + }, + "dateRange": { + "initDate": "Fecha de inicio", + "endDate": "Fecha fin" + }, + "order": { + "title": "Ordenar por", + "options": { + "date-desc": "Fecha (más reciente primero)", + "date-asc": "Fecha (más antiguo primero)", + "title-asc": "Título A–Z", + "title-desc": "Título Z–A" + } + }, + "itemsPerPage": { + "title": "Líneas por página", + "options": { + "10": "10", + "25": "25", + "50": "50" + } + } + }, + "table": { + "headers": { + "name": "Proyecto", + "promoter": "Impulsor", + "contractNumber": "Número de contrato", + "achieved": "Conseguido", + "paid": "Pagado", + "process": "Estado" + }, + "rows": { + "status": { + "in_draft": "Borrador", + "to_campaign_review": "Pendiente revisión campaña", + "in_campaign_review": "En revisión campaña", + "in_campaign_review_request_change": "Revisión campaña: cambios", + "campaign_review_rejected": "Campaña rechazada", + "to_campaign": "Pendiente campaña", + "in_campaign": "En campaña", + "campaign_failed": "Campaña fallida", + "to_funding_review": "Pendiente revisión cobro", + "in_funding_review": "En revisión cobro", + "in_funding_review_request_change": "Revisión cobro: cambios", + "funding_review_rejected": "Cobro rechazado", + "to_funding": "Pendiente cobro", + "in_funding": "En cobro", + "funding_paid": "Cobro pagado" + }, + "details": { + "datePublished": "Publicado", + "dateEnd1": "Fin primera ronda", + "dateEnd2": "Fin segunda ronda", + "minOptim": "Mínimo - Óptimo", + "email": "Email", + "phone": "Teléfono", + "contractExpiry": "Vencimiento del contrato", + "remaining": "Falta por pagar", + "btns": { + "changeStatus": "Cambiar cuentas", + "createContract": "Formulario de contrato", + "promoter": "Impulsor", + "contributions": "Aportes", + "donors": "Donantes de campaña", + "financialReport": "Informe financiación", + "certificates": "Certificados", + "contractPdf": "Contrato PDF", + "annotations": "Anotaciones" + } + }, + "noData": "No hay datos disponibles" + } + }, + "modals": { + "paid": { + "title": "Pagado", + "description": "Esta cantidad refleja el total de lo aportado a la campaña por todos los medios de pago posibles.", + "input": "Pago", + "matchfunding": "Pago matchfunding", + "save": "Guardar" + }, + "annotations": { + "title": "Anotaciones", + "placeholder": "Escribe tu anotación", + "empty": "Sin registro", + "add": "Añadir anotación", + "modify": "Modificar anotación" + } + }, + "informe": { + "title": "Informe de financiación del proyecto", + "subtitle": "Revisa los datos antes de actuar", + "description": "Edite lo necesario, descárguelo en formato PDF o envíe el informe final.", + "fields": { + "invoiceNumber": "Número de factura", + "transferDate": "Fecha de transferencia", + "platformFee": "Comisión de plataforma", + "email": "Correo para envío" + }, + "btns": { + "edit": "Editar datos", + "pdf": "Descargar PDF", + "send": "Enviar informe" + }, + "modal": { + "title": "Edita los datos antes de enviar", + "fields": { + "invoiceNumber": "Número de factura", + "transferDate": "Fecha de nueva transferencia", + "platformFee": "Comisión de plataforma", + "email": "Correo destinatario" + }, + "save": "Guardar" + } + }, + "pagination": { + "showing": "Mostrando {{ items }} de {{ total }}", + "itemsPerPage": "{{ count }} por página" } } }, - "individual": { - "firstName": "Name", - "lastName": "Surname", - "email": "Email", - "password": "Password", - "taxIdCheckbox": "I want to deduct my donations. It is necessary to indicate the DNI number to communicate it to the tax authorities.", - "taxId": { - "label": "ID*", - "msgDeduction": "I want to deduct my donations. It is necessary to indicate the DNI number to communicate it to the tax authorities." - } + "static": { + "title": "Goteo" }, - "organization": { - "legalName": "Corporate Name", - "email": "Email", - "password": "Password", - "taxId": "ID*", - "taxIdDescription": "*With the ID we will notify the Tax Agency for your income declaration.", - "representativeTitle": "Company representative data.", - "firstName": "Name", - "lastName": "Surname" + "profile": { + "share": "Compartir", + "shareModal": { + "title": "Comparte este perfil" + }, + "subscribe": "Subscríbete", + "tabs": { + "about": "Sobre el perfil", + "projects": "Proyectos impulsados", + "donorType": "Tipo de donante" + }, + "noProjects": "No hay proyectos impulsados aún.", + "noBio": "Todavía no hay información sobre este perfil.", + "donorType": { + "fan": { + "title": "Fan", + "description": "Este donante ha invertido más de 25€", + "projectsDonated": "Proyectos donados", + "moneyDonated": "Dinero donado" + }, + "nextLevel": { + "title": "Próximo nivel", + "description": "Sigue donando para subir de nivel" + } + }, + "title": "Goteo", + "notFound": "Usuario no encontrado" }, - "error": { - "incompletePersonFields ": "Please complete all mandatory fields for natural person.", - "incompleteOrgFields ": "Please complete all mandatory fields for legal entity.", - "unexpectedRegistration": "Error when registering. Please try again later." - } - }, - "login": { - "page": { - "title": "Tell us your personal data", - "description": "We need to know who you are before paying.", - "registerOr": "Enter your account or", - "registerBtnLabel": "Register", - "otherAccessMethods": "You can also access through:", - "forgotPassword": "Did you forget your password?", - "welcome": { - "title": "Welcome to the new Goteo.", - "subtitle": "We are starting a new stage and we want to share it with you from the beginning. The platform will grow little by little: we will activate sections and functionalities as they are ready and validated with our community. For now, you can access with your same email and password and explore the project file. Thank you for joining us on this journey." - } - }, - "form": { - "email": "Email or username", - "password": "Password", - "btnSubmit": "Login" - }, - "error": { - "invalidCredentials": "Invalid email or password.", - "unexpectedLogin": "Error logging. Please try again later.", - "invalidEmail": "The email is not valid.", - "maxCharacters": "The email cannot exceed 255 characters." - } - }, - "logout": { - "label": "Log Out", - "error": { - "cookieNotFound": "Session cookie has not been found.", - "invalidToken": "Your session is invalid. Please log in again.", - "logoutSuccess": "You have closed session correctly.", - "unexpectedLogout": "Error when closing session. Please try again later." - } - }, - "zod": { - "required": "This field is required", - "invalid_type": "Expected {expected} but received {received}", - "string": { - "too_small": "Must have at least {min} character(s)", - "too_big": "Must not exceed {max} characters", - "invalid": "Invalid string format" - }, - "number": { - "too_small": "Must be at least {min}", - "too_big": "Must not exceed {max}" - }, - "too_small": "Must be at least {min}", - "too_big": "Must not exceed {max}", - "email": { - "invalid": "Invalid email address" - }, - "url": { - "invalid": "Invalid URL format" - }, - "enum": { - "invalid": "Invalid option. Valid options are: {options}" - }, - "invalid": "Invalid input" - }, - "countdown": { - "expired": "Finished campaign", - "remaining": "They remain", - "error": { - "not-found": "The date of completion of the campaign has not been found." - } - }, - "rewards": { - "title": "Select your rewards", - "unavailable": "There are no rewards available.", - "donators": "{{donators}} donors", - "units-available": "{{units}} units remaining", - "donation-free": { - "title": "I give up an individual reward, I just want to help the project", - "description": "In this space, you can donate the amount you consider without obtaining a reward in return. Each contribution, big or small, makes a difference and brings us closer to the goal.", - "placeholder": "Select the contribution e.g. €20" - }, - "by-amount-or-more": "For {{amount}} or more", - "thanks-message": "Thanks for cooperating", - "reward-iva-message": "Remember that each contribution has a tax relief between 35% and 80%! Calculate here: {{link}}", - "reward-donate-close": "Add to basket and continue", - "error-invalid-amount": "Please enter a valid amount greater than or equal to the amount of the reward", - "error-amount": "Please enter a valid amount", - "loading": "Loading rewards..." - }, - "updates": { - "heading": "Do not lose detail of the advances of this campaign", - "empty": "There are no more planned updates." - }, - "updateCard": { - "readMore": "Continue reading" - }, - "metadata": { - "title": "Home", - "description": "Home Page" - }, - "hero": { - "title": "Finance the change the world needs", - "description": "We are starting a new stage with the same compass that has guided us from the beginning: to promote projects that transform the world from social justice, democratic participation, and the defense of human rights and ecosystems. Join a community that transforms realities. Be part of the solution!" - }, - "renovationBanner": { - "title": "We renewed Goteo!", - "description": "Goteo has built a unique story in the world of crowdfunding. Do you want to continue being part of it?", - "ctaButton": "Join our story" - }, - "title": "Hello!", - "header": { - "login": "Login", - "btnCart": "Go to checkout", - "search": "Search", - "createProject": "Create your project", - "goToProfile": "Go to Profile" - }, - "checkout": { - "title": "Summary of your selected donations", - "subtitle": "We show you the ones you have selected, press the \"Continue\" button to pay.", - "reward": { - "claimed": "people have already claimed this reward", - "unclaimed": "No one has claimed this reward. Be the first!" - }, - "tipjar": { - "community": "I want to support the Platoniq Foundation", - "checkboxLabel": "Boost a fairer and more supportive society", - "input": "Enter free amount", + "login": { + "page": { + "title": "Indícanos tus datos personales", + "description": "Necesitamos saber quién eres antes de pagar.", + "registerOr": "Entra en tu cuenta o", + "registerBtnLabel": "Regístrate", + "otherAccessMethods": "También puedes acceder a través de:", + "forgotPassword": "¿Olvidaste tu contraseña?", + "welcome": { + "title": "Bienvenid@ al nuevo Goteo.", + "subtitle": "Estamos estrenando una nueva etapa y queremos compartirla contigo desde el principio. La plataforma irá creciendo poco a poco: activaremos secciones y funcionalidades a medida que estén listas y validadas con nuestra comunidad. De momento puedes acceder con tu mismo correo y contraseña y explorar el archivo de proyectos. Gracias por acompañarnos en este camino." + } + }, + "form": { + "email": "Email o nombre de usuario", + "password": "Contraseña" + }, "error": { - "invalidAmount": "The minimum amount is €1" + "invalidCredentials": "Correo electrónico o contraseña inválidos.", + "unexpectedLogin": "Error al iniciar sesión. Por favor, inténtalo de nuevo más tarde.", + "invalidEmail": "El correo electrónico no es válido." } }, - "changeAddressLabel": "Send this reward to a different address than mine.", - "summary": { - "total": { - "title": "Total that you will donate", - "description": "Description of the total" - }, - "deduct": "You can deduct up to €100 in your income statement if you make this donation.", - "resume": { - "donationsPrefix": "of donations", - "foundationPrefix": "of contribution to the foundation" - }, - "show_details": "Show details", - "hide_details": "Hide details", - "show_more": "See more", - "hide": "Hide", - "donations": "Donations", - "foundation": "Platoniq Foundation" + "logout": { + "label": "Cerrar sesión" }, - "cart": { - "freeDonation": { - "title": "Free donation" + "metadata": { + "description": "Página de inicio" + }, + "hero": { + "title": "Financia el cambio que el mundo necesita", + "description": "Arrancamos una nueva etapa con la misma brújula que nos ha guiado desde el principio: impulsar proyectos que transformen el mundo desde la justicia social, la participación democrática y la defensa de los derechos humanos y de los ecosistemas. Únete a una comunidad que transforma realidades. ¡Sé parte de la solución!", + "buttons": { + "discover": "Descubre campañas", + "impulse": "Impulsa un proyecto" } }, - "steps": { - "reward": { - "title": "Reward" + "me": { + "page": { + "title": "Actividad", + "subtitle": "Consulta tu actividad en Goteo" }, - "data": { - "title": "Your data" + "period": { + "label": "Periodo", + "year": "Año {{year}}" }, - "payment": { - "title": "Payment method" + "donations": { + "title": "Mis donaciones", + "empty": "Aún no has donado a ningún proyecto", + "explore": "Explora proyectos", + "count": "Donaciones", + "total": "Total donado", + "recent": "Últimas donaciones", + "viewAll": "Ver todas", + "certificate": "Solicitar certificado" }, - "confirmation": { - "title": "Confirm" - } - }, - "btnContinue": { - "label": "Continue" - } - }, - "payment": { - "postPayment": { - "title": "Amazing! Thanks for your donation", - "emailSent": "We have sent a summary to:", - "certificate": { - "title": "How do I request my certificate?", - "description": "The certificate is automatically generated at the end of the fiscal year (December). We will send you an email once we have submitted the documentation to the Tax Office. If we encounter any issues, we will contact you via email." + "projects": { + "title": "Proyectos impulsados", + "empty": "Aún no has impulsado ningún proyecto", + "create": "Impulsa tu proyecto", + "count": "Proyectos", + "raised": "Recaudado", + "recent": "Últimos proyectos impulsados", + "viewAll": "Ver todos", + "createNew": "Crear un proyecto" + }, + "ownedProjects": { + "title": "Tus Proyectos Activos", + "messageToDonatorsButton": "Mensaje a donantes", + "uploadNewsButton": "Subir novedad" }, "donatedProjects": { - "title": "You have donated to the following projects", - "empty": "No donated projects found" + "title": "Proyectos activos donde donaste" }, - "suggestedProjects": { - "title": "Other projects that require your help", - "empty": "No projects available at this time" - } - }, - "page": { - "title": "Tell us your payment data", - "description": "Now we need you to tell us how you want to pay.", - "selectMethod": "Select payment method:" - }, - "page-approved": { - "title": "Your means of payment has been processed with overwhelming success", - "description": "Good! You are one step away from finishing and making the world a little better.", - "form-goal": { - "title": "If the campaign does not achieve its goal", - "options": { - "1": "Enter my Goteo wallet to be able to allocate it to other projects. Learn more", - "2": "Return it to my payment method" + "cta": { + "discover": { + "title": "Descubre, conecta y dona", + "description": "Imagina un lugar donde cientos de proyectos sociales esperan tu apoyo. En nuestra plataforma, explorar es fácil: navega por causas comunitarias, ambientales o educativas. Porque aquí, tú decides qué historias se escriben.", + "button": "Dona a un proyecto" + }, + "matchfunding": { + "title": "Donaciones que brillan. Proyectos que inspiran.", + "description": "Goteo es pionera en matchfunding. Ya seas un donante que buscas potenciar tu contribución a proyectos, un impulsor que quiere aumentar su presupuesto o una entidad que busca apoyar a proyectos e impulsores a través de refuerzos en el donativo... nuestro programa de matchfunding te permite multiplicar el impulso para el cambio", + "learnMore": "Descubre cómo funciona el matchfunding", + "becomeMatcher": "Hazte Matcher" } }, - "form-review": { - "title": "Leave a message in the projects you have supported", - "description": "Your donation will be reflected in the projects you have supported. You can leave a message or remain anonymous ", - "labels": { - "name": "Label", - "anonymous": "Leave an anonymous message", - "leave-a-message": "Leave a public message" - } - } - }, - "page-error": { - "title": "Something has failed... Try again to donate.", - "description": "Wow, perhaps it is our mistake, of the means of payment, or simply an evil computer goblin. Let's try again.", - "selectMethod": "Select a payment method again:", - "btnContinue": "Try again" - }, - "wallet-confirmation": { - "title": "Your virtual wallet", - "description": "Well, you will pay with the accumulated balance in your wallet. Remember that if you have a balance and do not spend it in two years, it will be transferred to the foundation.", - "currentBalance": "Current balance", - "amountToUse": "How much are you going to use", - "remainingBalance": "Remaining balance" - }, - "wallet": { - "balanceLabel": "Balance: " - }, - "error": { - "unexpectedPayment": "Error while processing payment. Please, try again later.", - "paymentFailed": "The payment has failed. Please, check the entered data and try again.", - "paymentCancelled": "Payment has been canceled. Please, try again.", - "missingAccountingId": "Not all the data has been found. Please, log in again." - } - }, - "global": { - "btn": { - "continue": "Continue", - "back": "Back", - "send": "Send" - } - }, - "contributions": { - "title": "Contributions", - "description": "Here are the latest contributions to campaigns, you can order and filter them as you want.", - "table": { - "headers": { - "target": "Target", - "amount": "Amount", - "origin": "Source", - "paymentMethod": "Method", - "date": "Date", - "chargeStatus": "Status of transaction", - "refundToWallet": "To my wallet" - }, - "rows": { - "payments": { - "tpv": "Card", - "paypal": "PayPal", - "stripe": "Stripe", - "wallet": "Wallet" - }, - "status": { - "to_charge": "Pending charge", - "in_charge": "Charged", - "to_refund": "To be refunded", - "refunded": "Refunded", - "to_wallet": "Por reembolsar a monedero", - "walleted": "Reembolsado a monedero" - }, - "refund": { - "to_wallet": "Yes", - "to_gateway": "No" - }, - "noData": "No data available" - } - }, - "grid": { - "details": { - "operationTime": "Time of transaction", - "trackingCodes": { - "title": "Tracking codes", - "headers": { - "title": "Title", - "trackingCode": "Tracking code" - } - }, - "toWallet": "To wallet", - "platformLinks": { - "title": "External link", - "headers": { - "type": "Link type", - "rel": "Relationship", - "href": "URL" - } - }, - "estimatedFee": "Estimated fee", - "concept": "Concepto" - }, - "actions": { - "refundToWallet": "Refund to wallet", - "refundToUser": "Refund to user", - "changeUser": "Change user", - "manageReward": "Manage reward" - }, - "content-footer": { - "lastEdited": "Last modification: {{date}} {{time}}", - "downloadDetails": "Download details", - "downloadCertificates": "Download certificates", - "downloadPdf": "Download PDF", - "annotations": "{{annotations}} annotations" - } - }, - "filters": { - "order": { - "title": "Order by:", - "options": { - "date-desc": "Date (most recent first)", - "date-asc": "Date (oldest first)", - "amount-desc": "Amount (highest to lowest)", - "amount-asc": "Amount (lowest to highest)", - "status-desc": "Status (Z-A)", - "status-asc": "Status (A-Z)" - } - }, - "itemsPerPage": { - "title": "Items per page:", - "options": { - "10": "10", - "20": "20", - "30": "30", - "50": "50" - } - }, - "search": { - "placeholder": "Search...", - "btnLabel": "Search", - "resultsFound": "{{totalItems}} results for \"{{query}}\"", - "noResults": "No results found", - "labels": { - "projects": "Projects", - "users": "Users", - "tipjars": "Tipjars", - "tipjar-id": "Tipjar ID" - } - }, - "paymentMethod": { - "title": "Payment method", - "options": { - "all": "All", - "paypal": "PayPal", - "stripe": "Stripe", - "wallet": "Wallet" - } - }, - "rangeAmount": { - "title": "Select amount", - "options": { - "all": "All", - "000..10000": "€0 - €100", - "10000..50000": "€100 - €500", - "50000..100000": "€500 - €1,000", - "100000..500000": "€1,000 - €5,000", - "500000": "€5,000+" - } - }, - "dateRange": { - "initDate": "Start date", - "endDate": "End date", - "from": "Date since", - "to": "Date until", - "errors": { - "invalidDate": "Invalid date", - "invalidRange": "The start date cannot be later than the end date." - } - }, - "chargeStatus": { - "title": "Charge status", - "options": { - "all": "All", - "to_charge": "Por cobrar", - "in_charge": "Charged", - "to_refund": "To be refunded", - "refunded": "Refunded", - "to_wallet": "Por reembolsar a monedero", - "walleted": "Reembolsado a monedero" - } - }, - "projectStatus": { - "title": "Campaign status", - "options": { - "all": "All", - "active": "Active", - "finished": "Finished", - "cancelled": "Cancelled" - } - }, - "btns": { - "openFilters": "Filter", - "closeFilters": "Close Filters", - "reset": "Reset filters", - "apply": "Apply filters" - } - }, - "pagination": { - "showing": "Showing {{items}} of {{total}} items", - "unloaded": "Showing...", - "noResults": "No results" - }, - "tootip": { - "copied": "Copied!" - }, - "export": { - "csv": "Export CSV", - "exporting": "Exporting...", - "error": "Error when exporting CSV file. Please, try again.", - "errorAuth": "Authentication error. Please, log in again.", - "filename": "gateway-charges", - "itemsText": "items", - "noData": "No data found to export" - } - }, - "home": { - "campaigns": { - "title": "Latest campaigns", - "tabsTitle": "Campaigns in Goteo", - "tabs": { - "endingSoon": "Ending soon", - "mostSuccessful": "Most successful", - "withMatchfunding": "With Matchfunding" - }, - "viewAll": "See all", - "obtained": "Raised", - "minimum": "Minimum", - "optimum": "Optimum", - "daysRemaining": "{{days}} days remaining", - "matchfunding": "Matchfunding", - "status": { - "minimumReached": "Minimum achieved!", - "goForMinimum": "Go for the minimum!" - }, - "remaining": { - "toOptimum": "Falta para óptimo", - "toMinimum": "Falta para mínimo" - }, - "userDonations": "Your donations:" - }, - "blog": { - "title": "Big changes are coming... don't miss anything!", - "excerpt": "A process involving a significant technical migration of the current platform, and to do it with all the guarantees of security and without the loss of information, we need to carry out an orderly transition, with key dates for the publication of campaigns and making donations…", - "readMore": "Continue reading" - }, - "matchfunding": { - "title": "Matchfunding", - "description": "The match effect is only seen on Goteo. Since 2013, we have been looking for organizations to multiply the impact of crowdfunding. For every euro a person contributes to a Goteo campaign, a collaborating institution (foundation, public administration, company…) contributes the same amount, or even more.", - "actions": { - "learnMore": "Quiero saber más", - "viewCalls": "Ver convocatorias activas" - }, - "impact": { - "title": "Impact data", - "description": "Matchfunding not only multiplies financial resources but also generates tangible social impact. Essentially, matchfunding transforms microdonations into macro impact, validating projects with civic support and optimizing public/private resources.", - "matchers": "Contributed in Matchfunding:", - "successfulProjects": "Projects benefited:", - "successRate": "Success rate:" - }, - "cards": { - "grant": "Dotación convocatoria", - "inscription": "Fecha de inscripción", - "participants": "Proyectos participantes", - "apply": "Inscríbete en la convocatoria", - "viewCampaigns": "Ver las campañas de la convocatoria", - "finished": "Finished {{date}}" - } - }, - "stats": { - "title": "Goteo Statistics", - "theoryToAction": { - "title": "De la teoría a la acción", - "subtitle": "The figures that reveal our real footprint. Data as of December 2024.", - "projectsReceived": { - "value": "10,281", - "label": "Proyectos recibidos" - }, - "projectsPublished": { - "value": "2,813", - "label": "Proyectos publicados" - }, - "successRate": { - "value": "83%", - "label": "Tasa de éxito" - }, - "button": "Ver Stats de Goteo" - }, - "whyGoteo": { - "title": "Why Goteo?", - "subtitle": "We have values and the data backs us up. Data as of December 2024.", - "community": { - "value": "384,341", - "label": "Community" - }, - "averageDonors": { - "value": "152", - "label": "Average donors per project" - }, - "totalRaised": { - "value": "23M €", - "label": "Total raised" - }, - "button": "Want to know more" - } - }, - "stories": { - "title": "Goteo launches new website!", - "subtitle": "Delighted to share it with you", - "description": "Goteo has a new look! Navigate our revamped site and discover a more intuitive and engaging social crowdfunding experience. Explore innovative projects, meet the creators behind them, and join our community.\nBecome a donor and bring your ideas to life!", - "button": "Become a donor" - }, - "featuredPromoters": { - "title": "Featured Promoters", - "viewAll": "See all", - "projectsPromoted": "Projects promoted", - "capitalRaised": "Capital raised", - "promoterImage": "Promoter's picture", - "shareProfile": "Share {{name}}'s profile" - }, - "resources": { - "title": "Resources: Fuel for Creative Projects", - "viewAll": "See more resources", - "readMore": "Read more", - "articles": { - "firstCampaign": { - "title": "How do I run my first campaign?", - "description": "Your project deserves to take off: Master social crowdfunding with our FREE TOOLBOX. Practical guide + downloadable templates to win over your audience." - }, - "testimonials": { - "title": "Testimonials from our promoters", - "description": "Who can better tell you what it's like to run successful campaigns? Our promoters share firsthand what it's like to manage a campaign with Goteo." - } - } - } - }, - "admin": { - "projects": { - "totalizers": { - "selectedCampaigns": "Selected campaigns:", - "totalEarned": "Total earned:", - "totalPaid": "Total paid:", - "totalUnpaid": "Pending payment:" - } - }, - "charges": { - "lastContributions": "Latest contributions:", - "totalizers": { - "totalCharges": "Total contributions:", - "totalTips": "Total tips:", - "totalFees": "Total commissions:" - } - } - }, - "footer": { - "funding": { - "title": "Funding:", - "partOf": "We are part of:" - }, - "navigation": { - "collaborate": { - "title": "Collaborate", - "createProject": "Create a project", - "matchfunding": "Matchfunding", - "workshops": "Workshops" - }, - "help": { - "title": "Help", - "faqs": "FAQs", - "contact": "Contact", - "taxCalculator": "Tax calculator" - }, - "aboutGoteo": { - "title": "About Goteo", - "whoWeAre": "Who are we?", - "goteoBenefits": "Benefits of Goteo", - "foundationImpact": "Foundation impact", - "platoniqFoundation": "Platoniq Foundation" - }, - "forUsers": { - "title": "For users", - "rechargeWallet": "Recharge your wallet", - "downloadCertificate": "Download certificate", - "accessComplementaryFunds": "Access complementary funds", - "designFundingProgram": "Design a funding program" - } - }, - "legal": { - "termsOfUse": "Terms of use", - "privacyPolicy": "Privacy policy", - "licenses": "Licenses", - "stats": "Stats" - }, - "platoniq": { - "foundation": "Foundation", - "name": "Platoniq" - } - }, - "create": { - "project": { - "title": "Propose a campaign", - "subtitle": "Briefly describe the project and the crowdfunding campaign.", - "budgetPreview": "Money needed", - "description": { - "title": "Briefly describe your proposal", - "titlePrompt": "Enter the name of your campaign", - "titlePlaceholder": "Campaign name", - "subtitle": "Enter the name of your campaign and briefly describe the project you will carry out if you obtain funding.", - "subtitlePrompt": "Briefly describe your project.", - "subtitlePlaceholder": "No description" - }, - "categories": { - "title": "Which category fits your campaign?", - "subtitle": "(You can choose a maximum of 2)" - }, - "budget": { - "title": "How much money do you need?", - "subtitle": "Select the amount you need to execute your project.", - "amountPrompt": "Enter the amount you need" - }, - "release": { - "title": "Campaign start date", - "subtitle": "(Consider that crowdfunding campaigns have a minimum duration of 40 days)" - }, - "submit": "Propose campaign" - } - }, - "profile": { - "share": "Share", - "subscribe": "Subscribe", - "tabs": { - "about": "About the profile", - "projects": "Projects promoted", - "donorType": "Donor type" - }, - "notFound": "User not found", - "location": "Location", - "userIntro": "Active member of the Goteo community", - "organizationIntro": "Organization committed to social change", - "bioSample": "With a track record dedicated to the crowdfunding and collective funding ecosystem, {{name}} has established itself as a reference in creating collaborative projects and resource mobilization. Their vision focuses on empowering initiatives that transform local realities, combining innovative strategies with a deep commitment to citizen participation and social equity.", - "noProjects": "No projects promoted yet.", - "donorType": { - "fan": { - "title": "Fan", - "description": "This donor has invested more than €25", - "projectsDonated": "Projects donated", - "moneyDonated": "Money donated" - }, - "nextLevel": { - "title": "Next level", - "description": "Keep donating to level up" - } - } - }, - "me": { - "page": { - "title": "Activity", - "subtitle": "Check your activity on Goteo" - }, - "period": { - "label": "Period", - "year": "Year {{year}}" - }, - "donations": { - "title": "My donations", - "empty": "You have not donated to any project yet", - "explore": "Explore projects", - "illustration": "/images/profile/ilustration-donations.png", - "ctaLink": "/projects", - "count": "Donations", - "total": "Total donated", - "recent": "Latest donations", - "viewAll": "See all", - "certificate": "Request certificate" - }, - "projects": { - "title": "Projects promoted", - "empty": "You have not promoted any project yet", - "create": "Promote your project", - "illustration": "/images/profile/ilustration-project.png", - "ctaLink": "/projects/new", - "count": "Projects", - "raised": "Raised", - "recent": "Latest projects promoted", - "viewAll": "See all", - "createNew": "Create a project" - }, - "ownedProjects": { - "title": "Your Active Projects", - "messageToDonatorsButton": "Message to donors", - "uploadNewsButton": "Upload update" - }, - "donatedProjects": { - "title": "Active projects you donated" - }, - "cta": { - "discover": { - "title": "Discover, connect and donate", - "description": "Imagine a place where hundreds of social projects await your support. On our platform, exploring is easy: browse through community, environmental or educational causes. Because here, you decide which stories are written.", - "button": "Donate to a project" - }, "matchfunding": { - "title": "Donations that shine. Projects that inspire.", - "description": "Goteo is a pioneer in matchfunding. Whether you are a donor looking to enhance your contribution to projects, a promoter wanting to increase your budget, or an entity aiming to support projects and promoters through a donation boost... our matchfunding program allows you to multiply the momentum for change", - "learnMore": "Discover how matchfunding works", - "becomeMatcher": "Become a Matcher" + "card": { + "title": "Matchfunding", + "calls": "Convocatorias", + "donated": "Donado", + "recent": "Últimas convocatorias", + "viewAll": "Ver convocatorias", + "create": "Crear convocatoria" + }, + "section": { + "title": "Tus convocatorias de Matchfunding activas", + "error": "Error al cargar las convocatorias" + }, + "callCard": { + "donationCall": "Dotación convocatoria", + "participatingProjects": "Proyectos participantes", + "successfulProjects": "Proyectos acogidos con éxito", + "projects": "proyectos", + "viewCall": "Ir a la convocatoria" + } } }, - "matchfunding": { - "card": { - "title": "Matchfunding", - "calls": "Calls", - "donated": "Donated", - "recent": "Latest calls", - "viewAll": "See calls", - "create": "Create call" - }, - "section": { - "title": "Your active Matchfunding calls", - "empty": "You have no active calls", - "error": "Error loading calls" - }, - "callCard": { - "donationCall": "Call Donation Package", - "participatingProjects": "Participating projects", - "successfulProjects": "Successfully accommodated projects", - "projects": "projects", - "viewCall": "Go to call" + "blog": { + "share": { + "modal": { + "title": "Difunde este blog por el mundo", + "description": "Puedes copiar el enlace para enviarlo y también compartirla en Facebook o X." + } } } - }, - "lang": { - "error": { - "notAvailable": "The selected language \"{{lang}}\" is not available for this project. Displaying content in the available language." - } } } From 0981a4e541a7d8550ef7b2bb70582b53fbd07966 Mon Sep 17 00:00:00 2001 From: Dayv1dDev Date: Thu, 16 Jul 2026 17:58:05 +0200 Subject: [PATCH 07/47] fix: change apiKey var to param in FilterSubject object, allow referent to be Date & number too, and add resources key to clarify in which resources is each filter subject attached --- src/utils/filterComposer.ts | 115 +++++++++++------------------------- 1 file changed, 34 insertions(+), 81 deletions(-) diff --git a/src/utils/filterComposer.ts b/src/utils/filterComposer.ts index 9ade1bed..bd70c3cb 100644 --- a/src/utils/filterComposer.ts +++ b/src/utils/filterComposer.ts @@ -2,7 +2,7 @@ export interface FilterRow { id: string; subject: FilterSubject; operator: FilterOperator; - referent: string | string[]; + referent: string | number | Date | string[]; serialize: () => Record; } @@ -18,89 +18,42 @@ export type FilterOperator = | "lte" | "lt"; +export type FilterResource = "project" | "gateway_charge"; + export interface FilterSubject { key: string; + param?: string; type: "string" | "date" | "number"; compatibleOperators: FilterOperator[]; + resources: FilterResource[]; } const filterSubjects: Record = { - title: { - key: "title", - type: "string", - compatibleOperators: ["equals"], - }, - subtitle: { - key: "subtitle", - type: "string", - compatibleOperators: ["equals"], - }, - description: { - key: "description", - type: "string", - compatibleOperators: ["equals"], - }, - status: { - key: "status", - type: "string", - compatibleOperators: ["equals", "is_any_of"], - }, - categories: { - key: "categories", - type: "string", - compatibleOperators: ["equals", "is_any_of"], - }, - owner: { - key: "owner", - type: "string", - compatibleOperators: ["equals", "is_any_of"], - }, - slug: { - key: "slug", - type: "string", - compatibleOperators: ["equals", "is_any_of"], - }, - accounting: { - key: "accounting", - type: "string", - compatibleOperators: ["equals", "is_any_of"], - }, - territoryCountry: { - key: "territory.country", - type: "string", - compatibleOperators: ["equals", "is_any_of"], - }, - territorySubLvl1: { - key: "territory.subLvl1", - type: "string", - compatibleOperators: ["equals", "is_any_of"], - }, - territorySubLvl2: { - key: "territory.subLvl2", - type: "string", - compatibleOperators: ["equals", "is_any_of"], - }, - releaseDate: { - key: "releaseDate", - type: "date", - compatibleOperators: ["before", "after", "strictly_before", "strictly_after"], - }, - dateUpdated: { - key: "dateUpdated", - type: "date", - compatibleOperators: ["before", "after", "strictly_before", "strictly_after"], - }, - budgetAmount: { - key: "budgetAmount", - type: "number", - compatibleOperators: ["gte", "gt", "lte", "lt"], - }, + title: { key: "title", type: "string", compatibleOperators: ["equals"], resources: ["project"] }, + subtitle: { key: "subtitle", type: "string", compatibleOperators: ["equals"], resources: ["project"] }, + description: { key: "description", type: "string", compatibleOperators: ["equals"], resources: ["project"] }, + status: { key: "status", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project", "gateway_charge"] }, + categories: { key: "categories", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project"] }, + owner: { key: "owner", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project"] }, + slug: { key: "slug", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project"] }, + accounting: { key: "accounting", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project"] }, + territoryCountry: { key: "territoryCountry", param: "territory.country", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project"] }, + territorySubLvl1: { key: "territorySubLvl1", param: "territory.subLvl1", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project"] }, + territorySubLvl2: { key: "territorySubLvl2", param: "territory.subLvl2", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project"] }, + dateUpdated: { key: "dateUpdated", type: "date", compatibleOperators: ["before", "after", "strictly_before", "strictly_after"], resources: ["project", "gateway_charge"] }, + budgetAmount: { key: "budgetAmount", type: "number", compatibleOperators: ["gte", "gt", "lte", "lt"], resources: ["project"] }, + gateway: { key: "gateway", param: "checkout.gateway", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["gateway_charge"] }, + type: { key: "type", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["gateway_charge"] }, + target: { key: "target", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["gateway_charge"] }, + currency: { key: "currency", param: "money.currency", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["gateway_charge"] }, + amount: { key: "amount", param: "money.amount", type: "number", compatibleOperators: ["gte", "gt", "lte", "lt"], resources: ["gateway_charge"] }, + dateCreated: { key: "dateCreated", type: "date", compatibleOperators: ["before", "after", "strictly_before", "strictly_after"], resources: ["gateway_charge"] }, }; export function createFilterRow( subject: FilterSubject, operator: FilterOperator, - referent: string | string[], + referent: string | number | Date | string[], ): FilterRow { const id = crypto.randomUUID(); @@ -110,28 +63,27 @@ export function createFilterRow( operator, referent, serialize: () => { - const key = subject.key; + const key = subject.param ?? subject.key; switch (operator) { case "equals": - return { [key]: referent as string }; + return { [key]: String(referent) }; case "is_any_of": return { [`${key}[]`]: referent as string[] }; case "before": case "after": case "strictly_before": case "strictly_after": { - const apiKey = key === "releaseDate" ? "dateCreated" : key; - const dateStr = new Date(referent as string).toISOString(); - return { [`${apiKey}[${operator}]`]: dateStr }; + const date = referent instanceof Date ? referent : new Date(referent as string); + return { [`${key}[${operator}]`]: date.toISOString() }; } case "gte": case "gt": case "lte": case "lt": - return { [`${key}[${operator}]`]: referent as string }; + return { [`${key}[${operator}]`]: String(referent) }; default: - return { [key]: referent as string }; + return { [key]: String(referent) }; } }, }; @@ -141,6 +93,7 @@ export function getFilterSubject(key: string): FilterSubject | undefined { return filterSubjects[key]; } -export function getAllFilterSubjects(): FilterSubject[] { - return Object.values(filterSubjects); +export function getAllFilterSubjects(resource?: FilterResource): FilterSubject[] { + const all = Object.values(filterSubjects); + return resource ? all.filter((s) => s.resources.includes(resource)) : all; } From 841e8cf0bfff204f785a421b6780e9c35d3d11ef Mon Sep 17 00:00:00 2001 From: Dayv1dDev Date: Thu, 16 Jul 2026 18:03:40 +0200 Subject: [PATCH 08/47] fix: format by prettier --- src/utils/filterComposer.ts | 139 +++++++++++++++++++++++++++++++----- 1 file changed, 120 insertions(+), 19 deletions(-) diff --git a/src/utils/filterComposer.ts b/src/utils/filterComposer.ts index bd70c3cb..670d0a0b 100644 --- a/src/utils/filterComposer.ts +++ b/src/utils/filterComposer.ts @@ -29,25 +29,126 @@ export interface FilterSubject { } const filterSubjects: Record = { - title: { key: "title", type: "string", compatibleOperators: ["equals"], resources: ["project"] }, - subtitle: { key: "subtitle", type: "string", compatibleOperators: ["equals"], resources: ["project"] }, - description: { key: "description", type: "string", compatibleOperators: ["equals"], resources: ["project"] }, - status: { key: "status", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project", "gateway_charge"] }, - categories: { key: "categories", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project"] }, - owner: { key: "owner", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project"] }, - slug: { key: "slug", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project"] }, - accounting: { key: "accounting", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project"] }, - territoryCountry: { key: "territoryCountry", param: "territory.country", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project"] }, - territorySubLvl1: { key: "territorySubLvl1", param: "territory.subLvl1", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project"] }, - territorySubLvl2: { key: "territorySubLvl2", param: "territory.subLvl2", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["project"] }, - dateUpdated: { key: "dateUpdated", type: "date", compatibleOperators: ["before", "after", "strictly_before", "strictly_after"], resources: ["project", "gateway_charge"] }, - budgetAmount: { key: "budgetAmount", type: "number", compatibleOperators: ["gte", "gt", "lte", "lt"], resources: ["project"] }, - gateway: { key: "gateway", param: "checkout.gateway", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["gateway_charge"] }, - type: { key: "type", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["gateway_charge"] }, - target: { key: "target", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["gateway_charge"] }, - currency: { key: "currency", param: "money.currency", type: "string", compatibleOperators: ["equals", "is_any_of"], resources: ["gateway_charge"] }, - amount: { key: "amount", param: "money.amount", type: "number", compatibleOperators: ["gte", "gt", "lte", "lt"], resources: ["gateway_charge"] }, - dateCreated: { key: "dateCreated", type: "date", compatibleOperators: ["before", "after", "strictly_before", "strictly_after"], resources: ["gateway_charge"] }, + title: { + key: "title", + type: "string", + compatibleOperators: ["equals"], + resources: ["project"], + }, + subtitle: { + key: "subtitle", + type: "string", + compatibleOperators: ["equals"], + resources: ["project"], + }, + description: { + key: "description", + type: "string", + compatibleOperators: ["equals"], + resources: ["project"], + }, + status: { + key: "status", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + resources: ["project", "gateway_charge"], + }, + categories: { + key: "categories", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + resources: ["project"], + }, + owner: { + key: "owner", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + resources: ["project"], + }, + slug: { + key: "slug", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + resources: ["project"], + }, + accounting: { + key: "accounting", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + resources: ["project"], + }, + territoryCountry: { + key: "territoryCountry", + param: "territory.country", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + resources: ["project"], + }, + territorySubLvl1: { + key: "territorySubLvl1", + param: "territory.subLvl1", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + resources: ["project"], + }, + territorySubLvl2: { + key: "territorySubLvl2", + param: "territory.subLvl2", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + resources: ["project"], + }, + dateUpdated: { + key: "dateUpdated", + type: "date", + compatibleOperators: ["before", "after", "strictly_before", "strictly_after"], + resources: ["project", "gateway_charge"], + }, + budgetAmount: { + key: "budgetAmount", + type: "number", + compatibleOperators: ["gte", "gt", "lte", "lt"], + resources: ["project"], + }, + gateway: { + key: "gateway", + param: "checkout.gateway", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + resources: ["gateway_charge"], + }, + type: { + key: "type", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + resources: ["gateway_charge"], + }, + target: { + key: "target", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + resources: ["gateway_charge"], + }, + currency: { + key: "currency", + param: "money.currency", + type: "string", + compatibleOperators: ["equals", "is_any_of"], + resources: ["gateway_charge"], + }, + amount: { + key: "amount", + param: "money.amount", + type: "number", + compatibleOperators: ["gte", "gt", "lte", "lt"], + resources: ["gateway_charge"], + }, + dateCreated: { + key: "dateCreated", + type: "date", + compatibleOperators: ["before", "after", "strictly_before", "strictly_after"], + resources: ["gateway_charge"], + }, }; export function createFilterRow( From 3256d064273642f58055aa40cff07c607b8f23e4 Mon Sep 17 00:00:00 2001 From: Pep Villalonga Date: Mon, 20 Jul 2026 12:29:03 +0200 Subject: [PATCH 09/47] feat: checkouts-store-dexie --- crowdin.yml | 6 +- src/components/checkout/Cart.svelte | 2 +- src/components/checkout/CartButton.svelte | 4 +- src/components/checkout/CartItem.svelte | 10 +- src/components/checkout/PaymentMethod.astro | 53 ++- src/components/checkout/Summary.svelte | 2 +- src/components/checkout/Tipjar.svelte | 8 +- .../checkout/WalletConfirmation.astro | 14 +- src/components/checkout/WalletInfo.svelte | 9 +- src/components/project/ProjectRewards.svelte | 3 +- src/components/project/RewardModal.svelte | 3 +- src/pages/[...locale]/checkout/verify.astro | 7 +- src/pages/[...locale]/register.astro | 4 +- src/stores/cart.ts | 185 ---------- src/stores/checkoutsStore.ts | 327 ++++++++++++++++++ src/utils/checkouts/db.ts | 11 + src/utils/checkouts/migrate.ts | 49 +++ src/utils/checkouts/repository.ts | 51 +++ 18 files changed, 497 insertions(+), 251 deletions(-) delete mode 100644 src/stores/cart.ts create mode 100644 src/stores/checkoutsStore.ts create mode 100644 src/utils/checkouts/db.ts create mode 100644 src/utils/checkouts/migrate.ts create mode 100644 src/utils/checkouts/repository.ts diff --git a/crowdin.yml b/crowdin.yml index 4b672f0a..6c93ec47 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,8 +1,8 @@ project_id_env: CROWDIN_PROJECT_ID api_token_env: CROWDIN_PERSONAL_TOKEN base_path: . -base_url: 'https://api.crowdin.com' +base_url: "https://api.crowdin.com" preserve_hierarchy: 1 files: - - source: /src/i18n/locales/es.json - translation: /src/i18n/locales/%two_letters_code%.json + - source: /src/i18n/locales/es.json + translation: /src/i18n/locales/%two_letters_code%.json diff --git a/src/components/checkout/Cart.svelte b/src/components/checkout/Cart.svelte index 1a394aaf..0a2ca444 100644 --- a/src/components/checkout/Cart.svelte +++ b/src/components/checkout/Cart.svelte @@ -1,7 +1,7 @@ - 1 ? $cartCount : undefined} /> + 0 ? $cartCount : undefined} /> diff --git a/src/components/checkout/CartItem.svelte b/src/components/checkout/CartItem.svelte index 3f2ac73d..958f6be0 100644 --- a/src/components/checkout/CartItem.svelte +++ b/src/components/checkout/CartItem.svelte @@ -3,12 +3,12 @@ import { formatCurrency } from "../../utils/currencies"; import MoreAndLess from "../icons/filters/MoreAndLess.svelte"; - import type { CartItem } from "../../stores/cart"; + import type { CheckoutItem } from "../../stores/checkoutsStore"; - export let item: CartItem; - export let onIncrement: (item: CartItem) => void; - export let onDecrement: (item: CartItem) => void; - export let onRemove: (item: CartItem) => void; + export let item: CheckoutItem; + export let onIncrement: (item: CheckoutItem) => void; + export let onDecrement: (item: CheckoutItem) => void; + export let onRemove: (item: CheckoutItem) => void; function handleDecrement() { if (item.quantity === 1) { diff --git a/src/components/checkout/PaymentMethod.astro b/src/components/checkout/PaymentMethod.astro index afa82994..86f2408e 100644 --- a/src/components/checkout/PaymentMethod.astro +++ b/src/components/checkout/PaymentMethod.astro @@ -88,6 +88,8 @@ const { accounting } = Astro.props; diff --git a/src/components/project/ProjectRewards.svelte b/src/components/project/ProjectRewards.svelte index dcb5eafc..c563e945 100644 --- a/src/components/project/ProjectRewards.svelte +++ b/src/components/project/ProjectRewards.svelte @@ -5,7 +5,7 @@ apiAccountingsIdGet, apiProjectRewardsGetCollection, } from "../../openapi/client/index"; - import { cart } from "../../stores/cart"; + import { cart, checkoutReady } from "../../stores/checkoutsStore"; import { getUnit } from "../../utils/currencies"; import { extractId } from "../../utils/extractId"; import Button from "../library/buttons/Button.svelte"; @@ -75,6 +75,7 @@ }, }); + await checkoutReady(); window.location.href = "/checkout"; } diff --git a/src/components/project/RewardModal.svelte b/src/components/project/RewardModal.svelte index 848f1537..f52cc202 100644 --- a/src/components/project/RewardModal.svelte +++ b/src/components/project/RewardModal.svelte @@ -3,7 +3,7 @@ import { onMount } from "svelte"; import { t } from "../../i18n/store"; - import { cart } from "../../stores/cart"; + import { cart, checkoutReady } from "../../stores/checkoutsStore"; import { formatCurrency, getUnit } from "../../utils/currencies"; import { renderMarkdown } from "../../utils/renderMarkdown"; import UnitIcon from "../icons/UnitIcon.svelte"; @@ -53,6 +53,7 @@ }); if (action === "checkout") { + await checkoutReady(); window.location.href = "/checkout"; } else { open = false; diff --git a/src/pages/[...locale]/checkout/verify.astro b/src/pages/[...locale]/checkout/verify.astro index d92e8095..7fb65a42 100644 --- a/src/pages/[...locale]/checkout/verify.astro +++ b/src/pages/[...locale]/checkout/verify.astro @@ -176,7 +176,12 @@ const postPaymentUrl = checkoutId
- +
+ ) } diff --git a/src/pages/[...locale]/register.astro b/src/pages/[...locale]/register.astro index 2409613f..67dd1e87 100644 --- a/src/pages/[...locale]/register.astro +++ b/src/pages/[...locale]/register.astro @@ -5,14 +5,14 @@ import RegisterForm from "../../components/auth/RegisterForm.astro"; import AuthSummary from "../../components/auth/AuthSummary.svelte"; import Button from "../../components/library/buttons/Button.svelte"; import BackButton from "../../components/library/buttons/BackButton.svelte"; -import { cartCount } from "../../stores/cart"; +import { cartCount } from "../../stores/checkoutsStore"; import Summary from "../../components/checkout/Summary.svelte"; const { t, lang, session } = Astro.locals; let hasCart = false; cartCount.subscribe((count) => { - hasCart = count > 1; + hasCart = count > 0; }); --- diff --git a/src/stores/cart.ts b/src/stores/cart.ts deleted file mode 100644 index 545e2663..00000000 --- a/src/stores/cart.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { writable, derived } from "svelte/store"; - -import { apiProjectRewardsIdGetUrl } from "../openapi/client/paths.gen"; - -import type { GatewayCharge, ProjectReward } from "../openapi/client"; - -export interface CartItem extends GatewayCharge { - key: string; - kind: "free" | "reward" | "tip"; - quantity: number; - - /** - * `target` references the Accounting that will receive the money\ - * `recipient` references the owner of that Accounting - */ - recipient: string; - recipientDisplayName: string; - - /** - * Items of kind "reward" must have an associated ProjectReward - */ - reward?: ProjectReward; -} - -type CartState = { - items: Record; -}; - -export interface CartStore { - subscribe: (run: (value: CartState) => void) => () => void; - - addItem: (item: Omit) => void; - removeItem: (key: string) => void; - updateQuantity: (key: string, quantity: number) => void; - - clear: () => void; - clearTarget: (target: string) => void; -} - -const isBrowser = typeof window !== "undefined"; - -function generateKey(item: Omit): string { - const base = `${item.kind}:${item.recipient}`; - if (item.kind === "reward" && item.reward?.id != null) { - return `${base};reward:${apiProjectRewardsIdGetUrl.replace("{id}", String(item.reward.id))}`; - } - return base; -} - -export function parseKey(key: string): { - kind: string; - recipient: string; - extra: { kind: string; recipient: string }[]; -} { - const segments = key.split(";"); - const colonIdx = segments[0].indexOf(":"); - const kind = segments[0].slice(0, colonIdx); - const recipient = segments[0].slice(colonIdx + 1); - const extra = segments.slice(1).map((s) => { - const idx = s.indexOf(":"); - return { kind: s.slice(0, idx), recipient: s.slice(idx + 1) }; - }); - return { kind, recipient, extra }; -} - -function loadInitialCart(): CartState { - const fresh = { items: {} }; - - if (!isBrowser) return fresh; - - try { - const stored = localStorage.getItem("cart"); - if (stored) return JSON.parse(stored); - } catch (e) { - console.warn("⚠️ Error loading cart from localStorage:", e); - } - - localStorage.setItem("cart", JSON.stringify(fresh)); - return fresh; -} - -function createCartStore(): CartStore { - const { subscribe, set, update } = writable(loadInitialCart()); - - subscribe((cart) => { - try { - if (!isBrowser) return; - - localStorage.setItem("cart", JSON.stringify(cart)); - } catch (e) { - console.error("Error to save cart to localStorage:", e); - } - }); - - return { - subscribe, - - addItem: (item: Omit) => - update((cart) => { - const key = generateKey(item); - const items = { ...cart.items }; - - if (item.quantity === 0) { - delete items[key]; - return { items }; - } - - items[key] = { ...item, key } as CartItem; - - return { items }; - }), - - removeItem: (key: string) => - update((cart) => { - const items = { ...cart.items }; - delete items[key]; - return { items }; - }), - - updateQuantity: (key: string, quantity: number) => - update((cart) => { - const items = { ...cart.items }; - - if (quantity <= 0) { - delete items[key]; - } else if (items[key]) { - items[key] = { ...items[key], quantity }; - } - - return { items }; - }), - - clear: () => { - set({ items: {} }); - localStorage.removeItem("cart"); - }, - - clearTarget: (target: string) => - update((cart) => { - const items = Object.fromEntries( - Object.entries(cart.items).filter((item) => item[1].target !== target), - ); - - return { items }; - }), - }; -} - -export const cart = createCartStore(); - -export const cartCount = derived(cart, ($cart) => - Object.values($cart.items).reduce((total, item) => total + item.quantity, 0), -); - -export const cartAmount = derived(cart, ($cart) => - Object.values($cart.items).reduce( - (total, item) => total + item.money.amount * item.quantity, - 0, - ), -); - -export const cartByTarget = derived(cart, ($cart) => { - const grouped: Record = {}; - - for (const item of Object.values($cart.items)) { - if (item.target != null) { - grouped[item.target] ??= []; - grouped[item.target].push(item); - } - } - - return grouped; -}); - -export const cartByRecipient = derived(cart, ($cart) => { - const grouped: Record = {}; - - for (const item of Object.values($cart.items)) { - const { recipient } = parseKey(item.key); - grouped[recipient] ??= []; - grouped[recipient].push(item); - } - - return grouped; -}); diff --git a/src/stores/checkoutsStore.ts b/src/stores/checkoutsStore.ts new file mode 100644 index 00000000..b11b9e0a --- /dev/null +++ b/src/stores/checkoutsStore.ts @@ -0,0 +1,327 @@ +import { derived, get, writable } from "svelte/store"; + +import { session } from "../auth/store"; +import { apiProjectRewardsIdGetUrl } from "../openapi/client/paths.gen"; +import { migrateLegacyCart } from "../utils/checkouts/migrate"; +import { checkoutRepo } from "../utils/checkouts/repository"; + +import type { GatewayCharge, ProjectReward } from "../openapi/client"; + +export interface CheckoutItem extends GatewayCharge { + key: string; + kind: "free" | "reward" | "tip"; + quantity: number; + + /** + * `target` references the Accounting that will receive the money\ + * `recipient` references the owner of that Accounting + */ + recipient: string; + recipientDisplayName: string; + + /** + * Items of kind "reward" must have an associated ProjectReward + */ + reward?: ProjectReward; +} + +/** + * IndexedDB record holding the pending checkout of a single user. + * Keyed by `user:` for authenticated sessions, `user:guest` otherwise. + */ +export interface StoredCheckout { + key: string; + items: Record; + updatedAt: number; +} + +type CheckoutState = { + items: Record; +}; + +export interface CheckoutStore { + subscribe: (run: (value: CheckoutState) => void) => () => void; + + addItem: (item: Omit) => void; + removeItem: (key: string) => void; + updateQuantity: (key: string, quantity: number) => void; + + clear: () => void; + clearTarget: (target: string) => void; + clearForUser: (userId?: string | number) => void; +} + +const isBrowser = typeof window !== "undefined"; + +export const GUEST_CHECKOUT_KEY = "user:guest"; + +export function checkoutKeyForUser(userId?: string | number): string { + return userId != null && userId !== "" ? `user:${userId}` : GUEST_CHECKOUT_KEY; +} + +function generateKey(item: Omit): string { + const base = `${item.kind}:${item.recipient}`; + if (item.kind === "reward" && item.reward?.id != null) { + return `${base};reward:${apiProjectRewardsIdGetUrl.replace("{id}", String(item.reward.id))}`; + } + return base; +} + +export function parseKey(key: string): { + kind: string; + recipient: string; + extra: { kind: string; recipient: string }[]; +} { + const segments = key.split(";"); + const colonIdx = segments[0].indexOf(":"); + const kind = segments[0].slice(0, colonIdx); + const recipient = segments[0].slice(colonIdx + 1); + const extra = segments.slice(1).map((s) => { + const idx = s.indexOf(":"); + return { kind: s.slice(0, idx), recipient: s.slice(idx + 1) }; + }); + return { kind, recipient, extra }; +} + +const state = writable({ items: {} }); + +const hydrated = writable(false); + +/** + * True once the active user's record has been loaded from IndexedDB + * into the in-memory store. + */ +export const checkoutHydrated = { subscribe: hydrated.subscribe }; + +let activeKey = GUEST_CHECKOUT_KEY; +let skipHydrationMerge = false; +let queue: Promise = Promise.resolve(); + +function enqueue(task: () => Promise): Promise { + queue = queue.then(task).catch((e) => { + console.error("Error persisting checkout to IndexedDB:", e); + }); + return queue; +} + +/** + * Resolves once every persistence operation enqueued so far has settled. + */ +export function checkoutReady(): Promise { + return queue; +} + +function schedulePersist() { + if (!isBrowser) return; + + const key = activeKey; + enqueue(async () => { + const { items } = get(state); + await checkoutRepo.update(key, { items }); + }); +} + +function removeLegacyStorageKey() { + try { + localStorage.removeItem("cart"); + } catch { + // ignore storage access errors + } +} + +if (isBrowser) { + enqueue(async () => { + // Snapshot the key: `activeKey` can flip mid-flight when the session + // store resolves (guest → user), and writing the guest-derived items + // back under the user key would wipe their stored checkout. + const key = activeKey; + await migrateLegacyCart(); + + const record = await checkoutRepo.get(key); + const loaded = record?.items ?? {}; + + if (!skipHydrationMerge) { + // Mutations made before hydration completes win over stored items + let merged: Record = {}; + state.update((current) => { + merged = { ...loaded, ...current.items }; + return { items: merged }; + }); + await checkoutRepo.update(key, { items: merged }); + } + + hydrated.set(true); + }); + + session.subscribe((currentSession) => { + const nextKey = checkoutKeyForUser(currentSession?.user?.id); + if (nextKey === activeKey) return; + + const previousKey = activeKey; + activeKey = nextKey; + + enqueue(async () => { + if (previousKey === GUEST_CHECKOUT_KEY && nextKey !== GUEST_CHECKOUT_KEY) { + // Merge the guest checkout into the user's record on login. + // On conflicting item keys the user's stored item wins. + const guest = await checkoutRepo.get(GUEST_CHECKOUT_KEY); + const guestItems = guest?.items ?? {}; + + if (Object.keys(guestItems).length > 0) { + const userRecord = await checkoutRepo.get(nextKey); + await checkoutRepo.update(nextKey, { + items: { ...guestItems, ...userRecord?.items }, + }); + } + + await checkoutRepo.remove(GUEST_CHECKOUT_KEY); + } + + const record = await checkoutRepo.get(nextKey); + state.set({ items: record?.items ?? {} }); + }); + }); + + const resyncActiveRecord = () => { + if (!get(hydrated)) return; + + const key = activeKey; + enqueue(async () => { + const record = await checkoutRepo.get(key); + state.set({ items: record?.items ?? {} }); + }); + }; + + window.addEventListener("pageshow", (event) => { + if (event.persisted) resyncActiveRecord(); + }); + + document.addEventListener("visibilitychange", () => { + if (document.visibilityState === "visible") resyncActiveRecord(); + }); +} + +export const cart: CheckoutStore = { + subscribe: state.subscribe, + + addItem: (item: Omit) => { + state.update((current) => { + const key = generateKey(item); + const items = { ...current.items }; + + if (item.quantity === 0) { + delete items[key]; + return { items }; + } + + items[key] = { ...item, key } as CheckoutItem; + + return { items }; + }); + schedulePersist(); + }, + + removeItem: (key: string) => { + state.update((current) => { + const items = { ...current.items }; + delete items[key]; + return { items }; + }); + schedulePersist(); + }, + + updateQuantity: (key: string, quantity: number) => { + state.update((current) => { + const items = { ...current.items }; + + if (quantity <= 0) { + delete items[key]; + } else if (items[key]) { + items[key] = { ...items[key], quantity }; + } + + return { items }; + }); + schedulePersist(); + }, + + clear: () => { + skipHydrationMerge = true; + state.set({ items: {} }); + + if (!isBrowser) return; + + const key = activeKey; + enqueue(() => checkoutRepo.remove(key)); + removeLegacyStorageKey(); + }, + + clearTarget: (target: string) => { + state.update((current) => { + const items = Object.fromEntries( + Object.entries(current.items).filter((item) => item[1].target !== target), + ); + + return { items }; + }); + schedulePersist(); + }, + + clearForUser: (userId?: string | number) => { + skipHydrationMerge = true; + state.set({ items: {} }); + + if (!isBrowser) return; + + enqueue(async () => { + await checkoutRepo.remove(GUEST_CHECKOUT_KEY); + + const key = checkoutKeyForUser(userId); + if (key !== GUEST_CHECKOUT_KEY) { + await checkoutRepo.remove(key); + } + }); + removeLegacyStorageKey(); + }, +}; + +/** + * Standalone export so Astro inline scripts can clear without pulling in the store object. + */ +export const clearForUser = cart.clearForUser; + +export const cartCount = derived(cart, ($cart) => + Object.values($cart.items).reduce((total, item) => total + item.quantity, 0), +); + +export const cartAmount = derived(cart, ($cart) => + Object.values($cart.items).reduce( + (total, item) => total + item.money.amount * item.quantity, + 0, + ), +); + +export const cartByTarget = derived(cart, ($cart) => { + const grouped: Record = {}; + + for (const item of Object.values($cart.items)) { + if (item.target != null) { + grouped[item.target] ??= []; + grouped[item.target].push(item); + } + } + + return grouped; +}); + +export const cartByRecipient = derived(cart, ($cart) => { + const grouped: Record = {}; + + for (const item of Object.values($cart.items)) { + const { recipient } = parseKey(item.key); + grouped[recipient] ??= []; + grouped[recipient].push(item); + } + + return grouped; +}); diff --git a/src/utils/checkouts/db.ts b/src/utils/checkouts/db.ts new file mode 100644 index 00000000..2a986fac --- /dev/null +++ b/src/utils/checkouts/db.ts @@ -0,0 +1,11 @@ +import { Dexie, type EntityTable } from "dexie"; + +import type { StoredCheckout } from "../../stores/checkoutsStore"; + +export const db = new Dexie("goteo-checkouts") as Dexie & { + checkouts: EntityTable; +}; + +db.version(1).stores({ + checkouts: "key", +}); diff --git a/src/utils/checkouts/migrate.ts b/src/utils/checkouts/migrate.ts new file mode 100644 index 00000000..90a002e5 --- /dev/null +++ b/src/utils/checkouts/migrate.ts @@ -0,0 +1,49 @@ +import { checkoutRepo } from "./repository"; + +import type { CheckoutItem } from "../../stores/checkoutsStore"; + +const LEGACY_STORAGE_KEY = "cart"; +const GUEST_KEY = "user:guest"; + +/** + * One-time migration of the legacy localStorage cart blob into the guest + * IndexedDB record. The old store re-seeded the "cart" key on every page + * load, so the key is removed unconditionally — even on unparseable data — + * to guarantee the migration runs at most once per browser. Never throws. + */ +export async function migrateLegacyCart(): Promise { + if (typeof window === "undefined") return; + + let raw: string | null = null; + try { + raw = localStorage.getItem(LEGACY_STORAGE_KEY); + if (raw === null) return; + + localStorage.removeItem(LEGACY_STORAGE_KEY); + } catch { + return; + } + + try { + const parsed = JSON.parse(raw) as { items?: Record }; + const legacyItems = Object.fromEntries( + Object.entries(parsed?.items ?? {}).filter( + ([key, item]) => + typeof key === "string" && + item != null && + typeof item.quantity === "number" && + item.quantity > 0, + ), + ); + + if (Object.keys(legacyItems).length === 0) return; + + // An existing guest record is newer than the legacy blob — it wins on conflicts + const existing = await checkoutRepo.get(GUEST_KEY); + await checkoutRepo.update(GUEST_KEY, { + items: { ...legacyItems, ...existing?.items }, + }); + } catch (e) { + console.warn("⚠️ Error migrating legacy cart from localStorage:", e); + } +} diff --git a/src/utils/checkouts/repository.ts b/src/utils/checkouts/repository.ts new file mode 100644 index 00000000..12ef994e --- /dev/null +++ b/src/utils/checkouts/repository.ts @@ -0,0 +1,51 @@ +import { db } from "./db"; + +import type { StoredCheckout } from "../../stores/checkoutsStore"; + +const isBrowser = typeof window !== "undefined"; + +/** + * IndexedDB stores via structured clone, which throws DataCloneError on + * Svelte 5 $state proxies (e.g. items built from $bindable props). + * A JSON round-trip strips proxies down to plain clonable objects. + */ +function toPlain(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +export const checkoutRepo = { + async create(record: StoredCheckout) { + if (!isBrowser) return record; + + await db.checkouts.put(toPlain({ ...record, updatedAt: Date.now() })); + return record; + }, + + async get(key: string) { + if (!isBrowser) return undefined; + + return db.checkouts.get(key); + }, + + async update(key: string, data: Partial) { + if (!isBrowser) return; + + const existing = await db.checkouts.get(key); + + await db.checkouts.put( + toPlain({ + key, + items: {}, + ...existing, + ...data, + updatedAt: Date.now(), + }), + ); + }, + + async remove(key: string) { + if (!isBrowser) return; + + await db.checkouts.delete(key); + }, +}; From 8a2628cfb29b8b429aac9b9910a348027fad082f Mon Sep 17 00:00:00 2001 From: Pep Villalonga Date: Mon, 20 Jul 2026 13:34:39 +0200 Subject: [PATCH 10/47] fix: solve PR mistakes --- src/actions/payment.ts | 6 +- src/components/admin/Contributions.svelte | 129 ++---------------- src/components/checkout/PaymentMethod.astro | 9 +- src/openapi/client/paths.gen.ts | 4 +- src/openapi/client/sdk.gen.ts | 31 ++++- src/openapi/client/types.gen.ts | 112 +++++++++++++-- .../create/CreateProjectForm.svelte | 10 +- 7 files changed, 160 insertions(+), 141 deletions(-) diff --git a/src/actions/payment.ts b/src/actions/payment.ts index 09fa7440..a88f3717 100644 --- a/src/actions/payment.ts +++ b/src/actions/payment.ts @@ -2,7 +2,7 @@ import { defineAction, ActionError } from "astro:actions"; import { apiGatewayCheckoutsPost, type GatewayCharge } from "../openapi/client"; import { client } from "../openapi/client/client.gen"; -import { apiGatewaysNameGetUrl } from "../openapi/client/paths.gen"; +import { apiGatewaysIdGetUrl } from "../openapi/client/paths.gen"; import { Unauthorized } from "../utils/responses"; export const payment = defineAction({ @@ -50,8 +50,8 @@ export const payment = defineAction({ body: { origin: session.user.accounting!, gateway: client.buildUrl({ - url: apiGatewaysNameGetUrl, - path: { name: paymentMethod }, + url: apiGatewaysIdGetUrl, + path: { id: paymentMethod }, }), returnUrl: `${context.url.origin}/checkout/verify`, charges, diff --git a/src/components/admin/Contributions.svelte b/src/components/admin/Contributions.svelte index 680dcf47..28d74d40 100644 --- a/src/components/admin/Contributions.svelte +++ b/src/components/admin/Contributions.svelte @@ -9,10 +9,10 @@ import Table, { type ExtendedCharge } from "./Table.svelte"; import { session } from "../../auth/store"; import { t } from "../../i18n/store"; - import { client } from "../../openapi/client/client.gen"; import { apiAccountingsIdGet, apiGatewayChargesGetCollection, + apiGatewayChargestotalsGetCollection, apiGatewayCheckoutsIdGet, apiGatewaysGetCollection, apiProjectsIdOrSlugGet, @@ -20,7 +20,7 @@ apiUsersIdOrHandleGet, type Accounting, type ApiGatewayChargesGetCollectionData, - type ApiProjectsGetCollectionData, + type ApiGatewayChargestotalsGetCollectionData, type GatewayCharge, type GatewayCheckout, type Project, @@ -82,47 +82,14 @@ } } - // Preferred source: the /v4/gateway_charges/stats aggregate (may not be deployed yet). - // Fallback: scan the filtered collection page by page (same approach as ExportCsv) - // and deduplicate targets — only viable for small result sets, hence the cap. - // Results are cached per filter set for the session. - const CHARGES_SCAN_PAGE_SIZE = 100; - const CHARGES_SCAN_CONCURRENCY = 3; - const MAX_SCANNABLE_CHARGES = 2000; - const PROJECTS_LOOKUP_CHUNK_SIZE = 50; - const projectsCountCache = new Map(); + // Distinct count of Projects targeted by the charges matching the current filters. + // Served by the /v4/gateway_charges/totals aggregate (same filters as the collection). let projectsCountRequestId = 0; - async function fetchDistinctProjectsFromStats( - chargeFilters: ApiGatewayChargesGetCollectionData["query"], - headers: HeadersInit, - ): Promise { - try { - const { data, response } = await client.get({ - url: "/v4/gateway_charges/stats", - query: chargeFilters as Record, - headers, - }); - if (!response.ok || !data || typeof data !== "object") return null; - - const count = (data as Record).distinctProjectCount; - return typeof count === "number" ? count : null; - } catch { - return null; - } - } - async function loadSelectedProjectsCount( chargeFilters: ApiGatewayChargesGetCollectionData["query"], ) { const requestId = ++projectsCountRequestId; - const cacheKey = JSON.stringify(chargeFilters ?? {}); - const cached = projectsCountCache.get(cacheKey); - if (cached !== undefined) { - selectedProjectsCount = cached; - return; - } - selectedProjectsCount = "—"; const headers = { @@ -130,85 +97,19 @@ ...($session?.token.asHttpHeaders ?? {}), }; - const fetchChargesPage = async (page: number) => { - const { data, error } = await apiGatewayChargesGetCollection({ - query: { ...chargeFilters, page, itemsPerPage: CHARGES_SCAN_PAGE_SIZE }, - headers, - }); - if (error) throw error; - return data; - }; + const { data, error } = await apiGatewayChargestotalsGetCollection({ + query: chargeFilters as ApiGatewayChargestotalsGetCollectionData["query"], + headers, + }); - try { - const statsCount = await fetchDistinctProjectsFromStats(chargeFilters, headers); - if (requestId !== projectsCountRequestId) return; - if (statsCount !== null) { - projectsCountCache.set(cacheKey, statsCount); - selectedProjectsCount = statsCount; - return; - } + if (requestId !== projectsCountRequestId) return; - const targets = new Set(); - const collectTargets = (collection: unknown) => { - for (const charge of toCollectionItems(collection)) { - if (charge.target) targets.add(charge.target); - } - }; - - const firstPage = await fetchChargesPage(1); - collectTargets(firstPage); - - // With a single target filter every charge shares it: page 1 is enough. - if (!chargeFilters?.target) { - const totalCharges = getCollectionTotalItems(firstPage); - - // Without the stats endpoint, big result sets would take minutes to - // scan and hammer the API — leave the card empty instead. - if (totalCharges > MAX_SCANNABLE_CHARGES) return; - - const totalPages = Math.ceil(totalCharges / CHARGES_SCAN_PAGE_SIZE); - - for (let page = 2; page <= totalPages; page += CHARGES_SCAN_CONCURRENCY) { - if (requestId !== projectsCountRequestId) return; - - const batch = []; - for ( - let p = page; - p < page + CHARGES_SCAN_CONCURRENCY && p <= totalPages; - p++ - ) { - batch.push(fetchChargesPage(p)); - } - (await Promise.all(batch)).forEach(collectTargets); - } - } - - if (requestId !== projectsCountRequestId) return; - - // Targets can also be user or tipjar accountings; keep only projects. - const targetList = [...targets]; - let projectCount = 0; - - for (let i = 0; i < targetList.length; i += PROJECTS_LOOKUP_CHUNK_SIZE) { - const query: ApiProjectsGetCollectionData["query"] = { - "accounting[]": targetList.slice(i, i + PROJECTS_LOOKUP_CHUNK_SIZE), - itemsPerPage: 1, - }; - const { data, error } = await apiProjectsGetCollection({ - query, - headers: { Accept: "application/ld+json" }, - }); - if (error) throw error; - projectCount += getCollectionTotalItems(data); - } - - if (requestId !== projectsCountRequestId) return; - - projectsCountCache.set(cacheKey, projectCount); - selectedProjectsCount = projectCount; - } catch (error) { - console.error("Failed to count projects for filtered charges:", error); + if (error || data?.projects === undefined) { + console.error("Failed to load selected projects count:", error); + return; } + + selectedProjectsCount = data.projects; } function buildChargesQuery( @@ -547,7 +448,7 @@ paymentMethodOptions = [ ["all", $t("pages.admin.charges.filters.paymentMethod.options.all")], - ...(paymentGateways ?? []).map((g): [string, string] => [g.id!, g.name]), + ...(paymentGateways ?? []).map((g): [string, string] => [g.id!, g.name ?? ""]), ]; chargeStatusOptions = Object.entries( diff --git a/src/components/checkout/PaymentMethod.astro b/src/components/checkout/PaymentMethod.astro index 38c0ca7b..5e785ad8 100644 --- a/src/components/checkout/PaymentMethod.astro +++ b/src/components/checkout/PaymentMethod.astro @@ -1,5 +1,5 @@ --- -import type { ApiGatewaysNameGetResponse } from "../../openapi/client/index"; +import type { ApiGatewaysIdGetResponse } from "../../openapi/client/index"; import CreditCardIcon from "../icons/payment/CreditCardIcon.astro"; import PaypalIcon from "../icons/payment/PaypalIcon.astro"; import StripeIcon from "../icons/payment/StripeIcon.astro"; @@ -13,7 +13,7 @@ const { selectedGateway, }: { hasError?: boolean; - paymentGateways?: ApiGatewaysNameGetResponse[]; + paymentGateways?: ApiGatewaysIdGetResponse[]; selectedGateway?: string; } = Astro.props; @@ -87,6 +87,7 @@ const { accounting } = Astro.props; + +
+ {#each rows as row, i} + removeRow(i)} + /> + {/each} + +
+ +
+
diff --git a/src/components/library/filters/FilterComposerRow.svelte b/src/components/library/filters/FilterComposerRow.svelte new file mode 100644 index 00000000..24ae1417 --- /dev/null +++ b/src/components/library/filters/FilterComposerRow.svelte @@ -0,0 +1,321 @@ + + +
+
+ +
+ +
+ +
+ +
+ {#if currentSubject?.options && operator === "equals" && subjectKey} + + {:else if currentSubject?.options && operator === "is_any_of" && subjectKey} + {#if !showStaticDropdown && dropdownSelected.length > 0} +
(showStaticDropdown = true)} + role="button" + tabindex="0" + onkeydown={(e) => e.key === "Enter" && (showStaticDropdown = true)} + > + {#each dropdownSelected as item} + + {item.label} + + + {/each} +
+ {:else} + +
(showStaticDropdown = false)}> + +
+ {/if} + {:else if currentSubject?.suggest && subjectKey && operator === "equals"} + {#if referent && !Array.isArray(referent) && !showSuggestSearch} +
+ + +
+ {:else} + + {/if} + {:else if currentSubject?.suggest && subjectKey && operator} + {#if !showSuggestSearch && dropdownSelected.length > 0} +
(showSuggestSearch = true)} + role="button" + tabindex="0" + onkeydown={(e) => e.key === "Enter" && (showSuggestSearch = true)} + > + {#each dropdownSelected as item} + + {item.label} + + + {/each} +
+ {:else} + +
(showSuggestSearch = false)}> + +
+ {/if} + {:else if currentSubject?.type === "date"} + (referent = date)} + labelText={$t("pages.admin.charges.filters.composer.referentPlaceholder")} + /> + {:else if currentSubject?.type === "number"} + + {:else} + + {/if} +
+ + +
From 96b0a167efaa5b10e8ff70a045ad4a03c83f3474 Mon Sep 17 00:00:00 2001 From: Pep Villalonga Date: Wed, 22 Jul 2026 10:50:27 +0200 Subject: [PATCH 23/47] fix: solve PR mistakes --- .env.example | 3 +- .../project/edit/ConfigurationStep.svelte | 33 +--------- .../project/edit/RoundSelector.svelte | 52 +++++---------- src/i18n/locales/es.json | 3 +- .../create/CreateProjectForm.svelte | 4 +- src/pages/[...locale]/create/validation.ts | 11 ++-- src/utils/campaign.ts | 65 ++++++------------- 7 files changed, 50 insertions(+), 121 deletions(-) diff --git a/.env.example b/.env.example index 0c186249..ccd8b08c 100644 --- a/.env.example +++ b/.env.example @@ -25,7 +25,8 @@ PUBLIC_TIPPING_DEFAULT_AMOUNT="" PUBLIC_TIPPING_DEFAULT_CHECKED="false" # Campaign limit -# Set a date (YYYY-MM-DD) to prevent projects from being in campaign past it +# Set a recurring day and month (DD-MM) to prevent projects from being in +# campaign past that date; the year is implicit and repeats every year # Leave the value empty to disable the limit PUBLIC_CAMPAIGN_MAX_END_DATE="" diff --git a/src/components/project/edit/ConfigurationStep.svelte b/src/components/project/edit/ConfigurationStep.svelte index 75cfee53..9099c649 100644 --- a/src/components/project/edit/ConfigurationStep.svelte +++ b/src/components/project/edit/ConfigurationStep.svelte @@ -24,8 +24,6 @@ updateConfiguration, updateProject, } from "../../../stores/drafts/projectDraft"; - import { getMaxReleaseDate, isEnabled, maxEndDate } from "../../../utils/campaign"; - import { formatDate } from "../../../utils/dates"; import { toCollectionItems } from "../../../utils/hydra"; import Button from "../../library/buttons/Button.svelte"; @@ -54,26 +52,6 @@ $currentDraft?.wizardForm.configuration.projectDeadline ?? "minimum", ); - // Campaign start date: server-computed calendar first, draft as fallback - let releaseDate = $derived.by(() => { - const release = project?.calendar?.release ?? $currentDraft?.createProject.release; - return release ? new Date(release) : undefined; - }); - - // The 2-rounds (optimum) campaign lasts 80 days; block it when it would - // extend past the platform-wide cutoff (PUBLIC_CAMPAIGN_MAX_END_DATE) - let optimumDisabled = $derived.by(() => { - if (!isEnabled || !releaseDate) return false; - const maxRelease = getMaxReleaseDate("optimum"); - return maxRelease !== undefined && releaseDate > maxRelease; - }); - - $effect(() => { - if (optimumDisabled && projectDeadline === "optimum") { - updateConfiguration({ projectDeadline: "minimum" }); - } - }); - /** * Handle Continue button * Simple navigation to next step (2) - validation happens on save/submit @@ -144,16 +122,7 @@ {$t("pages.project.edit.configuration.rounds.description")}

- + diff --git a/src/components/project/edit/RoundSelector.svelte b/src/components/project/edit/RoundSelector.svelte index 9cdb2520..c1cc5de7 100644 --- a/src/components/project/edit/RoundSelector.svelte +++ b/src/components/project/edit/RoundSelector.svelte @@ -21,16 +21,9 @@ interface RoundSelectorProps { deadline: Project["deadline"]; onChange: (selectedDeadline: "minimum" | "optimum") => void; - optimumDisabled?: boolean; - optimumDisabledReason?: string; } - let { - deadline = $bindable("minimum"), - onChange, - optimumDisabled = false, - optimumDisabledReason, - }: RoundSelectorProps = $props(); + let { deadline = $bindable("minimum"), onChange }: RoundSelectorProps = $props(); /** * Handle round selection change @@ -40,31 +33,22 @@ } -
-
- - handleChange("minimum")} - label={$t("pages.project.edit.configuration.rounds.option1")} - /> +
+ + handleChange("minimum")} + label={$t("pages.project.edit.configuration.rounds.option1")} + /> - -
- handleChange("optimum")} - label={$t("pages.project.edit.configuration.rounds.option2")} - /> -
-
- - {#if optimumDisabled && optimumDisabledReason} -

{optimumDisabledReason}

- {/if} + + handleChange("optimum")} + label={$t("pages.project.edit.configuration.rounds.option2")} + />
diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index c376d61d..925613c1 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -481,8 +481,7 @@ "title": "Elige las rondas que tendrá tu campaña", "description": "La configuración predeterminada son 40 días cada ronda, puedes solicitar modificaciones a tu asesor si tu proyecto es seleccionado", "option1": "1 Ronda", - "option2": "2 Rondas", - "optimumDisabled": "La opción de 2 rondas no está disponible: la campaña superaría la fecha límite del {{ date }}" + "option2": "2 Rondas" }, "continue": "Continuar con info de campaña", "geography": { diff --git a/src/pages/[...locale]/create/CreateProjectForm.svelte b/src/pages/[...locale]/create/CreateProjectForm.svelte index f736c472..f7aa5015 100644 --- a/src/pages/[...locale]/create/CreateProjectForm.svelte +++ b/src/pages/[...locale]/create/CreateProjectForm.svelte @@ -29,7 +29,7 @@ project, validationErrors, } from "../../../stores/drafts/projectDraft"; - import { getMaxReleaseDate } from "../../../utils/campaign"; + import { maxEndDate } from "../../../utils/campaign"; import { formatCurrency } from "../../../utils/currencies"; let releaseDate = $state($project.release ? new Date($project.release) : new Date()); @@ -341,7 +341,7 @@ name="release" bind:value={releaseDate} min={getMinDate()} - max={getMaxReleaseDate("minimum")} + max={maxEndDate} error={shouldShowError("release") ? $t($validationErrors.release) : undefined} onBlur={() => handleFieldBlur("release")} onInput={(date) => handleFieldChange("release", date)} diff --git a/src/pages/[...locale]/create/validation.ts b/src/pages/[...locale]/create/validation.ts index d5dfc02e..ee96a860 100644 --- a/src/pages/[...locale]/create/validation.ts +++ b/src/pages/[...locale]/create/validation.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { getMaxReleaseDate } from "../../../utils/campaign"; +import { maxEndDate } from "../../../utils/campaign"; /** * Validation schema for project creation form. @@ -62,16 +62,15 @@ export const projectCreationSchema = z.object({ ) .refine( (date) => { - // Latest release so the campaign cannot extend past the - // platform-wide cutoff (PUBLIC_CAMPAIGN_MAX_END_DATE) - const maxDate = getMaxReleaseDate("minimum"); - if (!maxDate) return true; + // Release must not be past the platform-wide cutoff + // (PUBLIC_CAMPAIGN_MAX_END_DATE) + if (!maxEndDate) return true; // Create a copy of the input date to avoid mutation const normalizedDate = new Date(date); normalizedDate.setHours(0, 0, 0, 0); - return normalizedDate <= maxDate; + return normalizedDate <= maxEndDate; }, { message: "system.validation.project.release.max" }, ) diff --git a/src/utils/campaign.ts b/src/utils/campaign.ts index a5af16c7..3e37b42b 100644 --- a/src/utils/campaign.ts +++ b/src/utils/campaign.ts @@ -1,58 +1,35 @@ /** - * Campaign duration in days for each funding round choice. - * Mirrors the server-side calendar computation: the minimum deadline is - * 40 days after release, the optimum deadline 40 days after the minimum. - */ -export const CAMPAIGN_DURATION_DAYS: Record<"minimum" | "optimum", number> = { - minimum: 40, - optimum: 80, -}; - -/** - * Parses a YYYY-MM-DD string into a local-midnight Date. + * Parses a recurring DD-MM string into the next upcoming local-midnight Date. + * The year is implicit: the cutoff recurs every year, so the value resolves to + * the next occurrence of that day/month on or after today. * Returns undefined for empty or malformed values. */ -export function parseMaxEndDate(raw: string | undefined): Date | undefined { +export function parseMaxEndDate(raw: string | undefined, now: Date = new Date()): Date | undefined { if (!raw || raw === "") return undefined; - const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw); + const match = /^(\d{2})-(\d{2})$/.exec(raw); if (!match) { - console.warn(`Invalid PUBLIC_CAMPAIGN_MAX_END_DATE value: ${raw}`); + console.warn(`Invalid PUBLIC_CAMPAIGN_MAX_END_DATE value (expected DD-MM): ${raw}`); return undefined; } - return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])); + const day = Number(match[1]); + const month = Number(match[2]); + + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const cutoff = new Date(today.getFullYear(), month - 1, day); + + // Already passed this year → use next year's occurrence + if (cutoff < today) { + cutoff.setFullYear(cutoff.getFullYear() + 1); + } + + return cutoff; } /** - * Latest allowed release date so a campaign with the given round choice - * ends on or before the cutoff date. - */ -export function computeMaxReleaseDate( - cutoff: Date, - deadline: "minimum" | "optimum" = "minimum", -): Date { - const maxRelease = new Date(cutoff); - maxRelease.setDate(maxRelease.getDate() - CAMPAIGN_DURATION_DAYS[deadline]); - return maxRelease; -} - -/** - * Last date a project may be in campaign, as configured by the platform owner. - * Leave PUBLIC_CAMPAIGN_MAX_END_DATE empty to disable the limit. + * Latest date a project may start its campaign, as configured by the platform + * owner. PUBLIC_CAMPAIGN_MAX_END_DATE is a recurring DD-MM value (year implicit); + * it resolves to the next upcoming occurrence. Leave empty to disable the limit. */ export const maxEndDate = parseMaxEndDate(import.meta.env.PUBLIC_CAMPAIGN_MAX_END_DATE); - -/** - * Is a campaign end limit configured? - */ -export const isEnabled = maxEndDate !== undefined; - -/** - * Latest allowed release date for the given round choice, - * or undefined when no limit is configured. - */ -export function getMaxReleaseDate(deadline: "minimum" | "optimum" = "minimum"): Date | undefined { - if (!maxEndDate) return undefined; - return computeMaxReleaseDate(maxEndDate, deadline); -} From e5a60b2b3814f3a820073cce93efd4737c4628a5 Mon Sep 17 00:00:00 2001 From: Pep Villalonga Date: Wed, 22 Jul 2026 11:17:25 +0200 Subject: [PATCH 24/47] fix: search abort error banner --- src/services/projectsService.ts | 10 ++++++++++ src/stores/searchStore.ts | 21 +++++++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/services/projectsService.ts b/src/services/projectsService.ts index fc6f8f19..bcc39659 100644 --- a/src/services/projectsService.ts +++ b/src/services/projectsService.ts @@ -60,6 +60,16 @@ export class ProjectsService { hasNextPage, }; } catch (error) { + // Re-throw aborted requests as-is so callers can detect cancellation. + // The fetch client may not preserve the DOMException name, so also + // check the signal directly. + if ( + options?.abortSignal?.aborted || + (error instanceof Error && error.name === "AbortError") + ) { + throw new DOMException("Aborted", "AbortError"); + } + // Re-throw auth errors as-is for component handling if (error && typeof error === "object" && "type" in error) { throw error as AuthError; diff --git a/src/stores/searchStore.ts b/src/stores/searchStore.ts index 56ea512a..f6630bcd 100644 --- a/src/stores/searchStore.ts +++ b/src/stores/searchStore.ts @@ -130,17 +130,18 @@ function createSearchStore() { })); } } catch (error) { - console.error(error); - // Only set error if request wasn't cancelled - if (!(error instanceof Error) || error.name !== "AbortError") { - update((state) => ({ - ...state, - isLoading: false, - hasError: true, - errorMessage: error instanceof Error ? error.message : "Search failed", - currentAbortController: undefined, - })); + // Ignore cancelled requests — a new search aborted this one + if (error instanceof Error && error.name === "AbortError") { + return; } + console.error(error); + update((state) => ({ + ...state, + isLoading: false, + hasError: true, + errorMessage: error instanceof Error ? error.message : "Search failed", + currentAbortController: undefined, + })); } }; From 87968f34e84da13599a0cb138e1d8da5a70cb57f Mon Sep 17 00:00:00 2001 From: Pep Villalonga Date: Wed, 22 Jul 2026 12:23:12 +0200 Subject: [PATCH 25/47] =?UTF-8?q?fix:=20change=20campa=C3=B1as=20to=20proy?= =?UTF-8?q?ects,=20fix=20style=20and=20add=20loading=20to=20filters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../library/dropdown/DropdownItem.svelte | 6 ++-- src/components/library/inputs/Search.svelte | 4 +-- src/components/search/SearchFilters.svelte | 1 - src/components/search/SearchInput.svelte | 2 +- .../search/SearchResultsClient.svelte | 26 +++++++++++---- src/i18n/locales/es.json | 32 +++++++++---------- src/stores/searchStore.ts | 9 ++++++ 7 files changed, 50 insertions(+), 30 deletions(-) diff --git a/src/components/library/dropdown/DropdownItem.svelte b/src/components/library/dropdown/DropdownItem.svelte index 6ddef268..e3f2212e 100644 --- a/src/components/library/dropdown/DropdownItem.svelte +++ b/src/components/library/dropdown/DropdownItem.svelte @@ -24,12 +24,12 @@
{#if variant === "multiselect"} -
diff --git a/src/components/search/SearchInput.svelte b/src/components/search/SearchInput.svelte index d0cfc4c8..9cd9b9fb 100644 --- a/src/components/search/SearchInput.svelte +++ b/src/components/search/SearchInput.svelte @@ -56,5 +56,5 @@ onsubmit={() => onEnter?.() || onSearch?.(searchQuery)} onclear={clearSearch} data-testid={dataTestId} - class="transition-colors focus-within:border-[#855a96] {className}" + class=" {className}" /> diff --git a/src/components/search/SearchResultsClient.svelte b/src/components/search/SearchResultsClient.svelte index e2885884..d29659a2 100644 --- a/src/components/search/SearchResultsClient.svelte +++ b/src/components/search/SearchResultsClient.svelte @@ -15,6 +15,7 @@ Manages real-time filtering of campaigns without page reloads searchError, searchResults, isSearching, + isLoadingMore, hasSearchResults, hasActualSearchResults, isEmpty, @@ -200,14 +201,25 @@ Manages real-time filtering of campaigns without page reloads {#if campaigns.length > 0} - - {#each campaigns as campaign} - -
- +
+ + {#if $isSearching && !$isLoadingMore} +
+
- {/each} - + {/if} + + {#each campaigns as campaign} + +
+ +
+ {/each} +
+
{/if}
diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 254f2ec3..b66aba66 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -116,7 +116,7 @@ }, "navigation": { "search": { - "placeholder": "Busca tu campaña" + "placeholder": "Busca tu proyecto" } } }, @@ -944,16 +944,16 @@ } }, "search": { - "title": "Buscar campañas - Goteo", - "description": "Explora nuestro catálogo de campañas y encuentra la que más te inspire para hacer tu donación", - "heading": "Encuentra la campaña que buscas", - "subtitle": "Explora nuestro catálogo de campañas y elige la que más te inspire para hacer tu donación", - "topResults": "Campañas más buscadas", - "showingTopResults": "Mostrando las {{count}} campañas más populares", - "loading": "Buscando campañas...", + "title": "Buscar Proyectos - Goteo", + "description": "Explora nuestro catálogo de proyectos y encuentra la que más te inspire para hacer tu donación", + "heading": "Encuentra los Proyectos que buscas", + "subtitle": "Explora nuestro catálogo de proyectos y elige la que más te inspire para hacer tu donación", + "topResults": "Proyectos más buscadas", + "showingTopResults": "Mostrando las {{count}} proyectos más populares", + "loading": "Buscando proyectos...", "input": { - "placeholder": "Buscar", - "label": "Buscar campañas", + "placeholder": "Buscar proyectos...", + "label": "Buscar proyectos", "clear": "Limpiar búsqueda", "button": "Buscar" }, @@ -967,20 +967,20 @@ "deselectCategory": "Deseleccionar categoría", "selectedCategories": "{{count}} categorías seleccionadas", "status": { - "label": "Estado de la campaña" + "label": "Estado de la proyecto" }, "territory": { "label": "Ámbito de actuación" } }, "empty": { - "title": "No se encontraron campañas", - "description": "No hay campañas que coincidan con tu búsqueda actual. Verifica la ortografía o prueba con palabras clave diferentes o términos más generales.", + "title": "No se encontraron proyectos", + "description": "No hay proyectos que coincidan con tu búsqueda actual. Verifica la ortografía o prueba con palabras clave diferentes o términos más generales.", "clearFilters": "Limpiar filtros" }, "initial": { "title": "Busca tu campaña ideal", - "description": "Utiliza los filtros de arriba para encontrar campañas que te inspiren." + "description": "Utiliza los filtros de arriba para encontrar proyectos que te inspiren." }, "results": { "count": "{{count}} resultados encontrados", @@ -989,7 +989,7 @@ "showing": "Mostrando {{count}} resultados", "empty": { "title": "No se encontraron resultados", - "description": "No hay campañas que coincidan con tu búsqueda actual.", + "description": "No hay proyectos que coincidan con tu búsqueda actual.", "clearFilters": "Limpiar filtros" } }, @@ -1021,7 +1021,7 @@ } }, "accessibility": { - "resultsFound": "Se encontraron {{count}} campañas" + "resultsFound": "Se encontraron {{count}} proyectos" }, "loadMore": { "button": "Cargar más", diff --git a/src/stores/searchStore.ts b/src/stores/searchStore.ts index f6630bcd..863bd316 100644 --- a/src/stores/searchStore.ts +++ b/src/stores/searchStore.ts @@ -20,6 +20,7 @@ export interface SearchState { itemsPerPage: number; hasNextPage: boolean; hasPrevPage: boolean; + isLoadingMore: boolean; currentAbortController?: AbortController; } @@ -45,6 +46,7 @@ function getInitialState(): SearchState { itemsPerPage: 20, hasNextPage: false, hasPrevPage: false, + isLoadingMore: false, currentAbortController: undefined, }; } @@ -102,6 +104,7 @@ function createSearchStore() { update((state) => ({ ...state, isLoading: true, + isLoadingMore: false, hasError: false, errorMessage: "", currentAbortController: abortController, @@ -387,6 +390,7 @@ function createSearchStore() { update((state) => ({ ...state, isLoading: true, + isLoadingMore: true, hasError: false, errorMessage: "", currentAbortController: abortController, @@ -410,6 +414,7 @@ function createSearchStore() { hasNextPage: response.hasNextPage, hasPrevPage, isLoading: false, + isLoadingMore: false, hasSearched: true, lastSearchTime: Date.now(), currentAbortController: undefined, @@ -422,6 +427,7 @@ function createSearchStore() { update((state) => ({ ...state, isLoading: false, + isLoadingMore: false, hasError: true, errorMessage: error instanceof Error ? error.message : "Failed to load more results", @@ -447,6 +453,9 @@ export const searchResults = derived(searchStore, ($searchStore) => $searchStore export const isSearching = derived(searchStore, ($searchStore) => $searchStore.isLoading); +// True only while appending more results (Load more) — not during a fresh search +export const isLoadingMore = derived(searchStore, ($searchStore) => $searchStore.isLoadingMore); + export const searchError = derived(searchStore, ($searchStore) => $searchStore.hasError ? $searchStore.errorMessage : null, ); From 06592db27bb8ee4ad529a83ea1dfdf953f9fdc39 Mon Sep 17 00:00:00 2001 From: Pep Villalonga Date: Wed, 22 Jul 2026 14:24:05 +0200 Subject: [PATCH 26/47] feat: add status sort option to admin charges selector --- src/components/admin/Table.svelte | 2 +- src/i18n/locales/es.json | 4 +++- src/openapi/client/types.gen.ts | 3 +++ src/stores/chargesPaginationAndSort.ts | 12 ++++++++++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/admin/Table.svelte b/src/components/admin/Table.svelte index dd3188a8..7b0f38fd 100644 --- a/src/components/admin/Table.svelte +++ b/src/components/admin/Table.svelte @@ -46,7 +46,7 @@ { name: "pages.admin.charges.headers.origin", sortable: false }, { name: "pages.admin.charges.headers.paymentMethod", sortable: false }, { name: "pages.admin.charges.headers.date", sortable: true, sortKey: "date" }, - { name: "pages.admin.charges.headers.chargeStatus", sortable: false, sortKey: "status" }, + { name: "pages.admin.charges.headers.chargeStatus", sortable: true, sortKey: "status" }, { name: "pages.admin.charges.headers.refundToWallet", sortable: false }, { name: "", sortable: false }, // For the empty expand/collapse button at the end of the table ]; diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 254f2ec3..8a3391df 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -1133,7 +1133,9 @@ "date-desc": "Fecha (más reciente primero)", "date-asc": "Fecha (más antigua primero)", "amount-desc": "Importe (mayor a menor)", - "amount-asc": "Importe (menor a mayor)" + "amount-asc": "Importe (menor a mayor)", + "status-asc": "Estado (A-Z)", + "status-desc": "Estado (Z-A)" } }, "itemsPerPage": { diff --git a/src/openapi/client/types.gen.ts b/src/openapi/client/types.gen.ts index 87937dfa..59f8a77f 100644 --- a/src/openapi/client/types.gen.ts +++ b/src/openapi/client/types.gen.ts @@ -3153,6 +3153,8 @@ export type ApiGatewayChargesGetCollectionData = { 'dateUpdated[strictly_before]'?: string; 'dateUpdated[after]'?: string; 'dateUpdated[strictly_after]'?: string; + 'order[money.amount]'?: 'asc' | 'desc'; + 'order[status]'?: 'asc' | 'desc'; 'order[dateCreated]'?: 'asc' | 'desc'; 'order[dateUpdated]'?: 'asc' | 'desc'; }; @@ -5441,6 +5443,7 @@ export type ApiUsersGetCollectionData = { 'territory.subLvl1[]'?: Array; 'territory.subLvl2'?: string; 'territory.subLvl2[]'?: Array; + 'order[handle]'?: 'asc' | 'desc'; }; url: '/v4/users'; }; diff --git a/src/stores/chargesPaginationAndSort.ts b/src/stores/chargesPaginationAndSort.ts index 13fe2b4f..2e4f6e9d 100644 --- a/src/stores/chargesPaginationAndSort.ts +++ b/src/stores/chargesPaginationAndSort.ts @@ -37,4 +37,16 @@ export const sortOptions: SortOption[] = [ direction: "asc", label: "pages.admin.charges.filters.order.options.amount-asc", }, + { + key: "status-asc", + field: "status", + direction: "asc", + label: "pages.admin.charges.filters.order.options.status-asc", + }, + { + key: "status-desc", + field: "status", + direction: "desc", + label: "pages.admin.charges.filters.order.options.status-desc", + }, ]; From 3ff5b9ab9aa70be88e21ae0abd468d27ad481021 Mon Sep 17 00:00:00 2001 From: OlivierSchulbaum Date: Wed, 22 Jul 2026 16:02:32 +0200 Subject: [PATCH 27/47] New translations es.json (Catalan) [ci skip] --- src/i18n/locales/ca.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/i18n/locales/ca.json b/src/i18n/locales/ca.json index e556de73..64a879aa 100644 --- a/src/i18n/locales/ca.json +++ b/src/i18n/locales/ca.json @@ -26,6 +26,9 @@ "project": { "terms": { "required": "Debes aceptar las condiciones del impulsor y las bases de la convocatoria" + }, + "release": { + "max": "La fecha de inicio es demasiado tardía: la campaña no puede extenderse más allá de la fecha límite de la plataforma" } }, "errors": { From b2271767963d15c122a8f9b47a44663330b88335 Mon Sep 17 00:00:00 2001 From: OlivierSchulbaum Date: Wed, 22 Jul 2026 16:02:34 +0200 Subject: [PATCH 28/47] New translations es.json (English) [ci skip] --- src/i18n/locales/en.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 356fc20b..ae0b2320 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -26,6 +26,9 @@ "project": { "terms": { "required": "Debes aceptar las condiciones del impulsor y las bases de la convocatoria" + }, + "release": { + "max": "La fecha de inicio es demasiado tardía: la campaña no puede extenderse más allá de la fecha límite de la plataforma" } }, "errors": { From 6cf37d42874344bab37de30fdbf8e50c0e6fd2d5 Mon Sep 17 00:00:00 2001 From: Pep Villalonga Date: Thu, 23 Jul 2026 12:03:36 +0200 Subject: [PATCH 29/47] fix: fix comunity tab cards --- src/components/project/ProjectCommunity.svelte | 14 ++++++-------- .../project/ProjectCommunityAnonymous.svelte | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/project/ProjectCommunity.svelte b/src/components/project/ProjectCommunity.svelte index 928337b6..18756ab2 100644 --- a/src/components/project/ProjectCommunity.svelte +++ b/src/components/project/ProjectCommunity.svelte @@ -67,7 +67,7 @@ ? groupedItems.default : groupedItems.default?.slice( 0, - PAGE_SIZE - 2 + Math.max(0, (currentPage - 1) * PAGE_SIZE), + PAGE_SIZE - 1 + Math.max(0, (currentPage - 1) * PAGE_SIZE), ), ); @@ -114,12 +114,10 @@ {#if groupedItems.default?.length} {#if !hasMatchfunding} -
- -
+ {/if} {#each visibleDefaultItems as item (item.id)} {#if selectedProjectSupport} diff --git a/src/components/project/ProjectCommunityAnonymous.svelte b/src/components/project/ProjectCommunityAnonymous.svelte index 0a07b460..d17bd12d 100644 --- a/src/components/project/ProjectCommunityAnonymous.svelte +++ b/src/components/project/ProjectCommunityAnonymous.svelte @@ -27,9 +27,9 @@
-
+
Community donation Date: Thu, 23 Jul 2026 12:04:06 +0200 Subject: [PATCH 30/47] fix: correct size territory --- src/components/project/ProjectTags.svelte | 2 +- src/components/project/TerritoryTag.svelte | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/project/ProjectTags.svelte b/src/components/project/ProjectTags.svelte index e422b00c..9b0f8d64 100644 --- a/src/components/project/ProjectTags.svelte +++ b/src/components/project/ProjectTags.svelte @@ -11,5 +11,5 @@ {#each project.categories as category} {/each} - +
diff --git a/src/components/project/TerritoryTag.svelte b/src/components/project/TerritoryTag.svelte index 0fa5fca5..7b237c9f 100644 --- a/src/components/project/TerritoryTag.svelte +++ b/src/components/project/TerritoryTag.svelte @@ -14,10 +14,12 @@ territory, lang: initialLang = undefined, class: classes = "", + iconSize = "32", }: { territory: Territory; lang?: Locale; class?: ClassNameValue; + iconSize?: string | number; } = $props(); const lang = initialLang || $locale; @@ -44,6 +46,6 @@ - + {displayName(territory)} From a90938f67a9c968308a182ec41af50cc60ed51de Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 23 Jul 2026 12:05:59 +0200 Subject: [PATCH 31/47] fix: update SDK types to new shapes of ProjectCalendar and sorting query params --- src/openapi/client/types.gen.ts | 107 +++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 36 deletions(-) diff --git a/src/openapi/client/types.gen.ts b/src/openapi/client/types.gen.ts index b10b25d0..8f830245 100644 --- a/src/openapi/client/types.gen.ts +++ b/src/openapi/client/types.gen.ts @@ -583,6 +583,10 @@ export type GatewayChargeChargesTotalsDto = { * The number of distinct Projects targeted by the charges matching the filter set. */ projects?: number; + /** + * The aggregated monetary value in the charges. + */ + money?: TotalizedMoney; }; /** @@ -595,6 +599,10 @@ export type GatewayChargeChargesTotalsDtoJsonld = { * The number of distinct Projects targeted by the charges matching the filter set. */ projects?: number; + /** + * The aggregated monetary value in the charges. + */ + money?: TotalizedMoneyJsonld; }; /** @@ -1532,7 +1540,7 @@ export type Project = { /** * The status of a Project represents how far it is in it's life-cycle. */ - status?: 'in_draft' | 'to_campaign_review' | 'in_campaign_review' | 'in_campaign_review.request_change' | 'campaign_review.rejected' | 'to_campaign' | 'in_campaign' | 'campaign.failed' | 'to_funding_review' | 'in_funding_review' | 'in_funding_review.request_change' | 'funding_review.rejected' | 'to_funding' | 'in_funding' | 'funding.paid'; + status?: 'in_draft' | 'to_campaign_review' | 'in_campaign_review' | 'in_campaign_review.to_change' | 'in_campaign_review.to_review' | 'campaign_review.rejected' | 'to_campaign' | 'in_campaign' | 'campaign.failed' | 'campaign.cancelled' | 'to_funding_review' | 'in_funding_review' | 'in_funding_review.to_change' | 'in_funding_review.to_review' | 'funding_review.rejected' | 'to_funding' | 'in_funding' | 'funding.paid'; /** * List of the ProjectRewards this Project offers. */ @@ -1580,11 +1588,10 @@ export type ProjectProjectCreationDto = { */ categories: Array; /** - * Desired date-time of release for the created Project.\ - * By default 28 days from now, at minimum 14 days from now. + * Deadlines and important Project dates. */ - release?: string; - status?: 'in_draft' | 'to_campaign_review' | 'in_campaign_review' | 'in_campaign_review.request_change' | 'campaign_review.rejected' | 'to_campaign' | 'in_campaign' | 'campaign.failed' | 'to_funding_review' | 'in_funding_review' | 'in_funding_review.request_change' | 'funding_review.rejected' | 'to_funding' | 'in_funding' | 'funding.paid'; + calendar?: ProjectCalendar; + status?: 'in_draft' | 'to_campaign_review' | 'in_campaign_review' | 'in_campaign_review.to_change' | 'in_campaign_review.to_review' | 'campaign_review.rejected' | 'to_campaign' | 'in_campaign' | 'campaign.failed' | 'campaign.cancelled' | 'to_funding_review' | 'in_funding_review' | 'in_funding_review.to_change' | 'in_funding_review.to_review' | 'funding_review.rejected' | 'to_funding' | 'in_funding' | 'funding.paid'; }; /** @@ -1604,11 +1611,10 @@ export type ProjectProjectCreationDtoJsonld = { */ categories: Array; /** - * Desired date-time of release for the created Project.\ - * By default 28 days from now, at minimum 14 days from now. + * Deadlines and important Project dates. */ - release?: string; - status?: 'in_draft' | 'to_campaign_review' | 'in_campaign_review' | 'in_campaign_review.request_change' | 'campaign_review.rejected' | 'to_campaign' | 'in_campaign' | 'campaign.failed' | 'to_funding_review' | 'in_funding_review' | 'in_funding_review.request_change' | 'funding_review.rejected' | 'to_funding' | 'in_funding' | 'funding.paid'; + calendar?: ProjectCalendarJsonld; + status?: 'in_draft' | 'to_campaign_review' | 'in_campaign_review' | 'in_campaign_review.to_change' | 'in_campaign_review.to_review' | 'campaign_review.rejected' | 'to_campaign' | 'in_campaign' | 'campaign.failed' | 'campaign.cancelled' | 'to_funding_review' | 'in_funding_review' | 'in_funding_review.to_change' | 'in_funding_review.to_review' | 'funding_review.rejected' | 'to_funding' | 'in_funding' | 'funding.paid'; }; /** @@ -1642,6 +1648,10 @@ export type ProjectProjectUpdationDto = { * and then until the optimum deadline if it did raise the minimum. */ deadline?: 'minimum' | 'optimum'; + /** + * Deadlines and important Project dates. + */ + calendar?: ProjectCalendar; /** * A URL to a video showcasing the Project. */ @@ -1649,7 +1659,7 @@ export type ProjectProjectUpdationDto = { /** * The status of a Project represents how far it is in it's life-cycle. */ - status?: 'in_draft' | 'to_campaign_review' | 'in_campaign_review' | 'in_campaign_review.request_change' | 'campaign_review.rejected' | 'to_campaign' | 'in_campaign' | 'campaign.failed' | 'to_funding_review' | 'in_funding_review' | 'in_funding_review.request_change' | 'funding_review.rejected' | 'to_funding' | 'in_funding' | 'funding.paid'; + status?: 'in_draft' | 'to_campaign_review' | 'in_campaign_review' | 'in_campaign_review.to_change' | 'in_campaign_review.to_review' | 'campaign_review.rejected' | 'to_campaign' | 'in_campaign' | 'campaign.failed' | 'campaign.cancelled' | 'to_funding_review' | 'in_funding_review' | 'in_funding_review.to_change' | 'in_funding_review.to_review' | 'funding_review.rejected' | 'to_funding' | 'in_funding' | 'funding.paid'; }; /** @@ -1713,7 +1723,7 @@ export type ProjectJsonld = { /** * The status of a Project represents how far it is in it's life-cycle. */ - status?: 'in_draft' | 'to_campaign_review' | 'in_campaign_review' | 'in_campaign_review.request_change' | 'campaign_review.rejected' | 'to_campaign' | 'in_campaign' | 'campaign.failed' | 'to_funding_review' | 'in_funding_review' | 'in_funding_review.request_change' | 'funding_review.rejected' | 'to_funding' | 'in_funding' | 'funding.paid'; + status?: 'in_draft' | 'to_campaign_review' | 'in_campaign_review' | 'in_campaign_review.to_change' | 'in_campaign_review.to_review' | 'campaign_review.rejected' | 'to_campaign' | 'in_campaign' | 'campaign.failed' | 'campaign.cancelled' | 'to_funding_review' | 'in_funding_review' | 'in_funding_review.to_change' | 'in_funding_review.to_review' | 'funding_review.rejected' | 'to_funding' | 'in_funding' | 'funding.paid'; /** * List of the ProjectRewards this Project offers. */ @@ -1821,21 +1831,17 @@ export type ProjectBudgetItemJsonld = { export type ProjectCalendar = { /** - * The date at which the Project started campaigning. + * Public campaign start date. */ - release?: string | null; + release: string | null; /** - * 40 days after the date of release.\ - * \ - * The minimum budget must be raised by the end of this date.\ - * Failure to do so will move the Project out of status `in_campaign` into status `unfunded`. + * The minimum budget must be raised by the end of this date, + * failure to do so will move the Project out of status `in_campaign` into status `unfunded`. */ minimum?: string | null; /** - * 40 days after the minimum deadline. Optional.\ - * \ * If the Project achieved their minimum budget by the minimum deadline, - * and this deadline is defined, it may still remain in campaign to raise the optimum. + * and this deadline is defined, it may still remain `in_campaign` to raise the optimum. */ optimum?: string | null; }; @@ -1849,21 +1855,17 @@ export type ProjectCalendarJsonld = { readonly '@id'?: string; readonly '@type'?: string; /** - * The date at which the Project started campaigning. + * Public campaign start date. */ - release?: string | null; + release: string | null; /** - * 40 days after the date of release.\ - * \ - * The minimum budget must be raised by the end of this date.\ - * Failure to do so will move the Project out of status `in_campaign` into status `unfunded`. + * The minimum budget must be raised by the end of this date, + * failure to do so will move the Project out of status `in_campaign` into status `unfunded`. */ minimum?: string | null; /** - * 40 days after the minimum deadline. Optional.\ - * \ * If the Project achieved their minimum budget by the minimum deadline, - * and this deadline is defined, it may still remain in campaign to raise the optimum. + * and this deadline is defined, it may still remain `in_campaign` to raise the optimum. */ optimum?: string | null; }; @@ -2428,6 +2430,25 @@ export type TipjarJsonld = { name: string; }; +export type TotalizedMoney = { + amount?: number; + currency?: string; + length?: number; +}; + +export type TotalizedMoneyJsonld = { + '@context'?: string | { + '@vocab': string; + hydra: 'http://www.w3.org/ns/hydra/core#'; + [key: string]: unknown | string | 'http://www.w3.org/ns/hydra/core#'; + }; + readonly '@id'?: string; + readonly '@type'?: string; + amount?: number; + currency?: string; + length?: number; +}; + export type Tracking = { /** * A descriptive title for the tracking number. @@ -3167,6 +3188,10 @@ export type ApiGatewayChargesGetCollectionData = { 'money.amount[gte]'?: string; 'money.amount[lt]'?: string; 'money.amount[lte]'?: string; + 'order[money.amount]'?: 'asc' | 'desc'; + 'order[status]'?: 'asc' | 'desc'; + 'order[dateCreated]'?: 'asc' | 'desc'; + 'order[dateUpdated]'?: 'asc' | 'desc'; 'dateCreated[before]'?: string; 'dateCreated[strictly_before]'?: string; 'dateCreated[after]'?: string; @@ -3175,10 +3200,6 @@ export type ApiGatewayChargesGetCollectionData = { 'dateUpdated[strictly_before]'?: string; 'dateUpdated[after]'?: string; 'dateUpdated[strictly_after]'?: string; - 'order[money.amount]'?: 'asc' | 'desc'; - 'order[status]'?: 'asc' | 'desc'; - 'order[dateCreated]'?: 'asc' | 'desc'; - 'order[dateUpdated]'?: 'asc' | 'desc'; }; url: '/v4/gateway_charges'; }; @@ -3213,6 +3234,10 @@ export type ApiGatewayChargestotalsGetCollectionData = { 'money.amount[gte]'?: string; 'money.amount[lt]'?: string; 'money.amount[lte]'?: string; + 'order[money.amount]'?: 'asc' | 'desc'; + 'order[status]'?: 'asc' | 'desc'; + 'order[dateCreated]'?: 'asc' | 'desc'; + 'order[dateUpdated]'?: 'asc' | 'desc'; 'dateCreated[before]'?: string; 'dateCreated[strictly_before]'?: string; 'dateCreated[after]'?: string; @@ -3221,8 +3246,6 @@ export type ApiGatewayChargestotalsGetCollectionData = { 'dateUpdated[strictly_before]'?: string; 'dateUpdated[after]'?: string; 'dateUpdated[strictly_after]'?: string; - 'order[dateCreated]'?: 'asc' | 'desc'; - 'order[dateUpdated]'?: 'asc' | 'desc'; }; url: '/v4/gateway_charges/totals'; }; @@ -4244,6 +4267,18 @@ export type ApiProjectsGetCollectionData = { description?: string; status?: string; 'status[]'?: Array; + 'calendar.release[before]'?: string; + 'calendar.release[strictly_before]'?: string; + 'calendar.release[after]'?: string; + 'calendar.release[strictly_after]'?: string; + 'calendar.minimum[before]'?: string; + 'calendar.minimum[strictly_before]'?: string; + 'calendar.minimum[after]'?: string; + 'calendar.minimum[strictly_after]'?: string; + 'calendar.optimum[before]'?: string; + 'calendar.optimum[strictly_before]'?: string; + 'calendar.optimum[after]'?: string; + 'calendar.optimum[strictly_after]'?: string; 'dateCreated[before]'?: string; 'dateCreated[strictly_before]'?: string; 'dateCreated[after]'?: string; @@ -5500,6 +5535,7 @@ export type ApiUsersGetCollectionData = { */ itemsPerPage?: number; handle?: string; + 'order[handle]'?: 'asc' | 'desc'; email?: string; accounting?: string; 'accounting[]'?: Array; @@ -5509,7 +5545,6 @@ export type ApiUsersGetCollectionData = { 'territory.subLvl1[]'?: Array; 'territory.subLvl2'?: string; 'territory.subLvl2[]'?: Array; - 'order[handle]'?: 'asc' | 'desc'; }; url: '/v4/users'; }; From 4f243d5fb1c9dbd7fa37917f7f710a192df30005 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Thu, 23 Jul 2026 12:20:24 +0200 Subject: [PATCH 32/47] fix: remove legacy /profile user page --- .../[...locale]/profile/[idOrHandle].astro | 238 ------------------ 1 file changed, 238 deletions(-) delete mode 100644 src/pages/[...locale]/profile/[idOrHandle].astro diff --git a/src/pages/[...locale]/profile/[idOrHandle].astro b/src/pages/[...locale]/profile/[idOrHandle].astro deleted file mode 100644 index 2c3e1ef5..00000000 --- a/src/pages/[...locale]/profile/[idOrHandle].astro +++ /dev/null @@ -1,238 +0,0 @@ ---- -import Layout from "../../../layouts/Layout.astro"; -import App from "../../../layouts/App.svelte"; -import ProfileHero from "../../../components/profile/ProfileHero.astro"; -import ProfileInfo from "../../../components/profile/ProfileInfo.svelte"; -import Tabs from "../../../components/library/layout/Tabs.svelte"; -import ProfileProjectsList from "../../../components/profile/ProfileProjectsList.astro"; -import ProfileDonorType from "../../../components/profile/ProfileDonorType.svelte"; -import { renderMarkdown } from "../../../utils/renderMarkdown"; -import { - apiUsersIdOrHandleGet, - apiProjectSupportsGetCollection, - apiProjectSupportsmoneyTotalGetCollection, - type Accounting, - type Money, - type ProjectSupport, -} from "../../../openapi/client"; -import type { User } from "../../../openapi/client/types.gen"; -import type { Campaign } from "../../../types/campaign"; -import { getLanguage } from "../../../middleware/utils"; -import { getDefaultCurrency } from "../../../utils/consts"; -import { client } from "../../../openapi/client/client.gen"; - -const { idOrHandle } = Astro.params; -const { t, lang, session } = Astro.locals; - -if (!idOrHandle) { - return Astro.redirect("/404"); -} - -// Fetch user data -let user: User | undefined; -let errorMessage: string | undefined; - -try { - const userResponse = await apiUsersIdOrHandleGet({ - path: { idOrHandle: idOrHandle }, - }); - - if (userResponse.error) { - throw new Error("User not found"); - } - - user = userResponse.data; -} catch (error) { - console.error("Error fetching user:", error); - errorMessage = t("pages.profile.notFound"); -} - -// Prepare profile data with proper fallbacks -const displayName = user?.displayName || user?.handle || "User"; - -// Location - API doesn't have location field yet -// TODO: Add location field to Person/Organization in API -const location = undefined; - -// Bio - API doesn't have bio/description field yet -// TODO: Add bio/description field to User in API -const bio = undefined; - -const tabs = [ - { id: "about", label: t("pages.profile.tabs.about") }, - { id: "projects", label: t("pages.profile.tabs.projects") }, - { id: "donorType", label: t("pages.profile.tabs.donorType") }, -]; - -// Fetch user's backed/supported projects from API -let userSupports: ProjectSupport[] = []; -let totalDonated: { amount: number; currency: string } = { - amount: 0, - currency: getDefaultCurrency(), -}; -let projectsDonatedCount = 0; - -// Users are guaranteed to have an accounting resource -if (user) { - try { - // Get all project supports for this user - const { data: supports } = await apiProjectSupportsGetCollection({ - query: { - origin: user.accounting!, - }, - }); - - if (supports) { - userSupports = supports; - projectsDonatedCount = supports.length; - - // Get total money donated - const { data: moneyTotal } = await apiProjectSupportsmoneyTotalGetCollection({ - query: { - origin: user.accounting!, - }, - }); - - if (moneyTotal?.amount && moneyTotal?.currency) { - totalDonated = { - amount: Number(moneyTotal.amount), - currency: moneyTotal.currency, - }; - } - } - } catch (error) { - console.error("Error fetching user supports:", error); - } -} - -// Fetch actual projects from the supports -const projectIRIs = userSupports - .slice(0, 10) - .map((support) => support.project) - .filter(Boolean); -let campaigns: Campaign[] = []; - -if (projectIRIs.length > 0) { - campaigns = await Promise.all( - projectIRIs.map(async (projectIRI) => { - try { - // Fetch project data directly using IRI - const { data: project } = (await client.get({ - url: client.buildUrl({ url: projectIRI! }), - headers: { - "Accept-Language": getLanguage(Astro), - }, - })) as { data: any }; - - // Fetch accounting data - const { data: accounting } = (await client.get({ - url: client.buildUrl({ url: project.accounting! }), - })) as { data: Accounting }; - - return { - size: "small", - id: project.slug!, - title: project.title!, - image: project.video?.thumbnail!, - minimum: project.budget?.minimum?.money!, - obtained: accounting!.balance as Money, - optimum: project.budget?.optimum?.money, - }; - } catch (error) { - console.error(`Error fetching project ${projectIRI}:`, error); - return null; - } - }), - ).then((results) => results.filter(Boolean) as Campaign[]); -} - -// Social links - would come from user.socialMedia or similar field -// API doesn't have social media fields yet -// TODO: Add social media fields to User in API -const socialLinks = user - ? { - twitter: undefined, - instagram: undefined, - facebook: undefined, - linkedin: undefined, - email: user.email ? `mailto:${user.email}` : undefined, - medium: undefined, - } - : {}; - -// Images with fallbacks -const coverImage = undefined; // API doesn't provide cover image, use default -const avatarImage = user?.avatar || undefined; // Use user avatar or default placeholder ---- - - - - { - errorMessage ? ( -
-

{errorMessage}

-
- ) : ( -
- {/* Hero Section with Cover and Avatar */} - - - {/* User Info Section */} - - - {/* Tabs Navigation */} -
- -
- - {/* Tab Content - About */} -
- {bio && ( -
-
-
- )} -
- - {/* Tab Content - Projects */} -
- {campaigns.length > 0 ? ( -
- -
- ) : ( -
-

- {t("pages.profile.noProjects")} -

-
- )} -
- - {/* Tab Content - Donor Type */} - -
- ) - } - - From 33d2241f2f6cdc3e3c7bb1718c18e490d4e56cd5 Mon Sep 17 00:00:00 2001 From: Pep Villalonga Date: Thu, 23 Jul 2026 12:21:37 +0200 Subject: [PATCH 33/47] fix: set search results per page to 18 --- src/stores/searchStore.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stores/searchStore.ts b/src/stores/searchStore.ts index 56ea512a..7d4e59b4 100644 --- a/src/stores/searchStore.ts +++ b/src/stores/searchStore.ts @@ -31,7 +31,7 @@ function getInitialState(): SearchState { return { filters: { page: 1, - itemsPerPage: 20, + itemsPerPage: 18, }, results: [], isLoading: false, @@ -42,7 +42,7 @@ function getInitialState(): SearchState { lastSearchTime: 0, // Pagination state currentPage: 1, - itemsPerPage: 20, + itemsPerPage: 18, hasNextPage: false, hasPrevPage: false, currentAbortController: undefined, @@ -225,7 +225,7 @@ function createSearchStore() { ...state, filters: { page: 1, - itemsPerPage: 20, + itemsPerPage: 18, }, })), From 69459902710a2f83fce76320e9838d534b68b7a2 Mon Sep 17 00:00:00 2001 From: Dayv1dDev Date: Thu, 23 Jul 2026 13:18:26 +0200 Subject: [PATCH 34/47] fix: use FilterResource type for FIlters component's prop --- src/components/admin/Filters.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/admin/Filters.svelte b/src/components/admin/Filters.svelte index bc61270f..c807d1c1 100644 --- a/src/components/admin/Filters.svelte +++ b/src/components/admin/Filters.svelte @@ -8,6 +8,7 @@ import DateInput from "../library/inputs/DateInput.svelte"; import Grid from "../library/layout/Grid.svelte"; import FilterComposer from "../library/filters/FilterComposer.svelte"; + import type { FilterResource } from "../../utils/filterComposer"; let { filters, @@ -23,7 +24,7 @@ paymentMethodOptions: [string, string][]; chargeStatusOptions: [string, string][]; rangeAmountOptions: [string, string][]; - composedFiltersResource: string[]; + composedFiltersResource: FilterResource[]; initialSearchQuery?: string; }>(); From f473d6956be286befa13c4249f1a9268f12c0eed Mon Sep 17 00:00:00 2001 From: Pep Villalonga Date: Mon, 27 Jul 2026 10:00:10 +0200 Subject: [PATCH 35/47] fix: improve progress chart usability --- src/components/project/Card.svelte | 2 +- src/components/project/ProgressChart.svelte | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/project/Card.svelte b/src/components/project/Card.svelte index f2412596..fd960084 100644 --- a/src/components/project/Card.svelte +++ b/src/components/project/Card.svelte @@ -37,7 +37,7 @@ {:else if hasReached(project.budget?.minimum?.money)} {$t("domain.project.campaign.minimumReached")} diff --git a/src/components/project/ProgressChart.svelte b/src/components/project/ProgressChart.svelte index e004a8e2..b4d096b9 100644 --- a/src/components/project/ProgressChart.svelte +++ b/src/components/project/ProgressChart.svelte @@ -183,12 +183,21 @@ fill: "start", tension: 0.25, pointRadius: 0, + pointHoverRadius: 4, + pointHoverBorderWidth: 2, + pointHoverBackgroundColor: "#fff", + pointHoverBorderColor: "#59E9D3", }, ], }, options: { responsive: true, maintainAspectRatio: false, + interaction: { + mode: "index", + intersect: false, + axis: "x", + }, scales: { x: { type: "linear", @@ -210,6 +219,9 @@ plugins: { legend: { display: false }, tooltip: { + mode: "index", + intersect: false, + position: "nearest", callbacks: { title: (items) => { const dataIndex = items[0].dataIndex; From a477cee61088366dccea80eb57924fb8c84c4030 Mon Sep 17 00:00:00 2001 From: Pep Villalonga Date: Mon, 27 Jul 2026 10:45:53 +0200 Subject: [PATCH 36/47] fix: preserve login callback on protected routes --- src/middleware/index.ts | 8 ++++++-- src/pages/[...locale]/login/callback.astro | 12 ++++++------ src/pages/[...locale]/login/index.astro | 11 ++++++++--- src/utils/navigation.ts | 3 ++- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/middleware/index.ts b/src/middleware/index.ts index a998da50..19900cfb 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -4,6 +4,7 @@ import { checkAuth } from "./firewall"; import { getLanguage, getUserLangPreferences } from "./utils"; import { getSession } from "../auth/session"; import { useTranslations } from "../i18n/utils"; +import { goto } from "../utils/navigation"; import type { Locale } from "../i18n/locales/index"; import type { APIContext } from "astro"; @@ -13,8 +14,11 @@ export const onRequest = defineMiddleware(async (context: APIContext, next) => { switch (auth.type) { case "basic-auth": return auth.response; - case "unauthorized": - return context.rewrite("/login"); + case "unauthorized": { + const callback = context.url.pathname + context.url.search; + + return goto("/login", { query: { callback } }); + } case "forbidden": return context.rewrite("/403"); } diff --git a/src/pages/[...locale]/login/callback.astro b/src/pages/[...locale]/login/callback.astro index 3438f256..164674f9 100644 --- a/src/pages/[...locale]/login/callback.astro +++ b/src/pages/[...locale]/login/callback.astro @@ -6,27 +6,27 @@ import { authorizationCode } from "../../../auth/grant"; const code = Astro.url.searchParams.get("code"); if (code === null) { console.error("Visited OAuth2 login callback without code"); - return Astro.redirect("/error"); + return Astro.redirect("/500"); } const csrfToken = Astro.url.searchParams.get("state"); if (csrfToken === null) { console.error("Visited OAuth2 login callback without CSRF token"); - return Astro.redirect("/error"); + return Astro.redirect("/500"); } const csrfCookie = Astro.cookies.get("oauth-csrf"); if (!csrfCookie || csrfToken !== csrfCookie.value) { console.error("The OAuth2 CSRF cookie was not present or mismatched"); - return Astro.redirect("/error"); + return Astro.redirect("/500"); } else { - Astro.cookies.delete("oauth-csrf"); + Astro.cookies.delete("oauth-csrf", { path: "/" }); } const csrf = new CSRF(); if (!csrf.verify(import.meta.env.OAUTH2_CLIENT_SECRET, csrfToken)) { console.error(`Could not verify CSRF token: ${csrfToken}`); - return Astro.redirect("/error"); + return Astro.redirect("/500"); } const oauth = await authorizationCode({ @@ -38,7 +38,7 @@ const session = await buildSession(oauth); setSession(Astro.cookies, session); const withCallback = Astro.cookies.get("callback"); -Astro.cookies.delete("callback"); +Astro.cookies.delete("callback", { path: "/" }); return Astro.redirect(withCallback?.value || "/"); --- diff --git a/src/pages/[...locale]/login/index.astro b/src/pages/[...locale]/login/index.astro index 32a45597..89aac93f 100644 --- a/src/pages/[...locale]/login/index.astro +++ b/src/pages/[...locale]/login/index.astro @@ -4,7 +4,7 @@ import { getSession, isExpired } from "../../../auth/session"; const wantsCallback = Astro.url.searchParams.get("callback"); if (wantsCallback) { - Astro.cookies.delete("callback"); + Astro.cookies.delete("callback", { path: "/" }); Astro.cookies.set("callback", wantsCallback, { path: "/" }); } @@ -23,8 +23,13 @@ oauth2Authorize.searchParams.set("scope", "email"); const csrf = new CSRF(); const csrfToken = csrf.create(import.meta.env.OAUTH2_CLIENT_SECRET); -Astro.cookies.delete("oauth-csrf"); -Astro.cookies.set("oauth-csrf", csrfToken); +Astro.cookies.delete("oauth-csrf", { path: "/" }); +Astro.cookies.set("oauth-csrf", csrfToken, { + path: "/", + httpOnly: true, + secure: true, + sameSite: "lax", +}); oauth2Authorize.searchParams.set("state", csrfToken); return Astro.redirect(oauth2Authorize); diff --git a/src/utils/navigation.ts b/src/utils/navigation.ts index 38cf2a85..f41b56d9 100644 --- a/src/utils/navigation.ts +++ b/src/utils/navigation.ts @@ -19,7 +19,8 @@ const defaultOptions: RedirectOptions = { export function goto(target: URL | string, options: RedirectOptions = defaultOptions): Response { const status = options.type === "permanent" ? 301 : 302; - const query = options.query ? "?" + options.query.toString() : ""; + const params = options.query ? new URLSearchParams(options.query).toString() : ""; + const query = params ? `?${params}` : ""; return new Response(null, { status, headers: { Location: target + query } }); } From 16575245628c9b227c9a425e4244d8e5a8d76d54 Mon Sep 17 00:00:00 2001 From: Dayv1dDev Date: Mon, 27 Jul 2026 10:51:49 +0200 Subject: [PATCH 37/47] fix: preview button styles in project edit form --- .../project/[idOrSlug]/edit/ProjectEditorShell.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/[...locale]/project/[idOrSlug]/edit/ProjectEditorShell.svelte b/src/pages/[...locale]/project/[idOrSlug]/edit/ProjectEditorShell.svelte index d0389e07..8185927b 100644 --- a/src/pages/[...locale]/project/[idOrSlug]/edit/ProjectEditorShell.svelte +++ b/src/pages/[...locale]/project/[idOrSlug]/edit/ProjectEditorShell.svelte @@ -203,8 +203,8 @@
- Date: Tue, 28 Jul 2026 13:21:52 +0200 Subject: [PATCH 38/47] feat: add admin section menu to header --- src/components/admin/AdminNav.svelte | 53 ++++++++ src/i18n/locales/es.json | 4 + src/layouts/AdminHeader.svelte | 12 ++ src/layouts/App.svelte | 20 ++- src/layouts/Header.svelte | 140 +++++++++++---------- src/pages/[...locale]/admin/charges.astro | 2 +- src/pages/[...locale]/admin/projects.astro | 2 +- 7 files changed, 160 insertions(+), 73 deletions(-) create mode 100644 src/components/admin/AdminNav.svelte create mode 100644 src/layouts/AdminHeader.svelte diff --git a/src/components/admin/AdminNav.svelte b/src/components/admin/AdminNav.svelte new file mode 100644 index 00000000..f7dc93ca --- /dev/null +++ b/src/components/admin/AdminNav.svelte @@ -0,0 +1,53 @@ + + + + + diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index c3bc68ec..a0c4a4f3 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -1096,6 +1096,10 @@ } }, "admin": { + "nav": { + "charges": "Cobros", + "projects": "Proyectos" + }, "charges": { "title": "Aportes", "description": "Aquí tienes las últimas contribuciones a campañas, puedes ordenarlas y filtrarlas como quieras.", diff --git a/src/layouts/AdminHeader.svelte b/src/layouts/AdminHeader.svelte new file mode 100644 index 00000000..20e6b171 --- /dev/null +++ b/src/layouts/AdminHeader.svelte @@ -0,0 +1,12 @@ + + +
+ {#snippet nav()} + + {/snippet} + +
diff --git a/src/layouts/App.svelte b/src/layouts/App.svelte index 752b893d..ad061770 100644 --- a/src/layouts/App.svelte +++ b/src/layouts/App.svelte @@ -3,6 +3,7 @@ import { get } from "svelte/store"; import { twMerge, type ClassNameValue } from "tailwind-merge"; + import AdminHeader from "./AdminHeader.svelte"; import Footer from "./Footer.svelte"; import Header from "./Header.svelte"; import HeaderSubmenu from "./HeaderSubmenu.svelte"; @@ -19,9 +20,16 @@ session?: Session; children?: Snippet; class?: ClassNameValue; + admin?: boolean; } - let { locale: localeProp, session: sessionProp, children, class: classes }: AppState = $props(); + let { + locale: localeProp, + session: sessionProp, + children, + class: classes, + admin = false, + }: AppState = $props(); onMount(() => { client.interceptors.request.use(async (request) => { @@ -53,9 +61,13 @@ }); -
- -
+{#if admin} + +{:else} +
+ +
+{/if}
{ const observer = new ResizeObserver(() => { document.documentElement.style.setProperty( @@ -51,73 +50,80 @@ return () => observer.disconnect(); }} > -
- - + + {@render nav?.()}
- +
diff --git a/src/pages/[...locale]/admin/projects.astro b/src/pages/[...locale]/admin/projects.astro index 91808bc4..c35a92e1 100644 --- a/src/pages/[...locale]/admin/projects.astro +++ b/src/pages/[...locale]/admin/projects.astro @@ -7,7 +7,7 @@ const { t, lang, session } = Astro.locals; --- - +
From 6893baabeac674b524099d6f146a7d624b3865da Mon Sep 17 00:00:00 2001 From: Pep Villalonga Date: Tue, 28 Jul 2026 13:22:00 +0200 Subject: [PATCH 39/47] chore: ignore graphify-out in eslint --- eslint.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/eslint.config.js b/eslint.config.js index 950deb95..a8f5d199 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -34,6 +34,7 @@ export default [ ".github/**", "env.d.ts", "storybook-static/**", + "graphify-out/**", ], }, { From d9681bc7164244efbc53b493a84fa31f63d7f0e7 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 29 Jul 2026 12:21:25 +0200 Subject: [PATCH 40/47] fix: bad logging of error response data --- src/actions/payment.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/actions/payment.ts b/src/actions/payment.ts index a88f3717..24255e3c 100644 --- a/src/actions/payment.ts +++ b/src/actions/payment.ts @@ -45,7 +45,7 @@ export const payment = defineAction({ const charges: GatewayCharge[] = Object.values(cart.items); - const response = await apiGatewayCheckoutsPost({ + const { data, error } = await apiGatewayCheckoutsPost({ headers: session.token.asHttpHeaders, body: { origin: session.user.accounting!, @@ -58,8 +58,8 @@ export const payment = defineAction({ }, }); - if (response.error) { - console.log(response); + if (error) { + console.error(error); throw new ActionError({ code: "BAD_REQUEST", @@ -67,7 +67,7 @@ export const payment = defineAction({ }); } - return { success: true, checkout: response.data }; + return { success: true, checkout: data }; } catch (err) { console.error(err); From 4a9b9df2a515feeb33ea58c38fc05489bc2a508f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 29 Jul 2026 12:25:07 +0200 Subject: [PATCH 41/47] fix: console log checkout payload --- src/actions/payment.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/actions/payment.ts b/src/actions/payment.ts index 24255e3c..d24a76fb 100644 --- a/src/actions/payment.ts +++ b/src/actions/payment.ts @@ -44,22 +44,24 @@ export const payment = defineAction({ } const charges: GatewayCharge[] = Object.values(cart.items); + const payload = { + origin: session.user.accounting!, + gateway: client.buildUrl({ + url: apiGatewaysIdGetUrl, + path: { id: paymentMethod }, + }), + returnUrl: `${context.url.origin}/checkout/verify`, + charges, + }; const { data, error } = await apiGatewayCheckoutsPost({ headers: session.token.asHttpHeaders, - body: { - origin: session.user.accounting!, - gateway: client.buildUrl({ - url: apiGatewaysIdGetUrl, - path: { id: paymentMethod }, - }), - returnUrl: `${context.url.origin}/checkout/verify`, - charges, - }, + body: payload, }); if (error) { console.error(error); + console.log(payload); throw new ActionError({ code: "BAD_REQUEST", From 0351b1d8786109e169eda068e55d9d3ef67241e0 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 29 Jul 2026 13:14:48 +0200 Subject: [PATCH 42/47] fix: eslint incompat issue --- package.json | 2 +- pnpm-lock.yaml | 292 +++++++++++++++++++++++++++++-------------------- 2 files changed, 175 insertions(+), 119 deletions(-) diff --git a/package.json b/package.json index 231de888..2b7d4c42 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "@vitest/browser-playwright": "^4.1.10", "@vitest/coverage-v8": "^4.1.10", "cypress": "^15.18.1", - "eslint": "^10.0.1", + "eslint": "^9.39.5", "eslint-plugin-cypress": "^6.4.3", "eslint-plugin-import": "^2.32.0", "eslint-plugin-storybook": "^10.5.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 839b2002..229697a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,7 +122,7 @@ importers: version: 5.20260722.1 '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) + version: 10.0.1(eslint@9.39.5(jiti@2.7.0)) '@hey-api/openapi-ts': specifier: ^0.69.2 version: 0.69.2(magicast@0.3.5)(typescript@5.9.3) @@ -152,10 +152,10 @@ importers: version: 26.1.1 '@typescript-eslint/eslint-plugin': specifier: ^8.64.0 - version: 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + version: 8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/parser': specifier: ^8.64.0 - version: 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + version: 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) '@vitest/browser-playwright': specifier: ^4.1.10 version: 4.1.10(playwright@1.61.1)(vite@6.4.2(@types/node@26.1.1)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.8.4))(vitest@4.1.10) @@ -166,23 +166,23 @@ importers: specifier: ^15.18.1 version: 15.19.0 eslint: - specifier: ^10.0.1 - version: 10.7.0(jiti@2.7.0) + specifier: ^9.39.5 + version: 9.39.5(jiti@2.7.0) eslint-plugin-cypress: specifier: ^6.4.3 - version: 6.4.3(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0)) + version: 6.4.3(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0)) + version: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-storybook: specifier: ^10.5.2 - version: 10.5.3(eslint@10.7.0(jiti@2.7.0))(storybook@10.5.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.10.0)(@types/react@19.2.14)(prettier@3.9.5)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + version: 10.5.3(eslint@9.39.5(jiti@2.7.0))(storybook@10.5.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.10.0)(@types/react@19.2.14)(prettier@3.9.5)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) eslint-plugin-svelte: specifier: ^3.22.0 - version: 3.22.0(eslint@10.7.0(jiti@2.7.0))(svelte@5.56.7(@typescript-eslint/types@8.65.0)) + version: 3.22.0(eslint@9.39.5(jiti@2.7.0))(svelte@5.56.7(@typescript-eslint/types@8.65.0)) eslint-plugin-unused-imports: specifier: ^4.4.1 - version: 4.4.1(@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0)) + version: 4.4.1(@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)) flowbite: specifier: ^3.1.2 version: 3.1.2(rollup@4.60.3) @@ -224,7 +224,7 @@ importers: version: 4.1.1(svelte@5.56.7(@typescript-eslint/types@8.65.0)) typescript-eslint: specifier: ^8.64.0 - version: 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + version: 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) vitest: specifier: ^4.1.10 version: 4.1.10(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(vite@6.4.2(@types/node@26.1.1)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.8.4)) @@ -1184,17 +1184,21 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.23.5': - resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.6.0': - resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@1.2.1': - resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.6': + resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@10.0.1': resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} @@ -1205,13 +1209,17 @@ packages: eslint: optional: true - '@eslint/object-schema@3.0.5': - resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.7.2': - resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -2326,9 +2334,6 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/esrecurse@4.3.1': - resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -2793,6 +2798,10 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + camelcase@8.0.0: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} @@ -3350,10 +3359,6 @@ packages: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-scope@9.1.2: - resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3366,9 +3371,9 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.7.0: - resolution: {integrity: sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@9.39.5: + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: jiti: '*' @@ -3383,10 +3388,6 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@11.2.0: - resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -3646,6 +3647,10 @@ packages: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + globals@16.5.0: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} @@ -3780,6 +3785,10 @@ packages: imagesloaded@5.0.0: resolution: {integrity: sha512-/0JGSubc1MTCoDKVmonLHgbifBWHdyLkun+R/151E1c5n79hiSxcd7cB7mPXFgojYu8xnRZv7GYxzKoxW8BetQ==} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -4005,6 +4014,10 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + jsbn@0.1.1: resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} @@ -4153,6 +4166,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} @@ -4580,6 +4596,10 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + parse-latin@7.0.0: resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} @@ -4952,6 +4972,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} @@ -5234,6 +5258,10 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + strtok3@10.3.5: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} @@ -6878,38 +6906,54 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.7.0(jiti@2.7.0))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.5(jiti@2.7.0))': dependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 9.39.5(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.5': + '@eslint/config-array@0.21.2': dependencies: - '@eslint/object-schema': 3.0.5 + '@eslint/object-schema': 2.1.7 debug: 4.4.3(supports-color@8.1.1) - minimatch: 10.2.5 + minimatch: 3.1.5 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.6.0': + '@eslint/config-helpers@0.4.2': dependencies: - '@eslint/core': 1.2.1 + '@eslint/core': 0.17.0 - '@eslint/core@1.2.1': + '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.7.0(jiti@2.7.0))': - optionalDependencies: - eslint: 10.7.0(jiti@2.7.0) - - '@eslint/object-schema@3.0.5': {} - - '@eslint/plugin-kit@0.7.2': + '@eslint/eslintrc@3.3.6': dependencies: - '@eslint/core': 1.2.1 + ajv: 6.15.0 + debug: 4.4.3(supports-color@8.1.1) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@10.0.1(eslint@9.39.5(jiti@2.7.0))': + optionalDependencies: + eslint: 9.39.5(jiti@2.7.0) + + '@eslint/js@9.39.5': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 levn: 0.4.1 '@floating-ui/core@1.7.5': @@ -7888,8 +7932,6 @@ snapshots: '@types/deep-eql@4.0.2': {} - '@types/esrecurse@4.3.1': {} - '@types/estree@1.0.8': {} '@types/hast@3.0.4': @@ -7936,15 +7978,15 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/type-utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.65.0 - eslint: 10.7.0(jiti@2.7.0) + eslint: 9.39.5(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -7952,14 +7994,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.65.0 '@typescript-eslint/types': 8.65.0 '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.65.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.7.0(jiti@2.7.0) + eslint: 9.39.5(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -7982,13 +8024,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.65.0 '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) debug: 4.4.3(supports-color@8.1.1) - eslint: 10.7.0(jiti@2.7.0) + eslint: 9.39.5(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -8011,13 +8053,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/utils@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.65.0 '@typescript-eslint/types': 8.65.0 '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) - eslint: 10.7.0(jiti@2.7.0) + eslint: 9.39.5(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -8582,6 +8624,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + callsites@3.1.0: {} + camelcase@8.0.0: {} caseless@0.12.0: {} @@ -9187,24 +9231,24 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@10.7.0(jiti@2.7.0)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.5(jiti@2.7.0)): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: - '@typescript-eslint/parser': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) - eslint: 10.7.0(jiti@2.7.0) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 transitivePeerDependencies: - supports-color - eslint-plugin-cypress@6.4.3(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-cypress@6.4.3(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)): dependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 9.39.5(jiti@2.7.0) globals: 17.7.0 optionalDependencies: - '@typescript-eslint/parser': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -9213,9 +9257,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 - eslint: 10.7.0(jiti@2.7.0) + eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@10.7.0(jiti@2.7.0)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.5(jiti@2.7.0)) hasown: 2.0.3 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -9227,27 +9271,27 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-storybook@10.5.3(eslint@10.7.0(jiti@2.7.0))(storybook@10.5.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.10.0)(@types/react@19.2.14)(prettier@3.9.5)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3): + eslint-plugin-storybook@10.5.3(eslint@9.39.5(jiti@2.7.0))(storybook@10.5.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.10.0)(@types/react@19.2.14)(prettier@3.9.5)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3): dependencies: '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) - eslint: 10.7.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0) storybook: 10.5.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.10.0)(@types/react@19.2.14)(prettier@3.9.5)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-svelte@3.22.0(eslint@10.7.0(jiti@2.7.0))(svelte@5.56.7(@typescript-eslint/types@8.65.0)): + eslint-plugin-svelte@3.22.0(eslint@9.39.5(jiti@2.7.0))(svelte@5.56.7(@typescript-eslint/types@8.65.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0)) '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 10.7.0(jiti@2.7.0) + eslint: 9.39.5(jiti@2.7.0) esutils: 2.0.3 globals: 16.5.0 known-css-properties: 0.37.0 @@ -9261,49 +9305,45 @@ snapshots: transitivePeerDependencies: - ts-node - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)): dependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 9.39.5(jiti@2.7.0) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 - eslint-scope@9.1.2: - dependencies: - '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.8 - esrecurse: 4.3.0 - estraverse: 5.3.0 - eslint-visitor-keys@3.4.3: {} eslint-visitor-keys@4.2.1: {} eslint-visitor-keys@5.0.1: {} - eslint@10.7.0(jiti@2.7.0): + eslint@9.39.5(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5 - '@eslint/config-helpers': 0.6.0 - '@eslint/core': 1.2.1 - '@eslint/plugin-kit': 0.7.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.6 + '@eslint/js': 9.39.5 + '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 ajv: 6.15.0 + chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 - eslint-scope: 9.1.2 - eslint-visitor-keys: 5.0.1 - espree: 11.2.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -9314,7 +9354,8 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.5 + lodash.merge: 4.6.2 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -9330,12 +9371,6 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 - espree@11.2.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 5.0.1 - esprima@4.0.1: {} esquery@1.7.0: @@ -9626,6 +9661,8 @@ snapshots: dependencies: ini: 2.0.0 + globals@14.0.0: {} + globals@16.5.0: {} globals@17.7.0: {} @@ -9817,6 +9854,11 @@ snapshots: dependencies: ev-emitter: 2.1.2 + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + import-meta-resolve@4.2.0: {} imurmurhash@0.1.4: {} @@ -10033,6 +10075,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + jsbn@0.1.1: {} json-buffer@3.0.1: {} @@ -10153,6 +10199,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} lodash@4.18.1: {} @@ -10823,6 +10871,10 @@ snapshots: package-manager-detector@1.6.0: {} + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + parse-latin@7.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -11188,6 +11240,8 @@ snapshots: require-from-string@2.0.2: {} + resolve-from@4.0.0: {} + resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -11607,6 +11661,8 @@ snapshots: dependencies: min-indent: 1.0.1 + strip-json-comments@3.1.1: {} + strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 @@ -11861,13 +11917,13 @@ snapshots: dependencies: semver: 7.7.4 - typescript-eslint@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3): + typescript-eslint@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/parser': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) - eslint: 10.7.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color From 8baacbfa388018f2ac41a1c6acd7dade00682d17 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 29 Jul 2026 13:14:59 +0200 Subject: [PATCH 43/47] fix: code format issues by eslint and prettier --- src/components/admin/Filters.svelte | 8 +++++-- .../library/filters/FilterComposer.svelte | 4 ++-- .../library/filters/FilterComposerRow.svelte | 22 +++++++++++-------- src/utils/filterComposer.ts | 7 +++--- src/utils/filterSuggestions.ts | 5 +++-- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/components/admin/Filters.svelte b/src/components/admin/Filters.svelte index c807d1c1..01dbc17e 100644 --- a/src/components/admin/Filters.svelte +++ b/src/components/admin/Filters.svelte @@ -5,9 +5,10 @@ import { type ApiGatewayChargesGetCollectionData } from "../../openapi/client/index"; import FiltersIcon from "../icons/filters/Filters.svelte"; import Button from "../library/buttons/Button.svelte"; + import FilterComposer from "../library/filters/FilterComposer.svelte"; import DateInput from "../library/inputs/DateInput.svelte"; import Grid from "../library/layout/Grid.svelte"; - import FilterComposer from "../library/filters/FilterComposer.svelte"; + import type { FilterResource } from "../../utils/filterComposer"; let { @@ -198,7 +199,10 @@

{$t("pages.admin.charges.filters.composer.title")}

- (composerParams = params)} /> + (composerParams = params)} + />
{/if}
diff --git a/src/components/library/filters/FilterComposer.svelte b/src/components/library/filters/FilterComposer.svelte index 17965fe3..e2da7670 100644 --- a/src/components/library/filters/FilterComposer.svelte +++ b/src/components/library/filters/FilterComposer.svelte @@ -1,6 +1,6 @@ + + + + + +{#if mobileOpen} + +{/if} + + +{#if mobileOpen} + +{/if} + + + diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 9f2800c8..f9fbd16d 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -1096,6 +1096,12 @@ } }, "admin": { + "sidebar": { + "aria": { + "close": "Cerrar menú", + "open": "Abrir menú de administración" + } + }, "charges": { "title": "Aportes", "description": "Aquí tienes las últimas contribuciones a campañas, puedes ordenarlas y filtrarlas como quieras.", diff --git a/src/layouts/AdminSidebar.svelte b/src/layouts/AdminSidebar.svelte new file mode 100644 index 00000000..c304e1f7 --- /dev/null +++ b/src/layouts/AdminSidebar.svelte @@ -0,0 +1,25 @@ + + +
+
+ + +
+ {@render children?.()} +
+
+
From abbf5218e7e1e25a64724366b3b80ac8b00bb701 Mon Sep 17 00:00:00 2001 From: Dayv1dDev Date: Fri, 31 Jul 2026 11:09:58 +0200 Subject: [PATCH 45/47] fix: es.json file --- src/i18n/locales/es.json | 1007 +++++++++++++++++++++----------------- 1 file changed, 547 insertions(+), 460 deletions(-) diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 99cff1b7..18470a45 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -1096,507 +1096,594 @@ } }, "admin": { + "filter": { + "btns": { + "openFilters": "Filtros", + "closeFilters": "Cerrar filtros", + "apply": "Aplicar filtros" + }, + "composer": { + "title": "Filtros personalizados", + "addFilter": "Añadir filtro", + "removeFilter": "Eliminar", + "subjectPlaceholder": "Seleccionar campo", + "operatorPlaceholder": "Seleccionar condición", + "referentPlaceholder": "Valor", + "subject": { + "title": "Título", + "subtitle": "Subtítulo", + "description": "Descripción", + "projectStatus": "Estado de proyecto", + "chargeStatus": "Estado de aporte", + "categories": "Categoría", + "owner": "Propietario", + "slug": "ID", + "territoryCountry": "Territorio", + "dateUpdated": "Última actualización", + "budgetAmount": "Importe", + "gateway": "Pasarela", + "type": "Tipo", + "target": "Destino", + "currency": "Moneda", + "amount": "Importe", + "dateCreated": "Fecha de creación", + "handle": "Nombre de usuario", + "email": "Email", + "displayName": "Nombre público", + "userType": "Tipo de usuario", + "active": "Activo", + "userRoles": "Roles", + "userTerritoryCountry": "Ubicación" + }, + "operator": { + "equals": "Igual a", + "is_any_of": "Incluido en", + "before": "Anterior a", + "after": "Posterior a", + "strictly_before": "Estrictamente anterior", + "strictly_after": "Estrictamente posterior", + "gte": "Mayor o igual que", + "gt": "Mayor que", + "lte": "Menor o igual que", + "lt": "Menor que" + }, + "subjectOption": { + "userType": { + "individual": "Persona física", + "organization": "Persona jurídica (empresas)" + }, + "active": { + "true": "Activo", + "false": "Inactivo" + } + } + } + }, + "nav": { + "charges": "Cobros", + "projects": "Proyectos", + "users": "Usuarios" + }, "sidebar": { "aria": { "close": "Cerrar menú", "open": "Abrir menú de administración" + } + }, + "charges": { + "title": "Aportes", + "description": "Aquí tienes las últimas contribuciones a campañas, puedes ordenarlas y filtrarlas como quieras.", + "headers": { + "target": "Destino", + "amount": "Importe", + "origin": "Origen", + "paymentMethod": "Método", + "date": "Fecha", + "chargeStatus": "Estado de la operación", + "refundToWallet": "A Monedero" }, - "nav": { - "charges": "Cobros", - "projects": "Proyectos" - }, - "charges": { - "title": "Aportes", - "description": "Aquí tienes las últimas contribuciones a campañas, puedes ordenarlas y filtrarlas como quieras.", - "headers": { - "target": "Destino", - "amount": "Importe", - "origin": "Origen", - "paymentMethod": "Método", - "date": "Fecha", - "chargeStatus": "Estado de la operación", - "refundToWallet": "A Monedero" - }, - "noData": "No hay datos disponibles", - "details": { - "operationTime": "Hora de la operación", - "trackingCodes": { - "title": "Códigos de seguimiento", - "headers": { - "title": "Título", - "trackingCode": "Código de seguimiento" - } - }, - "toWallet": "A monedero", - "platformLinks": { - "title": "Enlace a la plataforma", - "headers": { - "type": "Tipo de Enlace", - "rel": "Relación", - "href": "Url" - } - }, - "estimatedFee": "Comisión estimada", - "concept": "Concepto" - }, - "actions": { - "refundToWallet": "Devolución a monedero", - "refundToUser": "Devolución a usuario", - "changeUser": "Cambiar de usuario", - "manageReward": "Gestionar recompensa" - }, - "filters": { - "order": { - "title": "Ordenar por:", - "options": { - "date-desc": "Fecha (más reciente primero)", - "date-asc": "Fecha (más antigua primero)", - "amount-desc": "Importe (mayor a menor)", - "amount-asc": "Importe (menor a mayor)", - "status-asc": "Estado (A-Z)", - "status-desc": "Estado (Z-A)" - } - }, - "itemsPerPage": { - "title": "Líneas por página:", - "options": { - "10": "10", - "20": "20", - "30": "30", - "50": "50" - } - }, - "search": { - "placeholder": "Buscar...", - "resultsFound": "{{totalItems}} resultados para \"{{query}}\"", - "noResults": "No se han encontrado resultados" - }, - "paymentMethod": { - "title": "Método de pago", - "options": { - "all": "Todos", - "paypal": "Paypal", - "stripe": "Stripe", - "wallet": "Monedero" - } - }, - "rangeAmount": { - "title": "Selecciona importe", - "options": { - "500000": "+ 5.000€", - "all": "Todos", - "000..10000": "0€ - 100€", - "10000..50000": "100€ - 500€", - "50000..100000": "500€ - 1.000€", - "100000..500000": "1.000€ - 5.000€" - } - }, - "dateRange": { - "initDate": "Fecha Inicio", - "endDate": "Fecha Fin", - "errors": { - "invalidRange": "La fecha de inicio no puede ser mayor que la fecha fin." - } - }, - "chargeStatus": { - "title": "Estado de Aporte", - "options": { - "all": "Todos", - "to_charge": "Por cobrar", - "in_charge": "Cobrado", - "to_refund": "Por reembolsar", - "refunded": "Reembolsado", - "to_wallet": "Por reembolsar a monedero", - "walleted": "Reembolsado a monedero" - } - }, - "btns": { - "openFilters": "Filtros", - "closeFilters": "Cerrar filtros", - "apply": "Aplicar filtros" - }, - "composer": { - "title": "Filtros personalizados", - "addFilter": "Añadir filtro", - "removeFilter": "Eliminar", - "subjectPlaceholder": "Seleccionar campo", - "operatorPlaceholder": "Seleccionar condición", - "referentPlaceholder": "Valor", - "subject": { - "title": "Título", - "subtitle": "Subtítulo", - "description": "Descripción", - "projectStatus": "Estado de proyecto", - "chargeStatus": "Estado de aporte", - "categories": "Categoría", - "owner": "Propietario", - "slug": "ID", - "territoryCountry": "Territorio", - "dateUpdated": "Última actualización", - "budgetAmount": "Importe", - "gateway": "Pasarela", - "type": "Tipo", - "target": "Destino", - "currency": "Moneda", - "amount": "Importe", - "dateCreated": "Fecha de creación" - }, - "operator": { - "equals": "Igual a", - "is_any_of": "Incluido en", - "before": "Anterior a", - "after": "Posterior a", - "strictly_before": "Estrictamente anterior", - "strictly_after": "Estrictamente posterior", - "gte": "Mayor o igual que", - "gt": "Mayor que", - "lte": "Menor o igual que", - "lt": "Menor que" - } + "noData": "No hay datos disponibles", + "details": { + "operationTime": "Hora de la operación", + "trackingCodes": { + "title": "Códigos de seguimiento", + "headers": { + "title": "Título", + "trackingCode": "Código de seguimiento" } }, - "pagination": { - "showing": "Mostrando {{items}} de {{total}}", - "unloaded": "Mostrando...", - "noResults": "No hay resultados" + "toWallet": "A monedero", + "platformLinks": { + "title": "Enlace a la plataforma", + "headers": { + "type": "Tipo de Enlace", + "rel": "Relación", + "href": "Url" + } }, - "export": { - "csv": "Exportar CSV", - "exporting": "Exportando...", - "error": "Error al exportar el archivo CSV. Por favor, inténtalo de nuevo.", - "filename": "gateway-charges", - "itemsText": "items", - "noData": "No se encontraron datos para exportar" + "estimatedFee": "Comisión estimada", + "concept": "Concepto" + }, + "actions": { + "refundToWallet": "Devolución a monedero", + "refundToUser": "Devolución a usuario", + "changeUser": "Cambiar de usuario", + "manageReward": "Gestionar recompensa" + }, + "filters": { + "order": { + "title": "Ordenar por:", + "options": { + "date-desc": "Fecha (más reciente primero)", + "date-asc": "Fecha (más antigua primero)", + "amount-desc": "Importe (mayor a menor)", + "amount-asc": "Importe (menor a mayor)", + "status-asc": "Estado (A-Z)", + "status-desc": "Estado (Z-A)" + } }, - "downloadButtons": { - "certificates": "Descarga certificados", - "pdf": "Descarga PDF" + "itemsPerPage": { + "title": "Líneas por página:", + "options": { + "10": "10", + "20": "20", + "30": "30", + "50": "50" + } }, - "lastEdited": "Última modificación: {{date}} {{time}}", - "annotations": "{{annotations}} Anotaciones", - "lastContributions": "Últimos aportes:", - "totalizers": { - "selected": "Aportes seleccionados:", - "totalCharges": "Total aportes:", - "totalTips": "Total propinas:", - "totalFees": "Comisiones totales:" + "search": { + "placeholder": "Buscar...", + "resultsFound": "{{totalItems}} resultados para \"{{query}}\"", + "noResults": "No se han encontrado resultados" + }, + "paymentMethod": { + "title": "Método de pago", + "options": { + "all": "Todos", + "paypal": "Paypal", + "stripe": "Stripe", + "wallet": "Monedero" + } + }, + "rangeAmount": { + "title": "Selecciona importe", + "options": { + "500000": "+ 5.000€", + "all": "Todos", + "000..10000": "0€ - 100€", + "10000..50000": "100€ - 500€", + "50000..100000": "500€ - 1.000€", + "100000..500000": "1.000€ - 5.000€" + } + }, + "dateRange": { + "initDate": "Fecha Inicio", + "endDate": "Fecha Fin", + "errors": { + "invalidRange": "La fecha de inicio no puede ser mayor que la fecha fin." + } + }, + "chargeStatus": { + "title": "Estado de Aporte", + "options": { + "all": "Todos", + "to_charge": "Por cobrar", + "in_charge": "Cobrado", + "to_refund": "Por reembolsar", + "refunded": "Reembolsado", + "to_wallet": "Por reembolsar a monedero", + "walleted": "Reembolsado a monedero" + } } }, - "projects": { - "title": "Proyectos", - "description": "Busca proyectos y gestiónalos de la manera más rápida posible gracias al buscador", - "lastProjects": "Listado de campañas:", - "export": { - "csv": "Exportar .csv" + "pagination": { + "showing": "Mostrando {{items}} de {{total}}", + "unloaded": "Mostrando...", + "noResults": "No hay resultados" + }, + "export": { + "csv": "Exportar CSV", + "exporting": "Exportando...", + "error": "Error al exportar el archivo CSV. Por favor, inténtalo de nuevo.", + "filename": "gateway-charges", + "itemsText": "items", + "noData": "No se encontraron datos para exportar" + }, + "downloadButtons": { + "certificates": "Descarga certificados", + "pdf": "Descarga PDF" + }, + "lastEdited": "Última modificación: {{date}} {{time}}", + "annotations": "{{annotations}} Anotaciones", + "lastContributions": "Últimos aportes:", + "totalizers": { + "selected": "Aportes seleccionados:", + "totalCharges": "Total aportes:", + "totalTips": "Total propinas:", + "totalFees": "Comisiones totales:" + } + }, + "projects": { + "title": "Proyectos", + "description": "Busca proyectos y gestiónalos de la manera más rápida posible gracias al buscador", + "lastProjects": "Listado de campañas:", + "export": { + "csv": "Exportar .csv" + }, + "totalizers": { + "selected": "Campañas seleccionadas:", + "totalEarned": "Total conseguido:", + "totalPaid": "Total pagado:", + "totalUnpaid": "Falta por pagar:" + }, + "filters": { + "search": { + "placeholder": "Búsqueda", + "btn": "Buscar", + "minLength": "Es necesario meter más de 4 caracteres" }, - "totalizers": { - "selected": "Campañas seleccionadas:", - "totalEarned": "Total conseguido:", - "totalPaid": "Total pagado:", - "totalUnpaid": "Falta por pagar:" + "campaignStatus": { + "title": "Estado de campaña", + "options": { + "completed": "Completada", + "active": "Activa", + "failed": "Fallida" + } }, - "filters": { - "search": { - "placeholder": "Búsqueda", - "btn": "Buscar", - "minLength": "Es necesario meter más de 4 caracteres" - }, - "btns": { - "openFilters": "Filtros", - "closeFilters": "Cerrar filtros", - "apply": "Aplicar filtros" - }, - "campaignStatus": { - "title": "Estado de campaña", - "options": { - "completed": "Completada", - "active": "Activa", - "failed": "Fallida" - } - }, + "status": { + "title": "Estado de proyecto", + "options": { + "in_draft": "Borrador", + "to_campaign_review": "Pendiente revisión campaña", + "in_campaign_review": "En revisión campaña", + "to_campaign": "Pendiente campaña", + "in_campaign": "En campaña", + "campaign_failed": "Campaña fallida", + "to_funding_review": "Pendiente revisión cobro", + "in_funding_review": "En revisión cobro", + "to_funding": "Pendiente cobro", + "in_funding": "En cobro", + "funding_paid": "Cobro pagado" + } + }, + "dateRange": { + "initDate": "Fecha de inicio", + "endDate": "Fecha fin" + }, + "order": { + "title": "Ordenar por", + "options": { + "date-desc": "Fecha (más reciente primero)", + "date-asc": "Fecha (más antiguo primero)", + "title-asc": "Título A–Z", + "title-desc": "Título Z–A" + } + }, + "itemsPerPage": { + "title": "Líneas por página", + "options": { + "10": "10", + "25": "25", + "50": "50" + } + } + }, + "table": { + "headers": { + "name": "Proyecto", + "promoter": "Impulsor", + "contractNumber": "Número de contrato", + "achieved": "Conseguido", + "paid": "Pagado", + "process": "Estado" + }, + "rows": { "status": { - "title": "Estado de proyecto", - "options": { - "in_draft": "Borrador", - "to_campaign_review": "Pendiente revisión campaña", - "in_campaign_review": "En revisión campaña", - "to_campaign": "Pendiente campaña", - "in_campaign": "En campaña", - "campaign_failed": "Campaña fallida", - "to_funding_review": "Pendiente revisión cobro", - "in_funding_review": "En revisión cobro", - "to_funding": "Pendiente cobro", - "in_funding": "En cobro", - "funding_paid": "Cobro pagado" + "in_draft": "Borrador", + "to_campaign_review": "Pendiente revisión campaña", + "in_campaign_review": "En revisión campaña", + "in_campaign_review_request_change": "Revisión campaña: cambios", + "campaign_review_rejected": "Campaña rechazada", + "to_campaign": "Pendiente campaña", + "in_campaign": "En campaña", + "campaign_failed": "Campaña fallida", + "to_funding_review": "Pendiente revisión cobro", + "in_funding_review": "En revisión cobro", + "in_funding_review_request_change": "Revisión cobro: cambios", + "funding_review_rejected": "Cobro rechazado", + "to_funding": "Pendiente cobro", + "in_funding": "En cobro", + "funding_paid": "Cobro pagado" + }, + "details": { + "datePublished": "Publicado", + "dateEnd1": "Fin primera ronda", + "dateEnd2": "Fin segunda ronda", + "minOptim": "Mínimo - Óptimo", + "email": "Email", + "phone": "Teléfono", + "contractExpiry": "Vencimiento del contrato", + "remaining": "Falta por pagar", + "btns": { + "changeStatus": "Cambiar cuentas", + "createContract": "Formulario de contrato", + "promoter": "Impulsor", + "contributions": "Aportes", + "donors": "Donantes de campaña", + "financialReport": "Informe financiación", + "certificates": "Certificados", + "contractPdf": "Contrato PDF", + "annotations": "Anotaciones" } }, - "dateRange": { - "initDate": "Fecha de inicio", - "endDate": "Fecha fin" - }, - "order": { - "title": "Ordenar por", - "options": { - "date-desc": "Fecha (más reciente primero)", - "date-asc": "Fecha (más antiguo primero)", - "title-asc": "Título A–Z", - "title-desc": "Título Z–A" - } - }, - "itemsPerPage": { - "title": "Líneas por página", - "options": { - "10": "10", - "25": "25", - "50": "50" - } - } + "noData": "No hay datos disponibles" + } + }, + "modals": { + "paid": { + "title": "Pagado", + "description": "Esta cantidad refleja el total de lo aportado a la campaña por todos los medios de pago posibles.", + "input": "Pago", + "matchfunding": "Pago matchfunding", + "save": "Guardar" }, - "table": { - "headers": { - "name": "Proyecto", - "promoter": "Impulsor", - "contractNumber": "Número de contrato", - "achieved": "Conseguido", - "paid": "Pagado", - "process": "Estado" - }, - "rows": { - "status": { - "in_draft": "Borrador", - "to_campaign_review": "Pendiente revisión campaña", - "in_campaign_review": "En revisión campaña", - "in_campaign_review_request_change": "Revisión campaña: cambios", - "campaign_review_rejected": "Campaña rechazada", - "to_campaign": "Pendiente campaña", - "in_campaign": "En campaña", - "campaign_failed": "Campaña fallida", - "to_funding_review": "Pendiente revisión cobro", - "in_funding_review": "En revisión cobro", - "in_funding_review_request_change": "Revisión cobro: cambios", - "funding_review_rejected": "Cobro rechazado", - "to_funding": "Pendiente cobro", - "in_funding": "En cobro", - "funding_paid": "Cobro pagado" - }, - "details": { - "datePublished": "Publicado", - "dateEnd1": "Fin primera ronda", - "dateEnd2": "Fin segunda ronda", - "minOptim": "Mínimo - Óptimo", - "email": "Email", - "phone": "Teléfono", - "contractExpiry": "Vencimiento del contrato", - "remaining": "Falta por pagar", - "btns": { - "changeStatus": "Cambiar cuentas", - "createContract": "Formulario de contrato", - "promoter": "Impulsor", - "contributions": "Aportes", - "donors": "Donantes de campaña", - "financialReport": "Informe financiación", - "certificates": "Certificados", - "contractPdf": "Contrato PDF", - "annotations": "Anotaciones" - } - }, - "noData": "No hay datos disponibles" - } + "annotations": { + "title": "Anotaciones", + "placeholder": "Escribe tu anotación", + "empty": "Sin registro", + "add": "Añadir anotación", + "modify": "Modificar anotación" + } + }, + "informe": { + "title": "Informe de financiación del proyecto", + "subtitle": "Revisa los datos antes de actuar", + "description": "Edite lo necesario, descárguelo en formato PDF o envíe el informe final.", + "fields": { + "invoiceNumber": "Número de factura", + "transferDate": "Fecha de transferencia", + "platformFee": "Comisión de plataforma", + "email": "Correo para envío" }, - "modals": { - "paid": { - "title": "Pagado", - "description": "Esta cantidad refleja el total de lo aportado a la campaña por todos los medios de pago posibles.", - "input": "Pago", - "matchfunding": "Pago matchfunding", - "save": "Guardar" - }, - "annotations": { - "title": "Anotaciones", - "placeholder": "Escribe tu anotación", - "empty": "Sin registro", - "add": "Añadir anotación", - "modify": "Modificar anotación" - } + "btns": { + "edit": "Editar datos", + "pdf": "Descargar PDF", + "send": "Enviar informe" }, - "informe": { - "title": "Informe de financiación del proyecto", - "subtitle": "Revisa los datos antes de actuar", - "description": "Edite lo necesario, descárguelo en formato PDF o envíe el informe final.", + "modal": { + "title": "Edita los datos antes de enviar", "fields": { "invoiceNumber": "Número de factura", - "transferDate": "Fecha de transferencia", + "transferDate": "Fecha de nueva transferencia", "platformFee": "Comisión de plataforma", - "email": "Correo para envío" + "email": "Correo destinatario" }, - "btns": { - "edit": "Editar datos", - "pdf": "Descargar PDF", - "send": "Enviar informe" - }, - "modal": { - "title": "Edita los datos antes de enviar", - "fields": { - "invoiceNumber": "Número de factura", - "transferDate": "Fecha de nueva transferencia", - "platformFee": "Comisión de plataforma", - "email": "Correo destinatario" - }, - "save": "Guardar" + "save": "Guardar" + } + }, + "pagination": { + "showing": "Mostrando {{ items }} de {{ total }}", + "itemsPerPage": "{{ count }} por página" + } + }, + "users": { + "title": "Usuarios", + "description": "Aquí tienes todos los usuarios de la plataforma, puedes buscarlos y filtrarlos como quieras.", + "lastUsers": "Listado de usuarios:", + "export": { + "csv": "Exportar .csv" + }, + "totalizers": { + "selected": "Usuarios seleccionados:" + }, + "filters": { + "search": { + "placeholder": "Buscar por nombre de usuario...", + "btn": "Buscar", + "minLength": "Es necesario escribir al menos 4 caracteres" + }, + "btns": { + "openFilters": "Filtros", + "closeFilters": "Cerrar filtros", + "apply": "Aplicar filtros" + }, + "email": { + "title": "Email" + }, + "order": { + "title": "Ordenar por", + "options": { + "handle-asc": "Nombre de usuario A–Z", + "handle-desc": "Nombre de usuario Z–A" } }, - "pagination": { - "showing": "Mostrando {{ items }} de {{ total }}", - "itemsPerPage": "{{ count }} por página" + "itemsPerPage": { + "title": "Líneas por página", + "options": { + "10": "10", + "25": "25", + "50": "50" + } } - } - }, - "static": { - "title": "Goteo" - }, - "profile": { - "share": "Compartir", - "shareModal": { - "title": "Comparte este perfil" }, - "subscribe": "Subscríbete", - "tabs": { - "about": "Sobre el perfil", - "projects": "Proyectos impulsados", - "donorType": "Tipo de donante" - }, - "noProjects": "No hay proyectos impulsados aún.", - "noBio": "Todavía no hay información sobre este perfil.", - "donorType": { - "fan": { - "title": "Fan", - "description": "Este donante ha invertido más de 25€", - "projectsDonated": "Proyectos donados", - "moneyDonated": "Dinero donado" + "table": { + "headers": { + "handle": "Usuario", + "email": "Email", + "displayName": "Nombre", + "type": "Tipo", + "active": "Activo", + "roles": "Roles" }, - "nextLevel": { - "title": "Próximo nivel", - "description": "Sigue donando para subir de nivel" + "rows": { + "noData": "No hay datos disponibles", + "active": "Activo", + "inactive": "Inactivo", + "type": { + "individual": "Individual", + "organization": "Organización" + }, + "details": { + "id": "ID", + "handle": "Usuario", + "email": "Email", + "type": "Tipo", + "territory": "Ubicación", + "active": "Activo" + } } }, - "title": "Goteo", - "notFound": "Usuario no encontrado" + "pagination": { + "showing": "Mostrando {{ items }} de {{ total }}", + "itemsPerPage": "{{ count }} por página" + } + } + }, + "static": { + "title": "Goteo" + }, + "profile": { + "share": "Compartir", + "shareModal": { + "title": "Comparte este perfil" }, - "login": { - "page": { - "title": "Indícanos tus datos personales", - "description": "Necesitamos saber quién eres antes de pagar.", - "registerOr": "Entra en tu cuenta o", - "registerBtnLabel": "Regístrate", - "otherAccessMethods": "También puedes acceder a través de:", - "forgotPassword": "¿Olvidaste tu contraseña?", - "welcome": { - "title": "Bienvenid@ al nuevo Goteo.", - "subtitle": "Estamos estrenando una nueva etapa y queremos compartirla contigo desde el principio. La plataforma irá creciendo poco a poco: activaremos secciones y funcionalidades a medida que estén listas y validadas con nuestra comunidad. De momento puedes acceder con tu mismo correo y contraseña y explorar el archivo de proyectos. Gracias por acompañarnos en este camino." - } + "subscribe": "Subscríbete", + "tabs": { + "about": "Sobre el perfil", + "projects": "Proyectos impulsados", + "donorType": "Tipo de donante" + }, + "noProjects": "No hay proyectos impulsados aún.", + "noBio": "Todavía no hay información sobre este perfil.", + "donorType": { + "fan": { + "title": "Fan", + "description": "Este donante ha invertido más de 25€", + "projectsDonated": "Proyectos donados", + "moneyDonated": "Dinero donado" }, - "form": { - "email": "Email o nombre de usuario", - "password": "Contraseña" - }, - "error": { - "invalidCredentials": "Correo electrónico o contraseña inválidos.", - "unexpectedLogin": "Error al iniciar sesión. Por favor, inténtalo de nuevo más tarde.", - "invalidEmail": "El correo electrónico no es válido." + "nextLevel": { + "title": "Próximo nivel", + "description": "Sigue donando para subir de nivel" } }, - "logout": { - "label": "Cerrar sesión" - }, - "metadata": { - "description": "Página de inicio" - }, - "hero": { - "title": "Financia el cambio que el mundo necesita", - "description": "Arrancamos una nueva etapa con la misma brújula que nos ha guiado desde el principio: impulsar proyectos que transformen el mundo desde la justicia social, la participación democrática y la defensa de los derechos humanos y de los ecosistemas. Únete a una comunidad que transforma realidades. ¡Sé parte de la solución!", - "buttons": { - "discover": "Descubre campañas", - "impulse": "Impulsa un proyecto" + "title": "Goteo", + "notFound": "Usuario no encontrado" + }, + "login": { + "page": { + "title": "Indícanos tus datos personales", + "description": "Necesitamos saber quién eres antes de pagar.", + "registerOr": "Entra en tu cuenta o", + "registerBtnLabel": "Regístrate", + "otherAccessMethods": "También puedes acceder a través de:", + "forgotPassword": "¿Olvidaste tu contraseña?", + "welcome": { + "title": "Bienvenid@ al nuevo Goteo.", + "subtitle": "Estamos estrenando una nueva etapa y queremos compartirla contigo desde el principio. La plataforma irá creciendo poco a poco: activaremos secciones y funcionalidades a medida que estén listas y validadas con nuestra comunidad. De momento puedes acceder con tu mismo correo y contraseña y explorar el archivo de proyectos. Gracias por acompañarnos en este camino." } }, - "me": { - "page": { - "title": "Actividad", - "subtitle": "Consulta tu actividad en Goteo" - }, - "period": { - "label": "Periodo", - "year": "Año {{year}}" - }, - "donations": { - "title": "Mis donaciones", - "empty": "Aún no has donado a ningún proyecto", - "explore": "Explora proyectos", - "count": "Donaciones", - "total": "Total donado", - "recent": "Últimas donaciones", - "viewAll": "Ver todas", - "certificate": "Solicitar certificado" - }, - "projects": { - "title": "Proyectos impulsados", - "empty": "Aún no has impulsado ningún proyecto", - "create": "Impulsa tu proyecto", - "count": "Proyectos", - "raised": "Recaudado", - "recent": "Últimos proyectos impulsados", - "viewAll": "Ver todos", - "createNew": "Crear un proyecto" - }, - "ownedProjects": { - "title": "Tus Proyectos Activos", - "messageToDonatorsButton": "Mensaje a donantes", - "uploadNewsButton": "Subir novedad" - }, - "donatedProjects": { - "title": "Proyectos activos donde donaste" - }, - "cta": { - "discover": { - "title": "Descubre, conecta y dona", - "description": "Imagina un lugar donde cientos de proyectos sociales esperan tu apoyo. En nuestra plataforma, explorar es fácil: navega por causas comunitarias, ambientales o educativas. Porque aquí, tú decides qué historias se escriben.", - "button": "Dona a un proyecto" - }, - "matchfunding": { - "title": "Donaciones que brillan. Proyectos que inspiran.", - "description": "Goteo es pionera en matchfunding. Ya seas un donante que buscas potenciar tu contribución a proyectos, un impulsor que quiere aumentar su presupuesto o una entidad que busca apoyar a proyectos e impulsores a través de refuerzos en el donativo... nuestro programa de matchfunding te permite multiplicar el impulso para el cambio", - "learnMore": "Descubre cómo funciona el matchfunding", - "becomeMatcher": "Hazte Matcher" - } + "form": { + "email": "Email o nombre de usuario", + "password": "Contraseña" + }, + "error": { + "invalidCredentials": "Correo electrónico o contraseña inválidos.", + "unexpectedLogin": "Error al iniciar sesión. Por favor, inténtalo de nuevo más tarde.", + "invalidEmail": "El correo electrónico no es válido." + } + }, + "logout": { + "label": "Cerrar sesión" + }, + "metadata": { + "description": "Página de inicio" + }, + "hero": { + "title": "Financia el cambio que el mundo necesita", + "description": "Arrancamos una nueva etapa con la misma brújula que nos ha guiado desde el principio: impulsar proyectos que transformen el mundo desde la justicia social, la participación democrática y la defensa de los derechos humanos y de los ecosistemas. Únete a una comunidad que transforma realidades. ¡Sé parte de la solución!", + "buttons": { + "discover": "Descubre campañas", + "impulse": "Impulsa un proyecto" + } + }, + "me": { + "page": { + "title": "Actividad", + "subtitle": "Consulta tu actividad en Goteo" + }, + "period": { + "label": "Periodo", + "year": "Año {{year}}" + }, + "donations": { + "title": "Mis donaciones", + "empty": "Aún no has donado a ningún proyecto", + "explore": "Explora proyectos", + "count": "Donaciones", + "total": "Total donado", + "recent": "Últimas donaciones", + "viewAll": "Ver todas", + "certificate": "Solicitar certificado" + }, + "projects": { + "title": "Proyectos impulsados", + "empty": "Aún no has impulsado ningún proyecto", + "create": "Impulsa tu proyecto", + "count": "Proyectos", + "raised": "Recaudado", + "recent": "Últimos proyectos impulsados", + "viewAll": "Ver todos", + "createNew": "Crear un proyecto" + }, + "ownedProjects": { + "title": "Tus Proyectos Activos", + "messageToDonatorsButton": "Mensaje a donantes", + "uploadNewsButton": "Subir novedad" + }, + "donatedProjects": { + "title": "Proyectos activos donde donaste" + }, + "cta": { + "discover": { + "title": "Descubre, conecta y dona", + "description": "Imagina un lugar donde cientos de proyectos sociales esperan tu apoyo. En nuestra plataforma, explorar es fácil: navega por causas comunitarias, ambientales o educativas. Porque aquí, tú decides qué historias se escriben.", + "button": "Dona a un proyecto" }, "matchfunding": { - "card": { - "title": "Matchfunding", - "calls": "Convocatorias", - "donated": "Donado", - "recent": "Últimas convocatorias", - "viewAll": "Ver convocatorias", - "create": "Crear convocatoria" - }, - "section": { - "title": "Tus convocatorias de Matchfunding activas", - "error": "Error al cargar las convocatorias" - }, - "callCard": { - "donationCall": "Dotación convocatoria", - "participatingProjects": "Proyectos participantes", - "successfulProjects": "Proyectos acogidos con éxito", - "projects": "proyectos", - "viewCall": "Ir a la convocatoria" - } + "title": "Donaciones que brillan. Proyectos que inspiran.", + "description": "Goteo es pionera en matchfunding. Ya seas un donante que buscas potenciar tu contribución a proyectos, un impulsor que quiere aumentar su presupuesto o una entidad que busca apoyar a proyectos e impulsores a través de refuerzos en el donativo... nuestro programa de matchfunding te permite multiplicar el impulso para el cambio", + "learnMore": "Descubre cómo funciona el matchfunding", + "becomeMatcher": "Hazte Matcher" } }, - "blog": { - "share": { - "modal": { - "title": "Difunde este blog por el mundo", - "description": "Puedes copiar el enlace para enviarlo y también compartirla en Facebook o X." - } + "matchfunding": { + "card": { + "title": "Matchfunding", + "calls": "Convocatorias", + "donated": "Donado", + "recent": "Últimas convocatorias", + "viewAll": "Ver convocatorias", + "create": "Crear convocatoria" + }, + "section": { + "title": "Tus convocatorias de Matchfunding activas", + "error": "Error al cargar las convocatorias" + }, + "callCard": { + "donationCall": "Dotación convocatoria", + "participatingProjects": "Proyectos participantes", + "successfulProjects": "Proyectos acogidos con éxito", + "projects": "proyectos", + "viewCall": "Ir a la convocatoria" + } + } + }, + "blog": { + "share": { + "modal": { + "title": "Difunde este blog por el mundo", + "description": "Puedes copiar el enlace para enviarlo y también compartirla en Facebook o X." } } } From 1532b18435c7960f8b14d6b7aaeff5b162cff5aa Mon Sep 17 00:00:00 2001 From: Dayv1dDev Date: Fri, 31 Jul 2026 12:44:45 +0200 Subject: [PATCH 46/47] fix: solve some details missing/refactored in A AdminSidebar + rename components for clear naming understanding --- .../{Sidebar.svelte => AdminSidebar.svelte} | 50 +++++++++---------- ...AdminSidebar.svelte => WithSidebar.svelte} | 4 +- 2 files changed, 26 insertions(+), 28 deletions(-) rename src/components/admin/{Sidebar.svelte => AdminSidebar.svelte} (76%) rename src/layouts/{AdminSidebar.svelte => WithSidebar.svelte} (77%) diff --git a/src/components/admin/Sidebar.svelte b/src/components/admin/AdminSidebar.svelte similarity index 76% rename from src/components/admin/Sidebar.svelte rename to src/components/admin/AdminSidebar.svelte index 87c4b2a4..027a2c4a 100644 --- a/src/components/admin/Sidebar.svelte +++ b/src/components/admin/AdminSidebar.svelte @@ -5,6 +5,7 @@ import type { Component } from "svelte"; import Chevron from "../icons/navigation/Chevron.svelte"; + import Button from "../library/buttons/Button.svelte"; import { locale, t } from "../../i18n/store"; export interface AdminSidebarItem { @@ -22,31 +23,25 @@ let { navItems, activePath, class: classes = "" }: Props = $props(); let hoverOpen = $state(false); - let mobileOpen = $state(false); - - function handleNavClick() { - hoverOpen = false; - } + let isMobileOpen = $state(false); function toggleMobile() { - mobileOpen = !mobileOpen; + isMobileOpen = !isMobileOpen; } function closeMobile() { - mobileOpen = false; + isMobileOpen = false; } function isActive(item: AdminSidebarItem): boolean { return item.href === activePath; } - function navItemClass(item: AdminSidebarItem): string { - return ( - twMerge( - "hover:bg-variant1 flex content-center items-center w-fit gap-2 rounded-lg px-4 py-2 text-base font-bold transition-colors duration-300 text-secondary", - isActive(item) && "bg-variant2 hover:bg-variant3", - !hoverOpen && "justify-center mx-1 w-auto", - ) || "" + function navItemClass(item: AdminSidebarItem): ClassNameValue { + return twMerge( + "hover:bg-variant1 flex content-center items-center w-fit gap-2 rounded-lg px-4 py-2 text-base font-bold transition-colors duration-300 text-secondary", + isActive(item) && "bg-variant2 hover:bg-variant3", + !hoverOpen && "justify-center mx-1 w-auto", ); } @@ -67,7 +62,7 @@