@@ -129,10 +132,10 @@
class="font-heading text-3xl font-bold"
style:font-family="'Fraunces Variable', Georgia, serif"
>
- Mes candidatures validateur
+ {i18n.t('p26.validatorApplication.listTitle')}
@@ -140,11 +143,11 @@
{view.message}
-
+
{:else if view.apps.length === 0}
{#snippet action()}
-
+
{/snippet}
{:else if visibleApps.length === 0}
-
+
{:else}
{#each visibleApps as app (app.id)}
@@ -184,9 +187,9 @@
{app.domain}
- {app.origin === 'admin_invite' ? 'Invitation admin' : 'Candidature'}
+ {app.origin === 'admin_invite' ? i18n.t('p26.validatorApplication.originAdminInvite') : i18n.t('p26.validatorApplication.originApplication')}
- {STATUS_LABEL[app.status]}
+ {statusLabel(app.status)}
{#if app.motivation}
@@ -195,15 +198,15 @@
{#if app.admin_notes}
-
Note admin
+
{i18n.t('p26.validatorApplication.adminNote')}
{app.admin_notes}
{/if}
- Cree le {formatDate(app.created_at)}
+ {i18n.t('p26.validatorApplication.createdOn', { date: formatDate(app.created_at) })}
{#if app.updated_at && app.updated_at !== app.created_at}
- — maj {formatDate(app.updated_at)}
+ — {i18n.t('p26.validatorApplication.updatedOn', { date: formatDate(app.updated_at) })}
{/if}
@@ -216,14 +219,14 @@
loading={actingId === app.id}
onclick={() => accept(app)}
>
- Accepter l'invitation
+ {i18n.t('p26.validatorApplication.acceptInvitationBtn')}
{/if}
{/if}
diff --git a/src/routes/settings/validator-application/new/+page.svelte b/src/routes/settings/validator-application/new/+page.svelte
index b292389..83a52fd 100644
--- a/src/routes/settings/validator-application/new/+page.svelte
+++ b/src/routes/settings/validator-application/new/+page.svelte
@@ -13,6 +13,7 @@
} from '$api/validatorApplications';
import { auth } from '$stores/auth.svelte';
import { toast } from '$stores/toast.svelte';
+ import { i18n } from '$lib/i18n';
import Button from '$components/ui/Button.svelte';
import Select from '$components/ui/Select.svelte';
import { Check, X, ArrowLeft } from '@lucide/svelte';
@@ -51,7 +52,9 @@
const res = await api.get<{ data: ValidatorEligibilityStats }>('/users/me/stats');
stats = res.data;
} catch {
- // TODO(back): endpoint /users/me/stats attendu par SKI-96 (preview seuils).
+ // L'endpoint /users/me/stats est optionnel : quand il ne repond pas,
+ // on affiche un warning "seuils indisponibles" et on laisse l'user
+ // candidater. Le back tranchera par 403 si un critere manque.
statsUnavailable = true;
}
});
@@ -96,13 +99,13 @@
domain,
motivation: motivation.trim() || undefined
});
- toast.success('Candidature envoyee.');
+ toast.success(i18n.t('p26.validatorApplication.toastApplySuccess'));
await goto('/settings/my-validator-applications');
} catch (err) {
if (err instanceof SkilluError && err.status === 403) {
- toast.error('Un critere n est pas rempli. Verifie les seuils ci-dessus.');
+ toast.error(i18n.t('p26.validatorApplication.toastApplyCriteria'));
} else {
- toast.error(err instanceof SkilluError ? err.message : 'Erreur lors de l envoi.');
+ toast.error(err instanceof SkilluError ? err.message : i18n.t('p26.validatorApplication.toastApplyError'));
}
} finally {
submitting = false;
@@ -111,7 +114,7 @@
- Devenir validateur — Skilluv
+ {i18n.t('p26.validatorApplication.newSeoTitle')}
@@ -121,7 +124,7 @@
class="inline-flex items-center gap-1 text-sm text-text-muted hover:text-text-primary"
>
- Mes candidatures
+ {i18n.t('p26.validatorApplication.backToApplications')}
@@ -129,15 +132,14 @@
class="mb-3 font-heading text-3xl font-bold"
style:font-family="'Fraunces Variable', Georgia, serif"
>
- Devenir validateur Skilluv
+ {i18n.t('p26.validatorApplication.newTitle')}
- Les validateurs verifient les PRs Skilluv avant qu'elles soient marquees comme validees. Chaque
- validation te credite en fragments et augmente ta reputation.
+ {i18n.t('p26.validatorApplication.newSubtitle')}
-
+
-
+
-
{motivation.length}/500
+
{i18n.t('p26.validatorApplication.motivationCounter', { n: motivation.length })}
diff --git a/src/routes/settings/validator-invitations/[id]/+page.svelte b/src/routes/settings/validator-invitations/[id]/+page.svelte
index 67ebeb5..f204056 100644
--- a/src/routes/settings/validator-invitations/[id]/+page.svelte
+++ b/src/routes/settings/validator-invitations/[id]/+page.svelte
@@ -8,6 +8,7 @@
import { SkilluError } from '$api/client';
import { auth } from '$stores/auth.svelte';
import { toast } from '$stores/toast.svelte';
+ import { i18n } from '$lib/i18n';
import Button from '$components/ui/Button.svelte';
import Badge from '$components/ui/Badge.svelte';
import Skeleton from '$components/ui/Skeleton.svelte';
@@ -53,7 +54,7 @@
} catch (err) {
view = {
status: 'error',
- message: err instanceof SkilluError ? err.message : 'Erreur.'
+ message: err instanceof SkilluError ? err.message : i18n.t('p26.validatorApplication.inviteError')
};
}
}
@@ -65,10 +66,10 @@
acting = 'accept';
try {
await validatorApplicationsApi.accept(view.app.id);
- toast.success('Invitation acceptee.');
+ toast.success(i18n.t('p26.validatorApplication.toastInviteAccepted'));
await goto('/settings/my-validator-applications');
} catch (err) {
- toast.error(err instanceof SkilluError ? err.message : 'Erreur.');
+ toast.error(err instanceof SkilluError ? err.message : i18n.t('p26.validatorApplication.inviteError'));
} finally {
acting = null;
}
@@ -79,10 +80,10 @@
acting = 'decline';
try {
await validatorApplicationsApi.withdraw(view.app.id);
- toast.success('Invitation refusee.');
+ toast.success(i18n.t('p26.validatorApplication.toastInviteDeclined'));
await goto('/settings/my-validator-applications');
} catch (err) {
- toast.error(err instanceof SkilluError ? err.message : 'Erreur.');
+ toast.error(err instanceof SkilluError ? err.message : i18n.t('p26.validatorApplication.inviteError'));
} finally {
acting = null;
}
@@ -90,7 +91,7 @@
{view.message}
-
+
{:else}
@@ -127,29 +128,29 @@
class="mb-2 font-heading text-2xl font-bold"
style:font-family="'Fraunces Variable', Georgia, serif"
>
- Invitation admin — Devenir validateur {app.domain}
+ {i18n.t('p26.validatorApplication.inviteTitle', { domain: app.domain })}
- Invitation admin
- En attente
+ {i18n.t('p26.validatorApplication.inviteBadge')}
+ {i18n.t('p26.validatorApplication.invitePendingBadge')}
- Un admin Skilluv t'invite a rejoindre l'equipe de validateurs {app.domain}.
+ {i18n.t('p26.validatorApplication.inviteBody', { domain: app.domain })}
- Raison de l'invitation
+ {i18n.t('p26.validatorApplication.inviteReasonTitle')}
{#if app.admin_notes}
{app.admin_notes}
{:else}
- L'admin n'a pas laisse de note.
+ {i18n.t('p26.validatorApplication.inviteNoNote')}
{/if}
-
Recu le {formatDate(app.created_at)}
+
{i18n.t('p26.validatorApplication.inviteReceivedOn', { date: formatDate(app.created_at) })}
@@ -159,7 +160,7 @@
loading={acting === 'accept'}
onclick={accept}
>
- Accepter
+ {i18n.t('p26.validatorApplication.inviteAcceptBtn')}
{/if}
diff --git a/src/routes/sitemap.xml/+server.ts b/src/routes/sitemap.xml/+server.ts
new file mode 100644
index 0000000..24cc728
--- /dev/null
+++ b/src/routes/sitemap.xml/+server.ts
@@ -0,0 +1,63 @@
+import type { RequestHandler } from './$types';
+
+/**
+ * Sitemap XML dynamique — n'inclut que les pages publiques SEO-worthy.
+ * Les pages dynamiques (/profile/[username], /verify/[hash], /guilds/[slug])
+ * ne sont pas listees ici : elles seraient trop nombreuses ou volatiles. Elles
+ * sont decouvertes via les backlinks + le crawl a partir des pages listing
+ * (/mentors, /challenges, /guilds, /leaderboards).
+ *
+ * A regenerer via un job cron quand on aura des listings statiques (blog,
+ * etudes de cas, etc.). Pour l'instant, hardcode = simple, suffisant.
+ */
+
+const BASE_URL = 'https://skill-uv.com';
+
+const STATIC_ROUTES: { path: string; changefreq: string; priority: number }[] = [
+ { path: '/', changefreq: 'weekly', priority: 1.0 },
+ { path: '/challenges', changefreq: 'daily', priority: 0.9 },
+ { path: '/leaderboards', changefreq: 'daily', priority: 0.8 },
+ { path: '/mentors', changefreq: 'weekly', priority: 0.8 },
+ { path: '/guilds', changefreq: 'weekly', priority: 0.7 },
+ { path: '/tournaments', changefreq: 'weekly', priority: 0.7 },
+ { path: '/bounties', changefreq: 'weekly', priority: 0.7 },
+ { path: '/certifications', changefreq: 'weekly', priority: 0.7 },
+ { path: '/events', changefreq: 'weekly', priority: 0.6 },
+ { path: '/diplomas/verify', changefreq: 'monthly', priority: 0.6 },
+ { path: '/pricing', changefreq: 'monthly', priority: 0.8 },
+ { path: '/for-companies', changefreq: 'monthly', priority: 0.7 },
+ { path: '/for-companies/bounties', changefreq: 'monthly', priority: 0.6 },
+ { path: '/for-maintainers', changefreq: 'monthly', priority: 0.7 },
+ { path: '/auth/login', changefreq: 'yearly', priority: 0.4 },
+ { path: '/auth/register', changefreq: 'yearly', priority: 0.5 },
+ { path: '/legal/privacy', changefreq: 'yearly', priority: 0.3 },
+ { path: '/legal/terms', changefreq: 'yearly', priority: 0.3 },
+ { path: '/legal/gdpr', changefreq: 'yearly', priority: 0.3 },
+ { path: '/legal/mentions', changefreq: 'yearly', priority: 0.3 }
+];
+
+export const GET: RequestHandler = async () => {
+ const now = new Date().toISOString().slice(0, 10);
+ const urls = STATIC_ROUTES.map(
+ (r) => `