Browse Source

Oasis release 0.5.5

psy 1 day ago
parent
commit
578606471b

+ 7 - 0
README.md

@@ -64,6 +64,7 @@ Oasis is TRULY MODULAR. Here's a list of what comes deployed with the "core".
  + BlockExplorer: Module to navigate the blockchain.
  + Bookmarks: Module to discover and manage bookmarks.	
  + Cipher: Module to encrypt and decrypt your text symmetrically (using a shared password).	
+ + Courts: Module to resolve conflicts and emit veredicts.	
  + Documents: Module to discover and manage documents.	
  + Events: Module to discover and manage events.	
  + Feed: Module to discover and share short-texts (feeds).
@@ -117,6 +118,12 @@ Oasis contains its own Parliament (Government system).
 
   ![SNH](https://solarnethub.com/git/oasis-parliament.png "SolarNET.HuB")
   
+## Courts (justice)
+
+Oasis contains its own Courts (Justice system).
+
+  ![SNH](https://solarnethub.com/git/oasis-courts.png "SolarNET.HuB")
+  
 ## ECOin (crypto-economy)
 
 Oasis contains its own cryptocurrency. With it, you can exchange items and services in the marketplace. 

+ 6 - 0
docs/CHANGELOG.md

@@ -13,6 +13,12 @@ All notable changes to this project will be documented in this file.
 ### Security
 -->
 
+## v0.5.5 - 2025-11-15
+
+### Added
+
+ + Conflicts resolution system (Courts plugin).
+ 
 ## v0.5.4 - 2025-10-30
 
 ### Fixed

+ 454 - 5
src/backend/backend.js

@@ -299,9 +299,9 @@ const forumModel = require('../models/forum_model')({ cooler, isPublic: config.p
 const blockchainModel = require('../models/blockchain_model')({ cooler, isPublic: config.public });
 const jobsModel = require('../models/jobs_model')({ cooler, isPublic: config.public });
 const projectsModel = require("../models/projects_model")({ cooler, isPublic: config.public });
-const bankingModel = require("../models/banking_model")({ services: { cooler }, isPublic: config.public })
-const parliamentModel = require('../models/parliament_model')({ cooler, services: { tribes: tribesModel, votes: votesModel, inhabitants: inhabitantsModel, banking: bankingModel }
-});
+const bankingModel = require("../models/banking_model")({ services: { cooler }, isPublic: config.public });
+const parliamentModel = require('../models/parliament_model')({ cooler, services: { tribes: tribesModel, votes: votesModel, inhabitants: inhabitantsModel, banking: bankingModel } });
+const courtsModel = require('../models/courts_model')({ cooler, services: { votes: votesModel, inhabitants: inhabitantsModel, tribes: tribesModel, banking: bankingModel } });
 
 // starting warmup
 about._startNameWarmup();
@@ -546,6 +546,7 @@ const { jobsView, singleJobsView, renderJobForm } = require("../views/jobs_view"
 const { projectsView, singleProjectView } = require("../views/projects_view")
 const { renderBankingView, renderSingleAllocationView, renderEpochView } = require("../views/banking_views")
 const { parliamentView } = require("../views/parliament_view");
+const { courtsView, courtsCaseView } = require('../views/courts_view');
 
 let sharp;
 
@@ -672,7 +673,7 @@ router
     'popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet', 
     'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending', 
     'events', 'tasks', 'market', 'tribes', 'votes', 'reports', 'opinions', 'transfers', 
-    'feed', 'pixelia', 'agenda', 'ai', 'forum', 'jobs', 'projects', 'banking', 'parliament'
+    'feed', 'pixelia', 'agenda', 'ai', 'forum', 'jobs', 'projects', 'banking', 'parliament', 'courts'
     ];
     const moduleStates = modules.reduce((acc, mod) => {
       acc[`${mod}Mod`] = configMods[`${mod}Mod`];
@@ -1108,6 +1109,155 @@ router
       revocationsEnactedCount 
     });
   })
+  .get('/courts', async (ctx) => {
+    const mod = ctx.cookies.get('courtsMod') || 'on';
+    if (mod !== 'on') {
+      ctx.redirect('/modules');
+      return;
+    }
+    const filter = String(ctx.query.filter || 'cases').toLowerCase();
+    const search = String(ctx.query.search || '').trim();
+    const state = {
+      filter,
+      search,
+      cases: [],
+      myCases: [],
+      trials: [],
+      history: [],
+      nominations: [],
+      userId: null
+    };
+    const currentUserId = await courtsModel.getCurrentUserId();
+    state.userId = currentUserId;
+    if (filter === 'cases') {
+      let allCases = await courtsModel.listCases('open');
+      allCases = allCases.map((c) => ({
+        ...c,
+        respondent: c.respondentId || c.respondent
+      }));
+      if (search) {
+        const sLower = search.toLowerCase();
+        allCases = allCases.filter((c) => {
+          const t = String(c.title || '').toLowerCase();
+          const d = String(c.description || '').toLowerCase();
+          return t.includes(sLower) || d.includes(sLower);
+        });
+      }
+      state.cases = allCases;
+    }
+    if (filter === 'mycases' || filter === 'actions') {
+      let myCases = await courtsModel.listCasesForUser(currentUserId);
+      if (search) {
+        const sLower = search.toLowerCase();
+        myCases = myCases.filter((c) => {
+          const t = String(c.title || '').toLowerCase();
+          const d = String(c.description || '').toLowerCase();
+          return t.includes(sLower) || d.includes(sLower);
+        });
+      }
+      if (filter === 'actions') {
+        myCases = myCases.filter((c) => {
+          const status = String(c.status || '').toUpperCase();
+          const method = String(c.method || '').toUpperCase();
+          const isAccuser = !!c.isAccuser;
+          const isRespondent = !!c.isRespondent;
+          const isMediator = !!c.isMediator;
+          const isJudge = !!c.isJudge;
+          const isDictator = !!c.isDictator;
+          const canAnswer =
+            isRespondent && (status === 'OPEN' || status === 'IN_PROGRESS');
+          const canAssignJudge =
+            method === 'JUDGE' &&
+            !c.judgeId &&
+            (isAccuser || isRespondent) &&
+            (status === 'OPEN' || status === 'IN_PROGRESS');
+          const canIssueVerdict =
+            (isJudge || isDictator || isMediator) &&
+            status === 'OPEN';
+          const canProposeSettlement =
+            (isAccuser || isRespondent || isMediator) &&
+            method === 'MEDIATION' &&
+            (status === 'OPEN' || status === 'IN_PROGRESS');
+          const canAddEvidence =
+            (isAccuser ||
+              isRespondent ||
+              isMediator ||
+              isJudge ||
+              isDictator) &&
+            (status === 'OPEN' || status === 'IN_PROGRESS');
+          return (
+            canAnswer ||
+            canAssignJudge ||
+            canIssueVerdict ||
+            canProposeSettlement ||
+            canAddEvidence
+          );
+        });
+      }
+      state.myCases = myCases;
+    }
+    if (filter === 'judges') {
+      const nominations = await courtsModel.listNominations();
+      state.nominations = nominations || [];
+    }
+    if (filter === 'history') {
+      let history = await courtsModel.listCases('history');
+      history = history.map((c) => {
+        const id = String(currentUserId || '');
+        const isAccuser = String(c.accuser || '') === id;
+        const isRespondent = String(c.respondentId || '') === id;
+        const ma = Array.isArray(c.mediatorsAccuser)
+          ? c.mediatorsAccuser
+          : [];
+        const mr = Array.isArray(c.mediatorsRespondent)
+          ? c.mediatorsRespondent
+          : [];
+        const isMediator = ma.includes(id) || mr.includes(id);
+        const isJudge = String(c.judgeId || '') === id;
+        const mine = isAccuser || isRespondent || isMediator || isJudge;
+        const publicDetails =
+          c.publicPrefAccuser === true &&
+          c.publicPrefRespondent === true;
+        const decidedAt =
+          c.verdictAt ||
+          c.closedAt ||
+          c.decidedAt;
+        return {
+          ...c,
+          respondent: c.respondentId || c.respondent,
+          mine,
+          publicDetails,
+          decidedAt
+        };
+      });
+      if (search) {
+        const sLower = search.toLowerCase();
+        history = history.filter((c) => {
+          const t = String(c.title || '').toLowerCase();
+          const d = String(c.description || '').toLowerCase();
+          return t.includes(sLower) || d.includes(sLower);
+        });
+      }
+      state.history = history;
+    }
+    ctx.body = await courtsView(state);
+  })
+  .get('/courts/cases/:id', async (ctx) => {
+    const mod = ctx.cookies.get('courtsMod') || 'on';
+    if (mod !== 'on') {
+      ctx.redirect('/modules');
+      return;
+    }
+    const caseId = ctx.params.id;
+    let caseData = null;
+    try {
+      caseData = await courtsModel.getCaseDetails({ caseId });
+    } catch (e) {
+      caseData = null;
+    }
+    const state = { caseData };
+    ctx.body = await courtsCaseView(state);
+  })
   .get('/tribes', async ctx => {
     const filter = ctx.query.filter || 'all';
     const search = ctx.query.search || ''; 
@@ -2740,6 +2890,305 @@ router
     await parliamentModel.createRevocation({ lawId, title, reasons });
     ctx.redirect('/parliament?filter=revocations');
   })
+  .post('/courts/cases/create', koaBody(), async (ctx) => {
+    const body = ctx.request.body || {};
+    const titleSuffix = String(body.titleSuffix || '').trim();
+    const titlePreset = String(body.titlePreset || '').trim();
+    const respondentRaw = String(body.respondentId || '').trim();
+    const methodRaw = String(body.method || '').trim().toUpperCase();
+    const ALLOWED = new Set(['JUDGE', 'DICTATOR', 'POPULAR', 'MEDIATION', 'KARMATOCRACY']);
+    if (!titleSuffix && !titlePreset) {
+      ctx.flash = { message: 'Title is required.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    if (!respondentRaw) {
+      ctx.flash = { message: 'Accused / Respondent is required.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    if (!ALLOWED.has(methodRaw)) {
+      ctx.flash = { message: 'Invalid resolution method.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    const parts = [];
+    if (titlePreset) parts.push(titlePreset);
+    if (titleSuffix) parts.push(titleSuffix);
+    const titleBase = parts.join(' - ');
+    try {
+      await courtsModel.openCase({
+        titleBase,
+        respondentInput: respondentRaw,
+        method: methodRaw
+      });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect('/courts?filter=mycases');
+  })
+  .post('/courts/cases/:id/evidence/add', koaBody({ multipart: true }), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const text = String(body.text || '');
+    const link = String(body.link || '');
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    try {
+      const imageMarkdown = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null;
+      await courtsModel.addEvidence({
+        caseId,
+        text,
+        link,
+        imageMarkdown
+      });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/answer', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const answer = String(body.answer || '');
+    const stance = String(body.stance || '').toUpperCase();
+    const ALLOWED = new Set(['DENY', 'ADMIT', 'PARTIAL']);
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    if (!answer) {
+      ctx.flash = { message: 'Response brief is required.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    if (!ALLOWED.has(stance)) {
+      ctx.flash = { message: 'Invalid stance.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    try {
+      await courtsModel.answerCase({ caseId, stance, text: answer });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/decide', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const result = String(body.outcome || '').trim();
+    const orders = String(body.orders || '');
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    if (!result) {
+      ctx.flash = { message: 'Result is required.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    try {
+      await courtsModel.issueVerdict({ caseId, result, orders });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/settlements/propose', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const terms = String(body.terms || '');
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    if (!terms) {
+      ctx.flash = { message: 'Terms are required.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    try {
+      await courtsModel.proposeSettlement({ caseId, terms });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/settlements/accept', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    try {
+      await courtsModel.acceptSettlement({ caseId });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/mediators/accuser', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const raw = String(body.mediators || '');
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    const mediators = raw
+      .split(',')
+      .map((s) => s.trim())
+      .filter((s) => s.length > 0);
+    if (!mediators.length) {
+      ctx.flash = { message: 'At least one mediator is required.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    const currentUserId = ctx.state && ctx.state.user && ctx.state.user.id;
+    if (currentUserId && mediators.includes(currentUserId)) {
+      ctx.flash = { message: 'You cannot appoint yourself as mediator.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    try {
+      await courtsModel.setMediators({ caseId, side: 'accuser', mediators });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/mediators/respondent', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const raw = String(body.mediators || '');
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    const mediators = raw
+      .split(',')
+      .map((s) => s.trim())
+      .filter((s) => s.length > 0);
+    if (!mediators.length) {
+      ctx.flash = { message: 'At least one mediator is required.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    const currentUserId = ctx.state && ctx.state.user && ctx.state.user.id;
+    if (currentUserId && mediators.includes(currentUserId)) {
+      ctx.flash = { message: 'You cannot appoint yourself as mediator.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    try {
+      await courtsModel.setMediators({ caseId, side: 'respondent', mediators });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/judge', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const judgeId = String(body.judgeId || '').trim();
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    if (!judgeId) {
+      ctx.flash = { message: 'Judge is required.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    const currentUserId = ctx.state && ctx.state.user && ctx.state.user.id;
+    if (currentUserId && judgeId === currentUserId) {
+      ctx.flash = { message: 'You cannot assign yourself as judge.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    try {
+      await courtsModel.assignJudge({ caseId, judgeId });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/public', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    const body = ctx.request.body || {};
+    const pref = String(body.preference || '').toUpperCase();
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    if (pref !== 'YES' && pref !== 'NO') {
+      ctx.flash = { message: 'Invalid visibility preference.' };
+      ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+      return;
+    }
+    const preference = pref === 'YES';
+    try {
+      await courtsModel.setPublicPreference({ caseId, preference });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/cases/:id/openVote', koaBody(), async (ctx) => {
+    const caseId = ctx.params.id;
+    if (!caseId) {
+      ctx.flash = { message: 'Case not found.' };
+      ctx.redirect('/courts?filter=cases');
+      return;
+    }
+    try {
+      await courtsModel.openPopularVote({ caseId });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect(`/courts/cases/${encodeURIComponent(caseId)}`);
+  })
+  .post('/courts/judges/nominate', koaBody(), async (ctx) => {
+    const body = ctx.request.body || {};
+    const judgeId = String(body.judgeId || '').trim();
+    if (!judgeId) {
+      ctx.flash = { message: 'Judge is required.' };
+      ctx.redirect('/courts?filter=judges');
+      return;
+    }
+    try {
+      await courtsModel.nominateJudge({ judgeId });
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect('/courts?filter=judges');
+  })
+  .post('/courts/judges/:id/vote', koaBody(), async (ctx) => {
+    const nominationId = ctx.params.id;
+    if (!nominationId) {
+      ctx.flash = { message: 'Nomination not found.' };
+      ctx.redirect('/courts?filter=judges');
+      return;
+    }
+    try {
+      await courtsModel.voteNomination(nominationId);
+    } catch (e) {
+      ctx.flash = { message: String((e && e.message) || e) };
+    }
+    ctx.redirect('/courts?filter=judges');
+  })  
   .post('/market/create', koaBody({ multipart: true }), async ctx => {
     const { item_type, title, description, price, tags, item_status, deadline, includesShipping, stock } = ctx.request.body;
     const image = await handleBlobUpload(ctx, 'image');
@@ -3324,7 +3773,7 @@ router
     'popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet',
     'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending',
     'events', 'tasks', 'market', 'tribes', 'votes', 'reports', 'opinions', 'transfers',
-    'feed', 'pixelia', 'agenda', 'ai', 'forum', 'jobs', 'projects', 'banking', 'parliament'
+    'feed', 'pixelia', 'agenda', 'ai', 'forum', 'jobs', 'projects', 'banking', 'parliament', 'courts'
     ];
     const currentConfig = getConfig();
     modules.forEach(mod => {

+ 248 - 22
src/client/assets/translations/oasis_en.js

@@ -733,6 +733,7 @@ module.exports = {
     typeParliamentTerm: "Parliament · Term",
     typeParliamentProposal: "Parliament · Proposal",
     typeParliamentLaw: "Parliament · New Law",
+    typeCourts: "Courts",
     parliamentLawQuestion: "Question",
     parliamentStatus: "Status",
     parliamentCandidaturesListTitle: "List of Candidatures",
@@ -760,6 +761,121 @@ module.exports = {
     parliamentRulesLaws: "When a proposal meets its threshold, it becomes a Law and appears in the Laws tab with its enactment date.",
     parliamentRulesHistorical: "In the Historical tab you can see every government cycle that has occurred and data about its management.",
     parliamentRulesLeaders: "In the Leaders tab you can see a ranking of inhabitants/tribes that have governed (or stood as candidates), ordered by efficiency.",
+    //courts
+    courtsTitle: "Courts",
+    courtsDescription: "Explore forms of conflict resolution and collective justice management.",
+    courtsFilterCases: "CASES",
+    courtsFilterMyCases: "MINE",
+    courtsFilterJudges: "JUDGES",
+    courtsFilterHistory: "HISTORY",
+    courtsFilterRules: "RULES",
+    courtsFilterOpenCase: "OPEN CASE",
+    courtsCaseFormTitle: "Open Case",
+    courtsCaseTitle: "Title",
+    courtsCaseRespondent: "Accused / Respondent",
+    courtsCaseRespondentPh: "Oasis ID (@...) or Tribe name",
+    courtsCaseMediatorsAccuser: "Mediators (accuser)",
+    courtsCaseMediatorsPh: "Oasis IDs, separated by comma",
+    courtsCaseMethod: "Resolution method",
+    courtsCaseDescription: "Description (max 1000 chars.)",
+    courtsCaseEvidenceTitle: "Case evidence",
+    courtsCaseEvidenceHelp: "Attach images, audios, documents (PDF) or videos that support your case.",
+    courtsCaseSubmit: "File Case",
+    courtsNominateJudge: "Nominate Judge",
+    courtsJudgeId: "Judge",
+    courtsJudgeIdPh: "Oasis ID (@...) or Inhabitant name",
+    courtsNominateBtn: "Nominate",
+    courtsAddEvidence: "Add evidence",
+    courtsEvidenceText: "Text",
+    courtsEvidenceLink: "Link",
+    courtsEvidenceLinkPh: "https://…",
+    courtsEvidenceSubmit: "Attach",
+    courtsAnswerTitle: "Answer the claim",
+    courtsAnswerText: "Response brief",
+    courtsAnswerSubmit: "Send response",
+    courtsStanceDENY: "Deny",
+    courtsStanceADMIT: "Admit",
+    courtsStancePARTIAL: "Partial",
+    courtsVerdictTitle: "Issue verdict",
+    courtsVerdictResult: "Result",
+    courtsVerdictOrders: "Orders",
+    courtsVerdictOrdersPh: "Actions, deadlines, restorative steps ...",
+    courtsIssueVerdict: "Issue verdict",
+    courtsMediationPropose: "Propose settlement",
+    courtsSettlementText: "Terms",
+    courtsSettlementProposeBtn: "Propose",
+    courtsNominationsTitle: "Judiciary nominations",
+    courtsThJudge: "Judge",
+    courtsThSupports: "Supports",
+    courtsThDate: "Date",
+    courtsThVote: "Vote",
+    courtsNoNominations: "No nominations yet.",
+    courtsAccuser: "Accuser",
+    courtsRespondent: "Respondent",
+    courtsThStatus: "Status",
+    courtsThAnswerBy: "Answer by",
+    courtsThEvidenceBy: "Evidence by",
+    courtsThDecisionBy: "Decision by",
+    courtsThCase: "Case",
+    courtsThCreatedAt: "Start date",
+    courtsThActions: "Actions",
+    courtsCaseMediatorsRespondentTitle: "Add defence mediators",
+    courtsCaseMediatorsRespondent: "Mediators (defence)",
+    courtsMediatorsAccuserLabel: "Mediators (accuser)",
+    courtsMediatorsRespondentLabel: "Mediators (defence)",
+    courtsMediatorsSubmit: "Save mediators",
+    courtsVotesNeeded: "Votes needed",
+    courtsVotesSlashTotal: "YES / TOTAL",
+    courtsOpenVote: "Open vote",
+    courtsPublicPrefLabel: "Visibility after resolution",
+    courtsPublicPrefYes: "I agree this case can be fully public",
+    courtsPublicPrefNo: "I prefer to keep the details private",
+    courtsPublicPrefSubmit: "Save visibility preference",
+    courtsNoCases: "No cases.",
+    courtsNoMyCases: "You have no conflicts yet.",
+    courtsNoHistory: "No recorded trials yet.",
+    courtsMethodJUDGE: "Judge",
+    courtsMethodDICTATOR: "Dictator",
+    courtsMethodPOPULAR: "Popular",
+    courtsMethodMEDIATION: "Mediation",
+    courtsMethodKARMATOCRACY: "Karmatocracy",
+    courtsMethod: "Method",
+    courtsRulesTitle: "How Courts work",
+    courtsRulesIntro: "Courts are a community-run process to resolve conflicts and promote restorative justice. Dialogue, clear evidence, and proportional remedies are prioritized.",
+    courtsRulesLifecycle: "Process: 1) Open case  2) Select method  3) Submit evidence  4) Hearing and deliberation  5) Verdict and remedy  6) Compliance and closure  7) Appeal (if applicable).",
+    courtsRulesRoles: "Accuser: opens the case. Defence: accused person or tribe. Method: mechanism chosen by the community to facilitate, assess evidence, and issue a verdict. Witness: provides testimony or evidence. Mediators: neutral people invited by the accuser and/or the defence, with access to all details, who help de-escalate the conflict and co-create agreements.",
+    courtsRulesEvidence: "Description up to 1000 characters. Attach relevant and lawful images, audio, video, and PDF documents. Do not share sensitive private data without consent.",
+    courtsRulesDeliberation: "Hearings may be public or private. Judges ensure respect, request clarifications, and may discard irrelevant or unlawful material.",
+    courtsRulesVerdict: "Restoration is prioritized: apologies, mediation agreements, content moderation, temporary restrictions, or other proportional measures. The reasoning must be recorded.",
+    courtsRulesAppeals: "Appeal: allowed when there is new evidence or a clear procedural error. Must be filed within 7 days unless otherwise stated.",
+    courtsRulesPrivacy: "Respect privacy and safety. Doxing, hate, or threats are removed. Judges may edit or seal parts of the record to protect people at risk.",
+    courtsRulesMisconduct: "Harassment, manipulation, or fabricated evidence may lead to immediate negative resolution.",
+    courtsRulesGlossary: "Case: record of a conflict. Evidence: materials that support claims. Verdict: decision with remedy. Appeal: request to review the verdict.",
+    courtsFilterActions: "ACTIONS",
+    courtsNoActions: "No pending actions for your role.",
+    courtsCaseTitlePlaceholder: "Short description of the conflict",
+    courtsCaseSeverity: "Severity",
+    courtsCaseSeverityNone: "No severity tag",
+    courtsCaseSeverityLOW: "Low",
+    courtsCaseSeverityMEDIUM: "Medium",
+    courtsCaseSeverityHIGH: "High",
+    courtsCaseSeverityCRITICAL: "Critical",
+    courtsCaseSubject: "Topic",
+    courtsCaseSubjectNone: "No topic tag",
+    courtsCaseSubjectBEHAVIOUR: "Behaviour",
+    courtsCaseSubjectCONTENT: "Content",
+    courtsCaseSubjectGOVERNANCE: "Governance / rules",
+    courtsCaseSubjectFINANCIAL: "Financial / resources",
+    courtsCaseSubjectOTHER: "Other",
+    courtsHiddenRespondent: "Hidden (only visible to involved roles).",
+    courtsThRole: "Role",
+    courtsRoleAccuser: "Accuser",
+    courtsRoleDefence: "Defence",
+    courtsRoleMediator: "Mediator",
+    courtsRoleJudge: "Judge",
+    courtsRoleDictator: "Dictator",
+    courtsAssignJudgeTitle: "Choose judge",
+    courtsAssignJudgeBtn: "Choose judge",
     //trending
     trendingTitle: "Trending",
     exploreTrending: "Explore the most popular content in your network.",
@@ -1185,16 +1301,16 @@ module.exports = {
     allButton:            "ALL",
     mineButton:           "MINE",
     noActions:            "No activity available.",
-    performed:            "→",   
-    from: "From",
-    to: "To",
-    amount: "Amount",
-    concept: "Concept",
-    description: "Description",
-    meme: "Meme",
-    activityContact: "Contact",
-    activityBy: "Name",
-    activityPixelia: "New pixel added",
+    performed:            "→",
+    from:                 "From",
+    to:                   "To",
+    amount:               "Amount",
+    concept:              "Concept",
+    description:          "Description",
+    meme:                 "Meme",
+    activityContact:      "Contact",
+    activityBy:           "Name",
+    activityPixelia:      "New pixel added",
     viewImage:            "View image",
     playAudio:            "Play audio",
     playVideo:            "Play video",
@@ -1217,14 +1333,33 @@ module.exports = {
     typeEvent:            "EVENTS",
     typeTransfer:         "TRANSFER",
     typeTask:             "TASKS",
-    typePixelia: 	  "PIXELIA",
-    typeForum: 	          "FORUM",
+    typePixelia:          "PIXELIA",
+    typeForum:            "FORUM",
     typeReport:           "REPORTS",
     typeFeed:             "FEED",
     typeContact:          "CONTACT",
     typePub:              "PUB",
-    typeTombstone:	  "TOMBSTONE",
+    typeTombstone:        "TOMBSTONE",
     typeBanking:          "BANKING",
+    typeBankWallet:       "BANKING/WALLET",
+    typeBankClaim:        "BANKING/UBI",
+    typeKarmaScore:       "KARMA",
+    typeParliament:       "PARLIAMENT",
+    typeParliamentCandidature: "Parliament · Candidature",
+    typeParliamentTerm:   "Parliament · Term",
+    typeParliamentProposal:"Parliament · Proposal",
+    typeParliamentRevocation:"Parliament · Revocation",
+    typeParliamentLaw:    "Parliament · New Law",
+    typeCourts:           "COURTS",
+    typeCourtsCase:       "Courts · Case",
+    typeCourtsEvidence:   "Courts · Evidence",
+    typeCourtsAnswer:     "Courts · Answer",
+    typeCourtsVerdict:    "Courts · Verdict",
+    typeCourtsSettlement: "Courts · Settlement",
+    typeCourtsSettlementProposal: "Courts · Settlement Proposal",
+    typeCourtsSettlementAccepted: "Courts · Settlement Accepted",
+    typeCourtsNomination: "Courts · Nomination",
+    typeCourtsNominationVote: "Courts · Nomination Vote",
     activitySupport:      "New alliance forged",
     activityJoin:         "New PUB joined",
     question:             "Question",
@@ -1232,6 +1367,7 @@ module.exports = {
     status:               "Status",
     votes:                "Votes",
     totalVotes:           "Total Votes",
+    voteTotalVotes:       "Total Votes",
     name:                 "Name",
     skills:               "Skills",
     tags:                 "Tags",
@@ -1242,23 +1378,94 @@ module.exports = {
     activitySpread:       "->",
     visitLink:            "Visit Link",
     viewDocument:         "View Document",
-    description:          "Description",
     location:             "Location",
     contentWarning:       "Subject",
     personName:           "Inhabitant Name",
-    typeBankWallet:       "BANKING/WALLET",
-    typeBankClaim:        "BANKING/UBI",
-    typeKarmaScore:	  "KARMA",
     bankWalletConnected:  "ECOin Wallet",
     bankUbiReceived:      "UBI Received",
     bankTx:               "Tx",
     bankEpochShort:       "Epoch",
-    activityProjectFollow: "%OASIS% is now %ACTION% this project %PROJECT%",
+    bankAllocId:          "Allocation ID",
+    bankingUserEngagementScore: "User Engagement Score",
+    viewDetails:          "View details",
+    link:                 "Link",
+    aiSnippetsLearned:    "Snippets learned",
+    tribeFeedRefeeds:     "Refeeds",
+    activityProjectFollow:   "%OASIS% is now %ACTION% this project %PROJECT%",
     activityProjectUnfollow: "%OASIS% is now %ACTION% this project %PROJECT%",
-    activityProjectPledged: "%OASIS% has %ACTION% %AMOUNT% to project %PROJECT%",
-    following: "FOLLOWING",
-    unfollowing: "UNFOLLOWING",
-    pledged: "PLEDGED",
+    activityProjectPledged:  "%OASIS% has %ACTION% %AMOUNT% to project %PROJECT%",
+    following:            "FOLLOWING",
+    unfollowing:          "UNFOLLOWING",
+    pledged:              "PLEDGED",
+    parliamentCandidatureId: "Candidature",
+    parliamentGovMethod:     "Method",
+    parliamentVotesReceived: "Votes received",
+    parliamentMethodANARCHY:   "Anarchy",
+    parliamentMethodVOTE:      "Community Vote",
+    parliamentMethodRANKED:    "Ranked Choice",
+    parliamentMethodPLURALITY: "Plurality",
+    parliamentMethodCOUNCIL:   "Council",
+    parliamentMethodJURY:      "Jury",
+    parliamentAnarchy:         "ANARCHY",
+    parliamentElectionsStart:  "Elections start",
+    parliamentElectionsEnd:    "Elections end",
+    parliamentCurrentLeader:   "Winning candidature",
+    parliamentProposalTitle:   "Title",
+    parliamentOpenVote:        "Open vote",
+    parliamentStatus:          "Status",
+    parliamentLawQuestion:     "Question",
+    parliamentLawMethod:       "Method",
+    parliamentLawProposer:     "Proposer",
+    parliamentLawEnacted:      "Enacted at",
+    parliamentLawVotes:        "Votes",
+    createdAt:                 "Created at",
+    courtsCaseTitle:           "Case",
+    courtsMethod:              "Method",
+    courtsMethodJUDGE:         "Judge",
+    courtsMethodJUDGES:        "Judges Panel",
+    courtsMethodSINGLE_JUDGE:  "Single Judge",
+    courtsMethodJURY:          "Jury",
+    courtsMethodCOUNCIL:       "Council",
+    courtsMethodCOMMUNITY:     "Community",
+    courtsMethodMEDIATION:     "Mediation",
+    courtsMethodARBITRATION:   "Arbitration",
+    courtsMethodVOTE:          "Community Vote",
+    courtsAccuser:             "Accuser",
+    courtsRespondent:          "Respondent",
+    courtsThStatus:            "Status",
+    courtsThAnswerBy:          "Answer by",
+    courtsThEvidenceBy:        "Evidence by",
+    courtsThDecisionBy:        "Decision by",
+    courtsVotesNeeded:         "Votes needed",
+    courtsVotesSlashTotal:     "YES/TOTAL",
+    courtsOpenVote:            "Open vote",
+    courtsAnswerTitle:         "Answer",
+    courtsStanceADMIT:         "Admit",
+    courtsStanceDENY:          "Deny",
+    courtsStancePARTIAL:       "Partial",
+    courtsStanceCOUNTERCLAIM:  "Counterclaim",
+    courtsStanceNEUTRAL:       "Neutral",
+    courtsVerdictResult:       "Result",
+    courtsVerdictOrders:       "Orders",
+    courtsSettlementText:      "Settlement",
+    courtsSettlementAccepted:  "Accepted",
+    courtsSettlementPending:   "Pending",
+    courtsJudge:               "Judge",
+    courtsThSupports:          "Supports",
+    courtsFilterOpenCase: 'Open Case',
+    courtsEvidenceFileLabel: 'Evidence file (image, audio, video or PDF)',
+    courtsCaseMediators: 'Mediators',
+    courtsCaseMediatorsPh: 'Mediator Oasis IDs, separated by comma',
+    courtsMediatorsLabel: 'Mediators',
+    courtsThCase: 'Case',
+    courtsThCreatedAt: 'Start date',
+    courtsThActions: 'Actions',
+    courtsPublicPrefLabel: 'Visibility after resolution',
+    courtsPublicPrefYes: 'I agree this case can be fully public',
+    courtsPublicPrefNo: 'I prefer to keep the details private',
+    courtsPublicPrefSubmit: 'Save visibility preference',
+    courtsMethodMEDIATION: 'Mediation',
+    courtsNoCases: 'No cases.',
     //reports
     reportsTitle: "Reports",
     reportsDescription: "Manage and track reports related to issues, bugs, abuses and content warnings in your network.",
@@ -1700,6 +1907,23 @@ module.exports = {
     statsTombstoneTitle: "Tombstones",
     statsNetworkTombstones: "Network tombstones",
     statsTombstoneRatio: "Tombstone ratio (%)",
+    statsAITraining: "AI Training",
+    statsAIExchanges: "Exchanges",
+    bankingUserEngagementScore: "KARMA Score",
+    statsParliamentCandidature: "Parliament candidatures",
+    statsParliamentTerm: "Parliament terms",
+    statsParliamentProposal: "Parliament proposals",
+    statsParliamentRevocation: "Parliament revocations",
+    statsParliamentLaw: "Parliament laws",
+    statsCourtsCase: "Court cases",
+    statsCourtsEvidence: "Court evidence",
+    statsCourtsAnswer: "Court answers",
+    statsCourtsVerdict: "Court verdicts",
+    statsCourtsSettlement: "Court settlements",
+    statsCourtsSettlementProposal: "Settlement proposals",
+    statsCourtsSettlementAccepted: "Settlements accepted",
+    statsCourtsNomination: "Judge nominations",
+    statsCourtsNominationVote: "Nomination votes",
     //AI
     ai: "AI",
     aiTitle: "AI",
@@ -2010,6 +2234,8 @@ module.exports = {
     modulesFeedDescription: "Module to discover and share short-texts (feeds).",
     modulesParliamentLabel: "Parliament",
     modulesParliamentDescription: "Module to elect governments and vote on laws.",
+    modulesCourtsLabel: "Courts",
+    modulesCourtsDescription: "Module to resolve conflicts and emit veredicts.",
     modulesPixeliaLabel: "Pixelia",
     modulesPixeliaDescription: "Module to draw on a collaborative grid.",
     modulesAgendaLabel: "Agenda",

+ 285 - 72
src/client/assets/translations/oasis_es.js

@@ -756,6 +756,121 @@ module.exports = {
     parliamentRulesLaws: "Cuando una propuesta alcanza su umbral, se convierte en Ley y aparece en la pestaña Leyes con su fecha de entrada en vigor.",
     parliamentRulesHistorical: "En Histórico se puede ver cada ciclo de gobierno que ha habido y datos sobre su gestión.",
     parliamentRulesLeaders: "En Líderes se puede ver un ranking de habitantes/tribus que han gobernado (o se han presentado), ordenados por eficacia.",
+     //courts
+    courtsTitle: "Tribunales",
+    courtsDescription: "Explora formas de resolución de conflictos y de gestión colectiva de la justicia.",
+    courtsFilterCases: "CASOS",
+    courtsFilterMyCases: "MÍOS",
+    courtsFilterJudges: "JUECES",
+    courtsFilterHistory: "HISTORIAL",
+    courtsFilterRules: "REGLAS",
+    courtsFilterOpenCase: "NUEVO CASO",
+    courtsCaseFormTitle: "Abrir caso",
+    courtsCaseTitle: "Título",
+    courtsCaseRespondent: "Acusado / Parte demandada",
+    courtsCaseRespondentPh: "ID de Oasis (@...) o nombre de Tribu",
+    courtsCaseMediatorsAccuser: "Mediadores (acusación)",
+    courtsCaseMediatorsPh: "ID de Oasis, separados por comas",
+    courtsCaseMethod: "Método de resolución",
+    courtsCaseDescription: "Descripción (máx. 1000 caracteres)",
+    courtsCaseEvidenceTitle: "Pruebas del caso",
+    courtsCaseEvidenceHelp: "Adjunta imágenes, audios, documentos (PDF) o vídeos que apoyen tu caso.",
+    courtsCaseSubmit: "Presentar caso",
+    courtsNominateJudge: "Nominar juez",
+    courtsJudgeId: "Juez",
+    courtsJudgeIdPh: "ID de Oasis (@...) o nombre de Habitante",
+    courtsNominateBtn: "Nominar",
+    courtsAddEvidence: "Añadir pruebas",
+    courtsEvidenceText: "Texto",
+    courtsEvidenceLink: "Enlace",
+    courtsEvidenceLinkPh: "https://…",
+    courtsEvidenceSubmit: "Adjuntar",
+    courtsAnswerTitle: "Responder a la reclamación",
+    courtsAnswerText: "Resumen de la respuesta",
+    courtsAnswerSubmit: "Enviar respuesta",
+    courtsStanceDENY: "Negar",
+    courtsStanceADMIT: "Admitir",
+    courtsStancePARTIAL: "Parcial",
+    courtsVerdictTitle: "Emitir veredicto",
+    courtsVerdictResult: "Resultado",
+    courtsVerdictOrders: "Órdenes",
+    courtsVerdictOrdersPh: "Acciones, plazos, pasos restaurativos...",
+    courtsIssueVerdict: "Emitir veredicto",
+    courtsMediationPropose: "Proponer acuerdo",
+    courtsSettlementText: "Términos",
+    courtsSettlementProposeBtn: "Proponer",
+    courtsNominationsTitle: "Nominaciones a la judicatura",
+    courtsThJudge: "Juez",
+    courtsThSupports: "Apoyos",
+    courtsThDate: "Fecha",
+    courtsThVote: "Votar",
+    courtsNoNominations: "Todavía no hay nominaciones.",
+    courtsAccuser: "Acusación",
+    courtsRespondent: "Defensa",
+    courtsThStatus: "Estado",
+    courtsThAnswerBy: "Responder antes de",
+    courtsThEvidenceBy: "Aportar pruebas antes de",
+    courtsThDecisionBy: "Decisión antes de",
+    courtsThCase: "Caso",
+    courtsThCreatedAt: "Fecha de inicio",
+    courtsThActions: "Acciones",
+    courtsCaseMediatorsRespondentTitle: "Añadir mediadores de la defensa",
+    courtsCaseMediatorsRespondent: "Mediadores (defensa)",
+    courtsMediatorsAccuserLabel: "Mediadores (acusación)",
+    courtsMediatorsRespondentLabel: "Mediadores (defensa)",
+    courtsMediatorsSubmit: "Guardar mediadores",
+    courtsVotesNeeded: "Votos necesarios",
+    courtsVotesSlashTotal: "SÍ / TOTAL",
+    courtsOpenVote: "Abrir votación",
+    courtsPublicPrefLabel: "Visibilidad tras la resolución",
+    courtsPublicPrefYes: "Acepto que este caso sea totalmente público",
+    courtsPublicPrefNo: "Prefiero mantener los detalles en privado",
+    courtsPublicPrefSubmit: "Guardar preferencia de visibilidad",
+    courtsNoCases: "Sin casos.",
+    courtsNoMyCases: "Aún no tienes conflictos.",
+    courtsNoHistory: "Todavía no hay juicios registrados.",
+    courtsMethodJUDGE: "Juez",
+    courtsMethodDICTATOR: "Dictador",
+    courtsMethodPOPULAR: "Popular",
+    courtsMethodMEDIATION: "Mediación",
+    courtsMethodKARMATOCRACY: "Karmatocracia",
+    courtsMethod: "Método",
+    courtsRulesTitle: "Cómo funcionan los Tribunales",
+    courtsRulesIntro: "Los Tribunales son un proceso gestionado por la comunidad para resolver conflictos y promover la justicia restaurativa. Se priorizan el diálogo, las pruebas claras y los remedios proporcionales.",
+    courtsRulesLifecycle: "Proceso: 1) Abrir caso  2) Seleccionar método  3) Presentar pruebas  4) Audiencia y deliberación  5) Veredicto y remedio  6) Cumplimiento y cierre  7) Apelación (si procede).",
+    courtsRulesRoles: "Acusación: abre el caso. Defensa: persona o tribu acusada. Método: mecanismo elegido por la comunidad para facilitar, evaluar las pruebas y emitir veredicto. Testigo: aporta testimonio o pruebas. Mediadores: personas neutrales invitadas por la acusación y/o la defensa, con acceso a todos los detalles, que ayudan a desescalar el conflicto y co-crear acuerdos.",
+    courtsRulesEvidence: "Descripción de hasta 1000 caracteres. Adjunta imágenes, audio, vídeo y documentos PDF relevantes y legales. No compartas datos privados sensibles sin consentimiento.",
+    courtsRulesDeliberation: "Las audiencias pueden ser públicas o privadas. Los jueces garantizan el respeto, piden aclaraciones y pueden descartar material irrelevante o ilegal.",
+    courtsRulesVerdict: "Se prioriza la restauración: disculpas, acuerdos de mediación, moderación de contenido, restricciones temporales u otras medidas proporcionales. El razonamiento debe quedar registrado.",
+    courtsRulesAppeals: "Apelación: permitida cuando hay nuevas pruebas o un error procesal claro. Debe presentarse en un plazo de 7 días salvo que se indique lo contrario.",
+    courtsRulesPrivacy: "Respeta la privacidad y la seguridad. El doxing, el odio o las amenazas se eliminan. Los jueces pueden editar o sellar partes del expediente para proteger a personas en riesgo.",
+    courtsRulesMisconduct: "El acoso, la manipulación o las pruebas fabricadas pueden llevar a una resolución negativa inmediata.",
+    courtsRulesGlossary: "Caso: registro de un conflicto. Pruebas: materiales que respaldan las afirmaciones. Veredicto: decisión con remedio. Apelación: solicitud para revisar el veredicto.",
+    courtsFilterActions: "ACCIONES",
+    courtsNoActions: "No hay acciones pendientes para tu rol.",
+    courtsCaseTitlePlaceholder: "Breve descripción del conflicto",
+    courtsCaseSeverity: "Severidad",
+    courtsCaseSeverityNone: "Sin etiqueta de severidad",
+    courtsCaseSeverityLOW: "Baja",
+    courtsCaseSeverityMEDIUM: "Media",
+    courtsCaseSeverityHIGH: "Alta",
+    courtsCaseSeverityCRITICAL: "Crítica",
+    courtsCaseSubject: "Tema",
+    courtsCaseSubjectNone: "Sin etiqueta de tema",
+    courtsCaseSubjectBEHAVIOUR: "Comportamiento",
+    courtsCaseSubjectCONTENT: "Contenido",
+    courtsCaseSubjectGOVERNANCE: "Gobernanza / normas",
+    courtsCaseSubjectFINANCIAL: "Finanzas / recursos",
+    courtsCaseSubjectOTHER: "Otro",
+    courtsHiddenRespondent: "Oculto (solo visible para los roles implicados).",
+    courtsThRole: "Rol",
+    courtsRoleAccuser: "Acusación",
+    courtsRoleDefence: "Defensa",
+    courtsRoleMediator: "Mediador",
+    courtsRoleJudge: "Juez",
+    courtsRoleDictator: "Dictador",
+    courtsAssignJudgeTitle: "Elegir juez",
+    courtsAssignJudgeBtn: "Elegir juez",
     //trending
     trendingTitle: "Tendencias",
     exploreTrending: "Explora el contenido más popular en tu red.",
@@ -1177,13 +1292,13 @@ module.exports = {
     yourActivity:         "Tu actividad",
     globalActivity:       "Actividad global",
     activityList:         "Actividad",
-    activityDesc:         "Consulta la actividad reciente de tu red.",
-    allButton:            "TODOS",
+    activityDesc:         "Consulta la actividad más reciente de tu red.",
+    allButton:            "TODO",
     mineButton:           "MÍAS",
     noActions:            "No hay actividad disponible.",
     performed:            "→",
     from:                 "De",
-    to:                   "Para",
+    to:                   "A",
     amount:               "Cantidad",
     concept:              "Concepto",
     description:          "Descripción",
@@ -1196,11 +1311,11 @@ module.exports = {
     playVideo:            "Reproducir vídeo",
     typeRecent:           "RECIENTE",
     errorActivity:        "Error al recuperar la actividad",
-    typePost:             "PUBLICACIONES",
+    typePost:             "PUBLICACIÓN",
     typeTribe:            "TRIBUS",
     typeAbout:            "HABITANTES",
-    typeCurriculum:       "CVs",
-    typeImage:            "IMAGENES",
+    typeCurriculum:       "CV",
+    typeImage:            "IMÁGENES",
     typeBookmark:         "MARCADORES",
     typeDocument:         "DOCUMENTOS",
     typeVotes:            "VOTACIONES",
@@ -1211,16 +1326,35 @@ module.exports = {
     typeVideo:            "VÍDEOS",
     typeVote:             "DIFUSIÓN",
     typeEvent:            "EVENTOS",
-    typeTransfer:         "TRANSFERENCIAS",
+    typeTransfer:         "TRANSFERENCIA",
     typeTask:             "TAREAS",
     typePixelia:          "PIXELIA",
-    typeForum:            "FOROS",
+    typeForum:            "FORO",
     typeReport:           "REPORTES",
     typeFeed:             "FEED",
-    typeContact:          "CONTACTOS",
-    typePub:              "PUBs",
-    typeTombstone:        "TOMBSTONES",
+    typeContact:          "CONTACTO",
+    typePub:              "PUB",
+    typeTombstone:        "TOMBSTONE",
     typeBanking:          "BANCA",
+    typeBankWallet:       "BANCA/MONEDERO",
+    typeBankClaim:        "BANCA/UBI",
+    typeKarmaScore:       "KARMA",
+    typeParliament:       "PARLAMENTO",
+    typeParliamentCandidature: "Parlamento · Candidatura",
+    typeParliamentTerm:   "Parlamento · Mandato",
+    typeParliamentProposal:"Parlamento · Propuesta",
+    typeParliamentRevocation:"Parlamento · Revocación",
+    typeParliamentLaw:    "Parlamento · Nueva ley",
+    typeCourts:           "TRIBUNALES",
+    typeCourtsCase:       "Tribunales · Caso",
+    typeCourtsEvidence:   "Tribunales · Prueba",
+    typeCourtsAnswer:     "Tribunales · Respuesta",
+    typeCourtsVerdict:    "Tribunales · Veredicto",
+    typeCourtsSettlement: "Tribunales · Acuerdo",
+    typeCourtsSettlementProposal: "Tribunales · Propuesta de acuerdo",
+    typeCourtsSettlementAccepted: "Tribunales · Acuerdo aceptado",
+    typeCourtsNomination: "Tribunales · Nominación",
+    typeCourtsNominationVote: "Tribunales · Votación de nominación",
     activitySupport:      "Nueva alianza forjada",
     activityJoin:         "Nuevo PUB unido",
     question:             "Pregunta",
@@ -1228,6 +1362,7 @@ module.exports = {
     status:               "Estado",
     votes:                "Votos",
     totalVotes:           "Votos totales",
+    voteTotalVotes:       "Votos totales",
     name:                 "Nombre",
     skills:               "Habilidades",
     tags:                 "Etiquetas",
@@ -1241,19 +1376,91 @@ module.exports = {
     location:             "Ubicación",
     contentWarning:       "Asunto",
     personName:           "Nombre del habitante",
-    typeBankWallet:       "BANCA/CARTERA",
-    typeBankClaim:        "BANCA/UBI",
-    typeKarmaScore:	  "KARMA",
-    bankWalletConnected:  "Cartera ECOin",
-    bankUbiReceived:      "UBI recibida",
+    bankWalletConnected:  "Monedero ECOin",
+    bankUbiReceived:      "UBI recibido",
     bankTx:               "Tx",
     bankEpochShort:       "Época",
-    activityProjectFollow: "%OASIS% ahora está %ACTION% este proyecto %PROJECT%",
+    bankAllocId:          "ID de asignación",
+    bankingUserEngagementScore: "Puntuación de compromiso",
+    viewDetails:          "Ver detalles",
+    link:                 "Enlace",
+    aiSnippetsLearned:    "Fragmentos aprendidos",
+    tribeFeedRefeeds:     "Reenvíos",
+    activityProjectFollow:   "%OASIS% ahora está %ACTION% este proyecto %PROJECT%",
     activityProjectUnfollow: "%OASIS% ahora está %ACTION% este proyecto %PROJECT%",
-    activityProjectPledged: "%OASIS% ha %ACTION% %AMOUNT% al proyecto %PROJECT%",
-    following: "SIGUIENDO",
-    unfollowing: "DEJANDO DE SEGUIR",
-    pledged: "APORTADO",
+    activityProjectPledged:  "%OASIS% ha %ACTION% %AMOUNT% al proyecto %PROJECT%",
+    following:            "SIGUIENDO",
+    unfollowing:          "DEJANDO DE SEGUIR",
+    pledged:              "APORTADO",
+    parliamentCandidatureId: "Candidatura",
+    parliamentGovMethod:     "Método",
+    parliamentVotesReceived: "Votos recibidos",
+    parliamentMethodANARCHY:   "Anarquía",
+    parliamentMethodVOTE:      "Votación comunitaria",
+    parliamentMethodRANKED:    "Voto preferencial",
+    parliamentMethodPLURALITY: "Pluralidad",
+    parliamentMethodCOUNCIL:   "Consejo",
+    parliamentMethodJURY:      "Jurado",
+    parliamentAnarchy:         "ANARQUÍA",
+    parliamentElectionsStart:  "Inicio de elecciones",
+    parliamentElectionsEnd:    "Fin de elecciones",
+    parliamentCurrentLeader:   "Candidatura ganadora",
+    parliamentProposalTitle:   "Título",
+    parliamentOpenVote:        "Votación abierta",
+    parliamentStatus:          "Estado",
+    parliamentLawQuestion:     "Pregunta",
+    parliamentLawMethod:       "Método",
+    parliamentLawProposer:     "Proponente",
+    parliamentLawEnacted:      "Promulgada",
+    parliamentLawVotes:        "Votos",
+    createdAt:                 "Creado el",
+    courtsCaseTitle:           "Caso",
+    courtsMethod:              "Método",
+    courtsMethodJUDGE:         "Juez",
+    courtsMethodJUDGES:        "Panel de jueces",
+    courtsMethodSINGLE_JUDGE:  "Juez único",
+    courtsMethodJURY:          "Jurado",
+    courtsMethodCOUNCIL:       "Consejo",
+    courtsMethodCOMMUNITY:     "Comunidad",
+    courtsMethodMEDIATION:     "Mediación",
+    courtsMethodARBITRATION:   "Arbitraje",
+    courtsMethodVOTE:          "Votación comunitaria",
+    courtsAccuser:             "Acusación",
+    courtsRespondent:          "Defensa",
+    courtsThStatus:            "Estado",
+    courtsThAnswerBy:          "Responder antes de",
+    courtsThEvidenceBy:        "Pruebas hasta",
+    courtsThDecisionBy:        "Decisión antes de",
+    courtsVotesNeeded:         "Votos necesarios",
+    courtsVotesSlashTotal:     "SÍ/TOTAL",
+    courtsOpenVote:            "Votación abierta",
+    courtsAnswerTitle:         "Respuesta",
+    courtsStanceADMIT:         "Admite",
+    courtsStanceDENY:          "Niega",
+    courtsStancePARTIAL:       "Parcial",
+    courtsStanceCOUNTERCLAIM:  "Reconvención",
+    courtsStanceNEUTRAL:       "Neutral",
+    courtsVerdictResult:       "Resultado",
+    courtsVerdictOrders:       "Órdenes",
+    courtsSettlementText:      "Acuerdo",
+    courtsSettlementAccepted:  "Aceptado",
+    courtsSettlementPending:   "Pendiente",
+    courtsJudge:               "Juez",
+    courtsThSupports:          "Apoyos",
+    courtsFilterOpenCase: 'Abrir caso',
+    courtsEvidenceFileLabel: 'Archivo de prueba (imagen, audio, vídeo o PDF)',
+    courtsCaseMediators: 'Mediadores',
+    courtsCaseMediatorsPh: 'IDs de Oasis de mediadores, separados por comas',
+    courtsMediatorsLabel: 'Mediadores',
+    courtsThCase: 'Caso',
+    courtsThCreatedAt: 'Fecha de inicio',
+    courtsThActions: 'Acciones',
+    courtsPublicPrefLabel: 'Visibilidad tras la resolución',
+    courtsPublicPrefYes: 'Acepto que este caso sea totalmente público',
+    courtsPublicPrefNo: 'Prefiero mantener privados los detalles',
+    courtsPublicPrefSubmit: 'Guardar preferencia de visibilidad',
+    courtsMethodMEDIATION: 'Mediación',
+    courtsNoCases: 'No hay casos.',
     //reports
     reportsTitle: "Informes",
     reportsDescription: "Gestiona y realiza un seguimiento de los informes relacionados con problemas, errores, abusos y advertencias de contenido en tu red.",
@@ -1602,65 +1809,52 @@ module.exports = {
     //stats
     statsTitle: 'Estadísticas',
     statistics: "Estadísticas",
-    statsInhabitant: "Estadísticas de Habitantes",
-    statsDescription: "Descubre las estadísticas de tu red.",
-    ALLButton: "TODOS",
+    statsInhabitant: "Estadísticas del Habitante",
+    statsDescription: "Descubre estadísticas sobre tu red.",
+    ALLButton: "TODO",
     MINEButton: "MÍAS",
-    TOMBSTONEButton: "TUMBAS",
+    TOMBSTONEButton: "ELIMINADOS",
     statsYou: "Tú",
     statsUserId: "ID de Oasis",
     statsCreatedAt: "Creado el",
     statsYourContent: "Contenido",
     statsYourOpinions: "Opiniones",
-    statsYourTombstone: "Lápidas",
-    statsYourProject: "Proyectos",
-    statsDiscoveredProject: "Proyectos",
-    statsBankingTitle: "Banca",
-    statsEcoWalletLabel: "Cartera de ECOIN",
-    statsEcoWalletNotConfigured: "Sin configurar!",
-    statsTotalEcoAddresses: "Direcciones totales",
-    statsProject: "Proyectos",
+    statsYourTombstone: "Eliminados",
     statsNetwork: "Red",
     statsTotalInhabitants: "Habitantes",
     statsDiscoveredTribes: "Tribus (Públicas)",
     statsPrivateDiscoveredTribes: "Tribus (Privadas)",
     statsNetworkContent: "Contenido",
     statsYourMarket: "Mercado",
-    statsYourJob: "Trabajos",
+    statsYourJob: "Empleos",
+    statsYourProject: "Proyectos",
     statsYourTransfer: "Transferencias",
     statsYourForum: "Foros",   
-    statsProject: "Proyectos",
-    statsProjectsTitle: "Proyectos",
-    statsProjectsTotal: "Total de proyectos",
-    statsProjectsActive: "Activos",
-    statsProjectsCompleted: "Completados",
-    statsProjectsPaused: "Pausados",
-    statsProjectsCancelled: "Cancelados",
-    statsProjectsGoalTotal: "Meta total",
-    statsProjectsPledgedTotal: "Total comprometido",
-    statsProjectsSuccessRate: "Tasa de éxito",
-    statsProjectsAvgProgress: "Progreso promedio",
-    statsProjectsMedianProgress: "Progreso medio",
-    statsProjectsActiveFundingAvg: "Promedio de financiación activa",
     statsNetworkOpinions: "Opiniones",
     statsDiscoveredMarket: "Mercado",
-    statsDiscoveredJob: "Trabajos",
+    statsDiscoveredJob: "Empleos",
+    statsDiscoveredProject: "Proyectos",
+    statsBankingTitle: "Banca",
+    statsEcoWalletLabel: "Billetera ECOIN",
+    statsEcoWalletNotConfigured:  "¡No configurada!",
+    statsTotalEcoAddresses: "Direcciones totales",
     statsDiscoveredTransfer: "Transferencias",
     statsDiscoveredForum: "Foros",
-    statsNetworkTombstone: "Lápidas",
+    statsNetworkTombstone: "Eliminados",
     statsBookmark: "Marcadores",
     statsEvent: "Eventos",
     statsTask: "Tareas",
     statsVotes: "Votos",
     statsMarket: "Mercado",
     statsForum: "Foros",
-    statsJob: "Trabajos",
-    statsReport: "Informes",
-    statsFeed: "Feeds",
+    statsJob: "Empleos",
+    statsProject: "Proyectos",
+    statsReport: "Reportes",
+    statsFeed: "Publicaciones",
     statsTribe: "Tribus",
     statsImage: "Imágenes",
     statsAudio: "Audios",
-    statsVideo: "Videos",
+    statsVideo: "Vídeos",
     statsDocument: "Documentos",
     statsTransfer: "Transferencias",
     statsAiExchange: "IA",
@@ -1669,13 +1863,13 @@ module.exports = {
     statsOasisID: "ID de Oasis",
     statsSize: "Total (tamaño)",
     statsBlockchainSize: "Blockchain (tamaño)",
-    statsBlobsSize: "Blobs (tamaño)",   
+    statsBlobsSize: "Blobs (tamaño)",
     statsActivity7d: "Actividad (últimos 7 días)",
-    statsActivity7dTotal: "Total de 7 días",
-    statsActivity30dTotal: "Total de 30 días",
-    statsKarmaScore: "Puntuación de KARMA",
-    statsPublic: "Públicas",
-    statsPrivate: "Privadas",
+    statsActivity7dTotal: "Total 7 días",
+    statsActivity30dTotal: "Total 30 días",
+    statsKarmaScore: "Puntuación KARMA",
+    statsPublic: "Público",
+    statsPrivate: "Privado",
     day: "Día",
     messages: "Mensajes",
     statsProject: "Proyectos",
@@ -1686,32 +1880,49 @@ module.exports = {
     statsProjectsPaused: "Pausados",
     statsProjectsCancelled: "Cancelados",
     statsProjectsGoalTotal: "Meta total",
-    statsProjectsPledgedTotal: "Total comprometido",
+    statsProjectsPledgedTotal: "Aportación total",
     statsProjectsSuccessRate: "Tasa de éxito",
-    statsProjectsAvgProgress: "Progreso promedio",
-    statsProjectsMedianProgress: "Progreso medio",
-    statsProjectsActiveFundingAvg: "Promedio de financiación activa",
-    statsJobsTitle: "Trabajos",
-    statsJobsTotal: "Total de trabajos",
+    statsProjectsAvgProgress: "Progreso medio",
+    statsProjectsMedianProgress: "Progreso mediano",
+    statsProjectsActiveFundingAvg: "Financiación activa media",
+    statsJobsTitle: "Empleos",
+    statsJobsTotal: "Total de empleos",
     statsJobsOpen: "Abiertos",
     statsJobsClosed: "Cerrados",
     statsJobsOpenVacants: "Vacantes abiertas",
-    statsJobsSubscribersTotal: "Total de suscriptores",
-    statsJobsAvgSalary: "Salario promedio",
+    statsJobsSubscribersTotal: "Suscriptores totales",
+    statsJobsAvgSalary: "Salario medio",
     statsJobsMedianSalary: "Salario mediano",
     statsMarketTitle: "Mercado",
-    statsMarketTotal: "Total de artículos",
+    statsMarketTotal: "Artículos totales",
     statsMarketForSale: "En venta",
     statsMarketReserved: "Reservados",
     statsMarketClosed: "Cerrados",
     statsMarketSold: "Vendidos",
     statsMarketRevenue: "Ingresos",
-    statsMarketAvgSoldPrice: "Precio promedio de venta",
+    statsMarketAvgSoldPrice: "Precio medio vendido",
     statsUsersTitle: "Habitantes",
     user: "Habitante",
-    statsTombstoneTitle: "Tumbas",
-    statsNetworkTombstones: "Tumbas de la red",
-    statsTombstoneRatio: "Ratio de tumbas (%)",
+    statsTombstoneTitle: "Eliminados",
+    statsNetworkTombstones: "Eliminados en la red",
+    statsTombstoneRatio: "Ratio de eliminados (%)",
+    statsAITraining: "Entrenamiento de IA",
+    statsAIExchanges: "Intercambios",
+    bankingUserEngagementScore: "Puntuación de participación del usuario",
+    statsParliamentCandidature: "Candidaturas de parlamento",
+    statsParliamentTerm: "Mandatos de parlamento",
+    statsParliamentProposal: "Propuestas de parlamento",
+    statsParliamentRevocation: "Revocaciones de parlamento",
+    statsParliamentLaw: "Leyes de parlamento",
+    statsCourtsCase: "Casos judiciales",
+    statsCourtsEvidence: "Evidencias judiciales",
+    statsCourtsAnswer: "Respuestas judiciales",
+    statsCourtsVerdict: "Veredictos judiciales",
+    statsCourtsSettlement: "Acuerdos judiciales",
+    statsCourtsSettlementProposal: "Propuestas de acuerdo",
+    statsCourtsSettlementAccepted: "Acuerdos aceptados",
+    statsCourtsNomination: "Nominaciones de jueces",
+    statsCourtsNominationVote: "Votos de nominación",
     //AI
     ai: "IA",
     aiTitle: "IA",
@@ -2014,6 +2225,8 @@ module.exports = {
     modulesVotationsDescription: "Módulo para descubrir y gestionar votaciones.",
     modulesParliamentLabel: "Parlamento",
     modulesParliamentDescription: "Módulo para elegir gobiernos y votar leyes.",
+    modulesCourtsLabel: "Juzgados",
+    modulesCourtsDescription: "Módulo para resolver conflictos y emitir veredictos.",
     modulesReportsLabel: "Informes",
     modulesReportsDescription: "Módulo para gestionar y hacer un seguimiento de informes relacionados con problemas, errores, abusos y advertencias de contenido.",
     modulesOpinionsLabel: "Opiniones",

+ 278 - 112
src/client/assets/translations/oasis_eu.js

@@ -757,6 +757,121 @@ module.exports = {
     parliamentRulesLaws: "Proposamenak bere atalasea gainditzen duenean, Lege bihurtzen da eta Legeak fitxan agertzen da indarrean sartzeko datarekin.",
     parliamentRulesHistorical: "Historia fitxan egondako gobernu ziklo bakoitza eta haren kudeaketari buruzko datuak ikus daitezke.",
     parliamentRulesLeaders: "Liderak fitxan, gobernatu duten (edo aurkeztu diren) biztanle/tribuen sailkapena ikus daiteke, eraginkortasunaren arabera ordenatuta.",
+    //courts
+    courtsTitle: "Auzitegiak",
+    courtsDescription: "Aztertu gatazkak konpontzeko eta justizia kolektiboa kudeatzeko modu desberdinak.",
+    courtsFilterCases: "KASUAK",
+    courtsFilterMyCases: "NIREAK",
+    courtsFilterJudges: "EPAILEAK",
+    courtsFilterHistory: "HISTORIA",
+    courtsFilterRules: "ARAUAK",
+    courtsFilterOpenCase: "KASU BERRIA",
+    courtsCaseFormTitle: "Kasu ireki",
+    courtsCaseTitle: "Izenburua",
+    courtsCaseRespondent: "Akusatua / Erantzulea",
+    courtsCaseRespondentPh: "Oasis ID (@...) edo Tribu izena",
+    courtsCaseMediatorsAccuser: "Bitartekariak (akusazioa)",
+    courtsCaseMediatorsPh: "Oasis IDak, komaz bereizita",
+    courtsCaseMethod: "Ebazpen metodoa",
+    courtsCaseDescription: "Azalpena (gehienez 1000 karaktere)",
+    courtsCaseEvidenceTitle: "Kasuko frogak",
+    courtsCaseEvidenceHelp: "Erantsi zure kasua babesten duten irudiak, audioak, dokumentuak (PDF) edo bideoak.",
+    courtsCaseSubmit: "Kasu bidali",
+    courtsNominateJudge: "Epailea proposatu",
+    courtsJudgeId: "Epailea",
+    courtsJudgeIdPh: "Oasis ID (@...) edo Biztanle izena",
+    courtsNominateBtn: "Proposatu",
+    courtsAddEvidence: "Frogak gehitu",
+    courtsEvidenceText: "Testua",
+    courtsEvidenceLink: "Esteka",
+    courtsEvidenceLinkPh: "https://…",
+    courtsEvidenceSubmit: "Erantsi",
+    courtsAnswerTitle: "Erreklamazioari erantzun",
+    courtsAnswerText: "Erantzun laburra",
+    courtsAnswerSubmit: "Erantzuna bidali",
+    courtsStanceDENY: "Ukatu",
+    courtsStanceADMIT: "Onartu",
+    courtsStancePARTIAL: "Partziala",
+    courtsVerdictTitle: "Ebazpena eman",
+    courtsVerdictResult: "Emaitza",
+    courtsVerdictOrders: "Aginduak",
+    courtsVerdictOrdersPh: "Ekintzak, epeak, neurri zuzentzaileak...",
+    courtsIssueVerdict: "Ebazpena eman",
+    courtsMediationPropose: "Akordioa proposatu",
+    courtsSettlementText: "Baldintzak",
+    courtsSettlementProposeBtn: "Proposatu",
+    courtsNominationsTitle: "Epailetzako izendapenak",
+    courtsThJudge: "Epailea",
+    courtsThSupports: "Babesak",
+    courtsThDate: "Data",
+    courtsThVote: "Bozkatu",
+    courtsNoNominations: "Oraindik ez dago izendapenik.",
+    courtsAccuser: "Akusatzailea",
+    courtsRespondent: "Erantzulea",
+    courtsThStatus: "Egoera",
+    courtsThAnswerBy: "Erantzuteko epea",
+    courtsThEvidenceBy: "Frogak aurkezteko epea",
+    courtsThDecisionBy: "Ebazteko epea",
+    courtsThCase: "Kasua",
+    courtsThCreatedAt: "Hasiera-data",
+    courtsThActions: "Ekintzak",
+    courtsCaseMediatorsRespondentTitle: "Defentsako bitartekariak gehitu",
+    courtsCaseMediatorsRespondent: "Bitartekariak (defentsa)",
+    courtsMediatorsAccuserLabel: "Bitartekariak (akusazioa)",
+    courtsMediatorsRespondentLabel: "Bitartekariak (defentsa)",
+    courtsMediatorsSubmit: "Bitartekariak gorde",
+    courtsVotesNeeded: "Beharrezko botoak",
+    courtsVotesSlashTotal: "BAI / GUZTIRA",
+    courtsOpenVote: "Bozketa ireki",
+    courtsPublicPrefLabel: "Ebazpenaren ondorengo ikusgarritasuna",
+    courtsPublicPrefYes: "Ados nago kasu hau guztiz publikoa izatearekin",
+    courtsPublicPrefNo: "Hobe dut xehetasunak pribatuan mantentzea",
+    courtsPublicPrefSubmit: "Ikusgarritasun lehentasuna gorde",
+    courtsNoCases: "Kasurik ez.",
+    courtsNoMyCases: "Oraindik ez duzu gatazkarik.",
+    courtsNoHistory: "Oraindik ez dago auzirik erregistratuta.",
+    courtsMethodJUDGE: "Epailea",
+    courtsMethodDICTATOR: "Diktadorea",
+    courtsMethodPOPULAR: "Herritarren bozketa",
+    courtsMethodMEDIATION: "Bitartekaritza",
+    courtsMethodKARMATOCRACY: "Karmatokrazia",
+    courtsMethod: "Metodoa",
+    courtsRulesTitle: "Nola funtzionatzen duten Auzitegiek",
+    courtsRulesIntro: "Auzitegiak komunitateak kudeatutako prozesuak dira, gatazkak konpontzeko eta justizia zuzentzailea sustatzeko. Elkarrizketa, froga argiak eta neurri proportzionalak lehenesten dira.",
+    courtsRulesLifecycle: "Prozesua: 1) Kasua ireki  2) Metodoa aukeratu  3) Frogak aurkeztu  4) Entzunaldia eta eztabaida  5) Epai eta neurriak  6) Betetze eta itxiera  7) Helegitea (badagokio).",
+    courtsRulesRoles: "Akusatzailea: kasua irekitzen du. Defentsa: akusatutako pertsona edo tribua. Metodoa: komunitateak aukeratutako mekanismoa, froga baloratu eta ebazpena emateko. Lekukoa: testigantza edo frogak ematen dituen pertsona. Bitartekariak: akusatzaileak eta/edo defentsak gonbidatutako pertsona neutralak, xehetasun guztietara sarbidea dutenak, gatazka deseskalatzen eta akordioak lankidetzan eraikitzen laguntzen dutenak.",
+    courtsRulesEvidence: "Gehienez 1000 karaktereko azalpena. Erantsi irudi, audio, bideo eta PDF dokumentu garrantzitsu eta legezkoak. Ez partekatu datu pribatu sentikorrak baimenik gabe.",
+    courtsRulesDeliberation: "Entzunaldiak publikoak edo pribatuak izan daitezke. Epaileek errespetua bermatzen dute, argipenak eskatzen dituzte eta material ez-legala edo esanguragabea baztertu dezakete.",
+    courtsRulesVerdict: "Lehentasuna da konponketa: barkamenak, bitartekaritza-akordioak, edukiaren moderazioa, behin-behineko murrizketak edo beste neurri proportzional batzuk. Arrazoibidea erregistratuta geratu behar da.",
+    courtsRulesAppeals: "Helegitea: onartzen da frogak berriak direnean edo akats prozesal argia dagoenean. Gehienez 7 eguneko epean aurkeztu behar da, bestela adierazi ezean.",
+    courtsRulesPrivacy: "Errespetatu pribatutasuna eta segurtasuna. Doxing-a, gorrotoa edo mehatxuak ezabatzen dira. Epaileek erregistroaren zatiak editatu edo ezkutatu ditzakete arriskuan dauden pertsonak babesteko.",
+    courtsRulesMisconduct: "Jazarpenak, manipulazioak edo faltsututako frogek berehalako ebazpen negatiboa ekar dezakete.",
+    courtsRulesGlossary: "Kasua: gatazkaren erregistroa. Froga: aldarrikapenak babesten dituzten materialak. Epai/ebazpena: neurri edo konponketarekin datorren erabakia. Helegitea: ebazpena berrikusteko eskaera.",
+    courtsFilterActions: "EKINTZAK",
+    courtsNoActions: "Ez duzu zain dauden ekintzarik zure rolarentzat.",
+    courtsCaseTitlePlaceholder: "Gatazkaren deskribapen laburra",
+    courtsCaseSeverity: "Larritasuna",
+    courtsCaseSeverityNone: "Larritasun etiketarik ez",
+    courtsCaseSeverityLOW: "Baxua",
+    courtsCaseSeverityMEDIUM: "Ertaina",
+    courtsCaseSeverityHIGH: "Handia",
+    courtsCaseSeverityCRITICAL: "Larria",
+    courtsCaseSubject: "Gaia",
+    courtsCaseSubjectNone: "Gairik gabeko etiketa",
+    courtsCaseSubjectBEHAVIOUR: "Jokabidea",
+    courtsCaseSubjectCONTENT: "Edukia",
+    courtsCaseSubjectGOVERNANCE: "Gobernantza / arauak",
+    courtsCaseSubjectFINANCIAL: "Finantzak / baliabideak",
+    courtsCaseSubjectOTHER: "Bestelakoa",
+    courtsHiddenRespondent: "Ezkutatua (rol inplikatuek bakarrik ikus dezakete).",
+    courtsThRole: "Rola",
+    courtsRoleAccuser: "Akusatzailea",
+    courtsRoleDefence: "Defentsa",
+    courtsRoleMediator: "Bitartekaria",
+    courtsRoleJudge: "Epailea",
+    courtsRoleDictator: "Diktadorea",
+    courtsAssignJudgeTitle: "Epailea aukeratu",
+    courtsAssignJudgeBtn: "Epailea aukeratu",
     //trending
     trendingTitle: "Pil-pilean",
     exploreTrending: "Aurkitu pil-pileko edukia zure sarean.",
@@ -1179,56 +1294,76 @@ module.exports = {
     globalActivity:       "Jarduera globala",
     activityList:         "Jarduera",
     activityDesc:         "Ikusi zure sareko azken jarduera.",
-    allButton:            "GUZTIAK",
-    mineButton:           "NIREA",
-    noActions:            "Ez dago jarduerarik.",
+    allButton:            "GUZTIA",
+    mineButton:           "NIREAK",
+    noActions:            "Ez dago jarduerarik eskuragarri.",
     performed:            "→",
     from:                 "Nork",
     to:                   "Nori",
     amount:               "Kopurua",
     concept:              "Kontzeptua",
     description:          "Deskribapena",
-    meme:                 "Memea",
+    meme:                 "Meme",
     activityContact:      "Kontaktua",
     activityBy:           "Izena",
     activityPixelia:      "Pixel berria gehituta",
-    viewImage:            "Irudia ikusi",
+    viewImage:            "Ikusi irudia",
     playAudio:            "Audioa erreproduzitu",
     playVideo:            "Bideoa erreproduzitu",
-    typeRecent:           "AZKENAK",
+    typeRecent:           "AZKENA",
     errorActivity:        "Errorea jarduera eskuratzean",
     typePost:             "ARGITALPENA",
-    typeTribe:            "TRIBUA",
-    typeAbout:            "BIZTANLEA",
+    typeTribe:            "TRIBUAK",
+    typeAbout:            "BIZTANLEAK",
     typeCurriculum:       "CV",
-    typeImage:            "IRUDIA",
-    typeBookmark:         "LASTER-MARKA",
-    typeDocument:         "DOKUMENTUA",
-    typeVotes:            "GOBERNANTZA",
-    typeAudio:            "AUDIOA",
+    typeImage:            "IRUDIAK",
+    typeBookmark:         "LASTER-MARKAK",
+    typeDocument:         "DOKUMENTUAK",
+    typeVotes:            "BOZKETAK",
+    typeAudio:            "AUDIOAK",
     typeMarket:           "MERKATUA",
-    typeJob:              "LANA",
-    typeProject:          "PROIEKTUA",
-    typeVideo:            "BIDEOA",
+    typeJob:              "LANAK",
+    typeProject:          "PROIEKTUAK",
+    typeVideo:            "BIDEOAK",
     typeVote:             "ZABALKUNDEA",
-    typeEvent:            "GERTAERA",
+    typeEvent:            "EKITALDIAK",
     typeTransfer:         "TRANSFERENTZIA",
-    typeTask:             "ZEREGINAK",
+    typeTask:             "ATASKAK",
     typePixelia:          "PIXELIA",
     typeForum:            "FOROA",
-    typeReport:           "TXOSTENA",
-    typeFeed:             "JARIOA",
+    typeReport:           "TXOSTENAK",
+    typeFeed:             "FEEDA",
     typeContact:          "KONTAKTUA",
     typePub:              "PUB",
     typeTombstone:        "TOMBSTONE",
     typeBanking:          "BANKUA",
+    typeBankWallet:       "BANKUA/ZORROA",
+    typeBankClaim:        "BANKUA/UBI",
+    typeKarmaScore:       "KARMA",
+    typeParliament:       "PARLAMENTUA",
+    typeParliamentCandidature: "Parlamentua · Hautagaitza",
+    typeParliamentTerm:   "Parlamentua · Agintaldia",
+    typeParliamentProposal:"Parlamentua · Proposamena",
+    typeParliamentRevocation:"Parlamentua · Ezespena",
+    typeParliamentLaw:    "Parlamentua · Lege berria",
+    typeCourts:           "EPAITEGIAK",
+    typeCourtsCase:       "Epaitegiak · Kausa",
+    typeCourtsEvidence:   "Epaitegiak · Froga",
+    typeCourtsAnswer:     "Epaitegiak · Erantzuna",
+    typeCourtsVerdict:    "Epaitegiak · Epai",
+    typeCourtsSettlement: "Epaitegiak · Akordioa",
+    typeCourtsSettlementProposal: "Epaitegiak · Akordio proposamena",
+    typeCourtsSettlementAccepted: "Epaitegiak · Akordio onartua",
+    typeCourtsNomination: "Epaitegiak · Izendapena",
+    typeCourtsNominationVote: "Epaitegiak · Izendapen bozketak",
     activitySupport:      "Aliantza berria sortua",
-    activityJoin:         "PUB berri bat batu da",
+    activityJoin:         "PUB berria batu da",
     question:             "Galdera",
-    deadline:             "Epea",
+    deadline:             "Epemuga",
     status:               "Egoera",
     votes:                "Botoak",
-    totalVotes:           "Boto guztira",
+    totalVotes:           "Guztira botoak",
+    voteTotalVotes:       "Guztira botoak",
     name:                 "Izena",
     skills:               "Gaitasunak",
     tags:                 "Etiketak",
@@ -1237,24 +1372,49 @@ module.exports = {
     category:             "Kategoria",
     attendees:            "Parte-hartzaileak",
     activitySpread:       "->",
-    visitLink:            "Esteka ikusi",
+    visitLink:            "Esteka bisitatu",
     viewDocument:         "Dokumentua ikusi",
     location:             "Kokalekua",
     contentWarning:       "Gaia",
     personName:           "Biztanlearen izena",
-    typeBankWallet:       "BANKUA/ZORROA",
-    typeBankClaim:        "BANKUA/UBI",
-    typeKarmaScore:	  "KARMA",
-    bankWalletConnected:  "ECOin Zorroa",
-    bankUbiReceived:      "UBI jasota",
+    bankWalletConnected:  "ECOin zorroa",
+    bankUbiReceived:      "Jasotako UBI",
     bankTx:               "Tx",
     bankEpochShort:       "Epoka",
-    activityProjectFollow: "%OASIS% orain proiektu hau %ACTION% ari da %PROJECT%",
-    activityProjectUnfollow: "%OASIS% orain proiektu hau %ACTION% ari da %PROJECT%",
-    activityProjectPledged: "%OASIS%(e)k %AMOUNT% %ACTION% du proiektuan %PROJECT%",
-    following: "JARRAITZEN",
-    unfollowing: "UTZI DU JARRAITZEA",
-    pledged: "KONPROMISOA",
+    bankAllocId:          "Esleipen IDa",
+    bankingUserEngagementScore: "Erabiltzaile inplikazio puntuazioa",
+    viewDetails:          "Xehetasunak ikusi",
+    link:                 "Esteka",
+    aiSnippetsLearned:    "Ikasitako zatiak",
+    tribeFeedRefeeds:     "Birbidalketak",
+    activityProjectFollow:   "%OASIS% orain %ACTION% proiektu hau %PROJECT%",
+    activityProjectUnfollow: "%OASIS% orain %ACTION% proiektu hau %PROJECT%",
+    activityProjectPledged:  "%OASIS%-(e)k %ACTION% %AMOUNT% egin du %PROJECT% proiektuan",
+    following:            "JARRAITZEN",
+    unfollowing:          "EZ JARRAITZEN",
+    pledged:              "KONPROMISOA",
+    parliamentCandidatureId: "Kandidatura",
+    parliamentGovMethod:     "Metodoa",
+    parliamentVotesReceived: "Jasotako botoak",
+    parliamentMethodANARCHY:   "Anarkia",
+    parliamentMethodVOTE:      "Komunitate-bozketa",
+    parliamentMethodRANKED:    "Hobespen-bozketa",
+    parliamentMethodPLURALITY: "Pluralitatea",
+    parliamentMethodCOUNCIL:   "Kontseilua",
+    parliamentMethodJURY:      "Epaimahaia",
+    parliamentAnarchy:         "ANARKIA",
+    parliamentElectionsStart:  "Hauteskundeen hasiera",
+    parliamentElectionsEnd:    "Hauteskundeen amaiera",
+    parliamentCurrentLeader:   "Irabazleko kandidatura",
+    parliamentProposalTitle:   "Izenburua",
+    parliamentOpenVote:        "Bozketa irekia",
+    parliamentStatus:          "Egoera",
+    parliamentLawQuestion:     "Galdera",
+    parliamentLawMethod:       "Metodoa",
+    parliamentLawProposer:     "Proposatzailea",
+    parliamentLawEnacted:      "Onartutako eguna",
+    parliamentLawVotes:        "Botoak",
+    createdAt:                 "Sortze-data",
     //reports
     reportsTitle: "Txostenak",
     reportsDescription: "Kudeatu eta jarraitu arazo, akats, gehiegikeri eta eduki-abisuei buruzko txostena zure sarean.",
@@ -1603,17 +1763,17 @@ module.exports = {
     //stats
     statsTitle: 'Estatistikak',
     statistics: "Estatistikak",
-    statsDescription: "Aurkitu zure sareari buruzko estatistikak.",
+    statsInhabitant: "Bizilagunaren estatistikak",
+    statsDescription: "Ezagutu zure sareari buruzko estatistikak.",
     ALLButton: "GUZTIAK",
-    MINEButton: "NEUREAK",
-    VISUALButton: "BISUALA",
-    TOMBSTONEButton: "HILARRIAK",
+    MINEButton: "NIREAK",
+    TOMBSTONEButton: "EZABAKETAK",
     statsYou: "Zu",
-    statsUserId: "Oasis ID-a",
-    statsCreatedAt: "Noiz",
+    statsUserId: "Oasis ID",
+    statsCreatedAt: "Sortze data",
     statsYourContent: "Edukia",
     statsYourOpinions: "Iritziak",
-    statsYourTombstone: "Hilarriak",
+    statsYourTombstone: "Ezabaketak",
     statsNetwork: "Sarea",
     statsTotalInhabitants: "Bizilagunak",
     statsDiscoveredTribes: "Tribuak (Publikoak)",
@@ -1621,98 +1781,102 @@ module.exports = {
     statsNetworkContent: "Edukia",
     statsYourMarket: "Merkatua",
     statsYourJob: "Lanak",
+    statsYourProject: "Proiektuak",
     statsYourTransfer: "Transferentziak",
     statsYourForum: "Foroak",   
-    statsYourProject: "Proiektuak",
-    statsDiscoveredProject: "Proiektuak",
-    statsBankingTitle: "Banking",
-    statsEcoWalletLabel: "ECOIN Zorroa",
-    statsEcoWalletNotConfigured: "Konfiguratu gabe!",
-    statsTotalEcoAddresses: "Guztira helbideak",
-    statsProject: "Proiektuak",
-    statsProject: "Proiektuak",
-    statsProjectsTitle: "Proiektuen",
-    statsProjectsTotal: "Proiektu guztira",
-    statsProjectsActive: "Aktiboak",
-    statsProjectsCompleted: "Osatuak",
-    statsProjectsPaused: "Pausatuta",
-    statsProjectsCancelled: "Ezeztatuta",
-    statsProjectsGoalTotal: "Helburu guztira",
-    statsProjectsPledgedTotal: "Konprometitutako guztira",
-    statsProjectsSuccessRate: "Arrakasta tasa",
-    statsProjectsAvgProgress: "Progresu ertaina",
-    statsProjectsMedianProgress: "Progresu medianoa",
-    statsProjectsActiveFundingAvg: "Aktibo finantzatzeko ertaina",
-    statsNetworkOpinions:  "Iritziak",
+    statsNetworkOpinions: "Iritziak",
     statsDiscoveredMarket: "Merkatua",
-    statsDiscoveredJobs:   "Lanak",
+    statsDiscoveredJob: "Lanak",
+    statsDiscoveredProject: "Proiektuak",
+    statsBankingTitle: "Bankua",
+    statsEcoWalletLabel: "ECOIN zorroa",
+    statsEcoWalletNotConfigured:  "Konfiguratu gabe!",
+    statsTotalEcoAddresses: "Helbide kopurua",
     statsDiscoveredTransfer: "Transferentziak",
     statsDiscoveredForum: "Foroak",
-    statsNetworkTombstone: "Hilarriak",
-    statsBookmarks: "Markagailuak",
-    statsEvents: "Ekitaldiak",
-    statsTasks: "Atazak",
-    statsVotes: "Bozkak",
+    statsNetworkTombstone: "Ezabaketak",
+    statsBookmark: "Laster-markak",
+    statsEvent: "Ekitaldiak",
+    statsTask: "Zereginak",
+    statsVotes: "Botoak",
     statsMarket: "Merkatua",
     statsForum: "Foroak",
     statsJob: "Lanak",
-    statsReports: "Txostenak",
-    statsFeeds: "Jarioak",
-    statsTribes: "Tribuak",
-    statsImages: "Irudiak",
-    statsAudios: "Audioak",
-    statsVideos: "Bideoak",
-    statsDocuments: "Dokumentuak",
-    statsTransfers: "Transferentziak",
+    statsProject: "Proiektuak",
+    statsReport: "Txostenak",
+    statsFeed: "Jarioak",
+    statsTribe: "Tribuak",
+    statsImage: "Irudiak",
+    statsAudio: "Audioak",
+    statsVideo: "Bideoak",
+    statsDocument: "Dokumentuak",
+    statsTransfer: "Transferentziak",
     statsAiExchange: "IA",
-    statsPUBs: 'PUBs',
-    statsPosts: "Bidalketak",
-    statsOasisID: "Oasis ID-a",
-    statsSize: "Guztira (taimaina)",
+    statsPUBs: 'PUBack',
+    statsPost: "Mezuak",
+    statsOasisID: "Oasis ID",
+    statsSize: "Guztira (tamaina)",
     statsBlockchainSize: "Blockchain (tamaina)",
-    statsBlobsSize: "Blob-ak (tamaina)",
-    statsActivity7d: "Aktibitatea (azken 7 egunetan)",
-    statsActivity7dTotal: "7 egunerako guztira",
-    statsActivity30dTotal: "30 egunerako guztira",
-    statsKarmaScore: "KARMA Puntuazioa",
+    statsBlobsSize: "Blobak (tamaina)",
+    statsActivity7d: "Jarduera (azken 7 egun)",
+    statsActivity7dTotal: "7 eguneko guztira",
+    statsActivity30dTotal: "30 eguneko guztira",
+    statsKarmaScore: "KARMA puntuazioa",
     statsPublic: "Publikoa",
     statsPrivate: "Pribatua",
     day: "Eguna",
     messages: "Mezuak",
     statsProject: "Proiektuak",
-    statsProjectsTitle: "Proiektuen",
-    statsProjectsTotal: "Proiektu guztira",
-    statsProjectsActive: "Aktiboak",
-    statsProjectsCompleted: "Osatuak",
+    statsProjectsTitle: "Proiektuak",
+    statsProjectsTotal: "Proiektu kopurua",
+    statsProjectsActive: "Aktibo",
+    statsProjectsCompleted: "Amaituak",
     statsProjectsPaused: "Pausatuta",
-    statsProjectsCancelled: "Ezeztatuta",
-    statsProjectsGoalTotal: "Helburu guztira",
+    statsProjectsCancelled: "Bertan behera",
+    statsProjectsGoalTotal: "Helburu osoa",
     statsProjectsPledgedTotal: "Konprometitutako guztira",
     statsProjectsSuccessRate: "Arrakasta tasa",
-    statsProjectsAvgProgress: "Progresu ertaina",
-    statsProjectsMedianProgress: "Progresu medianoa",
-    statsProjectsActiveFundingAvg: "Aktibo finantzatzeko ertaina",
-    statsJobsTitle: "Lan",
-    statsJobsTotal: "Lan guztira",
-    statsJobsOpen: "Irekiak",
+    statsProjectsAvgProgress: "Batez besteko aurrerapena",
+    statsProjectsMedianProgress: "Aurrerapen mediana",
+    statsProjectsActiveFundingAvg: "Finantzaketa aktiboaren batez bestekoa",
+    statsJobsTitle: "Lanak",
+    statsJobsTotal: "Lan kopurua",
+    statsJobsOpen: "Irekita",
     statsJobsClosed: "Itxita",
-    statsJobsOpenVacants: "Hutsik dauden lanak",
-    statsJobsSubscribersTotal: "Suskribatzaile guztira",
+    statsJobsOpenVacants: "Plaza irekiak",
+    statsJobsSubscribersTotal: "Harpidedun kopurua",
     statsJobsAvgSalary: "Soldata ertaina",
-    statsJobsMedianSalary: "Soldata medianoa",
-    statsMarketTitle: "Merkatu",
-    statsMarketTotal: "Artikulu guztira",
-    statsMarketForSale: "Salmentan",
+    statsJobsMedianSalary: "Soldata mediana",
+    statsMarketTitle: "Merkatua",
+    statsMarketTotal: "Gai kopurua",
+    statsMarketForSale: "Salgai",
     statsMarketReserved: "Erreserbatuta",
     statsMarketClosed: "Itxita",
-    statsMarketSold: "Saltzen",
-    statsMarketRevenue: "Sarrera",
-    statsMarketAvgSoldPrice: "Salmenta bateko prezio ertaina",
-    statsUsersTitle: "Biztanleak",
-    user: "Biztanlea",
-    statsTombstoneTitle: "Hilerriak",
-    statsNetworkTombstones: "Sareko hilerriak",
-    statsTombstoneRatio: "Hilerri ratioa (%)",
+    statsMarketSold: "Salduta",
+    statsMarketRevenue: "Diru-sarrerak",
+    statsMarketAvgSoldPrice: "Salmenta prezio ertaina",
+    statsUsersTitle: "Bizilagunak",
+    user: "Bizilaguna",
+    statsTombstoneTitle: "Ezabaketak",
+    statsNetworkTombstones: "Sareko ezabaketak",
+    statsTombstoneRatio: "Ezabaketa ratioa (%)",
+    statsAITraining: "IA prestakuntza",
+    statsAIExchanges: "Trukeak",
+    bankingUserEngagementScore: "Erabiltzaile inplikazioaren puntuazioa",
+    statsParliamentCandidature: "Parlamenturako hautagaitzak",
+    statsParliamentTerm: "Parlamentuko agintaldiak",
+    statsParliamentProposal: "Parlamentuko proposamenak",
+    statsParliamentRevocation: "Parlamentuko ezeztapenak",
+    statsParliamentLaw: "Parlamentuko legeak",
+    statsCourtsCase: "Epaitegietako kasuak",
+    statsCourtsEvidence: "Frogak",
+    statsCourtsAnswer: "Erantzunak",
+    statsCourtsVerdict: "Epaia",
+    statsCourtsSettlement: "Akordioak",
+    statsCourtsSettlementProposal: "Akordio-proposamenak",
+    statsCourtsSettlementAccepted: "Onartutako akordioak",
+    statsCourtsNomination: "Epaileen izendapenak",
+    statsCourtsNominationVote: "Izendapenen botoak",
     //IA
     ai: "IA",
     aiTitle: "IA",
@@ -2013,6 +2177,8 @@ module.exports = {
     modulesVotationsDescription: "Bozketak aurkitu eta kudeatzeko modulua.", 
     modulesParliamentLabel: "Legebiltzarra",
     modulesParliamentDescription: "Gobernuak hautatu eta legeak bozkatzeko modulua.",
+    modulesCourtsLabel: "Epaitegiak",
+    modulesCourtsDescription: "Gatazkak konpontzeko eta epaia emateko modulua.",
     modulesReportsLabel: "Txostenak",
     modulesReportsDescription: "Arazo, akats, abusu eta eduki-abisuetan erlazionatutako txostenak kudeatzeko modulua.",
     modulesOpinionsLabel: "Iritziak",

+ 290 - 77
src/client/assets/translations/oasis_fr.js

@@ -756,6 +756,121 @@ module.exports = {
     parliamentRulesLaws: "Lorsqu’une proposition atteint son seuil, elle devient une Loi et apparaît dans l’onglet Lois avec sa date d’entrée en vigueur.",
     parliamentRulesHistorical: "Dans l’onglet Historique, vous pouvez voir chaque cycle de gouvernement qui a eu lieu ainsi que des données sur sa gestion.",
     parliamentRulesLeaders: "Dans l’onglet Dirigeants, vous pouvez voir un classement des habitants/tribus qui ont gouverné (ou se sont présentés), classés par efficacité.",
+    //courts
+    courtsTitle: "Tribunaux",
+    courtsDescription: "Explorez des formes de résolution des conflits et de gestion collective de la justice.",
+    courtsFilterCases: "AFFAIRES",
+    courtsFilterMyCases: "MIENNES",
+    courtsFilterJudges: "JUGES",
+    courtsFilterHistory: "HISTORIQUE",
+    courtsFilterRules: "RÈGLES",
+    courtsFilterOpenCase: "NOUVELLE AFFAIRE",
+    courtsCaseFormTitle: "Ouvrir une affaire",
+    courtsCaseTitle: "Titre",
+    courtsCaseRespondent: "Accusé / Défendeur",
+    courtsCaseRespondentPh: "ID Oasis (@...) ou nom de Tribu",
+    courtsCaseMediatorsAccuser: "Médiateurs (accusation)",
+    courtsCaseMediatorsPh: "ID Oasis, séparés par des virgules",
+    courtsCaseMethod: "Méthode de résolution",
+    courtsCaseDescription: "Description (1000 caractères max)",
+    courtsCaseEvidenceTitle: "Preuves de l'affaire",
+    courtsCaseEvidenceHelp: "Joignez des images, audios, documents (PDF) ou vidéos qui appuient votre affaire.",
+    courtsCaseSubmit: "Déposer l'affaire",
+    courtsNominateJudge: "Nommer un juge",
+    courtsJudgeId: "Juge",
+    courtsJudgeIdPh: "ID Oasis (@...) ou nom d'Habitant",
+    courtsNominateBtn: "Nommer",
+    courtsAddEvidence: "Ajouter des preuves",
+    courtsEvidenceText: "Texte",
+    courtsEvidenceLink: "Lien",
+    courtsEvidenceLinkPh: "https://…",
+    courtsEvidenceSubmit: "Joindre",
+    courtsAnswerTitle: "Répondre à la plainte",
+    courtsAnswerText: "Résumé de la réponse",
+    courtsAnswerSubmit: "Envoyer la réponse",
+    courtsStanceDENY: "Refuser",
+    courtsStanceADMIT: "Admettre",
+    courtsStancePARTIAL: "Partielle",
+    courtsVerdictTitle: "Rendre un verdict",
+    courtsVerdictResult: "Résultat",
+    courtsVerdictOrders: "Ordres",
+    courtsVerdictOrdersPh: "Actions, délais, mesures réparatrices...",
+    courtsIssueVerdict: "Rendre le verdict",
+    courtsMediationPropose: "Proposer un accord",
+    courtsSettlementText: "Termes",
+    courtsSettlementProposeBtn: "Proposer",
+    courtsNominationsTitle: "Nominations à la magistrature",
+    courtsThJudge: "Juge",
+    courtsThSupports: "Soutiens",
+    courtsThDate: "Date",
+    courtsThVote: "Vote",
+    courtsNoNominations: "Aucune nomination pour l’instant.",
+    courtsAccuser: "Accusation",
+    courtsRespondent: "Défense",
+    courtsThStatus: "Statut",
+    courtsThAnswerBy: "Répondre avant le",
+    courtsThEvidenceBy: "Preuves avant le",
+    courtsThDecisionBy: "Décision avant le",
+    courtsThCase: "Affaire",
+    courtsThCreatedAt: "Date de début",
+    courtsThActions: "Actions",
+    courtsCaseMediatorsRespondentTitle: "Ajouter des médiateurs de la défense",
+    courtsCaseMediatorsRespondent: "Médiateurs (défense)",
+    courtsMediatorsAccuserLabel: "Médiateurs (accusation)",
+    courtsMediatorsRespondentLabel: "Médiateurs (défense)",
+    courtsMediatorsSubmit: "Enregistrer les médiateurs",
+    courtsVotesNeeded: "Voix nécessaires",
+    courtsVotesSlashTotal: "OUI / TOTAL",
+    courtsOpenVote: "Ouvrir le vote",
+    courtsPublicPrefLabel: "Visibilité après la résolution",
+    courtsPublicPrefYes: "J’accepte que cette affaire soit entièrement publique",
+    courtsPublicPrefNo: "Je préfère garder les détails privés",
+    courtsPublicPrefSubmit: "Enregistrer la préférence de visibilité",
+    courtsNoCases: "Aucune affaire.",
+    courtsNoMyCases: "Vous n’avez pas encore de conflits.",
+    courtsNoHistory: "Aucun procès enregistré pour l’instant.",
+    courtsMethodJUDGE: "Juge",
+    courtsMethodDICTATOR: "Dictateur",
+    courtsMethodPOPULAR: "Populaire",
+    courtsMethodMEDIATION: "Médiation",
+    courtsMethodKARMATOCRACY: "Karmatocratie",
+    courtsMethod: "Méthode",
+    courtsRulesTitle: "Fonctionnement des Tribunaux",
+    courtsRulesIntro: "Les Tribunaux sont un processus géré par la communauté pour résoudre les conflits et promouvoir la justice réparatrice. Le dialogue, des preuves claires et des remèdes proportionnels sont privilégiés.",
+    courtsRulesLifecycle: "Processus : 1) Ouvrir une affaire  2) Choisir une méthode  3) Soumettre des preuves  4) Audience et délibération  5) Verdict et remède  6) Exécution et clôture  7) Appel (le cas échéant).",
+    courtsRulesRoles: "Accusation : ouvre l’affaire. Défense : personne ou tribu mise en cause. Méthode : mécanisme choisi par la communauté pour faciliter, évaluer les preuves et rendre un verdict. Témoin : fournit des témoignages ou des preuves. Médiateurs : personnes neutres invitées par l’accusation et/ou la défense, avec accès à tous les détails, qui aident à désamorcer le conflit et à co-créer des accords.",
+    courtsRulesEvidence: "Description jusqu’à 1000 caractères. Joignez des images, audios, vidéos et documents PDF pertinents et légaux. Ne partagez pas de données privées sensibles sans consentement.",
+    courtsRulesDeliberation: "Les audiences peuvent être publiques ou privées. Les juges garantissent le respect, demandent des clarifications et peuvent écarter le matériel non pertinent ou illégal.",
+    courtsRulesVerdict: "La restauration est prioritaire : excuses, accords de médiation, modération de contenu, restrictions temporaires ou autres mesures proportionnées. Le raisonnement doit être consigné.",
+    courtsRulesAppeals: "Appel : autorisé en présence de nouvelles preuves ou d’une erreur procédurale manifeste. Doit être déposé dans les 7 jours sauf indication contraire.",
+    courtsRulesPrivacy: "Respectez la vie privée et la sécurité. Le doxing, la haine ou les menaces sont supprimés. Les juges peuvent modifier ou sceller certaines parties du dossier pour protéger les personnes à risque.",
+    courtsRulesMisconduct: "Le harcèlement, la manipulation ou les preuves fabriquées peuvent entraîner une résolution négative immédiate.",
+    courtsRulesGlossary: "Affaire : enregistrement d’un conflit. Preuves : éléments à l’appui des affirmations. Verdict : décision avec remède. Appel : demande de réexamen du verdict.",
+    courtsFilterActions: "ACTIONS",
+    courtsNoActions: "Aucune action en attente pour votre rôle.",
+    courtsCaseTitlePlaceholder: "Brève description du conflit",
+    courtsCaseSeverity: "Gravité",
+    courtsCaseSeverityNone: "Sans étiquette de gravité",
+    courtsCaseSeverityLOW: "Faible",
+    courtsCaseSeverityMEDIUM: "Moyenne",
+    courtsCaseSeverityHIGH: "Élevée",
+    courtsCaseSeverityCRITICAL: "Critique",
+    courtsCaseSubject: "Sujet",
+    courtsCaseSubjectNone: "Sans étiquette de sujet",
+    courtsCaseSubjectBEHAVIOUR: "Comportement",
+    courtsCaseSubjectCONTENT: "Contenu",
+    courtsCaseSubjectGOVERNANCE: "Gouvernance / règles",
+    courtsCaseSubjectFINANCIAL: "Financier / ressources",
+    courtsCaseSubjectOTHER: "Autre",
+    courtsHiddenRespondent: "Caché (visible uniquement pour les rôles impliqués).",
+    courtsThRole: "Rôle",
+    courtsRoleAccuser: "Accusation",
+    courtsRoleDefence: "Défense",
+    courtsRoleMediator: "Médiateur",
+    courtsRoleJudge: "Juge",
+    courtsRoleDictator: "Dictateur",
+    courtsAssignJudgeTitle: "Choisir un juge",
+    courtsAssignJudgeBtn: "Choisir un juge",
     //trending
     trendingTitle: "Tendances",
     exploreTrending: "Explorez le contenu le plus populaire dans votre réseau.",
@@ -1177,9 +1292,9 @@ module.exports = {
     yourActivity:         "Votre activité",
     globalActivity:       "Activité globale",
     activityList:         "Activité",
-    activityDesc:         "Consultez l’activité récente de votre réseau.",
-    allButton:            "TOUS",
-    mineButton:           "MIEN",
+    activityDesc:         "Découvrez l’activité récente de votre réseau.",
+    allButton:            "TOUT",
+    mineButton:           "MIENNES",
     noActions:            "Aucune activité disponible.",
     performed:            "→",
     from:                 "De",
@@ -1197,40 +1312,60 @@ module.exports = {
     typeRecent:           "RÉCENT",
     errorActivity:        "Erreur lors de la récupération de l’activité",
     typePost:             "PUBLICATION",
-    typeTribe:            "TRIBU",
-    typeAbout:            "HABITANT",
+    typeTribe:            "TRIBUS",
+    typeAbout:            "HABITANTS",
     typeCurriculum:       "CV",
-    typeImage:            "IMAGE",
-    typeBookmark:         "MARQUE-PAGE",
-    typeDocument:         "DOCUMENT",
-    typeVotes:            "GOUVERNANCE",
-    typeAudio:            "AUDIO",
+    typeImage:            "IMAGES",
+    typeBookmark:         "SIGNETS",
+    typeDocument:         "DOCUMENTS",
+    typeVotes:            "VOTES",
+    typeAudio:            "AUDIOS",
     typeMarket:           "MARCHÉ",
-    typeJob:              "EMPLOI",
-    typeProject:          "PROJET",
-    typeVideo:            "VIDÉO",
-    typeVote:             "DIFFUSION",
-    typeEvent:            "ÉVÉNEMENT",
+    typeJob:              "EMPLOIS",
+    typeProject:          "PROJETS",
+    typeVideo:            "VIDÉOS",
+    typeVote:             "PARTAGE",
+    typeEvent:            "ÉVÉNEMENTS",
     typeTransfer:         "TRANSFERT",
     typeTask:             "TÂCHES",
     typePixelia:          "PIXELIA",
     typeForum:            "FORUM",
-    typeReport:           "RAPPORT",
-    typeFeed:             "FEED",
+    typeReport:           "RAPPORTS",
+    typeFeed:             "FLUX",
     typeContact:          "CONTACT",
     typePub:              "PUB",
     typeTombstone:        "TOMBSTONE",
     typeBanking:          "BANQUE",
+    typeBankWallet:       "BANQUE/PORTEFEUILLE",
+    typeBankClaim:        "BANQUE/UBI",
+    typeKarmaScore:       "KARMA",
+    typeParliament:       "PARLEMENT",
+    typeParliamentCandidature: "Parlement · Candidature",
+    typeParliamentTerm:   "Parlement · Mandat",
+    typeParliamentProposal:"Parlement · Proposition",
+    typeParliamentRevocation:"Parlement · Révocation",
+    typeParliamentLaw:    "Parlement · Nouvelle loi",
+    typeCourts:           "TRIBUNAUX",
+    typeCourtsCase:       "Tribunaux · Affaire",
+    typeCourtsEvidence:   "Tribunaux · Preuve",
+    typeCourtsAnswer:     "Tribunaux · Réponse",
+    typeCourtsVerdict:    "Tribunaux · Verdict",
+    typeCourtsSettlement: "Tribunaux · Règlement",
+    typeCourtsSettlementProposal: "Tribunaux · Proposition de règlement",
+    typeCourtsSettlementAccepted: "Tribunaux · Règlement accepté",
+    typeCourtsNomination: "Tribunaux · Nomination",
+    typeCourtsNominationVote: "Tribunaux · Vote de nomination",
     activitySupport:      "Nouvelle alliance forgée",
     activityJoin:         "Nouveau PUB rejoint",
     question:             "Question",
-    deadline:             "Date limite",
-    status:               "État",
+    deadline:             "Échéance",
+    status:               "Statut",
     votes:                "Votes",
-    totalVotes:           "Total des votes",
+    totalVotes:           "Votes totaux",
+    voteTotalVotes:       "Votes totaux",
     name:                 "Nom",
     skills:               "Compétences",
-    tags:                 "Étiquettes",
+    tags:                 "Tags",
     title:                "Titre",
     date:                 "Date",
     category:             "Catégorie",
@@ -1238,22 +1373,94 @@ module.exports = {
     activitySpread:       "->",
     visitLink:            "Visiter le lien",
     viewDocument:         "Voir le document",
-    location:             "Localisation",
+    location:             "Lieu",
     contentWarning:       "Sujet",
     personName:           "Nom de l’habitant",
-    typeBankWallet:       "BANQUE/PORTFEUILLE",
-    typeBankClaim:        "BANQUE/UBI",
-    typeKarmaScore:	  "KARMA",
     bankWalletConnected:  "Portefeuille ECOin",
-    bankUbiReceived:      "UBI reçue",
+    bankUbiReceived:      "UBI reçu",
     bankTx:               "Tx",
     bankEpochShort:       "Époque",
-    activityProjectFollow: "%OASIS% est maintenant en train de %ACTION% ce projet %PROJECT%",
-    activityProjectUnfollow: "%OASIS% est maintenant en train de %ACTION% ce projet %PROJECT%",
-    activityProjectPledged: "%OASIS% a %ACTION% %AMOUNT% au projet %PROJECT%",
-    following: "SUIVRE",
-    unfollowing: "CESSER DE SUIVRE",
-    pledged: "CONTRIBUÉ",
+    bankAllocId:          "Identifiant d’allocation",
+    bankingUserEngagementScore: "Score d’engagement utilisateur",
+    viewDetails:          "Voir les détails",
+    link:                 "Lien",
+    aiSnippetsLearned:    "Extraits appris",
+    tribeFeedRefeeds:     "Repartages",
+    activityProjectFollow:   "%OASIS% est maintenant %ACTION% ce projet %PROJECT%",
+    activityProjectUnfollow: "%OASIS% est maintenant %ACTION% ce projet %PROJECT%",
+    activityProjectPledged:  "%OASIS% a %ACTION% %AMOUNT% au projet %PROJECT%",
+    following:            "SUIVRE",
+    unfollowing:          "NE PLUS SUIVRE",
+    pledged:              "ENGAGÉ",
+    parliamentCandidatureId: "Candidature",
+    parliamentGovMethod:     "Méthode",
+    parliamentVotesReceived: "Votes reçus",
+    parliamentMethodANARCHY:   "Anarchie",
+    parliamentMethodVOTE:      "Vote communautaire",
+    parliamentMethodRANKED:    "Vote préférentiel",
+    parliamentMethodPLURALITY: "Pluralité",
+    parliamentMethodCOUNCIL:   "Conseil",
+    parliamentMethodJURY:      "Jury",
+    parliamentAnarchy:         "ANARCHIE",
+    parliamentElectionsStart:  "Début des élections",
+    parliamentElectionsEnd:    "Fin des élections",
+    parliamentCurrentLeader:   "Candidature gagnante",
+    parliamentProposalTitle:   "Titre",
+    parliamentOpenVote:        "Vote ouvert",
+    parliamentStatus:          "Statut",
+    parliamentLawQuestion:     "Question",
+    parliamentLawMethod:       "Méthode",
+    parliamentLawProposer:     "Proposant",
+    parliamentLawEnacted:      "Promulgué le",
+    parliamentLawVotes:        "Votes",
+    createdAt:                 "Créé le",
+    courtsCaseTitle:           "Affaire",
+    courtsMethod:              "Méthode",
+    courtsMethodJUDGE:         "Juge",
+    courtsMethodJUDGES:        "Collège de juges",
+    courtsMethodSINGLE_JUDGE:  "Juge unique",
+    courtsMethodJURY:          "Jury",
+    courtsMethodCOUNCIL:       "Conseil",
+    courtsMethodCOMMUNITY:     "Communauté",
+    courtsMethodMEDIATION:     "Médiation",
+    courtsMethodARBITRATION:   "Arbitrage",
+    courtsMethodVOTE:          "Vote communautaire",
+    courtsAccuser:             "Accusation",
+    courtsRespondent:          "Défense",
+    courtsThStatus:            "Statut",
+    courtsThAnswerBy:          "Réponse avant",
+    courtsThEvidenceBy:        "Preuves avant",
+    courtsThDecisionBy:        "Décision avant",
+    courtsVotesNeeded:         "Votes nécessaires",
+    courtsVotesSlashTotal:     "OUI/TOTAL",
+    courtsOpenVote:            "Vote ouvert",
+    courtsAnswerTitle:         "Réponse",
+    courtsStanceADMIT:         "Admettre",
+    courtsStanceDENY:          "Refuser",
+    courtsStancePARTIAL:       "Partiel",
+    courtsStanceCOUNTERCLAIM:  "Demande reconventionnelle",
+    courtsStanceNEUTRAL:       "Neutre",
+    courtsVerdictResult:       "Résultat",
+    courtsVerdictOrders:       "Ordonnances",
+    courtsSettlementText:      "Accord",
+    courtsSettlementAccepted:  "Accepté",
+    courtsSettlementPending:   "En attente",
+    courtsJudge:               "Juge",
+    courtsThSupports:          "Soutiens",
+    courtsFilterOpenCase: 'Ouvrir un dossier',
+    courtsEvidenceFileLabel: 'Fichier de preuve (image, audio, vidéo ou PDF)',
+    courtsCaseMediators: 'Médiateurs',
+    courtsCaseMediatorsPh: 'IDs Oasis des médiateurs, séparés par des virgules',
+    courtsMediatorsLabel: 'Médiateurs',
+    courtsThCase: 'Dossier',
+    courtsThCreatedAt: 'Date de début',
+    courtsThActions: 'Actions',
+    courtsPublicPrefLabel: 'Visibilité après la résolution',
+    courtsPublicPrefYes: "J'accepte que ce dossier soit entièrement public",
+    courtsPublicPrefNo: 'Je préfère garder les détails privés',
+    courtsPublicPrefSubmit: 'Enregistrer la préférence de visibilité',
+    courtsMethodMEDIATION: 'Médiation',
+    courtsNoCases: 'Aucun dossier.',
     //reports
     reportsTitle: "Rapports",
     reportsDescription: "Gérez et suivez les rapports liés aux problèmes, aux erreurs, aux abus et aux avertissements de contenu dans votre réseau.",
@@ -1602,61 +1809,48 @@ module.exports = {
     //stats
     statsTitle: 'Statistiques',
     statistics: "Statistiques",
-    statsInhabitant: "Statistiques des habitants",
-    statsDescription: "Découvrez les statistiques de votre réseau.",
-    ALLButton: "TOUS",
-    MINEButton: "MES",
-    TOMBSTONEButton: "PIERRES TOMB.",
+    statsInhabitant: "Statistiques de l'habitant",
+    statsDescription: "Découvrez des statistiques sur votre réseau.",
+    ALLButton: "TOUT",
+    MINEButton: "MIENS",
+    TOMBSTONEButton: "SUPPRESSIONS",
     statsYou: "Vous",
-    statsUserId: "ID d’Oasis",
+    statsUserId: "ID Oasis",
     statsCreatedAt: "Créé le",
     statsYourContent: "Contenu",
     statsYourOpinions: "Avis",
-    statsYourTombstone: "Pierres tombales",
-    statsYourProject: "Projets",
-    statsDiscoveredProject: "Projets",
-    statsBankingTitle: "Banque",
-    statsEcoWalletLabel: "Portefeuille ECOIN",
-    statsEcoWalletNotConfigured: "Non configuré !",
-    statsTotalEcoAddresses: "Total des adresses",
-    statsProject: "Projets",
+    statsYourTombstone: "Suppressions",
     statsNetwork: "Réseau",
     statsTotalInhabitants: "Habitants",
-    statsDiscoveredTribes: "Tribus (Publiques)",
-    statsPrivateDiscoveredTribes: "Tribus (Privées)",
+    statsDiscoveredTribes: "Tribus (publiques)",
+    statsPrivateDiscoveredTribes: "Tribus (privées)",
     statsNetworkContent: "Contenu",
     statsYourMarket: "Marché",
     statsYourJob: "Emplois",
+    statsYourProject: "Projets",
     statsYourTransfer: "Transferts",
     statsYourForum: "Forums",   
-    statsProject: "Projets",
-    statsProjectsTitle: "Projets",
-    statsProjectsTotal: "Total des projets",
-    statsProjectsActive: "Actifs",
-    statsProjectsCompleted: "Terminés",
-    statsProjectsPaused: "En pause",
-    statsProjectsCancelled: "Annulés",
-    statsProjectsGoalTotal: "Objectif total",
-    statsProjectsPledgedTotal: "Total engagé",
-    statsProjectsSuccessRate: "Taux de réussite",
-    statsProjectsAvgProgress: "Progression moyenne",
-    statsProjectsMedianProgress: "Progression médiane",
-    statsProjectsActiveFundingAvg: "Financement actif moyen",
     statsNetworkOpinions: "Avis",
     statsDiscoveredMarket: "Marché",
     statsDiscoveredJob: "Emplois",
+    statsDiscoveredProject: "Projets",
+    statsBankingTitle: "Banque",
+    statsEcoWalletLabel: "Portefeuille ECOIN",
+    statsEcoWalletNotConfigured:  "Non configuré !",
+    statsTotalEcoAddresses: "Adresses totales",
     statsDiscoveredTransfer: "Transferts",
     statsDiscoveredForum: "Forums",
-    statsNetworkTombstone: "Pierres tombales",
-    statsBookmark: "Marque-pages",
+    statsNetworkTombstone: "Suppressions",
+    statsBookmark: "Signets",
     statsEvent: "Événements",
     statsTask: "Tâches",
     statsVotes: "Votes",
     statsMarket: "Marché",
     statsForum: "Forums",
     statsJob: "Emplois",
+    statsProject: "Projets",
     statsReport: "Rapports",
-    statsFeed: "Feeds",
+    statsFeed: "Flux",
     statsTribe: "Tribus",
     statsImage: "Images",
     statsAudio: "Audios",
@@ -1666,52 +1860,69 @@ module.exports = {
     statsAiExchange: "IA",
     statsPUBs: 'PUBs',
     statsPost: "Publications",
-    statsOasisID: "ID d’Oasis",
+    statsOasisID: "ID Oasis",
     statsSize: "Total (taille)",
     statsBlockchainSize: "Blockchain (taille)",
-    statsBlobsSize: "Blobs (taille)",   
+    statsBlobsSize: "Blobs (taille)",
     statsActivity7d: "Activité (7 derniers jours)",
     statsActivity7dTotal: "Total 7 jours",
     statsActivity30dTotal: "Total 30 jours",
-    statsKarmaScore: "Score de KARMA",
+    statsKarmaScore: "Score KARMA",
     statsPublic: "Public",
     statsPrivate: "Privé",
     day: "Jour",
     messages: "Messages",
     statsProject: "Projets",
     statsProjectsTitle: "Projets",
-    statsProjectsTotal: "Total des projets",
+    statsProjectsTotal: "Projets au total",
     statsProjectsActive: "Actifs",
     statsProjectsCompleted: "Terminés",
     statsProjectsPaused: "En pause",
     statsProjectsCancelled: "Annulés",
     statsProjectsGoalTotal: "Objectif total",
-    statsProjectsPledgedTotal: "Total engagé",
+    statsProjectsPledgedTotal: "Montant promis total",
     statsProjectsSuccessRate: "Taux de réussite",
     statsProjectsAvgProgress: "Progression moyenne",
     statsProjectsMedianProgress: "Progression médiane",
     statsProjectsActiveFundingAvg: "Financement actif moyen",
     statsJobsTitle: "Emplois",
-    statsJobsTotal: "Total des emplois",
+    statsJobsTotal: "Emplois au total",
     statsJobsOpen: "Ouverts",
     statsJobsClosed: "Fermés",
     statsJobsOpenVacants: "Postes ouverts",
-    statsJobsSubscribersTotal: "Total des abonnés",
+    statsJobsSubscribersTotal: "Abonnés au total",
     statsJobsAvgSalary: "Salaire moyen",
     statsJobsMedianSalary: "Salaire médian",
     statsMarketTitle: "Marché",
-    statsMarketTotal: "Total des articles",
-    statsMarketForSale: "En vente",
+    statsMarketTotal: "Articles au total",
+    statsMarketForSale: "À vendre",
     statsMarketReserved: "Réservés",
     statsMarketClosed: "Fermés",
     statsMarketSold: "Vendus",
-    statsMarketRevenue: "Revenus",
+    statsMarketRevenue: "Chiffre d'affaires",
     statsMarketAvgSoldPrice: "Prix de vente moyen",
     statsUsersTitle: "Habitants",
     user: "Habitant",
-    statsTombstoneTitle: "Tombes",
-    statsNetworkTombstones: "Tombes du réseau",
-    statsTombstoneRatio: "Taux de tombes (%)",
+    statsTombstoneTitle: "Suppressions",
+    statsNetworkTombstones: "Suppressions du réseau",
+    statsTombstoneRatio: "Taux de suppressions (%)",
+    statsAITraining: "Entraînement IA",
+    statsAIExchanges: "Échanges",
+    bankingUserEngagementScore: "Score d'engagement utilisateur",
+    statsParliamentCandidature: "Candidatures au parlement",
+    statsParliamentTerm: "Mandats parlementaires",
+    statsParliamentProposal: "Propositions parlementaires",
+    statsParliamentRevocation: "Révocations parlementaires",
+    statsParliamentLaw: "Lois parlementaires",
+    statsCourtsCase: "Affaires judiciaires",
+    statsCourtsEvidence: "Preuves judiciaires",
+    statsCourtsAnswer: "Réponses judiciaires",
+    statsCourtsVerdict: "Verdicts judiciaires",
+    statsCourtsSettlement: "Accords judiciaires",
+    statsCourtsSettlementProposal: "Propositions d'accord",
+    statsCourtsSettlementAccepted: "Accords acceptés",
+    statsCourtsNomination: "Nominations de juges",
+    statsCourtsNominationVote: "Votes de nomination",
     //AI
     ai: "IA",
     aiTitle: "IA",
@@ -2014,6 +2225,8 @@ module.exports = {
     modulesVotationsDescription: "Module pour découvrir et gérer les votations.",  
     modulesParliamentLabel: "Parlement",
     modulesParliamentDescription: "Module pour élire des gouvernements et voter des lois.",
+    modulesCourtsLabel: "Tribunaux",
+    modulesCourtsDescription: "Module pour résoudre des conflits et émettre des verdicts.",
     modulesReportsLabel: "Rapports",
     modulesReportsDescription: "Module pour gérer et suivre les rapports liés aux problèmes, erreurs, abus et avertissements de contenu.",
     modulesOpinionsLabel: "Opinions",

+ 2 - 1
src/configs/config-manager.js

@@ -42,7 +42,8 @@ if (!fs.existsSync(configFilePath)) {
       "jobsMod": "on",
       "projectsMod": "on",
       "bankingMod": "on",
-      "parliamentMod": "on"
+      "parliamentMod": "on",
+      "courtsMod": "on"
     },
     "wallet": {
       "url": "http://localhost:7474",

+ 2 - 1
src/configs/oasis-config.json

@@ -36,7 +36,8 @@
     "jobsMod": "on",
     "projectsMod": "on",
     "bankingMod": "on",
-    "parliamentMod": "on"
+    "parliamentMod": "on",
+    "courtsMod": "on"
   },
   "wallet": {
     "url": "http://localhost:7474",

+ 17 - 12
src/models/activity_model.js

@@ -12,6 +12,15 @@ function inferType(c = {}) {
   if (c.type === 'wallet' && c.coin === 'ECO' && typeof c.address === 'string') return 'bankWallet';
   if (c.type === 'bankClaim') return 'bankClaim';
   if (c.type === 'karmaScore') return 'karmaScore';
+  if (c.type === 'courts_case') return 'courtsCase';
+  if (c.type === 'courts_evidence') return 'courtsEvidence';
+  if (c.type === 'courts_answer') return 'courtsAnswer';
+  if (c.type === 'courts_verdict') return 'courtsVerdict';
+  if (c.type === 'courts_settlement') return 'courtsSettlement';
+  if (c.type === 'courts_nomination') return 'courtsNomination';
+  if (c.type === 'courts_nom_vote') return 'courtsNominationVote';
+  if (c.type === 'courts_public_pref') return 'courtsPublicPref';
+  if (c.type === 'courts_mediators') return 'courtsMediators';
   return c.type || '';
 }
 
@@ -91,14 +100,11 @@ module.exports = ({ cooler }) => {
 
           if (ev.content.followersOp === 'follow') kind = 'follow';
           else if (ev.content.followersOp === 'unfollow') kind = 'unfollow';
-
           if (ev.content.backerPledge && typeof ev.content.backerPledge.amount !== 'undefined') {
             const amt = Math.max(0, parseFloat(ev.content.backerPledge.amount || 0) || 0);
             if (amt > 0) { kind = kind || 'pledge'; amount = amt }
           }
-
           if (!kind) continue;
-
           const augmented = {
             ...ev,
             type: 'project',
@@ -114,7 +120,6 @@ module.exports = ({ cooler }) => {
           idToTipId.set(ev.id, ev.id);
         }
       }
-
       const latest = [];
       for (const a of idToAction.values()) {
         if (tombstoned.has(a.id)) continue;
@@ -137,21 +142,17 @@ module.exports = ({ cooler }) => {
         }
         latest.push({ ...a, tipId: idToTipId.get(a.id) || a.id });
       }
-
       let deduped = latest.filter(a => !a.tipId || a.tipId === a.id);
-
       const mediaTypes = new Set(['image','video','audio','document','bookmark']);
       const perAuthorUnique = new Set(['karmaScore']);
       const byKey = new Map();
       const norm = s => String(s || '').trim().toLowerCase();
-
       for (const a of deduped) {
         const c = a.content || {};
         const effTs =
           (c.updatedAt && Date.parse(c.updatedAt)) ||
           (c.createdAt && Date.parse(c.createdAt)) ||
           (a.ts || 0);
-
         if (mediaTypes.has(a.type)) {
           const u = c.url || c.title || `${a.type}:${a.id}`;
           const key = `${a.type}:${u}`;
@@ -177,7 +178,6 @@ module.exports = ({ cooler }) => {
         }
       }
       deduped = Array.from(byKey.values()).map(x => { delete x.__effTs; return x });
-
       let out;
       if (filter === 'mine') out = deduped.filter(a => a.author === userId);
       else if (filter === 'recent') { const cutoff = Date.now() - 24 * 60 * 60 * 1000; out = deduped.filter(a => (a.ts || 0) >= cutoff) }
@@ -185,9 +185,14 @@ module.exports = ({ cooler }) => {
       else if (filter === 'banking') out = deduped.filter(a => a.type === 'bankWallet' || a.type === 'bankClaim');
       else if (filter === 'karma') out = deduped.filter(a => a.type === 'karmaScore');
       else if (filter === 'parliament')
-      out = deduped.filter(a =>
-        ['parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw'].includes(a.type)
-      );
+        out = deduped.filter(a =>
+          ['parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw'].includes(a.type)
+        );
+      else if (filter === 'courts')
+        out = deduped.filter(a => {
+          const t = String(a.type || '').toLowerCase();
+          return t === 'courtscase' || t === 'courtsnomination' || t === 'courtsnominationvote';
+        });
       else out = deduped.filter(a => a.type === filter);
 
       out.sort((a, b) => (b.ts || 0) - (a.ts || 0));

+ 19 - 0
src/models/banking_model.js

@@ -475,11 +475,13 @@ async function fetchUserActions(userId) {
   });
 }
 
+// karma scoring table
 function scoreFromActions(actions) {
   let score = 0;
   for (const action of actions) {
     const t = normalizeType(action);
     const c = action.content || {};
+    const rawType = String(c.type || "").toLowerCase();
     if (t === "post") score += 10;
     else if (t === "comment") score += 5;
     else if (t === "like") score += 2;
@@ -507,6 +509,23 @@ function scoreFromActions(actions) {
     else if (t === "about") score += 1;
     else if (t === "contact") score += 1;
     else if (t === "pub") score += 1;
+    else if (t === "parliamentcandidature" || rawType === "parliamentcandidature") score += 12;
+    else if (t === "parliamentterm" || rawType === "parliamentterm") score += 25;
+    else if (t === "parliamentproposal" || rawType === "parliamentproposal") score += 8;
+    else if (t === "parliamentlaw" || rawType === "parliamentlaw") score += 16;
+    else if (t === "parliamentrevocation" || rawType === "parliamentrevocation") score += 10;
+    else if (t === "courts_case" || t === "courtscase" || rawType === "courts_case") score += 4;
+    else if (t === "courts_evidence" || t === "courtsevidence" || rawType === "courts_evidence") score += 3;
+    else if (t === "courts_answer" || t === "courtsanswer" || rawType === "courts_answer") score += 4;
+    else if (t === "courts_verdict" || t === "courtsverdict" || rawType === "courts_verdict") score += 10;
+    else if (t === "courts_settlement" || t === "courtssettlement" || rawType === "courts_settlement") score += 8;
+    else if (t === "courts_nomination" || t === "courtsnomination" || rawType === "courts_nomination") score += 6;
+    else if (t === "courts_nom_vote" || t === "courtsnomvote" || rawType === "courts_nom_vote") score += 3;
+    else if (t === "courts_public_pref" || t === "courtspublicpref" || rawType === "courts_public_pref") score += 1;
+    else if (t === "courts_mediators" || t === "courtsmediators" || rawType === "courts_mediators") score += 6;
+    else if (t === "courts_open_support" || t === "courtsopensupport" || rawType === "courts_open_support") score += 2;
+    else if (t === "courts_verdict_vote" || t === "courtsverdictvote" || rawType === "courts_verdict_vote") score += 3;
+    else if (t === "courts_judge_assign" || t === "courtsjudgeassign" || rawType === "courts_judge_assign") score += 5;
   }
   return Math.max(0, Math.round(score));
 }

+ 4 - 0
src/models/blockchain_model.js

@@ -115,6 +115,10 @@ module.exports = ({ cooler }) => {
         const pset = new Set(['parliamentTerm','parliamentProposal','parliamentLaw','parliamentCandidature','parliamentRevocation']);
         filtered = blockData.filter(b => b && pset.has(b.type));
       }
+      if (filter === 'COURTS' || filter === 'courts') {
+        const cset = new Set(['courtsCase','courtsEvidence','courtsAnswer','courtsVerdict','courtsSettlement','courtsSettlementProposal','courtsSettlementAccepted','courtsNomination','courtsNominationVote']);
+        filtered = blockData.filter(b => b && cset.has(b.type));
+      }
 
       return filtered.filter(Boolean);
     },

+ 625 - 0
src/models/courts_model.js

@@ -0,0 +1,625 @@
+const pull = require('../server/node_modules/pull-stream');
+const moment = require('../server/node_modules/moment');
+const { getConfig } = require('../configs/config-manager.js');
+
+const logLimit = getConfig().ssbLogStream?.limit || 1000;
+const CASE_ANSWER_DAYS = 7;
+const CASE_EVIDENCE_DAYS = 14;
+const CASE_DECISION_DAYS = 21;
+const POPULAR_DAYS = 14;
+const FEED_ID_RE = /^@.+\.ed25519$/;
+
+module.exports = ({ cooler, services = {} }) => {
+  let ssb;
+  let userId;
+
+  const openSsb = async () => {
+    if (!ssb) {
+      ssb = await cooler.open();
+      userId = ssb.id;
+    }
+    return ssb;
+  };
+
+  const nowISO = () => new Date().toISOString();
+  const ensureArray = (x) => (Array.isArray(x) ? x : x ? [x] : []);
+
+  async function readLog() {
+    const ssbClient = await openSsb();
+    return new Promise((resolve, reject) => {
+      pull(
+        ssbClient.createLogStream({ limit: logLimit }),
+        pull.collect((err, arr) => (err ? reject(err) : resolve(arr)))
+      );
+    });
+  }
+
+  async function listByType(type) {
+    const msgs = await readLog();
+    const tomb = new Set();
+    const rep = new Map();
+    const map = new Map();
+    for (const m of msgs) {
+      const k = m.key || m.id;
+      const c = m.value?.content || m.content;
+      if (!c) continue;
+      if (c.type === 'tombstone' && c.target) tomb.add(c.target);
+      if (c.type === type) {
+        if (c.replaces) rep.set(c.replaces, k);
+        map.set(k, { id: k, ...c });
+      }
+    }
+    for (const oldId of rep.keys()) map.delete(oldId);
+    for (const tId of tomb) map.delete(tId);
+    return [...map.values()];
+  }
+
+  async function getCurrentUserId() {
+    await openSsb();
+    return userId;
+  }
+
+  async function resolveRespondent(candidateInput) {
+    const s = String(candidateInput || '').trim();
+    if (!s) return null;
+    if (FEED_ID_RE.test(s)) {
+      return { type: 'inhabitant', id: s };
+    }
+    if (services.tribes && services.tribes.getTribeById) {
+      try {
+        const t = await services.tribes.getTribeById(s);
+        if (t && t.id) return { type: 'tribe', id: t.id };
+      } catch {}
+    }
+    return null;
+  }
+
+  function computeDeadlines(openedAt) {
+    const answerBy = moment(openedAt).add(CASE_ANSWER_DAYS, 'days').toISOString();
+    const evidenceBy = moment(openedAt).add(CASE_EVIDENCE_DAYS, 'days').toISOString();
+    const decisionBy = moment(openedAt).add(CASE_DECISION_DAYS, 'days').toISOString();
+    return { answerBy, evidenceBy, decisionBy };
+  }
+
+  async function openCase({ titleBase, respondentInput, method }) {
+    const ssbClient = await openSsb();
+    const rawTitle = String(titleBase || '').trim();
+    if (!rawTitle) throw new Error('Title is required.');
+    const resp = await resolveRespondent(respondentInput);
+    if (!resp) throw new Error('Accused / Respondent not found.');
+    const m = String(method || '').trim().toUpperCase();
+    const ALLOWED = new Set(['JUDGE', 'DICTATOR', 'POPULAR', 'MEDIATION', 'KARMATOCRACY']);
+    if (!ALLOWED.has(m)) throw new Error('Invalid resolution method.');
+    if (m === 'DICTATOR' && services.parliament && services.parliament.getGovernmentCard) {
+      try {
+        const gov = await services.parliament.getGovernmentCard();
+        const gm = String(gov && gov.method ? gov.method : '').toUpperCase();
+        if (gm !== 'DICTATORSHIP') throw new Error('DICTATOR method requires DICTATORSHIP government.');
+      } catch (e) {
+        throw new Error('Unable to verify government method for DICTATOR.');
+      }
+    }
+    const openedAt = nowISO();
+    const prefix = moment(openedAt).format('MM/YYYY') + '_';
+    const title = prefix + rawTitle;
+    const { answerBy, evidenceBy, decisionBy } = computeDeadlines(openedAt);
+    const content = {
+      type: 'courtsCase',
+      title,
+      accuser: userId,
+      respondentType: resp.type,
+      respondentId: resp.id,
+      method: m,
+      status: 'OPEN',
+      openedAt,
+      answerBy,
+      evidenceBy,
+      decisionBy,
+      mediatorsAccuser: [],
+      mediatorsRespondent: [],
+      createdAt: openedAt
+    };
+    return await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+  }
+
+  async function listCases(filter = 'open') {
+    const all = await listByType('courtsCase');
+    const sorted = all.sort((a, b) => {
+      const ta = new Date(a.openedAt || a.createdAt || 0).getTime();
+      const tb = new Date(b.openedAt || b.createdAt || 0).getTime();
+      return tb - ta;
+    });
+    if (filter === 'open') {
+      return sorted.filter((c) => {
+        const s = String(c.status || '').toUpperCase();
+        return s !== 'DECIDED' && s !== 'CLOSED' && s !== 'SOLVED' && s !== 'UNSOLVED' && s !== 'DISCARDED';
+      });
+    }
+    if (filter === 'history') {
+      return sorted.filter((c) => {
+        const s = String(c.status || '').toUpperCase();
+        return s === 'DECIDED' || s === 'CLOSED' || s === 'SOLVED' || s === 'UNSOLVED' || s === 'DISCARDED';
+      });
+    }
+    return sorted;
+  }
+
+  async function listCasesForUser(uid) {
+    const all = await listByType('courtsCase');
+    const id = String(uid || userId || '');
+    const rows = [];
+    for (const c of all) {
+      const isAccuser = String(c.accuser || '') === id;
+      const isRespondent = String(c.respondentId || '') === id;
+      const ma = ensureArray(c.mediatorsAccuser || []);
+      const mr = ensureArray(c.mediatorsRespondent || []);
+      const isMediator = ma.includes(id) || mr.includes(id);
+      const isJudge = String(c.judgeId || '') === id;
+      const isDictator = false;
+      const mine = isAccuser || isRespondent || isMediator || isJudge || isDictator;
+      if (!mine) continue;
+      let myPublicPreference = null;
+      if (isAccuser && typeof c.publicPrefAccuser === 'boolean') {
+        myPublicPreference = c.publicPrefAccuser;
+      } else if (isRespondent && typeof c.publicPrefRespondent === 'boolean') {
+        myPublicPreference = c.publicPrefRespondent;
+      }
+      rows.push({
+        ...c,
+        respondent: c.respondentId || c.respondent,
+        isAccuser,
+        isRespondent,
+        isMediator,
+        isJudge,
+        isDictator,
+        mine,
+        myPublicPreference
+      });
+    }
+    rows.sort((a, b) => {
+      const ta = new Date(a.openedAt || a.createdAt || 0).getTime();
+      const tb = new Date(b.openedAt || b.createdAt || 0).getTime();
+      return tb - ta;
+    });
+    return rows;
+  }
+
+  async function getCaseById(caseId) {
+    const id = String(caseId || '').trim();
+    if (!id) return null;
+    const all = await listByType('courtsCase');
+    return all.find((c) => c.id === id) || null;
+  }
+
+  async function upsertCase(obj) {
+    const ssbClient = await openSsb();
+    const { id, ...rest } = obj;
+    const updated = {
+      ...rest,
+      type: 'courtsCase',
+      replaces: id,
+      updatedAt: nowISO()
+    };
+    return await new Promise((resolve, reject) =>
+      ssbClient.publish(updated, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+  }
+
+  function getCaseRole(caseObj, uid) {
+    const id = String(uid || '');
+    if (!id) return 'OTHER';
+    if (String(caseObj.accuser || '') === id) return 'ACCUSER';
+    if (String(caseObj.respondentId || '') === id) return 'DEFENCE';
+    const ma = ensureArray(caseObj.mediatorsAccuser || []);
+    const mr = ensureArray(caseObj.mediatorsRespondent || []);
+    if (ma.includes(id) || mr.includes(id)) return 'MEDIATOR';
+    if (String(caseObj.judgeId || '') === id) return 'JUDGE';
+    return 'OTHER';
+  }
+
+  async function setMediators({ caseId, side, mediators }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const role = side === 'accuser' ? 'ACCUSER' : side === 'respondent' ? 'DEFENCE' : null;
+    if (!role) throw new Error('Invalid side.');
+    const myRole = getCaseRole(c, userId);
+    if (role === 'ACCUSER' && myRole !== 'ACCUSER') throw new Error('Only accuser can set these mediators.');
+    if (role === 'DEFENCE' && myRole !== 'DEFENCE') throw new Error('Only defence can set these mediators.');
+    const list = Array.from(
+      new Set(
+        ensureArray(mediators || [])
+          .map((x) => String(x || '').trim())
+          .filter(Boolean)
+      )
+    );
+    const clean = list.filter((id) => id !== c.accuser && id !== c.respondentId);
+    if (side === 'accuser') c.mediatorsAccuser = clean;
+    else c.mediatorsRespondent = clean;
+    await upsertCase(c);
+    return c;
+  }
+
+  async function assignJudge({ caseId, judgeId }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const m = String(c.method || '').toUpperCase();
+    if (m !== 'JUDGE') throw new Error('This case does not use a judge.');
+    const myRole = getCaseRole(c, userId);
+    if (myRole !== 'ACCUSER' && myRole !== 'DEFENCE') throw new Error('Only parties can assign a judge.');
+    const id = String(judgeId || '').trim();
+    if (!id) throw new Error('Judge ID is required.');
+    if (!FEED_ID_RE.test(id)) throw new Error('Invalid judge ID.');
+    if (id === String(c.accuser || '') || id === String(c.respondentId || '')) {
+      throw new Error('Judge cannot be a party of the case.');
+    }
+    c.judgeId = id;
+    await upsertCase(c);
+    return c;
+  }
+
+  async function addEvidence({ caseId, text, link, imageMarkdown }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const role = getCaseRole(c, userId);
+    if (role === 'OTHER') throw new Error('You are not involved in this case.');
+    const t = String(text || '').trim();
+    const l = String(link || '').trim();
+    let imageUrl = null;
+    if (imageMarkdown) {
+      const match = imageMarkdown.match(/\(([^)]+)\)/);
+      imageUrl = match ? match[1] : imageMarkdown;
+    }
+    if (!t && !l && !imageUrl) throw new Error('Text, link or image is required.');
+    const ssbClient = await openSsb();
+    const content = {
+      type: 'courtsEvidence',
+      caseId: c.id,
+      author: userId,
+      role,
+      text: t,
+      link: l,
+      imageUrl,
+      createdAt: nowISO()
+    };
+    return await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+  }
+
+  async function answerCase({ caseId, stance, text }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    if (String(c.respondentId || '') !== String(userId || '')) throw new Error('Only the respondent can answer.');
+    const s = String(stance || '').trim().toUpperCase();
+    const ALLOWED = new Set(['DENY', 'ADMIT', 'PARTIAL']);
+    if (!ALLOWED.has(s)) throw new Error('Invalid stance.');
+    const t = String(text || '').trim();
+    if (!t) throw new Error('Response text is required.');
+    const ssbClient = await openSsb();
+    const content = {
+      type: 'courtsAnswer',
+      caseId: c.id,
+      respondent: userId,
+      stance: s,
+      text: t,
+      createdAt: nowISO()
+    };
+    await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+    c.status = 'IN_PROGRESS';
+    c.answeredAt = nowISO();
+    await upsertCase(c);
+    return c;
+  }
+
+  async function issueVerdict({ caseId, result, orders }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const involved =
+      String(c.accuser || '') === String(userId || '') ||
+      String(c.respondentId || '') === String(userId || '') ||
+      ensureArray(c.mediatorsAccuser || []).includes(userId) ||
+      ensureArray(c.mediatorsRespondent || []).includes(userId);
+    if (involved) throw new Error('You cannot be judge and party in the same case.');
+    const r = String(result || '').trim();
+    if (!r) throw new Error('Result is required.');
+    const o = String(orders || '').trim();
+    const ssbClient = await openSsb();
+    const content = {
+      type: 'courtsVerdict',
+      caseId: c.id,
+      judgeId: userId,
+      result: r,
+      orders: o,
+      createdAt: nowISO()
+    };
+    await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+    c.status = 'DECIDED';
+    c.verdictAt = nowISO();
+    c.judgeId = userId;
+    await upsertCase(c);
+    return c;
+  }
+
+  async function proposeSettlement({ caseId, terms }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const role = getCaseRole(c, userId);
+    if (role === 'OTHER') throw new Error('You are not involved in this case.');
+    const t = String(terms || '').trim();
+    if (!t) throw new Error('Terms are required.');
+    const ssbClient = await openSsb();
+    const content = {
+      type: 'courtsSettlementProposal',
+      caseId: c.id,
+      proposer: userId,
+      terms: t,
+      createdAt: nowISO()
+    };
+    return await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+  }
+
+  async function acceptSettlement({ caseId }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const role = getCaseRole(c, userId);
+    if (role !== 'ACCUSER' && role !== 'DEFENCE') throw new Error('Only parties can accept a settlement.');
+    const ssbClient = await openSsb();
+    const content = {
+      type: 'courtsSettlementAccepted',
+      caseId: c.id,
+      by: userId,
+      createdAt: nowISO()
+    };
+    await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+    c.status = 'CLOSED';
+    c.closedAt = nowISO();
+    await upsertCase(c);
+    return c;
+  }
+
+  async function setPublicPreference({ caseId, preference }) {
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const id = String(userId || '');
+    const pref = !!preference;
+    if (String(c.accuser || '') === id) {
+      c.publicPrefAccuser = pref;
+    } else if (String(c.respondentId || '') === id) {
+      c.publicPrefRespondent = pref;
+    } else {
+      throw new Error('Only parties can set visibility preference.');
+    }
+    await upsertCase(c);
+    return c;
+  }
+
+  async function openPopularVote({ caseId }) {
+    if (!services.votes || !services.votes.createVote) throw new Error('Votes service not available.');
+    const c = await getCaseById(caseId);
+    if (!c) throw new Error('Case not found.');
+    const m = String(c.method || '').toUpperCase();
+    if (m !== 'POPULAR' && m !== 'KARMATOCRACY') throw new Error('This case does not use public voting.');
+    if (c.voteId) throw new Error('Vote already opened.');
+    const question = c.title || `Case ${caseId}`;
+    const deadline = moment().add(POPULAR_DAYS, 'days').toISOString();
+    const voteMsg = await services.votes.createVote(
+      question,
+      deadline,
+      ['YES', 'NO', 'ABSTENTION'],
+      [`courtsCase:${caseId}`, `courtsMethod:${m}`]
+    );
+    c.voteId = voteMsg.key || voteMsg.id;
+    await upsertCase(c);
+    return c;
+  }
+
+  async function getInhabitantKarma(feedId) {
+    if (services.banking && services.banking.getUserEngagementScore) {
+      try {
+        const v = await services.banking.getUserEngagementScore(feedId);
+        return Number(v || 0) || 0;
+      } catch {
+        return 0;
+      }
+    }
+    return 0;
+  }
+
+  async function getFirstUserTimestamp(feedId) {
+    const ssbClient = await openSsb();
+    return new Promise((resolve) => {
+      pull(
+        ssbClient.createUserStream({ id: feedId, reverse: false }),
+        pull.filter((m) => m && m.value && m.value.content && m.value.content.type !== 'tombstone'),
+        pull.take(1),
+        pull.collect((err, arr) => {
+          if (err || !arr || !arr.length) return resolve(Date.now());
+          const m = arr[0];
+          const ts = (m.value && m.value.timestamp) || m.timestamp || Date.now();
+          resolve(ts < 1e12 ? ts * 1000 : ts);
+        })
+      );
+    });
+  }
+
+  async function nominateJudge({ judgeId }) {
+    const id = String(judgeId || '').trim();
+    if (!id) throw new Error('Judge ID is required.');
+    if (!FEED_ID_RE.test(id)) throw new Error('Invalid judge ID.');
+    const ssbClient = await openSsb();
+    const content = {
+      type: 'courtsNomination',
+      judgeId: id,
+      createdAt: nowISO()
+    };
+    return await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+  }
+
+  async function voteNomination(nominationId) {
+    const id = String(nominationId || '').trim();
+    if (!id) throw new Error('Nomination not found.');
+    const nominations = await listByType('courtsNomination');
+    const nomination = nominations.find((n) => n.id === id);
+    if (!nomination) throw new Error('Nomination not found.');
+    if (String(nomination.judgeId || '') === String(userId || '')) {
+      throw new Error('You cannot vote for yourself.');
+    }
+    const votes = await listByType('courtsNominationVote');
+    const already = votes.find(
+      (v) =>
+        String(v.nominationId || '') === id &&
+        String(v.voter || '') === String(userId || '')
+    );
+    if (already) throw new Error('You have already voted.');
+    const ssbClient = await openSsb();
+    const content = {
+      type: 'courtsNominationVote',
+      nominationId: id,
+      voter: userId,
+      createdAt: nowISO()
+    };
+    return await new Promise((resolve, reject) =>
+      ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
+    );
+  }
+
+  async function listNominations() {
+    const nominations = await listByType('courtsNomination');
+    const votes = await listByType('courtsNominationVote');
+    const byId = new Map();
+    for (const n of nominations) {
+      byId.set(n.id, { ...n, supports: 0, karma: 0, profileSince: 0 });
+    }
+    for (const v of votes) {
+      const rec = byId.get(v.nominationId);
+      if (rec) rec.supports = (rec.supports || 0) + 1;
+    }
+    const rows = [];
+    for (const rec of byId.values()) {
+      const karma = await getInhabitantKarma(rec.judgeId);
+      const since = await getFirstUserTimestamp(rec.judgeId);
+      rows.push({ ...rec, karma, profileSince: since });
+    }
+    rows.sort((a, b) => {
+      if ((b.supports || 0) !== (a.supports || 0)) return (b.supports || 0) - (a.supports || 0);
+      if ((b.karma || 0) !== (a.karma || 0)) return (b.karma || 0) - (a.karma || 0);
+      if ((a.profileSince || 0) !== (b.profileSince || 0)) return (a.profileSince || 0) - (b.profileSince || 0);
+      const ta = new Date(a.createdAt || 0).getTime();
+      const tb = new Date(b.createdAt || 0).getTime();
+      if (ta !== tb) return ta - tb;
+      return String(a.judgeId || '').localeCompare(String(b.judgeId || ''));
+    });
+    return rows;
+  }
+
+  async function getCaseDetails({ caseId }) {
+    const id = String(caseId || '').trim();
+    if (!id) return null;
+    const base = await getCaseById(id);
+    if (!base) return null;
+    const currentUser = await getCurrentUserId();
+    const me = String(currentUser || '');
+    const accuserId = String(base.accuser || '');
+    const respondentId = String(base.respondentId || '');
+    const ma = ensureArray(base.mediatorsAccuser || []);
+    const mr = ensureArray(base.mediatorsRespondent || []);
+    const judgeId = String(base.judgeId || '');
+    const dictatorId = String(base.dictatorId || '');
+    const isAccuser = accuserId === me;
+    const isRespondent = respondentId === me;
+    const isMediator = ma.includes(me) || mr.includes(me);
+    const isJudge = judgeId === me;
+    const isDictator = dictatorId === me;
+    const mine = isAccuser || isRespondent || isMediator || isJudge || isDictator;
+    let myPublicPreference = null;
+    if (isAccuser && typeof base.publicPrefAccuser === 'boolean') {
+      myPublicPreference = base.publicPrefAccuser;
+    } else if (isRespondent && typeof base.publicPrefRespondent === 'boolean') {
+      myPublicPreference = base.publicPrefRespondent;
+    }
+    const publicDetails = base.publicPrefAccuser === true && base.publicPrefRespondent === true;
+    const evidencesAll = await listByType('courtsEvidence');
+    const answersAll = await listByType('courtsAnswer');
+    const settlementsAll = await listByType('courtsSettlementProposal');
+    const verdictsAll = await listByType('courtsVerdict');
+    const acceptedAll = await listByType('courtsSettlementAccepted');
+    const evidences = evidencesAll
+      .filter((e) => String(e.caseId || '') === id)
+      .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
+    const answers = answersAll
+      .filter((a) => String(a.caseId || '') === id)
+      .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
+    const settlements = settlementsAll
+      .filter((s) => String(s.caseId || '') === id)
+      .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
+    const verdicts = verdictsAll
+      .filter((v) => String(v.caseId || '') === id)
+      .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
+    const verdict = verdicts.length ? verdicts[verdicts.length - 1] : null;
+    const acceptedSettlements = acceptedAll
+      .filter((s) => String(s.caseId || '') === id)
+      .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
+    const decidedAt =
+      base.verdictAt ||
+      base.closedAt ||
+      (verdict && verdict.createdAt) ||
+      base.decidedAt;
+    const hasVerdict = !!verdict;
+    const supportCount = typeof base.supportCount !== 'undefined' ? base.supportCount : 0;
+    return {
+      ...base,
+      id,
+      respondent: base.respondentId || base.respondent,
+      evidences,
+      answers,
+      settlements,
+      acceptedSettlements,
+      verdict,
+      decidedAt,
+      isAccuser,
+      isRespondent,
+      isMediator,
+      isJudge,
+      isDictator,
+      mine,
+      publicDetails,
+      myPublicPreference,
+      supportCount,
+      hasVerdict
+    };
+  }
+
+  return {
+    getCurrentUserId,
+    openCase,
+    listCases,
+    listCasesForUser,
+    getCaseById,
+    setMediators,
+    assignJudge,
+    addEvidence,
+    answerCase,
+    issueVerdict,
+    proposeSettlement,
+    acceptSettlement,
+    setPublicPreference,
+    openPopularVote,
+    nominateJudge,
+    voteNomination,
+    listNominations,
+    getCaseDetails
+  };
+};
+

+ 7 - 6
src/models/stats_model.js

@@ -36,7 +36,9 @@ module.exports = ({ cooler }) => {
   const types = [
     'bookmark','event','task','votes','report','feed','project',
     'image','audio','video','document','transfer','post','tribe',
-    'market','forum','job','aiExchange'
+    'market','forum','job','aiExchange',
+    'parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw',
+    'courtsCase','courtsEvidence','courtsAnswer','courtsVerdict','courtsSettlement','courtsSettlementProposal','courtsSettlementAccepted','courtsNomination','courtsNominationVote'
   ];
 
   const getFolderSize = (folderPath) => {
@@ -141,10 +143,10 @@ module.exports = ({ cooler }) => {
     for (const m of scopedMsgs) {
       const k = m.key;
       const c = m.value.content;
-      const t = c.type;
-      if (!types.includes(t)) continue;
-      byType[t].set(k, { key: k, ts: m.value.timestamp, content: c, author: m.value.author });
-      if (c.replaces) parentOf[t].set(k, c.replaces);
+      theType = c.type;
+      if (!types.includes(theType)) continue;
+      byType[theType].set(k, { key: k, ts: m.value.timestamp, content: c, author: m.value.author });
+      if (c.replaces) parentOf[theType].set(k, c.replaces);
     }
 
     const findRoot = (t, id) => {
@@ -198,7 +200,6 @@ module.exports = ({ cooler }) => {
     const content = {};
     const opinions = {};
     for (const t of types) {
-      if (t === 'karmaScore') continue;
       let vals;
       if (t === 'tribe') {
         vals = tribeDedupContents;

+ 1 - 1
src/server/package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.5.4",
+  "version": "0.5.5",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {

+ 1 - 1
src/server/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.5.4",
+  "version": "0.5.5",
   "description": "Oasis Social Networking Project Utopia",
   "repository": {
     "type": "git",

+ 209 - 130
src/views/activity_view.js

@@ -71,17 +71,26 @@ function renderActionCards(actions, userId) {
 
   const seenDocumentTitles = new Set();
 
-  return deduped.map(action => {
+  const cards = deduped.map(action => {
     const date = action.ts ? new Date(action.ts).toLocaleString() : "";
     const userLink = action.author
       ? a({ href: `/author/${encodeURIComponent(action.author)}` }, action.author)
       : 'unknown';
     const type = action.type || 'unknown';
+    let skip = false;
 
     let headerText;
     if (type.startsWith('parliament')) {
       const sub = type.replace('parliament', '');
       headerText = `[PARLIAMENT · ${sub.toUpperCase()}]`;
+    } else if (type.startsWith('courts')) {
+      const rawSub = type.slice('courts'.length);
+      const pretty = rawSub
+        .replace(/^[_\s]+/, '')
+        .replace(/[_\s]+/g, ' · ')
+        .replace(/([a-z])([A-Z])/g, '$1 · $2');
+      const finalSub = pretty || 'EVENT';
+      headerText = `[COURTS · ${finalSub.toUpperCase()}]`;
     } else {
       const typeLabel = i18n[`type${capitalize(type)}`] || type;
       headerText = `[${String(typeLabel).toUpperCase()}]`;
@@ -180,14 +189,13 @@ function renderActionCards(actions, userId) {
             location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeLocationLabel.toUpperCase()) + ':'), span({ class: 'card-value' }, ...renderUrl(location))) : "",
             typeof isAnonymous === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel+ ':'), span({ class: 'card-value' }, isAnonymous ? i18n.tribePrivate : i18n.tribePublic)) : "",
             inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeModeLabel) + ':'), span({ class: 'card-value' }, inviteMode.toUpperCase())) : "",
-            typeof isLARP === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeLARPLabel+ ':'), span({ class: 'card-value' }, isLARP ? i18n.tribeYes : i18n.tribeNo)) : "",
+            typeof isLARP === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeLARPLabel+ ':'), span({ class: 'card-value' }, isLARP ? i18n.tribeYes : i18n.tribeNo)) : ""
          ), 
           Array.isArray(members) ? h2(`${i18n.tribeMembersCount}: ${members.length}`) : "",
           image  
             ? img({ src: `/blob/${encodeURIComponent(image)}`, class: 'feed-image tribe-image' })
             : img({ src: '/assets/images/default-tribe.png', class: 'feed-image tribe-image' }),
           p({ class: 'tribe-description' }, ...renderUrl(description || '')),
-         
           validTags.length
             ? div({ class: 'card-tags' }, validTags.map(tag =>
               a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)))
@@ -333,7 +341,7 @@ function renderActionCards(actions, userId) {
           location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.location || 'Location') + ':'), span({ class: 'card-value' }, location)) : "",
           typeof isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.isPublic || 'Public') + ':'), span({ class: 'card-value' }, isPublic ? 'Yes' : 'No')) : "",
           price ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.price || 'Price') + ':'), span({ class: 'card-value' }, price + " ECO")) : "",
-          br,
+          br(),
           organizer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.organizer || 'Organizer') + ': '), a({ class: "user-link", href: `/author/${encodeURIComponent(organizer)}` }, organizer)) : "",
           Array.isArray(attendees) ? h2({ class: 'card-label' }, (i18n.attendees || 'Attendees') + ': ' + attendees.length) : ""
         )
@@ -463,19 +471,18 @@ function renderActionCards(actions, userId) {
           div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStatus + ": " ), span({ class: 'card-value' }, status.toUpperCase())),
           div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : "")),
           div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, stock)),
-          br,
+          br(),
           image
             ? img({ src: `/blob/${encodeURIComponent(image)}` })
             : img({ src: '/assets/images/default-market.png', alt: title }),
-          br,
+          br(),
           div({ class: "market-card price" },
             p(`${i18n.marketItemPrice}: ${price} ECO`)
           ),
           item_type === 'auction' && status !== 'SOLD' && status !== 'DISCARDED' && !isSeller
             ? div({ class: "auction-info" },
                 auctions_poll && auctions_poll.length > 0
-                  ? [
-                      p({ class: "auction-bid-text" }, i18n.marketAuctionBids),
+                  ?
                       table({ class: 'auction-bid-table' },
                         tr(
                           th(i18n.marketAuctionBidTime),
@@ -491,7 +498,6 @@ function renderActionCards(actions, userId) {
                           );
                         })
                       )
-                    ]
                   : p(i18n.marketNoBids),
                 form({ method: "POST", action: `/market/bid/${encodeURIComponent(action.id)}` },
                   input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }),
@@ -525,102 +531,102 @@ function renderActionCards(actions, userId) {
         deadline, followers, backers, milestones,
         bounty, bountyAmount, bounty_currency,
         activity, activityActor
-    } = content;
-
-    const ratio = goal ? Math.min(100, Math.round((parseFloat(pledged || 0) / parseFloat(goal)) * 100)) : 0;
-    const displayStatus = String(status || 'ACTIVE').toUpperCase();
-    const followersCount = Array.isArray(followers) ? followers.length : 0;
-    const backersCount = Array.isArray(backers) ? backers.length : 0;
-    const backersTotal = sumAmounts(backers || []);
-    const msCount = Array.isArray(milestones) ? milestones.length : 0;
-    const lastMs = Array.isArray(milestones) && milestones.length ? milestones[milestones.length - 1] : null;
-    const bountyVal = typeof bountyAmount !== 'undefined'
+      } = content;
+
+      const ratio = goal ? Math.min(100, Math.round((parseFloat(pledged || 0) / parseFloat(goal)) * 100)) : 0;
+      const displayStatus = String(status || 'ACTIVE').toUpperCase();
+      const followersCount = Array.isArray(followers) ? followers.length : 0;
+      const backersCount = Array.isArray(backers) ? backers.length : 0;
+      const backersTotal = sumAmounts(backers || []);
+      const msCount = Array.isArray(milestones) ? milestones.length : 0;
+      const lastMs = Array.isArray(milestones) && milestones.length ? milestones[milestones.length - 1] : null;
+      const bountyVal = typeof bountyAmount !== 'undefined'
         ? bountyAmount
         : (typeof bounty === 'number' ? bounty : null);
 
-    if (activity && activity.kind) {
+      if (activity && activity.kind) {
         const tmpl =
-            activity.kind === 'follow'
-                ? (i18n.activityProjectFollow || '%OASIS% is now %ACTION% this project: %PROJECT%')
-                : activity.kind === 'unfollow'
-                    ? (i18n.activityProjectUnfollow || '%OASIS% is now %ACTION% this project: %PROJECT%')
-                    : '%OASIS% performed an unknown action on %PROJECT%';
+          activity.kind === 'follow'
+            ? (i18n.activityProjectFollow || '%OASIS% is now %ACTION% this project: %PROJECT%')
+            : activity.kind === 'unfollow'
+              ? (i18n.activityProjectUnfollow || '%OASIS% is now %ACTION% this project: %PROJECT%')
+              : '%OASIS% performed an unknown action on %PROJECT%';
 
         const actionWord =
-            activity.kind === 'follow'
-                ? (i18n.following || 'FOLLOWING')
-                : activity.kind === 'unfollow'
-                    ? (i18n.unfollowing || 'UNFOLLOWING')
-                    : 'ACTION';
+          activity.kind === 'follow'
+            ? (i18n.following || 'FOLLOWING')
+            : activity.kind === 'unfollow'
+              ? (i18n.unfollowing || 'UNFOLLOWING')
+              : 'ACTION';
 
         const msgHtml = tmpl
-            .replace('%OASIS%', `<a class="user-link" href="/author/${encodeURIComponent(activity.activityActor || '')}">${activity.activityActor || ''}</a>`)
-            .replace('%PROJECT%', `<a class="user-link" href="/projects/${encodeURIComponent(action.tipId || action.id)}">${title || ''}</a>`)
-            .replace('%ACTION%', `<strong>${actionWord}</strong>`);
+          .replace('%OASIS%', `<a class="user-link" href="/author/${encodeURIComponent(activity.activityActor || '')}">${activity.activityActor || ''}</a>`)
+          .replace('%PROJECT%', `<a class="user-link" href="/projects/${encodeURIComponent(action.tipId || action.id)}">${title || ''}</a>`)
+          .replace('%ACTION%', `<strong>${actionWord}</strong>`);
 
         return div({ class: 'card card-rpg' },
-            div({ class: 'card-header' },
-                h2({ class: 'card-label' }, `[${(i18n.typeProject || 'PROJECT').toUpperCase()}]`),
-                form({ method: "GET", action: `/projects/${encodeURIComponent(action.tipId || action.id)}` },
-                    button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-                )
-            ),
-            div(
-                p({ innerHTML: msgHtml })
-            ),
-            p({ class: 'card-footer' },
-                span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `),
-                a({ href: `/author/${encodeURIComponent(action.author)}`, class: 'user-link' }, `${action.author}`)
+          div({ class: 'card-header' },
+            h2({ class: 'card-label' }, `[${(i18n.typeProject || 'PROJECT').toUpperCase()}]`),
+            form({ method: "GET", action: `/projects/${encodeURIComponent(action.tipId || action.id)}` },
+              button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
             )
+          ),
+          div(
+            p({ innerHTML: msgHtml })
+          ),
+          p({ class: 'card-footer' },
+            span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `),
+            a({ href: `/author/${encodeURIComponent(action.author)}`, class: 'user-link' }, `${action.author}`)
+          )
         );
-    }
+      }
 
-    cardBody.push(
+      cardBody.push(
         div({ class: 'card-section project' },
-            title ? div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.title + ':'),
-                span({ class: 'card-value' }, title)
-            ) : "",
-            typeof goal !== 'undefined' ? div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectGoal + ':'),
-                span({ class: 'card-value' }, `${goal} ECO`)
-            ) : "",
-            typeof progress !== 'undefined' ? div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectProgress + ':'),
-                span({ class: 'card-value' }, `${progress || 0}%`)
-            ) : "",
-            deadline ? div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectDeadline + ':'),
-                span({ class: 'card-value' }, moment(deadline).format('YYYY/MM/DD HH:mm'))
-            ) : "",
-            div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectStatus + ':'),
-                span({ class: 'card-value' }, i18n['projectStatus' + displayStatus] || displayStatus)
-            ),
-            div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectFunding + ':'),
-                span({ class: 'card-value' }, `${ratio}%`)
-            ),
-            typeof pledged !== 'undefined' ? div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectPledged + ':'),
-                span({ class: 'card-value' }, `${pledged || 0} ECO`)
-            ) : "",
-            div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectFollowers + ':'),
-                span({ class: 'card-value' }, `${followersCount}`)
-            ),
-            div({ class: 'card-field' },
-                span({ class: 'card-label' }, i18n.projectBackers + ':'),
-                span({ class: 'card-value' }, `${backersCount} · ${backersTotal} ECO`)
-            ),
-            msCount ? div({ class: 'card-field' },
-                span({ class: 'card-label' }, (i18n.projectMilestones || 'Milestones') + ':'),
-                span({ class: 'card-value' }, `${msCount}${lastMs && lastMs.title ? ' · ' + lastMs.title : ''}`)
-            ) : "",
-            bountyVal != null ? div({ class: 'card-field' },
-                span({ class: 'card-label' }, (i18n.projectBounty || 'Bounty') + ':'),
-                span({ class: 'card-value' }, `${bountyVal} ${(bounty_currency || 'ECO').toUpperCase()}`)
-            ) : ""
+          title ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.title + ':'),
+            span({ class: 'card-value' }, title)
+          ) : "",
+          typeof goal !== 'undefined' ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectGoal + ':'),
+            span({ class: 'card-value' }, `${goal} ECO`)
+          ) : "",
+          typeof progress !== 'undefined' ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectProgress + ':'),
+            span({ class: 'card-value' }, `${progress || 0}%`)
+          ) : "",
+          deadline ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectDeadline + ':'),
+            span({ class: 'card-value' }, moment(deadline).format('YYYY/MM/DD HH:mm'))
+          ) : "",
+          div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectStatus + ':'),
+            span({ class: 'card-value' }, i18n['projectStatus' + displayStatus] || displayStatus)
+          ),
+          div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectFunding + ':'),
+            span({ class: 'card-value' }, `${ratio}%`)
+          ),
+          typeof pledged !== 'undefined' ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectPledged + ':'),
+            span({ class: 'card-value' }, `${pledged || 0} ECO`)
+          ) : "",
+          div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectFollowers + ':'),
+            span({ class: 'card-value' }, `${followersCount}`)
+          ),
+          div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.projectBackers + ':'),
+            span({ class: 'card-value' }, `${backersCount} · ${backersTotal} ECO`)
+          ),
+          msCount ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, (i18n.projectMilestones || 'Milestones') + ':'),
+            span({ class: 'card-value' }, `${msCount}${lastMs && lastMs.title ? ' · ' + lastMs.title : ''}`)
+          ) : "",
+          bountyVal != null ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, (i18n.projectBounty || 'Bounty') + ':'),
+            span({ class: 'card-value' }, `${bountyVal} ${(bounty_currency || 'ECO').toUpperCase()}`)
+          ) : ""
         )
       );
     }
@@ -792,14 +798,58 @@ function renderActionCards(actions, userId) {
       );
     }
 
+    if (type.startsWith('courts')) {
+      if (type === 'courtsCase') {
+        const { title, method, accuser, status, answerBy, evidenceBy, decisionBy, needed, yes, total, voteId } = content;
+        cardBody.push(
+          div({ class: 'card-section courts' },
+            title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsCaseTitle.toUpperCase() + ':'), span({ class: 'card-value' }, title)) : '',
+            status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThStatus.toUpperCase() + ':'), span({ class: 'card-value' }, status)) : '',
+            method ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsMethod.toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['courtsMethod' + String(method).toUpperCase()] || method).toUpperCase())) : '',
+            answerBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThAnswerBy + ':'), span({ class: 'card-value' }, new Date(answerBy).toLocaleString())) : '',
+            evidenceBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThEvidenceBy + ':'), span({ class: 'card-value' }, new Date(evidenceBy).toLocaleString())) : '',
+            decisionBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThDecisionBy + ':'), span({ class: 'card-value' }, new Date(decisionBy).toLocaleString())) : '',
+            accuser ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsAccuser + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(accuser)}`, class: 'user-link' }, accuser))) : '',
+            typeof needed !== 'undefined' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsVotesNeeded + ':'), span({ class: 'card-value' }, String(needed))) : '',
+            (typeof yes !== 'undefined' || typeof total !== 'undefined') ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsVotesSlashTotal + ':'), span({ class: 'card-value' }, `${Number(yes || 0)}/${Number(total || 0)}`)) : '',
+            voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsOpenVote + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : ''
+          )
+        );
+      } else if (type === 'courtsNomination') {
+        const { judgeId } = content;
+        cardBody.push(
+          div({ class: 'card-section courts' },
+            judgeId ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsJudge + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(judgeId)}`, class: 'user-link' }, judgeId))) : ''
+          )
+        );
+      } else {
+        skip = true;
+      }
+    }
+
     const viewHref = getViewDetailsAction(type, action);
     const isParliamentTarget =
       viewHref === '/parliament?filter=candidatures' ||
       viewHref === '/parliament?filter=government'   ||
       viewHref === '/parliament?filter=proposals'    ||
-      viewHref === '/parliament?filter=revocations'    ||
+      viewHref === '/parliament?filter=revocations'  ||
       viewHref === '/parliament?filter=laws';
+
+    const isCourtsTarget =
+      viewHref === '/courts?filter=cases'    ||
+      viewHref === '/courts?filter=mycases'  ||
+      viewHref === '/courts?filter=actions'  ||
+      viewHref === '/courts?filter=judges'   ||
+      viewHref === '/courts?filter=history'  ||
+      viewHref === '/courts?filter=rules'    ||
+      viewHref === '/courts?filter=open';
+
     const parliamentFilter = isParliamentTarget ? (viewHref.split('filter=')[1] || '') : '';
+    const courtsFilter     = isCourtsTarget     ? (viewHref.split('filter=')[1] || '') : '';
+
+    if (skip) {
+      return null;
+    }
 
     return div({ class: 'card card-rpg' },
       div({ class: 'card-header' },
@@ -807,13 +857,21 @@ function renderActionCards(actions, userId) {
         type !== 'feed' && type !== 'aiExchange' && type !== 'bankWallet' && (!action.tipId || action.tipId === action.id)
           ? (
               isParliamentTarget
-                ? form({ method: "GET", action: "/parliament" },
+                ? form(
+                    { method: "GET", action: "/parliament" },
                     input({ type: "hidden", name: "filter", value: parliamentFilter }),
                     button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
                   )
-                : form({ method: "GET", action: viewHref },
-                    button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
-                  )
+                : isCourtsTarget
+                  ? form(
+                      { method: "GET", action: "/courts" },
+                      input({ type: "hidden", name: "filter", value: courtsFilter }),
+                      button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+                    )
+                  : form(
+                      { method: "GET", action: viewHref },
+                      button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+                    )
             )
           : ''
       ),
@@ -824,43 +882,58 @@ function renderActionCards(actions, userId) {
       )
     );
   });
+
+  const filteredCards = cards.filter(Boolean);
+  if (!filteredCards.length) {
+    return div({ class: "no-actions" }, p(i18n.noActions));
+  }
+  return filteredCards;
 }
 
 function getViewDetailsAction(type, action) {
-    const id = encodeURIComponent(action.tipId || action.id);
-    switch (type) {
-        case 'parliamentCandidature': return `/parliament?filter=candidatures`;
-        case 'parliamentTerm':        return `/parliament?filter=government`;
-        case 'parliamentProposal':    return `/parliament?filter=proposals`;
-        case 'parliamentRevocation':  return `/parliament?filter=revocations`;
-        case 'parliamentLaw':         return `/parliament?filter=laws`;
-        case 'votes':      return `/votes/${id}`;
-        case 'transfer':   return `/transfers/${id}`;
-        case 'pixelia':    return `/pixelia`;
-        case 'tribe':      return `/tribe/${id}`;
-        case 'curriculum': return `/inhabitant/${encodeURIComponent(action.author)}`;
-        case 'karmaScore': return `/author/${encodeURIComponent(action.author)}`;
-        case 'image':      return `/images/${id}`;
-        case 'audio':      return `/audios/${id}`;
-        case 'video':      return `/videos/${id}`;
-        case 'forum':      return `/forum/${encodeURIComponent(action.content?.key || action.tipId || action.id)}`;
-        case 'document':   return `/documents/${id}`;
-        case 'bookmark':   return `/bookmarks/${id}`;
-        case 'event':      return `/events/${id}`;
-        case 'task':       return `/tasks/${id}`;
-        case 'about':      return `/author/${encodeURIComponent(action.author)}`;
-        case 'post':       return `/thread/${id}#${id}`;
-        case 'vote':       return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`;
-        case 'contact':    return `/inhabitants`;
-        case 'pub':        return `/invites`;
-        case 'market':     return `/market/${id}`;
-        case 'job':        return `/jobs/${id}`;
-        case 'project':    return `/projects/${id}`;
-        case 'report':     return `/reports/${id}`;
-        case 'bankWallet': return `/wallet`;
-        case 'bankClaim':  return `/banking${action.content?.epochId ? `/epoch/${encodeURIComponent(action.content.epochId)}` : ''}`;
-        default:           return `/activity`;
-    }
+  const id = encodeURIComponent(action.tipId || action.id);
+  switch (type) {
+    case 'parliamentCandidature':   return `/parliament?filter=candidatures`;
+    case 'parliamentTerm':          return `/parliament?filter=government`;
+    case 'parliamentProposal':      return `/parliament?filter=proposals`;
+    case 'parliamentRevocation':    return `/parliament?filter=revocations`;
+    case 'parliamentLaw':           return `/parliament?filter=laws`;
+    case 'courtsCase':              return `/courts/cases/${encodeURIComponent(action.id)}`;
+    case 'courtsEvidence':          return `/courts?filter=actions`;
+    case 'courtsAnswer':            return `/courts?filter=actions`;
+    case 'courtsVerdict':           return `/courts?filter=actions`;
+    case 'courtsSettlement':        return `/courts?filter=actions`;
+    case 'courtsSettlementProposal':return `/courts?filter=actions`;
+    case 'courtsSettlementAccepted':return `/courts?filter=actions`;
+    case 'courtsNomination':        return `/courts?filter=judges`;
+    case 'courtsNominationVote':    return `/courts?filter=judges`;
+    case 'votes':      return `/votes/${id}`;
+    case 'transfer':   return `/transfers/${id}`;
+    case 'pixelia':    return `/pixelia`;
+    case 'tribe':      return `/tribe/${id}`;
+    case 'curriculum': return `/inhabitant/${encodeURIComponent(action.author)}`;
+    case 'karmaScore': return `/author/${encodeURIComponent(action.author)}`;
+    case 'image':      return `/images/${id}`;
+    case 'audio':      return `/audios/${id}`;
+    case 'video':      return `/videos/${id}`;
+    case 'forum':      return `/forum/${encodeURIComponent(action.content?.key || action.tipId || action.id)}`;
+    case 'document':   return `/documents/${id}`;
+    case 'bookmark':   return `/bookmarks/${id}`;
+    case 'event':      return `/events/${id}`;
+    case 'task':       return `/tasks/${id}`;
+    case 'about':      return `/author/${encodeURIComponent(action.author)}`;
+    case 'post':       return `/thread/${id}#${id}`;
+    case 'vote':       return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`;
+    case 'contact':    return `/inhabitants`;
+    case 'pub':        return `/invites`;
+    case 'market':     return `/market/${id}`;
+    case 'job':        return `/jobs/${id}`;
+    case 'project':    return `/projects/${id}`;
+    case 'report':     return `/reports/${id}`;
+    case 'bankWallet': return `/wallet`;
+    case 'bankClaim':  return `/banking${action.content?.epochId ? `/epoch/${encodeURIComponent(action.content.epochId)}` : ''}`;
+    default:           return `/activity`;
+  }
 }
 
 exports.activityView = (actions, filter, userId) => {
@@ -877,6 +950,7 @@ exports.activityView = (actions, filter, userId) => {
     { type: 'job',       label: i18n.typeJob },
     { type: 'transfer',  label: i18n.typeTransfer },
     { type: 'parliament',label: i18n.typeParliament },
+    { type: 'courts',    label: i18n.typeCourts },
     { type: 'votes',     label: i18n.typeVotes },
     { type: 'event',     label: i18n.typeEvent },
     { type: 'task',      label: i18n.typeTask },
@@ -907,6 +981,11 @@ exports.activityView = (actions, filter, userId) => {
     filteredActions = actions.filter(action => action.type !== 'tombstone' && (action.type === 'bankWallet' || action.type === 'bankClaim'));
   } else if (filter === 'parliament') {
     filteredActions = actions.filter(action => ['parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw'].includes(action.type));
+  } else if (filter === 'courts') {
+    filteredActions = actions.filter(action => {
+      const t = String(action.type || '').toLowerCase();
+      return t === 'courtscase' || t === 'courtsnomination' || t === 'courtsnominationvote';
+    });
   } else {
     filteredActions = actions.filter(action => (action.type === filter || filter === 'all') && action.type !== 'tombstone');
   }

+ 15 - 2
src/views/blockchain_view.js

@@ -11,11 +11,11 @@ const FILTER_LABELS = {
   forum: i18n.typeForum, about: i18n.typeAbout, contact: i18n.typeContact, pub: i18n.typePub,
   transfer: i18n.typeTransfer, market: i18n.typeMarket, job: i18n.typeJob, tribe: i18n.typeTribe,
   project: i18n.typeProject, banking: i18n.typeBanking, bankWallet: i18n.typeBankWallet, bankClaim: i18n.typeBankClaim,
-  aiExchange: i18n.typeAiExchange, parliament: i18n.typeParliament
+  aiExchange: i18n.typeAiExchange, parliament: i18n.typeParliament, courts: i18n.typeCourts
 };
 
 const BASE_FILTERS = ['recent', 'all', 'mine', 'tombstone'];
-const CAT_BLOCK1  = ['votes', 'event', 'task', 'report', 'parliament'];
+const CAT_BLOCK1  = ['votes', 'event', 'task', 'report', 'parliament', 'courts'];
 const CAT_BLOCK2  = ['pub', 'tribe', 'about', 'contact', 'curriculum', 'vote', 'aiExchange'];
 const CAT_BLOCK3  = ['banking', 'job', 'market', 'project', 'transfer', 'feed', 'post', 'pixelia'];
 const CAT_BLOCK4  = ['forum', 'bookmark', 'image', 'video', 'audio', 'document'];
@@ -29,6 +29,10 @@ const filterBlocks = (blocks, filter, userId) => {
     const pset = new Set(['parliamentTerm','parliamentProposal','parliamentLaw','parliamentCandidature','parliamentRevocation']);
     return blocks.filter(b => pset.has(b.type));
   }
+  if (filter === 'courts') {
+    const cset = new Set(['courtsCase','courtsEvidence','courtsAnswer','courtsVerdict','courtsSettlement','courtsSettlementProposal','courtsSettlementAccepted','courtsNomination','courtsNominationVote']);
+    return blocks.filter(b => cset.has(b.type));
+  }
   return blocks.filter(b => b.type === filter);
 };
 
@@ -76,6 +80,15 @@ const getViewDetailsAction = (type, block) => {
     case 'parliamentLaw': return `/parliament`;
     case 'parliamentCandidature': return `/parliament`;
     case 'parliamentRevocation': return `/parliament`;
+    case 'courtsCase': return `/courts`;
+    case 'courtsEvidence': return `/courts`;
+    case 'courtsAnswer': return `/courts`;
+    case 'courtsVerdict': return `/courts`;
+    case 'courtsSettlement': return `/courts`;
+    case 'courtsSettlementProposal': return `/courts`;
+    case 'courtsSettlementAccepted': return `/courts`;
+    case 'courtsNomination': return `/courts`;
+    case 'courtsNominationVote': return `/courts`;
     default: return null;
   }
 };

File diff suppressed because it is too large
+ 1395 - 0
src/views/courts_view.js


+ 10 - 0
src/views/main_views.js

@@ -283,6 +283,15 @@ const renderParliamentLink = () => {
   return parliamentMod 
     ? [
         navLink({ href: "/parliament", emoji: "ꗞ", text: i18n.parliamentTitle, class: "parliament-link enabled" }),
+      ]
+    : '';
+};
+
+const renderCourtsLink = () => {
+  const courtsMod = getConfig().modules.courtsMod === 'on';
+  return courtsMod 
+    ? [
+        navLink({ href: "/courts", emoji: "ꖻ", text: i18n.courtsTitle, class: "courts-link enabled" }),
         hr(),
       ]
     : '';
@@ -470,6 +479,7 @@ const template = (titlePrefix, ...elements) => {
               navLink({ href: "/inhabitants", emoji: "ꖘ", text: i18n.inhabitantsLabel }),
               renderTribesLink(),
               renderParliamentLink(),
+              renderCourtsLink(),
               renderVotationsLink(),
               renderEventsLink(),
               renderTasksLink(),

+ 1 - 0
src/views/modules_view.js

@@ -11,6 +11,7 @@ const modulesView = () => {
     { name: 'banking', label: i18n.modulesBankingLabel, description: i18n.modulesBankingDescription },
     { name: 'bookmarks', label: i18n.modulesBookmarksLabel, description: i18n.modulesBookmarksDescription },
     { name: 'cipher', label: i18n.modulesCipherLabel, description: i18n.modulesCipherDescription },
+    { name: 'courts', label: i18n.modulesCourtsLabel, description: i18n.modulesCourtsDescription },
     { name: 'docs', label: i18n.modulesDocsLabel, description: i18n.modulesDocsDescription },
     { name: 'events', label: i18n.modulesEventsLabel, description: i18n.modulesEventsDescription },
     { name: 'feed', label: i18n.modulesFeedLabel, description: i18n.modulesFeedDescription },

+ 60 - 19
src/views/stats_view.js

@@ -1,6 +1,23 @@
 const { div, h2, p, section, button, form, input, ul, li, a, h3, span, strong, table, tr, td, th } = require("../server/node_modules/hyperaxe");
 const { template, i18n } = require('./main_views');
 
+Object.assign(i18n, {
+  statsParliamentCandidature: "Parliament candidatures",
+  statsParliamentTerm: "Parliament terms",
+  statsParliamentProposal: "Parliament proposals",
+  statsParliamentRevocation: "Parliament revocations",
+  statsParliamentLaw: "Parliament laws",
+  statsCourtsCase: "Court cases",
+  statsCourtsEvidence: "Court evidence",
+  statsCourtsAnswer: "Court answers",
+  statsCourtsVerdict: "Court verdicts",
+  statsCourtsSettlement: "Court settlements",
+  statsCourtsSettlementProposal: "Settlement proposals",
+  statsCourtsSettlementAccepted: "Settlements accepted",
+  statsCourtsNomination: "Judge nominations",
+  statsCourtsNominationVote: "Nomination votes"
+});
+
 const C = (stats, t) => Number((stats && stats.content && stats.content[t]) || 0);
 const O = (stats, t) => Number((stats && stats.opinions && stats.opinions[t]) || 0);
 
@@ -11,8 +28,44 @@ exports.statsView = (stats, filter) => {
   const types = [
     'bookmark', 'event', 'task', 'votes', 'report', 'feed', 'project',
     'image', 'audio', 'video', 'document', 'transfer', 'post', 'tribe',
-    'market', 'forum', 'job', 'aiExchange'
+    'market', 'forum', 'job', 'aiExchange',
+    'parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw',
+    'courtsCase','courtsEvidence','courtsAnswer','courtsVerdict','courtsSettlement','courtsSettlementProposal','courtsSettlementAccepted','courtsNomination','courtsNominationVote'
   ];
+  const labels = {
+    bookmark: i18n.statsBookmark,
+    event: i18n.statsEvent,
+    task: i18n.statsTask,
+    votes: i18n.statsVotes,
+    report: i18n.statsReport,
+    feed: i18n.statsFeed,
+    project: i18n.statsProject,
+    image: i18n.statsImage,
+    audio: i18n.statsAudio,
+    video: i18n.statsVideo,
+    document: i18n.statsDocument,
+    transfer: i18n.statsTransfer,
+    post: i18n.statsPost,
+    tribe: i18n.statsTribe,
+    market: i18n.statsMarket,
+    forum: i18n.statsForum,
+    job: i18n.statsJob,
+    aiExchange: i18n.statsAiExchange,
+    parliamentCandidature: i18n.statsParliamentCandidature,
+    parliamentTerm: i18n.statsParliamentTerm,
+    parliamentProposal: i18n.statsParliamentProposal,
+    parliamentRevocation: i18n.statsParliamentRevocation,
+    parliamentLaw: i18n.statsParliamentLaw,
+    courtsCase: i18n.statsCourtsCase,
+    courtsEvidence: i18n.statsCourtsEvidence,
+    courtsAnswer: i18n.statsCourtsAnswer,
+    courtsVerdict: i18n.statsCourtsVerdict,
+    courtsSettlement: i18n.statsCourtsSettlement,
+    courtsSettlementProposal: i18n.statsCourtsSettlementProposal,
+    courtsSettlementAccepted: i18n.statsCourtsSettlementAccepted,
+    courtsNomination: i18n.statsCourtsNomination,
+    courtsNominationVote: i18n.statsCourtsNominationVote
+  };
   const totalContent = types.filter(t => t !== 'karmaScore').reduce((sum, t) => sum + C(stats, t), 0);
   const totalOpinions = types.reduce((sum, t) => sum + O(stats, t), 0);
   const blockStyle = 'padding:16px;border:1px solid #ddd;border-radius:8px;margin-bottom:24px;';
@@ -112,30 +165,18 @@ exports.statsView = (stats, filter) => {
                   li(`${i18n.statsProjectsActiveFundingAvg}: ${((stats.projectsKPIs?.activeFundingAvg || 0)).toFixed(1)}%`)
                 ])
               ),
-              div({ style: blockStyle },
-                h2(i18n.statsJobsTitle),
-                ul([
-                  li(`${i18n.statsJobsTotal}: ${stats.jobsKPIs?.total || 0}`),
-                  li(`${i18n.statsJobsOpen}: ${stats.jobsKPIs?.open || 0}`),
-                  li(`${i18n.statsJobsClosed}: ${stats.jobsKPIs?.closed || 0}`),
-                  li(`${i18n.statsJobsOpenVacants}: ${stats.jobsKPIs?.openVacants || 0}`),
-                  li(`${i18n.statsJobsSubscribersTotal}: ${stats.jobsKPIs?.subscribersTotal || 0}`),
-                  li(`${i18n.statsJobsAvgSalary}: ${((stats.jobsKPIs?.avgSalary || 0)).toFixed(2)} ECO`),
-                  li(`${i18n.statsJobsMedianSalary}: ${((stats.jobsKPIs?.medianSalary || 0)).toFixed(2)} ECO`)
-                ])
-              ),
               div({ style: blockStyle },
                 h2(`${i18n.statsNetworkOpinions}: ${totalOpinions}`),
-                ul(types.map(t => O(stats, t) > 0 ? li(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${O(stats, t)}`) : null).filter(Boolean))
+                ul(types.map(t => O(stats, t) > 0 ? li(`${labels[t]}: ${O(stats, t)}`) : null).filter(Boolean))
               ),
               div({ style: blockStyle },
                 h2(`${i18n.statsNetworkContent}: ${totalContent}`),
                 ul(
                   types.filter(t => t !== 'karmaScore').map(t => {
                     if (C(stats, t) <= 0) return null;
-                    if (t !== 'tribe') return li(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${C(stats, t)}`);
+                    if (t !== 'tribe') return li(`${labels[t]}: ${C(stats, t)}`);
                     return li(
-                      span(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${C(stats, t)}`),
+                      span(`${labels[t]}: ${C(stats, t)}`),
                       ul([
                         li(`${i18n.statsPublic}: ${stats.tribePublicCount || 0}`),
                         li(`${i18n.statsPrivate}: ${stats.tribePrivateCount || 0}`)
@@ -202,16 +243,16 @@ exports.statsView = (stats, filter) => {
                 ),
                 div({ style: blockStyle },
                   h2(`${i18n.statsYourOpinions}: ${totalOpinions}`),
-                  ul(types.map(t => O(stats, t) > 0 ? li(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${O(stats, t)}`) : null).filter(Boolean))
+                  ul(types.map(t => O(stats, t) > 0 ? li(`${labels[t]}: ${O(stats, t)}`) : null).filter(Boolean))
                 ),
                 div({ style: blockStyle },
                   h2(`${i18n.statsYourContent}: ${totalContent}`),
                   ul(
                     types.filter(t => t !== 'karmaScore').map(t => {
                       if (C(stats, t) <= 0) return null;
-                      if (t !== 'tribe') return li(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${C(stats, t)}`);
+                      if (t !== 'tribe') return li(`${labels[t]}: ${C(stats, t)}`);
                       return li(
-                        span(`${i18n[`stats${t.charAt(0).toUpperCase() + t.slice(1)}`]}: ${C(stats, t)}`),
+                        span(`${labels[t]}: ${C(stats, t)}`),
                         ul([
                           li(`${i18n.statsPublic}: ${stats.tribePublicCount || 0}`),
                           li(`${i18n.statsPrivate}: ${stats.tribePrivateCount || 0}`),