From 9a49c8e6055199d46c428e4269d259e5321ca75f Mon Sep 17 00:00:00 2001 From: Alabi Ibrahim Abimbola Date: Wed, 15 Jul 2026 23:50:01 +0100 Subject: [PATCH 1/6] feat: Add new locales (fr-CA, pt-BR, sw) and CSP violation reporting ## Frontend Locales - Add fr-CA (French Canadian), pt-BR (Brazilian Portuguese), sw (Swahili) to supported locales - Create new translation files for each locale - Update LanguageSelector component to include new locales in dropdown - Update i18n.ts to import and use new translation files - Locales now available: en, es, fr, fr-CA, pt-BR, sw ## Backend CSP Report - Add CspReportController that logs CSP violations to LoggerService at warn level - Controller has @Public decorator to allow unauthenticated access - Endpoint path: /api/v1/csp-report - Update SecurityModule to register CspReportController - Add reportUri to Helmet CSP directives pointing to /api/v1/csp-report --- .../common/security/csp-report.controller.ts | 28 +++ .../src/common/security/security.module.ts | 6 +- .../src/components/LanguageSelector.tsx | 3 + app/frontend/src/i18n.ts | 8 +- app/frontend/src/messages/fr-CA.json | 196 ++++++++++++++++++ app/frontend/src/messages/pt-BR.json | 196 ++++++++++++++++++ app/frontend/src/messages/sw.json | 196 ++++++++++++++++++ 7 files changed, 631 insertions(+), 2 deletions(-) create mode 100644 app/backend/src/common/security/csp-report.controller.ts create mode 100644 app/frontend/src/messages/fr-CA.json create mode 100644 app/frontend/src/messages/pt-BR.json create mode 100644 app/frontend/src/messages/sw.json diff --git a/app/backend/src/common/security/csp-report.controller.ts b/app/backend/src/common/security/csp-report.controller.ts new file mode 100644 index 00000000..7337a229 --- /dev/null +++ b/app/backend/src/common/security/csp-report.controller.ts @@ -0,0 +1,28 @@ +import { Controller, Post, Body, Version } from '@nestjs/common'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { Public } from '../decorators/public.decorator'; +import { LoggerService } from '../../logger/logger.service'; +import { API_VERSIONS } from '../constants/api-version.constants'; + +@ApiTags('CSP') +@Controller('csp-report') +export class CspReportController { + constructor(private readonly loggerService: LoggerService) {} + + @Public() + @Post() + @Version(API_VERSIONS.V1) + @ApiOperation({ + summary: 'Receive CSP violation reports', + description: 'Endpoint to receive and log Content Security Policy violations.', + }) + handleCspReport(@Body() report: any) { + this.loggerService.warn( + { cspReport: report }, + 'CSP violation reported', + 'CspReportController', + ); + + return { status: 'ok' }; + } +} diff --git a/app/backend/src/common/security/security.module.ts b/app/backend/src/common/security/security.module.ts index f0961b12..71b08e1a 100644 --- a/app/backend/src/common/security/security.module.ts +++ b/app/backend/src/common/security/security.module.ts @@ -3,6 +3,7 @@ import type { CorsOptions } from '@nestjs/common/interfaces/external/cors-option import { ConfigService } from '@nestjs/config'; import type { NextFunction, Request, RequestHandler, Response } from 'express'; import helmet, { HelmetOptions } from 'helmet'; +import { CspReportController } from './csp-report.controller'; const DEFAULT_ALLOWED_ORIGINS = [ 'http://localhost:3000', @@ -87,6 +88,7 @@ const buildHelmetOptions = (config: ConfigService): HelmetOptions => { objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"], + reportUri: '/api/v1/csp-report', }, } : false, @@ -280,5 +282,7 @@ export const createRateLimiter = (config: ConfigService): RequestHandler => { * If cookie-based session management or any browser-managed credentials are introduced * in the future, CSRF protection middleware MUST be implemented. */ -@Module({}) +@Module({ + controllers: [CspReportController], +}) export class SecurityModule {} diff --git a/app/frontend/src/components/LanguageSelector.tsx b/app/frontend/src/components/LanguageSelector.tsx index 0e2d1eb8..3be1a408 100644 --- a/app/frontend/src/components/LanguageSelector.tsx +++ b/app/frontend/src/components/LanguageSelector.tsx @@ -11,6 +11,9 @@ const localeNames: Record = { en: 'English', es: 'Español', fr: 'Français', + 'fr-CA': 'Français (Canada)', + 'pt-BR': 'Português (Brasil)', + sw: 'Kiswahili', }; export function LanguageSelector() { diff --git a/app/frontend/src/i18n.ts b/app/frontend/src/i18n.ts index 668e4746..c8ef02ab 100644 --- a/app/frontend/src/i18n.ts +++ b/app/frontend/src/i18n.ts @@ -3,14 +3,20 @@ import { getRequestConfig } from 'next-intl/server'; import en from './messages/en.json'; import es from './messages/es.json'; import fr from './messages/fr.json'; +import frCA from './messages/fr-CA.json'; +import ptBR from './messages/pt-BR.json'; +import sw from './messages/sw.json'; -export const locales = ['en', 'es', 'fr'] as const; +export const locales = ['en', 'es', 'fr', 'fr-CA', 'pt-BR', 'sw'] as const; export type Locale = (typeof locales)[number]; const messages = { en, es, fr, + 'fr-CA': frCA, + 'pt-BR': ptBR, + sw, }; export default getRequestConfig(async ({ requestLocale }) => { diff --git a/app/frontend/src/messages/fr-CA.json b/app/frontend/src/messages/fr-CA.json new file mode 100644 index 00000000..5048eadb --- /dev/null +++ b/app/frontend/src/messages/fr-CA.json @@ -0,0 +1,196 @@ +{ + "common": { + "chainforge": "ChainForge", + "loading": "Chargement...", + "error": "Erreur", + "success": "Succès", + "cancel": "Annuler", + "confirm": "Confirmer", + "retry": "Réessayer", + "close": "Fermer", + "back": "Retour", + "next": "Suivant", + "previous": "Précédent", + "save": "Sauvegarder", + "delete": "Supprimer", + "edit": "Modifier", + "view": "Voir", + "share": "Partager", + "download": "Télécharger", + "copy": "Copier", + "copied": "Copié", + "search": "Rechercher", + "filter": "Filtrer", + "sort": "Trier", + "status": "Statut", + "date": "Date", + "time": "Heure", + "amount": "Montant", + "description": "Description", + "name": "Nom", + "email": "E-mail", + "phone": "Téléphone", + "address": "Adresse" + }, + "navigation": { + "home": "Accueil", + "dashboard": "Tableau de bord", + "campaigns": "Campagnes", + "claims": "Réclamations", + "settings": "Paramètres", + "profile": "Profil", + "logout": "Déconnexion", + "role": "Rôle", + "wallet": "Portefeuille", + "environment": "Environnement", + "activity": "Activité", + "homeDescription": "Aperçu de la plateforme et points forts de l'aide.", + "dashboardDescription": "Suivre les colis d'aide et l'activité de distribution.", + "campaignsDescription": "Créer et gérer les campagnes de financement des ONG.", + "claimsDescription": "Voir et gérer vos réclamations d'aide.", + "settingsDescription": "Configurer votre compte et vos préférences.", + "profileDescription": "Gérer les informations de votre profil." + }, + "roles": { + "guest": "Invité", + "client": "Client", + "operator": "Opérateur", + "ngo": "ONG", + "admin": "Administrateur" + }, + "activity": { + "center": "Centre d'Activité", + "noRecentActivity": "Aucune activité récente", + "clearCompleted": "Effacer les terminés", + "remove": "Supprimer l'activité", + "viewTransaction": "Voir la transaction", + "preparingTransaction": "Préparation de la transaction...", + "transactionCompleted": "Transaction terminée", + "transactionFailed": "Transaction échouée", + "processing": "Traitement...", + "completedSuccessfully": "Terminé avec succès", + "failed": "Échoué", + "retrying": "Nouvelle tentative..." + }, + "verification": { + "submitEvidence": "Soumettre des Preuves pour Vérification", + "analyzing": "Analyse...", + "result": "Résultat", + "upload": "Téléverser", + "evidenceRequired": "Veuillez fournir au moins une preuve — une image ou une description textuelle.", + "imageTooLarge": "L'image doit être de 5 Mo ou moins.", + "invalidImageType": "Seules les images PNG, JPEG et WebP sont acceptées.", + "textTooShort": "Le texte doit contenir au moins 20 caractères.", + "verificationInProgress": "Vérification en cours...", + "verificationComplete": "Vérification terminée", + "verificationFailed": "Vérification échouée" + }, + "campaigns": { + "createCampaign": "Créer une Campagne", + "campaignName": "Nom de la Campagne", + "budget": "Budget", + "token": "Jeton", + "expiryDate": "Date d'Expiration", + "description": "Description", + "create": "Créer", + "creating": "Création de la campagne...", + "campaignCreated": "Campagne créée avec succès", + "campaignCreationFailed": "Échec de la création de la campagne", + "noCampaigns": "Aucune campagne trouvée", + "loadingCampaigns": "Chargement des campagnes...", + "errorFetchingCampaigns": "Erreur lors de la récupération des campagnes" + }, + "claims": { + "claimReceipt": "Reçu de Réclamation", + "viewReceipt": "Voir et partager votre preuve de réclamation", + "loadingReceipt": "Chargement de votre reçu...", + "claimId": "ID de Réclamation", + "packageId": "ID de Colis", + "recipient": "Destinataire", + "status": "Statut", + "amount": "Montant", + "timestamp": "Horodatage", + "transactionHash": "Hash de Transaction", + "viewOnExplorer": "Voir sur l'Explorateur" + }, + "wallet": { + "connect": "Connecter le Portefeuille", + "disconnect": "Déconnecter", + "connecting": "Connexion...", + "connected": "Connecté", + "notConnected": "Non Connecté", + "network": "Réseau", + "publicKey": "Clé Publique", + "balance": "Solde", + "send": "Envoyer", + "receive": "Recevoir", + "transactionHistory": "Historique des Transactions" + }, + "errors": { + "generic": "Une erreur inattendue s'est produite. Veuillez réessayer.", + "networkError": "Impossible d'accéder au service. Vérifiez votre connexion et réessayez.", + "validationError": "Vérifiez vos données et réessayez.", + "unauthorized": "Vous n'êtes pas autorisé à effectuer cette action.", + "notFound": "La ressource demandée n'a pas été trouvée.", + "serverError": "Erreur du serveur. Veuillez réessayer plus tard." + }, + "status": { + "pending": "En attente", + "processing": "Traitement", + "verified": "Vérifié", + "approved": "Approuvé", + "disbursed": "Décaissé", + "archived": "Archivé", + "requested": "Demandé", + "completed": "Terminé", + "failed": "Échoué", + "active": "Actif", + "inactive": "Inactif", + "draft": "Brouillon", + "paused": "En pause" + }, + "dates": { + "today": "Aujourd'hui", + "yesterday": "Hier", + "tomorrow": "Demain", + "justNow": "À l'instant", + "minutesAgo": "Il y a {{count}} minutes", + "hoursAgo": "Il y a {{count}} heures", + "daysAgo": "Il y a {{count}} jours", + "weeksAgo": "Il y a {{count}} semaines", + "monthsAgo": "Il y a {{count}} mois", + "yearsAgo": "Il y a {{count}} ans" + }, + "demoChecklist": { + "title": "Liste de vérification démo Testnet", + "subtitle": "Suivez ces étapes pour vérifier le flux de démonstration ChainForge de bout en bout sur Testnet. Cochez chaque étape au fur et à mesure.", + "progress": "{{completed}} sur {{total}} étapes terminées", + "allComplete": "Toutes les étapes sont terminées — le flux de démonstration est vérifié !", + "stepConnectWallet": "Connectez votre portefeuille Freighter", + "stepConnectWalletDesc": "Ouvrez la page d'accueil et cliquez sur \"Connecter le Portefeuille Freighter\" dans la barre de navigation. Approuvez la connexion dans la fenêtre popup de l'extension Freighter. Assurez-vous que Freighter est configuré sur Testnet.", + "stepViewCampaign": "Consulter une campagne active", + "stepViewCampaignDesc": "Accédez à la page Campagnes et ouvrez une campagne active. Vérifiez que les détails, le budget et le statut de la campagne s'affichent correctement.", + "stepSubmitClaim": "Soumettre une réclamation test", + "stepSubmitClaimDesc": "Depuis la page de détail d'une campagne ou le flux de vérification, soumettez une réclamation en utilisant un texte de preuve exemple. Confirmez que la soumission réussit.", + "stepVerifyReceipt": "Vérifier le reçu de réclamation", + "stepVerifyReceiptDesc": "Ouvrez la page de reçu de réclamation et vérifiez qu'il affiche l'ID de réclamation, l'ID de colis, le montant et le statut on-chain corrects. Confirmez que le reçu peut être partagé ou téléchargé.", + "goHome": "Aller à l'Accueil", + "goCampaigns": "Ouvrir les Campagnes", + "goClaimReceipt": "Ouvrir le Reçu", + "systemHealth": "Santé du Système", + "healthOk": "Sain", + "healthDegraded": "Dégradé", + "healthDown": "Hors service", + "healthChecking": "Vérification…", + "lastChecked": "Dernière vérification", + "prerequisites": "Prérequis", + "prereqFreighter": "Extension Freighter installée", + "prereqWallet": "Portefeuille connecté", + "prereqBackend": "API backend accessible", + "quickLinks": "Liens Rapides", + "helpPage": "Guide du Contributeur", + "dashboardPage": "Tableau de Bord", + "verificationReviewPage": "Révision de Vérification", + "resetChecklist": "Réinitialiser la progression" + } +} \ No newline at end of file diff --git a/app/frontend/src/messages/pt-BR.json b/app/frontend/src/messages/pt-BR.json new file mode 100644 index 00000000..625c8034 --- /dev/null +++ b/app/frontend/src/messages/pt-BR.json @@ -0,0 +1,196 @@ +{ + "common": { + "chainforge": "ChainForge", + "loading": "Carregando...", + "error": "Erro", + "success": "Sucesso", + "cancel": "Cancelar", + "confirm": "Confirmar", + "retry": "Tentar novamente", + "close": "Fechar", + "back": "Voltar", + "next": "Próximo", + "previous": "Anterior", + "save": "Salvar", + "delete": "Excluir", + "edit": "Editar", + "view": "Visualizar", + "share": "Compartilhar", + "download": "Baixar", + "copy": "Copiar", + "copied": "Copiado", + "search": "Pesquisar", + "filter": "Filtrar", + "sort": "Ordenar", + "status": "Status", + "date": "Data", + "time": "Hora", + "amount": "Valor", + "description": "Descrição", + "name": "Nome", + "email": "E-mail", + "phone": "Telefone", + "address": "Endereço" + }, + "navigation": { + "home": "Início", + "dashboard": "Painel", + "campaigns": "Campanhas", + "claims": "Reclamações", + "settings": "Configurações", + "profile": "Perfil", + "logout": "Sair", + "role": "Cargo", + "wallet": "Carteira", + "environment": "Ambiente", + "activity": "Atividade", + "homeDescription": "Visão geral da plataforma e destaques da ajuda.", + "dashboardDescription": "Acompanhe os pacotes de ajuda e a atividade de distribuição.", + "campaignsDescription": "Crie e gerencie campanhas de financiamento de ONGs.", + "claimsDescription": "Veja e gerencie suas reclamações de ajuda.", + "settingsDescription": "Configure sua conta e preferências.", + "profileDescription": "Gerencie suas informações de perfil." + }, + "roles": { + "guest": "Convidado", + "client": "Cliente", + "operator": "Operador", + "ngo": "ONG", + "admin": "Administrador" + }, + "activity": { + "center": "Centro de Atividades", + "noRecentActivity": "Nenhuma atividade recente", + "clearCompleted": "Limpar concluídos", + "remove": "Remover atividade", + "viewTransaction": "Ver transação", + "preparingTransaction": "Preparando transação...", + "transactionCompleted": "Transação concluída", + "transactionFailed": "Transação falhou", + "processing": "Processando...", + "completedSuccessfully": "Concluído com sucesso", + "failed": "Falhou", + "retrying": "Tentando novamente..." + }, + "verification": { + "submitEvidence": "Enviar Evidências para Verificação", + "analyzing": "Analisando...", + "result": "Resultado", + "upload": "Enviar", + "evidenceRequired": "Por favor, forneça pelo menos uma evidência — uma imagem ou uma descrição de texto.", + "imageTooLarge": "A imagem deve ter 5 MB ou menos.", + "invalidImageType": "Apenas imagens PNG, JPEG e WebP são aceitas.", + "textTooShort": "O texto deve ter pelo menos 20 caracteres.", + "verificationInProgress": "Verificação em andamento...", + "verificationComplete": "Verificação concluída", + "verificationFailed": "Verificação falhou" + }, + "campaigns": { + "createCampaign": "Criar Campanha", + "campaignName": "Nome da Campanha", + "budget": "Orçamento", + "token": "Token", + "expiryDate": "Data de Expiração", + "description": "Descrição", + "create": "Criar", + "creating": "Criando campanha...", + "campaignCreated": "Campanha criada com sucesso", + "campaignCreationFailed": "Falha ao criar campanha", + "noCampaigns": "Nenhuma campanha encontrada", + "loadingCampaigns": "Carregando campanhas...", + "errorFetchingCampaigns": "Erro ao buscar campanhas" + }, + "claims": { + "claimReceipt": "Recibo de Reclamação", + "viewReceipt": "Veja e compartilhe sua prova de reclamação", + "loadingReceipt": "Carregando seu recibo...", + "claimId": "ID da Reclamação", + "packageId": "ID do Pacote", + "recipient": "Destinatário", + "status": "Status", + "amount": "Valor", + "timestamp": "Timestamp", + "transactionHash": "Hash da Transação", + "viewOnExplorer": "Ver no Explorer" + }, + "wallet": { + "connect": "Conectar Carteira", + "disconnect": "Desconectar", + "connecting": "Conectando...", + "connected": "Conectado", + "notConnected": "Não conectado", + "network": "Rede", + "publicKey": "Chave Pública", + "balance": "Saldo", + "send": "Enviar", + "receive": "Receber", + "transactionHistory": "Histórico de Transações" + }, + "errors": { + "generic": "Ocorreu um erro inesperado. Por favor, tente novamente.", + "networkError": "Não foi possível acessar o serviço. Verifique sua conexão e tente novamente.", + "validationError": "Verifique seus dados e tente novamente.", + "unauthorized": "Você não está autorizado a realizar esta ação.", + "notFound": "O recurso solicitado não foi encontrado.", + "serverError": "Erro do servidor. Por favor, tente novamente mais tarde." + }, + "status": { + "pending": "Pendente", + "processing": "Processando", + "verified": "Verificado", + "approved": "Aprovado", + "disbursed": "Desembolsado", + "archived": "Arquivado", + "requested": "Solicitado", + "completed": "Concluído", + "failed": "Falhou", + "active": "Ativo", + "inactive": "Inativo", + "draft": "Rascunho", + "paused": "Pausado" + }, + "dates": { + "today": "Hoje", + "yesterday": "Ontem", + "tomorrow": "Amanhã", + "justNow": "Agora mesmo", + "minutesAgo": "{{count}} minutos atrás", + "hoursAgo": "{{count}} horas atrás", + "daysAgo": "{{count}} dias atrás", + "weeksAgo": "{{count}} semanas atrás", + "monthsAgo": "{{count}} meses atrás", + "yearsAgo": "{{count}} anos atrás" + }, + "demoChecklist": { + "title": "Checklist de Demo Testnet", + "subtitle": "Siga estas etapas para verificar o fluxo de demonstração ChainForge de ponta a ponta na Testnet. Marque cada etapa à medida que a conclui.", + "progress": "{{completed}} de {{total}} etapas concluídas", + "allComplete": "Todas as etapas concluídas — o fluxo de demonstração está ok!", + "stepConnectWallet": "Conecte sua carteira Freighter", + "stepConnectWalletDesc": "Abra a página inicial e clique em \"Conectar Carteira Freighter\" na barra de navegação. Aprove a conexão no popup da extensão Freighter. Certifique-se de que o Freighter esteja configurado na Testnet.", + "stepViewCampaign": "Ver uma campanha ativa", + "stepViewCampaignDesc": "Navegue até a página de Campanhas e abra uma campanha ativa. Verifique se os detalhes, orçamento e status da campanha são exibidos corretamente.", + "stepSubmitClaim": "Enviar uma reclamação de teste", + "stepSubmitClaimDesc": "A partir da página de detalhes de uma campanha ou do fluxo de verificação, envie uma reclamação usando texto de evidência de exemplo. Confirme que o envio da reclamação tenha sucesso.", + "stepVerifyReceipt": "Verificar o recibo da reclamação", + "stepVerifyReceiptDesc": "Abra a página de recibo da reclamação e verifique se o recibo mostra o ID da reclamação, ID do pacote, valor e status on-chain corretos. Confirme que o recibo pode ser compartilhado ou baixado.", + "goHome": "Ir para Início", + "goCampaigns": "Abrir Campanhas", + "goClaimReceipt": "Abrir Recibo", + "systemHealth": "Saúde do Sistema", + "healthOk": "Saudável", + "healthDegraded": "Degradado", + "healthDown": "Fora de serviço", + "healthChecking": "Verificando…", + "lastChecked": "Última verificação", + "prerequisites": "Pré-requisitos", + "prereqFreighter": "Extensão Freighter instalada", + "prereqWallet": "Carteira conectada", + "prereqBackend": "API backend acessível", + "quickLinks": "Links Rápidos", + "helpPage": "Guia do Contribuidor", + "dashboardPage": "Painel", + "verificationReviewPage": "Revisão de Verificação", + "resetChecklist": "Redefinir progresso" + } +} \ No newline at end of file diff --git a/app/frontend/src/messages/sw.json b/app/frontend/src/messages/sw.json new file mode 100644 index 00000000..886c3124 --- /dev/null +++ b/app/frontend/src/messages/sw.json @@ -0,0 +1,196 @@ +{ + "common": { + "chainforge": "ChainForge", + "loading": "Inapakia...", + "error": "Hitilafu", + "success": "Mafanikio", + "cancel": "Ghairi", + "confirm": "Thibitisha", + "retry": "Jaribu tena", + "close": "Funga", + "back": "Rudi", + "next": "Ifuatayo", + "previous": "Iliyotangulia", + "save": "Hifadhi", + "delete": "Futa", + "edit": "Hariri", + "view": "Tazama", + "share": "Shirikisha", + "download": "Pakua", + "copy": "Nakala", + "copied": "Nakiliwa", + "search": "Tafuta", + "filter": "Chuja", + "sort": "Panga", + "status": "Hali", + "date": "Tarehe", + "time": "Muda", + "amount": "Kiasi", + "description": "Maelezo", + "name": "Jina", + "email": "Barua pepe", + "phone": "Simu", + "address": "Anwani" + }, + "navigation": { + "home": "Nyumbani", + "dashboard": "Dashibodi", + "campaigns": "Mashambulio", + "claims": "Madai", + "settings": "Mipangilio", + "profile": "Wasifu", + "logout": "Toka", + "role": "Jukumu", + "wallet": "Pochi", + "environment": "Mazingira", + "activity": "Shughuli", + "homeDescription": "Mtazamo wa jukwaa na mambo muhimu ya msaada.", + "dashboardDescription": "Fuata vifurushi vya msaada na shughuli za usambazaji.", + "campaignsDescription": "Unda na usimamie mashambulio ya uwezo wa ONG.", + "claimsDescription": "Tazama na usimamie madai yako ya msaada.", + "settingsDescription": "Usakinishe akaunti yako na mapendekezo.", + "profileDescription": "Usimamie maelezo yako ya wasifu." + }, + "roles": { + "guest": "Mgeni", + "client": "Mteja", + "operator": "Opereta", + "ngo": "ONG", + "admin": "Msimamizi" + }, + "activity": { + "center": "Kituo cha Shughuli", + "noRecentActivity": "Hakuna shughuli ya hivi karibuni", + "clearCompleted": "Futa yaliyomalizika", + "remove": "Ondoa shughuli", + "viewTransaction": "Tazama muamala", + "preparingTransaction": "Inatayarisha muamala...", + "transactionCompleted": "Muamala umekamilika", + "transactionFailed": "Muamala umeshindwa", + "processing": "Inasindika...", + "completedSuccessfully": "Imekamilika kwa mafanikio", + "failed": "Imeshindwa", + "retrying": "Inajaribu tena..." + }, + "verification": { + "submitEvidence": "Wasilisha Ushahidi kwa Uthibitisho", + "analyzing": "Inachambua...", + "result": "Matokeo", + "upload": "Pakia", + "evidenceRequired": "Tafadhali toa angalau ushahidi moja — picha au maelezo ya maandishi.", + "imageTooLarge": "Picha lazima iwe 5 MB au chini.", + "invalidImageType": "Picha za PNG, JPEG na WebP tu zimekubaliwa.", + "textTooShort": "Maandishi lazima yawe angalau herufi 20.", + "verificationInProgress": "Uthibitisho unaendelea...", + "verificationComplete": "Uthibitisho umekamilika", + "verificationFailed": "Uthibitisho umeshindwa" + }, + "campaigns": { + "createCampaign": "Unda Mashambulio", + "campaignName": "Jina la Mashambulio", + "budget": "Bajeti", + "token": "Tokeni", + "expiryDate": "Tarehe ya Kumaliza", + "description": "Maelezo", + "create": "Unda", + "creating": "Inaunda mashambulio...", + "campaignCreated": "Mashambulio yameundwa kwa mafanikio", + "campaignCreationFailed": "Imeshindwa kuunda mashambulio", + "noCampaigns": "Hakuna mashambulio yaliyopatikana", + "loadingCampaigns": "Inapakia mashambulio...", + "errorFetchingCampaigns": "Hitilafu ya kupata mashambulio" + }, + "claims": { + "claimReceipt": "Risiti la Madai", + "viewReceipt": "Tazama na shirikisha ushahidi wako wa madai", + "loadingReceipt": "Inapakia risiti yako...", + "claimId": "ID ya Madai", + "packageId": "ID ya Vifurushi", + "recipient": "Mpokeaji", + "status": "Hali", + "amount": "Kiasi", + "timestamp": "Mchoro wa muda", + "transactionHash": "Hash ya Muamala", + "viewOnExplorer": "Tazama kwenye Explorer" + }, + "wallet": { + "connect": "Unganisha Pochi", + "disconnect": "Katangana", + "connecting": "Inunganisha...", + "connected": "Unganishwa", + "notConnected": "Haujunganishwa", + "network": "Mtandao", + "publicKey": "Ufunguo wa Umma", + "balance": "Salio", + "send": "Tuma", + "receive": "Pokea", + "transactionHistory": "Historia ya Muamala" + }, + "errors": { + "generic": "Hitilafu isiyotarajiwa imetokea. Tafadhali jaribu tena.", + "networkError": "Hawezi kufikia huduma. Angalia mtandao wako na ujaribu tena.", + "validationError": "Angalia data yako na ujaribu tena.", + "unauthorized": "Hauwezi kufanya hatua hii.", + "notFound": "Rasilimali iliyohakikishwa haijapatikana.", + "serverError": "Hitilafu ya seva. Tafadhali jaribu tena baadaye." + }, + "status": { + "pending": "Inasubiri", + "processing": "Inasindika", + "verified": "Imethibitishwa", + "approved": "Imekubaliwa", + "disbursed": "Imetolewa", + "archived": "Imehifadhiwa", + "requested": "Imeombwa", + "completed": "Imekamilika", + "failed": "Imeshindwa", + "active": "Inatumika", + "inactive": "Haiwezi kutumika", + "draft": "Mchakato", + "paused": "Imechezwa" + }, + "dates": { + "today": "Leo", + "yesterday": "Jana", + "tomorrow": "Kesho", + "justNow": "Sasa hivi", + "minutesAgo": "Dakika {{count}} zilizopita", + "hoursAgo": "Masaa {{count}} yaliyopita", + "daysAgo": "Siku {{count}} zilizopita", + "weeksAgo": "Wiki {{count}} zilizopita", + "monthsAgo": "Miezi {{count}} yaliyopita", + "yearsAgo": "Miaka {{count}} iliyopita" + }, + "demoChecklist": { + "title": "Orodha ya Uthibitisho ya Demo Testnet", + "subtitle": "Fuata hatua hizi kuthibitisha mtiririko wa demo ChainForge kutoka mwanzo hadi mwisho kwenye Testnet. Vunja kila hatua ukiendelea.", + "progress": "{{completed}} kati ya {{total}} hatua zimekamilika", + "allComplete": "Hatua zote zimekamilika — mtiririko wa demo unakubalika!", + "stepConnectWallet": "Unganisha pochi yako Freighter", + "stepConnectWalletDesc": "Fungua ukurasa wa nyumbani na bonyeza \"Unganisha Pochi Freighter\" kwenye baa ya usambazaji. Kumbatia unganisho kwenye popup ya uwezo Freighter. Hakikisha Freighter imewewa kwenye Testnet.", + "stepViewCampaign": "Tazama mashambulio yenye shughuli", + "stepViewCampaignDesc": "Nenda kwenye ukurasa wa Mashambulio na ufungue mashambulio yenye shughuli. Thibitisha kwamba maelezo, bajeti na hali ya mashambulio zinaonyeshwa vizuri.", + "stepSubmitClaim": "Wasilisha madai ya jaribio", + "stepSubmitClaimDesc": "Kutoka kwenye ukurasa wa maelezo ya mashambulio au mtiririko wa uthibitisho, wasilisha madai kwa kutumia maandishi ya ushahidi wa mfano. Thibitisha kwamba wasilisho la madai limefanikiwa.", + "stepVerifyReceipt": "Thibitisha risiti ya madai", + "stepVerifyReceiptDesc": "Fungua ukurasa wa risiti ya madai na uthibitisha kwamba risiti inaonyesha ID ya madai, ID ya vifurushi, kiasi na hali ya on-chain sahihi. Thibitisha kwamba risiti inaweza kushirikishwa au kupakiwa.", + "goHome": "Nenda Nyumbani", + "goCampaigns": "Fungua Mashambulio", + "goClaimReceipt": "Fungua Risiti", + "systemHealth": "Afya ya Mfumo", + "healthOk": "Afya nzuri", + "healthDegraded": "Kuzungumzia", + "healthDown": "Haifanyi kazi", + "healthChecking": "Inachunguza…", + "lastChecked": "Uchunguzi wa mwisho", + "prerequisites": "Masharti ya awali", + "prereqFreighter": "Uwezo Freighter umewekwa", + "prereqWallet": "Pochi imeunganishwa", + "prereqBackend": "API backend inapatikana", + "quickLinks": "Vigezo vya Haraka", + "helpPage": "Mwongozo wa Mchanga", + "dashboardPage": "Dashibodi", + "verificationReviewPage": "Uchapishaji wa Uthibitisho", + "resetChecklist": "Weweza upya maendeleo" + } +} \ No newline at end of file From c1264a6c454b214a1ee80757aae0e5ba607df519 Mon Sep 17 00:00:00 2001 From: Alabi Ibrahim Abimbola Date: Wed, 15 Jul 2026 23:57:27 +0100 Subject: [PATCH 2/6] fix: Correct LoggerService argument order in CspReportController The LoggerService.warn method expects (message, context, meta), not (meta, message, context). --- app/backend/src/common/security/csp-report.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/backend/src/common/security/csp-report.controller.ts b/app/backend/src/common/security/csp-report.controller.ts index 7337a229..df181a29 100644 --- a/app/backend/src/common/security/csp-report.controller.ts +++ b/app/backend/src/common/security/csp-report.controller.ts @@ -18,9 +18,9 @@ export class CspReportController { }) handleCspReport(@Body() report: any) { this.loggerService.warn( - { cspReport: report }, 'CSP violation reported', 'CspReportController', + { cspReport: report }, ); return { status: 'ok' }; From 8d4c563cacf48e095d094f61f59c7d8a4f3db86d Mon Sep 17 00:00:00 2001 From: Alabi Ibrahim Abimbola Date: Thu, 16 Jul 2026 00:11:43 +0100 Subject: [PATCH 3/6] chore: update pnpm lockfile --- pnpm-lock.yaml | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdd7d856..e393c248 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,6 +136,9 @@ importers: class-validator: specifier: ^0.14.3 version: 0.14.4 + compression: + specifier: 1.7.5 + version: 1.7.5 dotenv: specifier: ^17.2.3 version: 17.4.2 @@ -185,6 +188,9 @@ importers: '@nestjs/testing': specifier: ^11.0.1 version: 11.1.17(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)) + '@types/compression': + specifier: 1.7.5 + version: 1.7.5 '@types/express': specifier: ^5.0.0 version: 5.0.6 @@ -3242,6 +3248,9 @@ packages: '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/compression@1.7.5': + resolution: {integrity: sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -4370,8 +4379,8 @@ packages: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} - compression@1.8.1: - resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + compression@1.7.5: + resolution: {integrity: sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==} engines: {node: '>= 0.8.0'} concat-map@0.0.1: @@ -6973,8 +6982,8 @@ packages: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} - on-headers@1.1.0: - resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} engines: {node: '>= 0.8'} once@1.4.0: @@ -9792,7 +9801,7 @@ snapshots: bplist-parser: 0.3.2 chalk: 4.1.2 ci-info: 3.9.0 - compression: 1.8.1 + compression: 1.7.5 connect: 3.7.0 debug: 4.4.3(supports-color@10.2.2) env-editor: 0.4.2 @@ -12192,6 +12201,10 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 25.9.1 + '@types/compression@1.7.5': + dependencies: + '@types/express': 5.0.6 + '@types/connect@3.4.38': dependencies: '@types/node': 25.9.1 @@ -13661,13 +13674,13 @@ snapshots: dependencies: mime-db: 1.54.0 - compression@1.8.1: + compression@1.7.5: dependencies: bytes: 3.1.2 compressible: 2.0.18 debug: 2.6.9 negotiator: 0.6.4 - on-headers: 1.1.0 + on-headers: 1.0.2 safe-buffer: 5.2.1 vary: 1.1.2 transitivePeerDependencies: @@ -14157,7 +14170,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -17178,7 +17191,7 @@ snapshots: dependencies: ee-first: 1.1.1 - on-headers@1.1.0: {} + on-headers@1.0.2: {} once@1.4.0: dependencies: From 1d7d30040c4ee1a8cd26830d489b2f6ce0aea4eb Mon Sep 17 00:00:00 2001 From: Alabi Ibrahim Abimbola Date: Thu, 16 Jul 2026 00:19:08 +0100 Subject: [PATCH 4/6] fix: import LoggerModule into SecurityModule --- app/backend/src/common/security/security.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/backend/src/common/security/security.module.ts b/app/backend/src/common/security/security.module.ts index 71b08e1a..d760a2bd 100644 --- a/app/backend/src/common/security/security.module.ts +++ b/app/backend/src/common/security/security.module.ts @@ -4,6 +4,7 @@ import { ConfigService } from '@nestjs/config'; import type { NextFunction, Request, RequestHandler, Response } from 'express'; import helmet, { HelmetOptions } from 'helmet'; import { CspReportController } from './csp-report.controller'; +import { LoggerModule } from '../../logger/logger.module'; const DEFAULT_ALLOWED_ORIGINS = [ 'http://localhost:3000', @@ -283,6 +284,7 @@ export const createRateLimiter = (config: ConfigService): RequestHandler => { * in the future, CSRF protection middleware MUST be implemented. */ @Module({ + imports: [LoggerModule], controllers: [CspReportController], }) export class SecurityModule {} From a9d625c5c2fc7cc3472c18dfad416893abbf6124 Mon Sep 17 00:00:00 2001 From: Alabi Ibrahim Abimbola Date: Thu, 16 Jul 2026 08:32:54 +0100 Subject: [PATCH 5/6] chore: update openapi.json with csp-report endpoint --- app/frontend/openapi.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/frontend/openapi.json b/app/frontend/openapi.json index c7f11262..30e46a38 100644 --- a/app/frontend/openapi.json +++ b/app/frontend/openapi.json @@ -1975,6 +1975,22 @@ ] } }, + "/api/v1/csp-report": { + "post": { + "description": "Endpoint to receive and log Content Security Policy violations.", + "operationId": "CspReportController_handleCspReport_v1", + "parameters": [], + "responses": { + "201": { + "description": "" + } + }, + "summary": "Receive CSP violation reports", + "tags": [ + "CSP" + ] + } + }, "/api/v1/test-error/generic": { "get": { "operationId": "TestErrorController_getGenericError_v1", From 219de86a894ff211b88d219cff582176b19bf6e5 Mon Sep 17 00:00:00 2001 From: Alabi Ibrahim Abimbola Date: Fri, 17 Jul 2026 15:16:49 +0100 Subject: [PATCH 6/6] fix: remove trailing comma in package.json scripts block --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index df23dfa0..ef8969d5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "pnpm -r build", "lint": "pnpm -r lint", - "test": "pnpm -r test", + "test": "pnpm -r test" }, "pnpm": { "overrides": {