Jelajahi Sumber

Oasis release 0.5.2

psy 1 hari lalu
induk
melakukan
090ce67fd9

+ 6 - 0
README.md

@@ -110,6 +110,12 @@ Our AI is trained with content from the OASIS network and its purpose is to take
 
 ----------
 
+## Parliament (politics)
+
+Oasis contains its own Parliament (Government system).
+
+  ![SNH](https://solarnethub.com/git/oasis-parliament.png "SolarNET.HuB")
+  
 ## ECOin (crypto-economy)
 
 Oasis contains its own cryptocurrency. With it, you can exchange items and services in the marketplace. 

+ 10 - 0
docs/CHANGELOG.md

@@ -13,6 +13,16 @@ All notable changes to this project will be documented in this file.
 ### Security
 -->
 
+## v0.5.2 - 2025-10-22
+
+### Added
+
+ + Government system (Parliament plugin).
+ 
+### Fixed
+
+ + Forum category translations (Forum plugin).
+
 ## v0.5.1 - 2025-09-26
 
 ### Added

+ 179 - 4
src/backend/backend.js

@@ -77,6 +77,66 @@ function readWalletMap() {
   return {};
 }
 
+//parliament
+async function buildState(filter) {
+  const f = (filter || 'government').toLowerCase();
+  const [govCard, candidatures, proposals, canPropose, laws, historical] = await Promise.all([
+    parliamentModel.getGovernmentCard(),
+    parliamentModel.listCandidatures('OPEN'),
+    parliamentModel.listProposalsCurrent(),
+    parliamentModel.canPropose(),
+    parliamentModel.listLaws(),
+    parliamentModel.listHistorical()
+  ]);
+  return { filter: f, governmentCard: govCard, candidatures, proposals, canPropose, laws, historical };
+}
+
+function pickLeader(cands = []) {
+  if (!cands.length) return null;
+  return [...cands].sort((a, b) => {
+    const va = Number(a.votes || 0), vb = Number(b.votes || 0);
+    if (vb !== va) return vb - va;
+    const ka = Number(a.karma || 0), kb = Number(b.karma || 0);
+    if (kb !== ka) return kb - ka;
+    const sa = Number(a.profileSince || 0), sb = Number(b.profileSince || 0);
+    if (sa !== sb) return sa - sb;
+    const ca = new Date(a.createdAt).getTime(), cb = new Date(b.createdAt).getTime();
+    if (ca !== cb) return ca - cb;
+    return String(a.targetId).localeCompare(String(b.targetId));
+  })[0];
+}
+
+async function buildLeaderMeta(leader) {
+  if (!leader) return null;
+  if (leader.targetType === 'inhabitant') {
+    let name = null;
+    let image = null;
+    let description = null;
+    try { if (about && typeof about.name === 'function') name = await about.name(leader.targetId); } catch {}
+    try { if (about && typeof about.image === 'function') image = await about.image(leader.targetId); } catch {}
+    try { if (about && typeof about.description === 'function') description = await about.description(leader.targetId); } catch {}
+    const imgId = typeof image === 'string' ? image : (image && (image.link || image.url)) || null;
+    const avatarUrl = imgId ? `/image/256/${encodeURIComponent(imgId)}` : '/assets/images/default-avatar.png';
+    return {
+      isTribe: false,
+      name: name || leader.targetId,
+      avatarUrl,
+      bio: typeof description === 'string' ? description : ''
+    };
+  } else {
+    let tribe = null;
+    try { tribe = await tribesModel.getTribeById(leader.targetId); } catch {}
+    const imgId = tribe && tribe.image ? tribe.image : null;
+    const avatarUrl = imgId ? `/image/256/${encodeURIComponent(imgId)}` : '/assets/images/default-tribe.png';
+    return {
+      isTribe: true,
+      name: leader.targetTitle || (tribe && (tribe.title || tribe.name)) || leader.targetId,
+      avatarUrl,
+      bio: (tribe && tribe.description) || ''
+    };
+  }
+}
+
 //custom styles
 const customStyleFile = path.join(
   envPaths("oasis", { suffix: "" }).config,
@@ -240,6 +300,8 @@ const blockchainModel = require('../models/blockchain_model')({ cooler, isPublic
 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 }
+});
 
 // starting warmup
 about._startNameWarmup();
@@ -483,6 +545,7 @@ const { renderBlockchainView, renderSingleBlockView } = require("../views/blockc
 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");
 
 let sharp;
 
@@ -608,8 +671,8 @@ router
     const modules = [
     'popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet', 
     'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending', 
-    'events', 'tasks', 'market', 'tribes', 'governance', 'reports', 'opinions', 'transfers', 
-    'feed', 'pixelia', 'agenda', 'ai', 'forum', 'jobs', 'projects', 'banking'
+    'events', 'tasks', 'market', 'tribes', 'votes', 'reports', 'opinions', 'transfers', 
+    'feed', 'pixelia', 'agenda', 'ai', 'forum', 'jobs', 'projects', 'banking', 'parliament'
     ];
     const moduleStates = modules.reduce((acc, mod) => {
       acc[`${mod}Mod`] = configMods[`${mod}Mod`];
@@ -973,6 +1036,76 @@ router
     const currentUserId = SSBconfig.config.keys.id;
     ctx.body = await inhabitantsProfileView({ about, cv, feed }, currentUserId);
   })
+ .get('/parliament', async (ctx) => {
+    const mod = ctx.cookies.get('parliamentMod') || 'on';
+    if (mod !== 'on') { ctx.redirect('/modules'); return }
+    const filter = (ctx.query.filter || 'government').toLowerCase();
+    let governmentCard = await parliamentModel.getGovernmentCard();
+    if (!governmentCard || !governmentCard.end || moment().isAfter(moment(governmentCard.end))) {
+      await parliamentModel.resolveElection();
+      governmentCard = await parliamentModel.getGovernmentCard();
+    }
+    const [
+      candidatures, proposals, futureLaws, canPropose, laws,
+      historical, leaders, revocations, futureRevocations, revocationsEnactedCount,
+      inhabitantsAll
+      ] = await Promise.all([
+      parliamentModel.listCandidatures('OPEN'),
+      parliamentModel.listProposalsCurrent(),
+      parliamentModel.listFutureLawsCurrent(),
+      parliamentModel.canPropose(),
+      parliamentModel.listLaws(),
+      parliamentModel.listHistorical(),
+      parliamentModel.listLeaders(),
+      parliamentModel.listRevocationsCurrent(),
+      parliamentModel.listFutureRevocationsCurrent(),
+      parliamentModel.countRevocationsEnacted(),
+      inhabitantsModel.listInhabitants({ filter: 'all' })
+    ]); 
+    const inhabitantsTotal = Array.isArray(inhabitantsAll) ? inhabitantsAll.length : 0;
+    const leader = pickLeader(candidatures || []);
+    const leaderMeta = leader ? await parliamentModel.getActorMeta({ targetType: leader.targetType || leader.powerType || 'inhabitant', targetId: leader.targetId || leader.powerId }) : null;
+    const powerMeta = (governmentCard && (governmentCard.powerType === 'tribe' || governmentCard.powerType === 'inhabitant'))
+      ? await parliamentModel.getActorMeta({ targetType: governmentCard.powerType, targetId: governmentCard.powerId })
+      : null;
+    const historicalMetas = {};
+    for (const g of (historical || []).slice(0, 12)) {
+      if (g.powerType === 'tribe' || g.powerType === 'inhabitant') {
+        const k = `${g.powerType}:${g.powerId}`;
+        if (!historicalMetas[k]) {
+          historicalMetas[k] = await parliamentModel.getActorMeta({ targetType: g.powerType, targetId: g.powerId });
+        }
+      }
+    }
+    const leadersMetas = {};
+    for (const r of (leaders || []).slice(0, 20)) {
+      if (r.powerType === 'tribe' || r.powerType === 'inhabitant') {
+        const k = `${r.powerType}:${r.powerId}`;
+        if (!leadersMetas[k]) {
+          leadersMetas[k] = await parliamentModel.getActorMeta({ targetType: r.powerType, targetId: r.powerId });
+        }
+      }
+    }
+    const govWithPopulation = governmentCard ? { ...governmentCard, inhabitantsTotal } : { inhabitantsTotal };
+    ctx.body = await parliamentView({
+      filter,
+      governmentCard: govWithPopulation,
+      candidatures,
+      proposals,
+      futureLaws,
+      canPropose,
+      laws,
+      historical,
+      leaders,
+      leaderMeta,
+      powerMeta,
+      historicalMetas,
+      leadersMetas,
+      revocations,
+      futureRevocations,
+      revocationsEnactedCount
+    });
+  })
   .get('/tribes', async ctx => {
     const filter = ctx.query.filter || 'all';
     const search = ctx.query.search || ''; 
@@ -2563,6 +2696,48 @@ router
     await votesModel.createOpinion(voteId, category);
     ctx.redirect('/votes');
   })
+  .post('/parliament/candidatures/propose', koaBody(), async (ctx) => {
+    const { candidateId = '', method = '' } = ctx.request.body || {};
+    const id = String(candidateId || '').trim();
+    const m = String(method || '').trim().toUpperCase();
+    const ALLOWED = new Set(['DEMOCRACY','MAJORITY','MINORITY','DICTATORSHIP','KARMATOCRACY']);
+    if (!id) ctx.throw(400, 'Candidate is required.');
+    if (!ALLOWED.has(m)) ctx.throw(400, 'Invalid method.');
+    await parliamentModel.proposeCandidature({ candidateId: id, method: m }).catch(e => ctx.throw(400, String((e && e.message) || e)));
+    ctx.redirect('/parliament?filter=candidatures');
+  })
+  .post('/parliament/candidatures/:id/vote', koaBody(), async (ctx) => {
+    await parliamentModel.voteCandidature(ctx.params.id).catch(e => ctx.throw(400, String((e && e.message) || e)));
+    ctx.redirect('/parliament?filter=candidatures');
+  })
+  .post('/parliament/proposals/create', koaBody(), async (ctx) => {
+    const { title = '', description = '' } = ctx.request.body || {};
+    const t = String(title || '').trim();
+    const d = String(description || '').trim();
+    if (!t) ctx.throw(400, 'Title is required.');
+    if (d.length > 1000) ctx.throw(400, 'Description must be ≤ 1000 chars.');
+    await parliamentModel.createProposal({ title: t, description: d }).catch(e => ctx.throw(400, String((e && e.message) || e)));
+    ctx.redirect('/parliament?filter=proposals');
+  })
+  .post('/parliament/proposals/close/:id', koaBody(), async (ctx) => {
+    await parliamentModel.closeProposal(ctx.params.id).catch(e => ctx.throw(400, String((e && e.message) || e)));
+    ctx.redirect('/parliament?filter=proposals');
+  })
+  .post('/parliament/resolve', koaBody(), async (ctx) => {
+    await parliamentModel.resolveElection().catch(e => ctx.throw(400, String((e && e.message) || e)));
+    ctx.redirect('/parliament?filter=government');
+  })
+  .post('/parliament/revocations/create', koaBody(), async (ctx) => {
+    const body = ctx.request.body || {};
+    const rawLawId =
+      Array.isArray(body.lawId) ? body.lawId[0] :
+      (body.lawId ?? body['lawId[]'] ?? body.law_id ?? '');
+    const lawId = String(rawLawId || '').trim();
+    if (!lawId) ctx.throw(400, 'Law required');
+    const { title, reasons } = body;
+    await parliamentModel.createRevocation({ lawId, title, reasons });
+    ctx.redirect('/parliament?filter=revocations');
+  })
   .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');
@@ -3146,8 +3321,8 @@ router
     const modules = [
     'popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'invites', 'wallet',
     'legacy', 'cipher', 'bookmarks', 'videos', 'docs', 'audios', 'tags', 'images', 'trending',
-    'events', 'tasks', 'market', 'tribes', 'governance', 'reports', 'opinions', 'transfers',
-    'feed', 'pixelia', 'agenda', 'ai', 'forum', 'jobs', 'projects', 'banking'
+    'events', 'tasks', 'market', 'tribes', 'votes', 'reports', 'opinions', 'transfers',
+    'feed', 'pixelia', 'agenda', 'ai', 'forum', 'jobs', 'projects', 'banking', 'parliament'
     ];
     const currentConfig = getConfig();
     modules.forEach(mod => {

TEMPAT SAMPAH
src/client/assets/images/anarchy.png


TEMPAT SAMPAH
src/client/assets/images/democracy.png


TEMPAT SAMPAH
src/client/assets/images/dictatorship.png


TEMPAT SAMPAH
src/client/assets/images/karmatocracy.png


TEMPAT SAMPAH
src/client/assets/images/majority.png


TEMPAT SAMPAH
src/client/assets/images/minority.png


+ 68 - 0
src/client/assets/styles/style.css

@@ -2310,4 +2310,72 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0;
 .error-title{margin:0 0 6px 0;font-weight:600}
 .error-pre{margin:0;white-space:pre-wrap;font-family:monospace}
 
+/* parliament */
+.cycle-info {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: .75rem;
+  margin-bottom: 1rem;
+}
+.kpi {
+  border: 1px solid rgba(0,0,0,.08);
+  border-radius: 8px;
+  padding: .75rem;
+}
+.kpi__label {
+  display: block;
+  letter-spacing: .06em;
+  text-transform: uppercase;
+  color: var(--parl-accent, #ff9f1a);
+  opacity: 1;
+}
+.kpi__value {
+  font-weight: 600;
+}
+.method-badge,
+.method-hero {
+  display: inline-flex;
+  align-items: center;
+  gap: .5rem;
+}
+.method-cell {
+  text-align: center;
+}
+.leader-cell {
+  text-align: center;
+}
+.mt-2 { margin-top: .5rem; }
 
+.parliament-actor-table {
+  table-layout: auto;
+  width: 100%;
+}
+.parliament-actor-table th,
+.parliament-actor-table td {
+  vertical-align: top;
+}
+
+.parliament-actor-col {
+  white-space: nowrap;
+  width: 1%;
+  text-align: left;
+}
+.parliament-actor-col .user-link {
+  display: inline-block;
+}
+
+.parliament-description-col {
+  white-space: normal;
+  text-align: left;
+}
+
+.parliament-members-row td {
+  padding-top: .5rem;
+}
+.parliament-members-list {
+  margin: .25rem 0 0;
+  padding-left: 1.25rem;
+}
+.parliament-members-list li {
+  margin: .125rem 0;
+}

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

@@ -356,7 +356,7 @@ module.exports = {
     postLabel: "POSTS",
     aboutLabel: "INHABITANTS",
     feedLabel: "FEEDS",
-    votesLabel: "GOVERNANCE",
+    votesLabel: "VOTATIONS",
     reportLabel: "REPORTS",
     imageLabel: "IMAGES",
     videoLabel: "VIDEOS",
@@ -586,7 +586,7 @@ module.exports = {
     audioNoFile: "No audio file provided",
     audioNotSupported: "Your browser does not support the audio element.",
     noAudios: "No audios available.",
-    // inhabitants
+    //inhabitants
     yourContacts: "Your Contacts",
     allInhabitants: "Inhabitants",
     allCVs: "All CVs",
@@ -622,6 +622,144 @@ module.exports = {
     oasisId: "ID",
     noInhabitantsFound: "No inhabitants found, yet.",
     inhabitantActivityLevel: "Activity Level",
+    //parliament
+    parliamentTitle: "Parliament",
+    parliamentDescription: "Explore forms of government and collective management laws.",
+    parliamentFilterGovernment: "GOVERMENT",
+    parliamentFilterCandidatures: "CANDIDATURES",
+    parliamentFilterProposals: "PROPOSALS",
+    parliamentFilterLaws: "LAWS",
+    parliamentFilterHistorical: "HISTORICAL",
+    parliamentFilterLeaders: "LEADERS",
+    parliamentFilterRules: "RULES",
+    parliamentGovernmentCard: "Current Government",
+    parliamentGovMethod: "GOVERMENT METHOD",
+    parliamentActorInPowerInhabitant: 'INHABITANT RULING',
+    parliamentActorInPowerTribe: 'TRIBE RULING',
+    parliamentHistoricalGovernmentsTitle: 'GOVERMENTS',
+    parliamentHistoricalElectionsTitle: 'ELECTION CYCLES',
+    parliamentHistoricalLawsTitle: 'HISTORICAL',
+    parliamentHistoricalLeadersTitle: 'LEADERS',
+    parliamentThCycles: 'CYCLES',
+    parliamentThTimesInPower: 'RULING',
+    parliamentThTotalCandidatures: 'CANDIDATURES',
+    parliamentThProposed: 'LAWS PROPOSED',
+    parliamentThApproved: 'LAWS APPROVED',
+    parliamentThDeclined: 'LAWS DECLINED',
+    parliamentThDiscarded: 'LAWS DISCARDED',
+    parliamentVotesReceived: "VOTES RECIEVED",
+    parliamentMembers: "MEMBERS",
+    parliamentLegSince: "CYCLE SINCE",
+    parliamentLegEnd: "CYCLE END",
+    parliamentPoliciesProposal: "LAWS PROPOSAL",
+    parliamentPoliciesApproved: "LAWS APPROVED",
+    parliamentPoliciesDeclined: "LAWS DECLINED",
+    parliamentPoliciesDiscarded: "LAWS DISCARDED",
+    parliamentEfficiency: "% EFFICIENCY",
+    parliamentFilterRevocations: 'REVOCATIONS',
+    parliamentRevocationFormTitle: 'Revocate Law',
+    parliamentRevocationLaw: 'Law',
+    parliamentRevocationTitle: 'Title',
+    parliamentRevocationReasons: 'Reasons',
+    parliamentRevocationPublish: 'Publish Revocation',
+    parliamentCurrentRevocationsTitle: 'Current Revocations',
+    parliamentFutureRevocationsTitle: 'Future Revocations',
+    parliamentPoliciesRevocated: 'LAWS REVOCATED',
+    parliamentPoliciesTitle: 'HISTORICAL',
+    parliamentLawsTitle: 'LAWS APPROVED',
+    parliamentRulesRevocations: 'Any approved law can be revocate using current goverment method ruling.',
+    parliamentNoStableGov: "No goverment choosen, yet.",
+    parliamentNoGovernments: "There are not governments, yet.",
+    parliamentCandidatureFormTitle: "Propose Candidature",
+    parliamentCandidatureId: "Candidature",
+    parliamentCandidatureIdPh: "Oasis ID (@...) or Tribe name",
+    parliamentCandidatureSlogan: "Slogan (max 140 chars)",
+    parliamentCandidatureSloganPh: "A short motto",
+    parliamentCandidatureMethod: "Method",
+    parliamentCandidatureProposeBtn: "Publish Candidature",
+    parliamentThType: "Type",
+    parliamentThId: "ID",
+    parliamentThDate: "Proposal date",
+    parliamentThSlogan: "Slogan",
+    parliamentThMethod: "Method",
+    parliamentThKarma: "Karma",
+    parliamentThSince: "Profile since",
+    parliamentThVotes: "Votes recieved",
+    parliamentThVoteAction: "Vote",
+    parliamentTypeUser: "Inhabitant",
+    parliamentTypeTribe: "Tribe",
+    parliamentVoteBtn: "Vote",
+    parliamentProposalFormTitle: "Propose Law",
+    parliamentProposalTitle: "Title",
+    parliamentProposalDescription: "Description (≤1000)",
+    parliamentProposalPublish: "Publish Proposal",
+    parliamentOpenVote: "Open vote",
+    parliamentFinalize: "Finalize",
+    parliamentDeadline: "Deadline",
+    parliamentStatus: "Status",
+    parliamentNoProposals: "There are not law proposals, yet.",
+    parliamentNoLaws: "There are not laws approved, yet.",
+    parliamentLawMethod: "Method",
+    parliamentLawProposer: "Proposed by",
+    parliamentLawVotes: "YES/Total",
+    parliamentLawEnacted: "Enacted at",
+    parliamentMethodDEMOCRACY: "Democracy",
+    parliamentMethodMAJORITY: "Majority (80%)",
+    parliamentMethodMINORITY: "Minority (20%)",
+    parliamentMethodDICTATORSHIP: "Dictatorship",
+    parliamentMethodKARMATOCRACY: "Karmatocracy",
+    parliamentMethodANARCHY: "Anarchy",
+    parliamentThId: "ID",
+    parliamentThProposalDate: "Proposal date",
+    parliamentThMethod: "Method",
+    parliamentThKarma: "Karma",
+    parliamentThSupports: "Supports",
+    parliamentThVote: "Vote",
+    parliamentCurrentProposalsTitle: "Current Proposals",
+    parliamentVotesSlashTotal: "Votes/Total",
+    parliamentVotesNeeded: "Votes Needed",
+    parliamentFutureLawsTitle: "Future Laws",
+    parliamentNoFutureLaws: "No future laws, yet.",
+    parliamentVoteAction: "Vote",
+    parliamentLeadersTitle: "Leaders",
+    parliamentThLeader: "AVATAR",
+    parliamentPopulation: "Population",
+    parliamentThType: "Type",
+    parliamentThInPower: "In power",
+    parliamentThPresented: "CANDIDATURES",
+    parliamentNoLeaders: "No leaders, yet.",
+    typeParliament: "PARLIAMENT",
+    typeParliamentCandidature: "Parliament · Candidature",
+    typeParliamentTerm: "Parliament · Term",
+    typeParliamentProposal: "Parliament · Proposal",
+    typeParliamentLaw: "Parliament · New Law",
+    parliamentLawQuestion: "Question",
+    parliamentStatus: "Status",
+    parliamentCandidaturesListTitle: "List of Candidatures",
+    parliamentElectionsEnd: "Election end",
+    parliamentTimeRemaining: "Time remaining",
+    parliamentCurrentLeader: "Winning candidature",
+    parliamentNoLeader: "No winning candidature, yet.",
+    parliamentElectionsStatusTitle: "Next Government",
+    parliamentElectionsStart: "Election start",
+    parliamentProposalDeadlineLabel: "Deadline",
+    parliamentProposalTimeLeft: "Time left",
+    parliamentProposalOnTrack: "On track to pass",
+    parliamentProposalOffTrack: "Not enough support yet",
+    parliamentRulesTitle: "How Parliament works",
+    parliamentRulesIntro: "Election resolve every 2 months; candidatures are continuous and reset when a government is chosen.",
+    parliamentRulesCandidates: "Any inhabitant may propose themself, another inhabitant, or any tribe. Each inhabitant can propose up to 3 candidatures per cycle; duplicates in the same cycle are rejected.",
+    parliamentRulesElection: "The winner is the candidature with the most votes at resolution time.",
+    parliamentRulesTies: "Tie-break order: highest inhabitant karma; if tied, oldest profile; if still tied, earliest proposal; then lexicographic by ID.",
+    parliamentRulesFallback: "If no one votes, the latest proposed candidature wins. If there are no candidatures, a random tribe is selected.",
+    parliamentRulesTerm: "The Government tab shows the current government and stats.",
+    parliamentRulesMethods: "Forms: Anarchy (simple majority), Democracy (50%+1), Majority (80%), Minority (20%), Karmatocracy (highest-karma proposals), Dictatorship (instant approval).",
+    parliamentRulesAnarchy: "Anarchy is the default mode: if no candidature is elected at resolution, Anarchy is proclaimed. Under Anarchy, any inhabitant can propose laws.",
+    parliamentRulesProposals: "If you are the ruling inhabitant or a member of the ruling tribe, you can publish law proposals. Non-dictatorship methods create a public vote.",
+    parliamentRulesLimit: "Each inhabitant may publish at most 3 law proposals per cycle.",
+    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.",
     //trending
     trendingTitle: "Trending",
     exploreTrending: "Explore the most popular content in your network.",
@@ -633,7 +771,7 @@ module.exports = {
     transferButton: "TRANSFERS",
     eventButton: "EVENTS",
     taskButton: "TASKS",
-    votesButton: "GOVERNANCE",
+    votesButton: "VOTATIONS",
     reportButton: "REPORTS",
     feedButton: "FEED",
     marketButton: "MARKET",
@@ -837,15 +975,15 @@ module.exports = {
     transfersClosedSectionTitle: "Closed Transfers",
     transfersDiscardedSectionTitle: "Discarded Transfers",
     transfersAllSectionTitle: "Transfers",
-    //governance (voting/polls)
-    governanceTitle: "Governance",
-    governanceDescription: "Discover and manage votations in your network.",
+    //votations (voting/polls)
+    votationsTitle: "Votations",
+    votationsDescription: "Discover and manage votations in your network.",
     voteMineSectionTitle: "Your Votations",
     voteCreateSectionTitle: "Create Votation",
     voteUpdateSectionTitle: "Update",
     voteOpenTitle: "Open Votations",
     voteClosedTitle: "Closed Votations",
-    voteAllSectionTitle: "Governance",
+    voteAllSectionTitle: "Votations",
     voteCreateButton: "Create Votation",
     voteUpdateButton: "Update",
     voteDeleteButton: "Delete",
@@ -931,7 +1069,7 @@ module.exports = {
     blogImage: "Upload Image (jpeg, jpg, png, gif) (max-size: 500px x 400px)",
     blogPublish: "Preview",
     noPopularMessages: "No popular messages published, yet",
-    // forum
+    //forum
     forumTitle: "Forums",
     forumCategoryLabel: "Category",
     forumTitleLabel: "Title",
@@ -956,6 +1094,22 @@ module.exports = {
     forumVisitForum: "Visit Forum",
     noForums: "No forums found.",
     forumVisitButton: "Visit forum",
+    forumCatGENERAL: "General",
+    forumCatOASIS: "Oasis",
+    forumCatLARP: "L.A.R.P.",
+    forumCatPOLITICS: "Politics",
+    forumCatTECH: "Tech",
+    forumCatSCIENCE: "Science",
+    forumCatMUSIC: "Music",
+    forumCatART: "Art",
+    forumCatGAMING: "Gaming",
+    forumCatBOOKS: "Books",
+    forumCatFILMS: "Films",
+    forumCatPHILOSOPHY: "Philosophy",
+    forumCatSOCIETY: "Society",
+    forumCatPRIVACY: "Privacy",
+    forumCatCYBERWARFARE: "Cyberwarfare",
+    forumCatSURVIVALISM: "Survivalism",
     //images
     imageTitle: "Images",
     imagePluginTitle: "Title",
@@ -1047,25 +1201,25 @@ module.exports = {
     typeRecent:           "RECENT",
     errorActivity:        "Error retrieving activity",
     typePost:             "POST",
-    typeTribe:            "TRIBE",
-    typeAbout:            "INHABITANT",
+    typeTribe:            "TRIBES",
+    typeAbout:            "INHABITANTS",
     typeCurriculum:       "CV",
-    typeImage:            "IMAGE",
-    typeBookmark:         "BOOKMARK",
-    typeDocument:         "DOCUMENT",
-    typeVotes:            "GOVERNANCE",
-    typeAudio:            "AUDIO",
+    typeImage:            "IMAGES",
+    typeBookmark:         "BOOKMARKS",
+    typeDocument:         "DOCUMENTS",
+    typeVotes:            "VOTATIONS",
+    typeAudio:            "AUDIOS",
     typeMarket:           "MARKET",
-    typeJob:              "JOB",
-    typeProject:          "PROJECT",
-    typeVideo:            "VIDEO",
+    typeJob:              "JOBS",
+    typeProject:          "PROJECTS",
+    typeVideo:            "VIDEOS",
     typeVote:             "SPREAD",
-    typeEvent:            "EVENT",
+    typeEvent:            "EVENTS",
     typeTransfer:         "TRANSFER",
     typeTask:             "TASKS",
     typePixelia: 	  "PIXELIA",
     typeForum: 	          "FORUM",
-    typeReport:           "REPORT",
+    typeReport:           "REPORTS",
     typeFeed:             "FEED",
     typeContact:          "CONTACT",
     typePub:              "PUB",
@@ -1841,8 +1995,8 @@ module.exports = {
     modulesMarketDescription: "Module to exchange goods or services.",
     modulesTribesLabel: "Tribes",
     modulesTribesDescription: "Module to explore or create tribes (groups).",
-    modulesGovernanceLabel: "Governance",
-    modulesGovernanceDescription: "Module to discover and manage votes.",
+    modulesVotationsLabel: "Votations",
+    modulesVotationsDescription: "Module to discover and manage votations.",
     modulesReportsLabel: "Reports",
     modulesReportsDescription: "Module to manage and track reports related to issues, bugs, abuses, and content warnings.",
     modulesOpinionsLabel: "Opinions",
@@ -1851,6 +2005,8 @@ module.exports = {
     modulesTransfersDescription: "Module to discover and manage smart-contracts (transfers).",
     modulesFeedLabel: "Feed",
     modulesFeedDescription: "Module to discover and share short-texts (feeds).",
+    modulesParliamentLabel: "Parliament",
+    modulesParliamentDescription: "Module to elect governments and vote on laws.",
     modulesPixeliaLabel: "Pixelia",
     modulesPixeliaDescription: "Module to draw on a collaborative grid.",
     modulesAgendaLabel: "Agenda",

+ 195 - 39
src/client/assets/translations/oasis_es.js

@@ -69,7 +69,7 @@ module.exports = {
     readThread: "leer el resto del hilo",    
     // pixelia
     pixeliaTitle: 'Pixelia',
-    pixeliaDescription: 'Dibuja pixeles en un grid y colabor-ART con otros en tu red.',
+    pixeliaDescription: 'Dibuja pixeles en un grid y colabor-ART con habitantes en tu red.',
     coordLabel: 'Coordenadas (e.g., A3)',
     coordPlaceholder: 'Introduce coordenada',
     contributorsTitle: "Contribuciones",
@@ -305,7 +305,7 @@ module.exports = {
     searchStatusLabel:"Estado",
     statusLabel:"Estado",
     totalVotesLabel:"Votos Totales",
-    votesLabel:"Votos",
+    votesLabel:"Votaciones",
     noResultsFound:"No se han encontrado resultados.",
     author:"Autoría",
     createdAtLabel:"Creado el",
@@ -582,7 +582,7 @@ module.exports = {
     audioNoFile: "No se proporcionó ningún archivo de audio",
     audioNotSupported: "Tu navegador no soporta el elemento de audio.",
     noAudios: "No hay audios disponibles.",
-    // inhabitants
+    //inhabitants
     yourContacts:       "Tus Contactos",
     allInhabitants:     "Habitantes",
     allCVs:             "Todos los CVs",
@@ -618,11 +618,149 @@ module.exports = {
     oasisId: "ID",
     noInhabitantsFound:    "No se encontraron habitantes, aún.",
     inhabitantActivityLevel: "Nivel Actividad",
+    //parliament
+    parliamentTitle: "Parlamento",
+    parliamentDescription: "Explora formas de gobierno y políticas de gestión colectiva.",
+    parliamentFilterGovernment: "GOBIERNO",
+    parliamentFilterCandidatures: "CANDIDATURAS",
+    parliamentFilterProposals: "PROPUESTAS",
+    parliamentFilterLaws: "LEYES",
+    parliamentFilterHistorical: "HISTÓRICO",
+    parliamentFilterLeaders: "LÍDERES",
+    parliamentFilterRules: "REGLAS",
+    parliamentGovernmentCard: "Gobierno actual",
+    parliamentGovMethod: "MÉTODO DE GOBIERNO",
+    parliamentActorInPowerInhabitant: 'HABITANTE EN EL PODER',
+    parliamentActorInPowerTribe: 'TRIBU EN EL PODER',
+    parliamentHistoricalGovernmentsTitle: 'GOBIERNOS',
+    parliamentHistoricalElectionsTitle: 'CICLOS ELECTORALES',
+    parliamentHistoricalLawsTitle: 'HISTÓRICO',
+    parliamentHistoricalLeadersTitle: 'LÍDERES',
+    parliamentThCycles: 'CICLOS',
+    parliamentThTimesInPower: 'MANDATOS',
+    parliamentThTotalCandidatures: 'CANDIDATURAS',
+    parliamentThProposed: 'LEYES PROPUESTAS',
+    parliamentThApproved: 'LEYES APROBADAS',
+    parliamentThDeclined: 'LEYES RECHAZADAS',
+    parliamentThDiscarded: 'LEYES DESCARTADAS',
+    parliamentVotesReceived: "VOTOS RECIBIDOS",
+    parliamentMembers: "MIEMBROS",
+    parliamentLegSince: "INICIO DEL CICLO",
+    parliamentLegEnd: "FIN DEL CICLO",
+    parliamentPoliciesProposal: "PROPUESTAS DE LEYES",
+    parliamentPoliciesApproved: "LEYES APROBADAS",
+    parliamentPoliciesDeclined: "LEYES RECHAZADAS",
+    parliamentPoliciesDiscarded: "LEYES DESCARTADAS",
+    parliamentEfficiency: "% EFICIENCIA",
+    parliamentFilterRevocations: 'REVOCACIONES',
+    parliamentRevocationFormTitle: 'Revocar Ley',
+    parliamentRevocationLaw: 'Ley',
+    parliamentRevocationTitle: 'Título',
+    parliamentRevocationReasons: 'Motivos',
+    parliamentRevocationPublish: 'Publicar Revocación',
+    parliamentCurrentRevocationsTitle: 'Revocaciones Actuales',
+    parliamentFutureRevocationsTitle: 'Revocaciones Futuras',
+    parliamentPoliciesRevocated: 'LEYES REVOCADAS',
+    parliamentPoliciesTitle: 'HISTÓRICO',
+    parliamentLawsTitle: 'LEYES APROBADAS',
+    parliamentRulesRevocations: 'Cualquier ley aprobada puede ser revocada utilizando el método de gobierno vigente.',
+    parliamentNoStableGov: "Aún no hay un gobierno elegido.",
+    parliamentNoGovernments: "Todavía no hay gobiernos en el histórico.",
+    parliamentCandidatureFormTitle: "Proponer Candidatura",
+    parliamentCandidatureId: "Candidatura",
+    parliamentCandidatureIdPh: "Oasis ID (@...) o nombre de la tribu",
+    parliamentCandidatureSlogan: "Lema (máx. 140 car.)",
+    parliamentCandidatureSloganPh: "Un lema corto",
+    parliamentCandidatureMethod: "Método",
+    parliamentCandidatureProposeBtn: "Publicar Candidatura",
+    parliamentThType: "Tipo",
+    parliamentThId: "ID",
+    parliamentThDate: "Fecha de propuesta",
+    parliamentThSlogan: "Lema",
+    parliamentThMethod: "Método",
+    parliamentThKarma: "Karma",
+    parliamentThSince: "Perfil desde",
+    parliamentThVotes: "Votos recibidos",
+    parliamentThVoteAction: "Votar",
+    parliamentTypeUser: "Habitante",
+    parliamentTypeTribe: "Tribu",
+    parliamentVoteBtn: "Votar",
+    parliamentProposalFormTitle: "Proponer Ley",
+    parliamentProposalTitle: "Título",
+    parliamentProposalDescription: "Descripción (≤1000)",
+    parliamentProposalPublish: "Publicar Propuesta",
+    parliamentOpenVote: "Votación abierta",
+    parliamentFinalize: "Finalizar",
+    parliamentDeadline: "Fecha límite",
+    parliamentStatus: "Estado",
+    parliamentNoProposals: "Aún no hay propuestas de ley.",
+    parliamentNoLaws: "Aún no hay leyes aprobadas.",
+    parliamentLawMethod: "Método",
+    parliamentLawProposer: "Propuesta por",
+    parliamentLawVotes: "SÍ/Total",
+    parliamentLawEnacted: "Promulgada el",
+    parliamentMethodDEMOCRACY: "Democracia",
+    parliamentMethodMAJORITY: "Mayoría (80%)",
+    parliamentMethodMINORITY: "Minoría (20%)",
+    parliamentMethodDICTATORSHIP: "Dictadura",
+    parliamentMethodKARMATOCRACY: "Karmatocracia",
+    parliamentMethodANARCHY: "Anarquía",
+    parliamentThId: "ID",
+    parliamentThProposalDate: "Fecha de propuesta",
+    parliamentThMethod: "Método",
+    parliamentThKarma: "Karma",
+    parliamentThSupports: "Apoyos",
+    parliamentThVote: "Votar",
+    parliamentCurrentProposalsTitle: "Propuestas actuales",
+    parliamentVotesSlashTotal: "Votos/Total",
+    parliamentVotesNeeded: "Votos Necesarios",
+    parliamentFutureLawsTitle: "Futuras leyes",
+    parliamentNoFutureLaws: "Aún no hay futuras leyes.",
+    parliamentVoteAction: "Votar",
+    parliamentLeadersTitle: "Líderes",
+    parliamentThLeader: "AVATAR",
+    parliamentPopulation: "Población",
+    parliamentThType: "Tipo",
+    parliamentThInPower: "Gobiernos",
+    parliamentThPresented: "CANDIDATURAS",
+    parliamentNoLeaders: "Aún no hay líderes.",
+    typeParliament: "PARLAMENTO",
+    typeParliamentCandidature: "Parlamento · Candidatura",
+    typeParliamentTerm: "Parlamento · Ciclo",
+    typeParliamentProposal: "Parlamento · Propuesta",
+    typeParliamentLaw: "Parlamento · Nueva Ley",
+    parliamentLawQuestion: "Pregunta",
+    parliamentStatus: "Estado",
+    parliamentCandidaturesListTitle: "Lista de Candidaturas",
+    parliamentElectionsEnd: "Fin de las elecciones",
+    parliamentTimeRemaining: "Tiempo restante",
+    parliamentCurrentLeader: "Candidatura ganadora",
+    parliamentNoLeader: "Aún no hay candidatura ganadora.",
+    parliamentElectionsStatusTitle: "Próximo Gobierno",
+    parliamentElectionsStart: "Inicio de las Elecciones",
+    parliamentProposalDeadlineLabel: "Fecha límite",
+    parliamentProposalTimeLeft: "Tiempo restante",
+    parliamentProposalOnTrack: "Con apoyo suficiente",
+    parliamentProposalOffTrack: "Apoyo insuficiente",
+    parliamentRulesTitle: "Cómo funciona el Parlamento",
+    parliamentRulesIntro: "Las elecciones se resuelven cada 2 meses; las candidaturas son continuas y se reinician cuando se elige un gobierno.",
+    parliamentRulesCandidates: "Cualquier habitante puede proponerse a sí mismo, a otro habitante o a cualquier tribu. Cada habitante puede proponer hasta 3 candidaturas por ciclo; se rechazan duplicados en el mismo ciclo.",
+    parliamentRulesElection: "Gana la candidatura con más votos en el momento de la resolución.",
+    parliamentRulesTies: "Desempate: mayor karma del habitante; si persiste, perfil más antiguo; si persiste, propuesta más temprana; después, orden lexicográfico por ID.",
+    parliamentRulesFallback: "Si nadie vota, gana la última candidatura propuesta. Si no hay candidaturas, se selecciona una tribu al azar.",
+    parliamentRulesTerm: "La pestaña Gobierno muestra el gobierno actual y sus estadísticas.",
+    parliamentRulesMethods: "Formas: Anarquía (mayoría simple), Democracia (50%+1), Mayoría (80%), Minoría (20%), Karmatocracia (propuestas con mayor karma), Dictadura (aprobación instantánea).",
+    parliamentRulesAnarchy: "Anarquía es el modo por defecto: si al resolver no se elige ninguna candidatura, se proclama la Anarquía. En Anarquía, cualquier habitante puede proponer leyes.",
+    parliamentRulesProposals: "Si eres el habitante gobernante o miembro de la tribu gobernante, puedes publicar propuestas de ley. Los métodos no dictatoriales crean una votación pública.",
+    parliamentRulesLimit: "Cada habitante puede publicar como máximo 3 propuestas de ley por ciclo.",
+    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.",
     //trending
     trendingTitle: "Tendencias",
     exploreTrending: "Explora el contenido más popular en tu red.",
     ALLButton: "TODOS",
-    MINEButton: "MIAS",
+    MINEButton: "MÍAS",
     RECENTButton: "RECIENTES",
     TOPButton: "TOP",
     bookmarkButton: "MARCADORES",
@@ -696,7 +834,7 @@ module.exports = {
     taskUpdateButton: "Actualizar",
     taskDeleteButton: "Eliminar",
     taskFilterAll: "TODOS",
-    taskFilterMine: "MIAS",
+    taskFilterMine: "MÍAS",
     taskFilterOpen: "ABIERTAS",
     taskFilterInProgress: "EN-PROCESO",
     taskFilterClosed: "CERRADAS",
@@ -801,7 +939,7 @@ module.exports = {
     transfersFrom: "De",
     transfersTo: "A",
     transfersFilterAll: "TODOS",
-    transfersFilterMine: "MIAS",
+    transfersFilterMine: "MÍAS",
     transfersFilterMarket: "MERCADO",
     transfersFilterTop: "TOP",
     transfersFilterPending: "PENDIENTES",
@@ -833,9 +971,9 @@ module.exports = {
     transfersClosedSectionTitle: "Transferencias Cerradas",
     transfersDiscardedSectionTitle: "Transferencias Descartadas",
     transfersAllSectionTitle: "Transferencias",
-    //governance (voting/polls)
-    governanceTitle: "Gobierno",
-    governanceDescription: "Descubre y gestiona votaciones en tu red.",
+    //votations (voting/polls)
+    votationsTitle: "Votaciones",
+    votationsDescription: "Descubre y gestiona votaciones en tu red.",
     voteMineSectionTitle: "Tus Votaciones",
     voteCreateSectionTitle: "Crear Votación",
     voteUpdateSectionTitle: "Actualizar",
@@ -852,7 +990,7 @@ module.exports = {
     voteFollowMajority: "SEGUIR MAYORÍA",
     voteNotInterested: "SIN INTERÉS",
     voteFilterAll: "TODOS",
-    voteFilterMine: "MIAS",
+    voteFilterMine: "MÍAS",
     voteFilterOpen: "ABIERTAS",
     voteFilterClosed: "CERRADAS",
     voteQuestionLabel: "Pregunta",
@@ -934,9 +1072,9 @@ module.exports = {
     forumTitlePlaceholder: "Título del foro...",
     forumCreateButton: "Crear foro",
     forumCreateSectionTitle: "Crear foro",
-    forumDescription: "Habla abiertamente con otras habitantes de tu red.",
+    forumDescription: "Habla abiertamente con habitantes de tu red.",
     forumFilterAll: "TODOS",
-    forumFilterMine: "MIAS",
+    forumFilterMine: "MÍAS",
     forumFilterRecent: "RECIENTES",
     forumFilterTop: "TOP",
     forumMineSectionTitle: "Tus Foros",
@@ -952,6 +1090,22 @@ module.exports = {
     forumVisitForum: "Visitar Foro",
     noForums: "No hay foros disponibles, aún.",
     forumVisitButton: "Visitar foro", 
+    forumCatGENERAL: "General",
+    forumCatOASIS: "Oasis",
+    forumCatLARP: "L.A.R.P.",
+    forumCatPOLITICS: "Política",
+    forumCatTECH: "Tecnología",
+    forumCatSCIENCE: "Ciencia",
+    forumCatMUSIC: "Música",
+    forumCatART: "Arte",
+    forumCatGAMING: "Videojuegos",
+    forumCatBOOKS: "Libros",
+    forumCatFILMS: "Cine",
+    forumCatPHILOSOPHY: "Filosofía",
+    forumCatSOCIETY: "Sociedad",
+    forumCatPRIVACY: "Privacidad",
+    forumCatCYBERWARFARE: "Ciberguerra",
+    forumCatSURVIVALISM: "Supervivencia",
     // images
     imageTitle: "Imágenes",
     imagePluginTitle: "Título",
@@ -963,7 +1117,7 @@ module.exports = {
     imageUpdateSectionTitle: "Actualizar Imagen",
     imageAllSectionTitle: "Imágenes",
     imageFilterAll: "TODOS",
-    imageFilterMine: "MIAS",
+    imageFilterMine: "MÍAS",
     imageCreateButton: "Subir Imagen",
     imageEditDescription: "Edita los detalles de tu imagen.",
     imageCreateDescription: "Crea una imagen.",
@@ -1025,7 +1179,7 @@ module.exports = {
     activityList:         "Actividad",
     activityDesc:         "Consulta la actividad reciente de tu red.",
     allButton:            "TODOS",
-    mineButton:           "MÍO",
+    mineButton:           "MÍAS",
     noActions:            "No hay actividad disponible.",
     performed:            "→",
     from:                 "De",
@@ -1042,30 +1196,30 @@ module.exports = {
     playVideo:            "Reproducir vídeo",
     typeRecent:           "RECIENTE",
     errorActivity:        "Error al recuperar la actividad",
-    typePost:             "PUBLICACIÓN",
-    typeTribe:            "TRIBU",
-    typeAbout:            "HABITANTE",
-    typeCurriculum:       "CV",
-    typeImage:            "IMAGEN",
-    typeBookmark:         "MARCADOR",
-    typeDocument:         "DOCUMENTO",
-    typeVotes:            "GOBERNANZA",
-    typeAudio:            "AUDIO",
+    typePost:             "PUBLICACIONES",
+    typeTribe:            "TRIBUS",
+    typeAbout:            "HABITANTES",
+    typeCurriculum:       "CVs",
+    typeImage:            "IMAGENES",
+    typeBookmark:         "MARCADORES",
+    typeDocument:         "DOCUMENTOS",
+    typeVotes:            "VOTACIONES",
+    typeAudio:            "AUDIOS",
     typeMarket:           "MERCADO",
-    typeJob:              "TRABAJO",
-    typeProject:          "PROYECTO",
-    typeVideo:            "VÍDEO",
+    typeJob:              "TRABAJOS",
+    typeProject:          "PROYECTOS",
+    typeVideo:            "VÍDEOS",
     typeVote:             "DIFUSIÓN",
-    typeEvent:            "EVENTO",
-    typeTransfer:         "TRANSFERENCIA",
+    typeEvent:            "EVENTOS",
+    typeTransfer:         "TRANSFERENCIAS",
     typeTask:             "TAREAS",
     typePixelia:          "PIXELIA",
-    typeForum:            "FORO",
-    typeReport:           "REPORTE",
+    typeForum:            "FOROS",
+    typeReport:           "REPORTES",
     typeFeed:             "FEED",
-    typeContact:          "CONTACTO",
-    typePub:              "PUB",
-    typeTombstone:        "TOMBSTONE",
+    typeContact:          "CONTACTOS",
+    typePub:              "PUBs",
+    typeTombstone:        "TOMBSTONES",
     typeBanking:          "BANCA",
     activitySupport:      "Nueva alianza forjada",
     activityJoin:         "Nuevo PUB unido",
@@ -1175,7 +1329,7 @@ module.exports = {
     tribeviewTribeButton: "Visitar Tribu",
     tribeDescription: "Explora o crea tribus en tu red.",
     tribeFilterAll: "TODOS",
-    tribeFilterMine: "MIAS",
+    tribeFilterMine: "MÍAS",
     tribeFilterMembership: "MIEMBROS",
     tribeFilterRecent: "RECIENTES",
     tribeFilterLarp: "L.A.R.P.",
@@ -1283,7 +1437,7 @@ module.exports = {
     alreadyVoted:         "Ya has opinado.",
     noOpinionsFound:      "No se encontraron opiniones.",
     ALLButton:            "TODOS",
-    MINEButton:           "MIAS",
+    MINEButton:           "MÍAS",
     RECENTButton:         "RECIENTES",
     TOPButton:            "TOP",
     interestingButton:    "INTERESANTE",
@@ -1451,8 +1605,8 @@ module.exports = {
     statsInhabitant: "Estadísticas de Habitantes",
     statsDescription: "Descubre las estadísticas de tu red.",
     ALLButton: "TODOS",
-    MINEButton: "MIS",
-    TOMBSTONEButton: "LÁPIDAS",
+    MINEButton: "MÍAS",
+    TOMBSTONEButton: "TUMBAS",
     statsYou: "Tú",
     statsUserId: "ID de Oasis",
     statsCreatedAt: "Creado el",
@@ -1853,8 +2007,10 @@ module.exports = {
     modulesMarketDescription: "Módulo para intercambiar bienes o servicios.",
     modulesTribesLabel: "Tribus",
     modulesTribesDescription: "Módulo para explorar o crear tribus (grupos).",
-    modulesGovernanceLabel: "Gobierno",
-    modulesGovernanceDescription: "Módulo para descubrir y gestionar votos.",
+    modulesVotationsLabel: "Votaciones",
+    modulesVotationsDescription: "Módulo para descubrir y gestionar votaciones.",
+    modulesParliamentLabel: "Parlamento",
+    modulesParliamentDescription: "Módulo para elegir gobiernos y votar leyes.",
     modulesReportsLabel: "Informes",
     modulesReportsDescription: "Módulo para gestionar y hacer un seguimiento de informes relacionados con problemas, errores, abusos y advertencias de contenido.",
     modulesOpinionsLabel: "Opiniones",

+ 163 - 7
src/client/assets/translations/oasis_eu.js

@@ -305,7 +305,7 @@ module.exports = {
     searchStatusLabel:"Egoera",
     statusLabel:"Egoera",
     totalVotesLabel:"Bozkak Guztira",
-    votesLabel:"Bozkak",
+    votesLabel:"Bozketak",
     noResultsFound:"Emaitzik ez.",
     author:"Egilea",
     createdAtLabel:"Noiz",
@@ -583,7 +583,7 @@ module.exports = {
     audioNoFile: "Ez duzu audio fitxategirik eman",
     audioNotSupported: "Zure arakatzaileak ez du audio elementua jasaten.",
     noAudios: "Audiorik ez.",
-    // inhabitants
+    //inhabitants
     yourContacts:       "Zure Kontaktuak",
     allInhabitants:     "Bizilagunak",
     allCVs:             "CV guztiak",
@@ -619,6 +619,144 @@ module.exports = {
     oasisId: "ID-a",
     noInhabitantsFound:  "Bizilagunik ez, oraindik.",
     inhabitantActivityLevel: "Jarduera Maila",
+    //parliament
+    parliamentTitle: "Legebiltzarra",
+    parliamentDescription: "Gobernu formak eta kudeaketa kolektiboko politikak arakatu.",
+    parliamentFilterGovernment: "GOBERNUA",
+    parliamentFilterCandidatures: "KANDIDATURAK",
+    parliamentFilterProposals: "PROPOSAMENAK",
+    parliamentFilterLaws: "LEGEAK",
+    parliamentFilterHistorical: "HISTORIKOA",
+    parliamentFilterLeaders: "LIDERAK",
+    parliamentFilterRules: "ARAUAK",
+    parliamentGovernmentCard: "Uneko gobernua",
+    parliamentGovMethod: "GOBERNU METODOA",
+    parliamentActorInPowerInhabitant: 'BIZTANLEA AGINTEAN',
+    parliamentActorInPowerTribe: 'TRIBUA AGINTEAN',
+    parliamentHistoricalGovernmentsTitle: 'GOBERNUAK',
+    parliamentHistoricalElectionsTitle: 'HAUTESKUNDE ZIKLOAK',
+    parliamentHistoricalLawsTitle: 'HISTORIKOA',
+    parliamentHistoricalLeadersTitle: 'BURUZAGIAK',
+    parliamentThCycles: 'ZIKLOAK',
+    parliamentThTimesInPower: 'AGINTEAN',
+    parliamentThTotalCandidatures: 'KANDIDATURAK',
+    parliamentThProposed: 'PROPOSATUTAKO LEGEAK',
+    parliamentThApproved: 'ONARTUTAKO LEGEAK',
+    parliamentThDeclined: 'UKATUTAKO LEGEAK',
+    parliamentThDiscarded: 'BAZTERTUTAKO LEGEAK',
+    parliamentVotesReceived: "JASOTAKO BOTOAK",
+    parliamentMembers: "KIDEAK",
+    parliamentLegSince: "ZIKLOAREN HASIERA",
+    parliamentLegEnd: "ZIKLOAREN AMAIERA",
+    parliamentPoliciesProposal: "POLITIKA PROPOSAMENAK",
+    parliamentPoliciesApproved: "ONARTUTAKO LEGEAK",
+    parliamentPoliciesDeclined: "BAZTERTUTAKO LEGEAK",
+    parliamentPoliciesDiscarded: "POLITIKA BAZTERTUAK",
+    parliamentEfficiency: "% ERAGINKORTASUNA",
+    parliamentFilterRevocations: 'INDARGABETZEAK',
+    parliamentRevocationFormTitle: 'INDARGABETU',
+    parliamentRevocationLaw: 'Legea',
+    parliamentRevocationTitle: 'Izenburua',
+    parliamentRevocationReasons: 'Arrazoiak',
+    parliamentRevocationPublish: "Indargabetzea Argitaratu",
+    parliamentCurrentRevocationsTitle: 'Uneko Indargabetzeak',
+    parliamentFutureRevocationsTitle: 'Etorkizuneko Indargabetzeak',
+    parliamentPoliciesRevocated: 'HISTORIKOA',
+    parliamentPoliciesTitle: 'HISTORICAL',
+    parliamentLawsTitle: 'ONARTUTAKO LEGEAK',
+    parliamentRulesRevocations: 'Onartutako edozein lege indargabetu daiteke indarrean dagoen gobernu metodoa erabiliz.',
+    parliamentNoStableGov: "Oraindik ez da gobernurik hautatu.",
+    parliamentNoGovernments: "Oraindik ez dago gobernurik.",
+    parliamentCandidatureFormTitle: "Kandidatura Proposatu",
+    parliamentCandidatureId: "Kandidatura",
+    parliamentCandidatureIdPh: "Oasis ID (@...) edo tribuaren izena",
+    parliamentCandidatureSlogan: "Leloa (geh. 140 karaktere)",
+    parliamentCandidatureSloganPh: "Lelo labur bat",
+    parliamentCandidatureMethod: "Metodoa",
+    parliamentCandidatureProposeBtn: "Hautagaitza Proposatu",
+    parliamentThType: "Mota",
+    parliamentThId: "ID",
+    parliamentThDate: "Proposamen data",
+    parliamentThSlogan: "Leloa",
+    parliamentThMethod: "Metodoa",
+    parliamentThKarma: "Karma",
+    parliamentThSince: "Profila noiztik",
+    parliamentThVotes: "Jasotako botoak",
+    parliamentThVoteAction: "Bozkatu",
+    parliamentTypeUser: "Biztanlea",
+    parliamentTypeTribe: "Tribu",
+    parliamentVoteBtn: "Bozkatu",
+    parliamentProposalFormTitle: "Legea Proposatu",
+    parliamentProposalTitle: "Izenburua",
+    parliamentProposalDescription: "Deskribapena (≤1000)",
+    parliamentProposalPublish: "Proposamena Argitaratu",
+    parliamentOpenVote: "Bozketa irekia",
+    parliamentFinalize: "Amaitu",
+    parliamentDeadline: "Epea",
+    parliamentStatus: "Egoera",
+    parliamentNoProposals: "Oraindik ez dago lege-proposamenik.",
+    parliamentNoLaws: "Oraindik ez dago onartutako legerik.",
+    parliamentLawMethod: "Metodoa",
+    parliamentLawProposer: "Nork proposatua",
+    parliamentLawVotes: "BAI/Guztira",
+    parliamentLawEnacted: "Indarrean sartu den data",
+    parliamentMethodDEMOCRACY: "Demokrazia",
+    parliamentMethodMAJORITY: "Gehiengoa (80%)",
+    parliamentMethodMINORITY: "Gutxiengoa (20%)",
+    parliamentMethodDICTATORSHIP: "Diktadura",
+    parliamentMethodKARMATOCRACY: "Karmatokrazia",
+    parliamentMethodANARCHY: "Anarkia",
+    parliamentThId: "ID",
+    parliamentThProposalDate: "Proposamenaren data",
+    parliamentThMethod: "Metodoa",
+    parliamentThKarma: "Karma",
+    parliamentThSupports: "Babesak",
+    parliamentThVote: "Bozkatu",
+    parliamentCurrentProposalsTitle: "Uneko proposamenak",
+    parliamentVotesSlashTotal: "Botoak/Guztira",
+    parliamentVotesNeeded: "Beharrezko Botoak",
+    parliamentFutureLawsTitle: "Etorkizuneko legeak",
+    parliamentNoFutureLaws: "Oraindik ez dago etorkizuneko legerik",
+    parliamentVoteAction: "Bozkatu",
+    parliamentLeadersTitle: "Liderak",
+    parliamentThLeader: "AVATAR",
+    parliamentPopulation: "Biztanleria",
+    parliamentThType: "Mota",
+    parliamentThInPower: "Agintean",
+    parliamentThPresented: "KANDIDATURAK",
+    parliamentNoLeaders: "Oraindik ez dago liderrik.",
+    typeParliament: "PARLAMENTU",
+    typeParliamentCandidature: "Parlamentu · Hautagaitza",
+    typeParliamentTerm: "Parlamentu · Zikloa",
+    typeParliamentProposal: "Parlamentu · Proposamena",
+    typeParliamentLaw: "Parlamentu · Lege Berria",
+    parliamentLawQuestion: "Galdera",
+    parliamentStatus: "Egoera",
+    parliamentCandidaturesListTitle: "Kandidaturen Zerrenda",
+    parliamentElectionsEnd: "Hauteskundeen amaiera",
+    parliamentTimeRemaining: "Geratzen den denbora",
+    parliamentCurrentLeader: "Irabazten ari den kandidatura",
+    parliamentNoLeader: "Oraindik ez dago irabazten ari den kandidaturarik.",
+    parliamentElectionsStatusTitle: "Hurrengo Gobernua",
+    parliamentElectionsStart: "Hauteskundeen hasiera",
+    parliamentProposalDeadlineLabel: "Epemuga",
+    parliamentProposalTimeLeft: "Geratzen den denbora",
+    parliamentProposalOnTrack: "Onartzeko bidean",
+    parliamentProposalOffTrack: "Euskarri nahikorik ez",
+    parliamentRulesTitle: "Legebiltzarrak nola funtzionatzen duen",
+    parliamentRulesIntro: "Hauteskundeak 2 hilean behin ebazten dira; hautagaitzak etengabeak dira eta gobernua aukeratzean berrabiarazten dira.",
+    parliamentRulesCandidates: "Edozein biztanlek bere burua, beste biztanle bat edo edozein tribu proposa dezake. Biztanle bakoitzak gehienez 3 hautagaitza proposa ditzake ziklo bakoitzean; ziklo bereko bikoiztuak baztertzen dira.",
+    parliamentRulesElection: "Irabazlea ebazpen unean boto gehien dituen hautagaitza da.",
+    parliamentRulesTies: "Berdinketa haustea: biztanlearen karma handiena; berdin jarraituz gero, profilaren antzinatasun handiena; oraindik berdin, proposamen goiztiarrena; ondoren, IDaren hurrenkera lexikografikoa.",
+    parliamentRulesFallback: "Inork bozkatzen ez badu, azken proposatutako hautagaitzak irabazten du. Hautagaitzarik ez badago, ausazko tribu bat hautatzen da.",
+    parliamentRulesTerm: "Gobernua fitxak uneko gobernua eta estatistikak erakusten ditu.",
+    parliamentRulesMethods: "Ereduak: Anarkia (gehiengo sinplea), Demokrazia (%50+1), Gehiengoa (%80), Gutxiengoa (%20), Karmatokrazia (karma handiena duten proposamenak), Diktadura (berehalako onarpena).",
+    parliamentRulesAnarchy: "Anarkia modu lehenetsia da: ebazpenean ez bada kandidaturarik hautatzen, Anarkia ezartzen da. Anarkian, edozein biztanlek legeak proposa ditzake.",
+    parliamentRulesProposals: "Agintean dagoen biztanlea bazara edo aginteko tribuko kidea bazara, lege-proposamenak argitaratu ditzakezu. Diktaduraz besteko metodoek boz publiko bat sortzen dute.",
+    parliamentRulesLimit: "Biztanle bakoitzak gehienez 3 proposamen argitaratu ditzake ziklo bakoitzean.",
+    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.",
     //trending
     trendingTitle: "Pil-pilean",
     exploreTrending: "Aurkitu pil-pileko edukia zure sarean.",
@@ -834,9 +972,9 @@ module.exports = {
     transfersClosedSectionTitle: "Transferentziak Itxita",
     transfersDiscardedSectionTitle: "Transferentziak Baztertuta",
     transfersAllSectionTitle: "Transferentziak",
-    //governance (voting/polls)
-    governanceTitle: "Gobernua",
-    governanceDescription: "Aurkitu eta kudeatu bozketak zure sarean.",
+    //votations (voting/polls)
+    votationsTitle: "Bozketak",
+    votationsDescription: "Aurkitu eta kudeatu bozketak zure sarean.",
     voteMineSectionTitle: "Zeure Bozketak",
     voteCreateSectionTitle: "Sortu Bozketa",
     voteUpdateSectionTitle: "Eguneratu",
@@ -953,6 +1091,22 @@ module.exports = {
     forumVisitForum: "Bisitatu Foroaren",
     noForums: "Ez da fororik aurkitu.",
     forumVisitButton: "Foroa bisitatu", 
+    forumCatGENERAL: "Orokorra",
+    forumCatOASIS: "Oasis",
+    forumCatLARP: "L.A.R.P.",
+    forumCatPOLITICS: "Politika",
+    forumCatTECH: "Teknologia",
+    forumCatSCIENCE: "Zientzia",
+    forumCatMUSIC: "Musika",
+    forumCatART: "Artea",
+    forumCatGAMING: "Bideojokoak",
+    forumCatBOOKS: "Liburuak",
+    forumCatFILMS: "Filmak",
+    forumCatPHILOSOPHY: "Filosofia",
+    forumCatSOCIETY: "Gizartea",
+    forumCatPRIVACY: "Pribatutasuna",
+    forumCatCYBERWARFARE: "Zibergerra",
+    forumCatSURVIVALISM: "Biziraupena",
     // images
     imageTitle: "Irudiak",
     imagePluginTitle: "Izenburua",
@@ -1852,8 +2006,10 @@ module.exports = {
     modulesMarketDescription: "Ondasun edo zerbitzuak trukatzeko modulua.",
     modulesTribesLabel: "Tribuak",
     modulesTribesDescription: "Tribuak (taldeak) esploratu edo sortzeko modulua.",
-    modulesGovernanceLabel: "Gobernua",
-    modulesGovernanceDescription: "Bozketak aurkitu eta kudeatzeko modulua.",
+    modulesVotationsLabel: "Bozketak",
+    modulesVotationsDescription: "Bozketak aurkitu eta kudeatzeko modulua.", 
+    modulesParliamentLabel: "Legebiltzarra",
+    modulesParliamentDescription: "Gobernuak hautatu eta legeak bozkatzeko modulua.",
     modulesReportsLabel: "Txostenak",
     modulesReportsDescription: "Arazo, akats, abusu eta eduki-abisuetan erlazionatutako txostenak kudeatzeko modulua.",
     modulesOpinionsLabel: "Iritziak",

+ 163 - 7
src/client/assets/translations/oasis_fr.js

@@ -305,7 +305,7 @@ module.exports = {
     searchStatusLabel:"État",
     statusLabel:"État",
     totalVotesLabel:"Total des votes",
-    votesLabel:"Votes",
+    votesLabel:"Votations",
     noResultsFound:"Aucun résultat trouvé.",
     author:"Auteur",
     createdAtLabel:"Créé le",
@@ -582,7 +582,7 @@ module.exports = {
     audioNoFile: "Aucun fichier audio fourni",
     audioNotSupported: "Votre navigateur ne prend pas en charge l’élément audio.",
     noAudios: "Aucun audio disponible.",
-    // inhabitants
+    //inhabitants
     yourContacts:       "Vos contacts",
     allInhabitants:     "Habitants",
     allCVs:             "Tous les CV",
@@ -618,6 +618,144 @@ module.exports = {
     oasisId: "ID",
     noInhabitantsFound:    "Aucun habitant trouvé pour l’instant.",
     inhabitantActivityLevel: "Niveau Activité",
+    //parliament
+    parliamentTitle: "Parlement",
+    parliamentDescription: "Explorez les formes de gouvernement et les politiques de gestion collective.",
+    parliamentFilterGovernment: "GOUVERNEMENT",
+    parliamentFilterCandidatures: "CANDIDATURES",
+    parliamentFilterProposals: "PROPOSITIONS",
+    parliamentFilterLaws: "LOIS",
+    parliamentFilterHistorical: "HISTORIQUE",
+    parliamentFilterLeaders: "DIRIGEANTS",
+    parliamentFilterRules: "RÈGLES",
+    parliamentGovernmentCard: "Gouvernement actuel",
+    parliamentGovMethod: "MÉTHODE DE GOUVERNEMENT",
+    parliamentActorInPowerInhabitant: 'HABITANT AU POUVOIR',
+    parliamentActorInPowerTribe: 'TRIBU AU POUVOIR',
+    parliamentHistoricalGovernmentsTitle: 'GOUVERNEMENTS',
+    parliamentHistoricalElectionsTitle: 'CYCLES ÉLECTORAUX',
+    parliamentHistoricalLawsTitle: 'HISTORIQUE',
+    parliamentHistoricalLeadersTitle: 'DIRIGEANTS',
+    parliamentThCycles: 'CYCLES',
+    parliamentThTimesInPower: 'AU POUVOIR',
+    parliamentThTotalCandidatures: 'CANDIDATURES',
+    parliamentThProposed: 'LOIS PROPOSÉES',
+    parliamentThApproved: 'LOIS APPROUVÉES',
+    parliamentThDeclined: 'LOIS REJETÉES',
+    parliamentThDiscarded: 'LOIS ÉCARTÉES',
+    parliamentVotesReceived: "VOTES REÇUS",
+    parliamentMembers: "MEMBRES",
+    parliamentLegSince: "DÉBUT DU CYCLE",
+    parliamentLegEnd: "FIN DU CYCLE",
+    parliamentPoliciesProposal: "PROPOSITIONS DE LOIS",
+    parliamentPoliciesApproved: "LOIS APPROUVÉES",
+    parliamentPoliciesDeclined: "LOIS REJETÉES",
+    parliamentPoliciesDiscarded: "LOIS REJETÉES",
+    parliamentEfficiency: "% EFFICACITÉ",
+    parliamentFilterRevocations: 'RÉVOCATIONS',
+    parliamentRevocationFormTitle: 'Révoquer la Loi',
+    parliamentRevocationLaw: 'Loi',
+    parliamentRevocationTitle: 'Titre',
+    parliamentRevocationReasons: 'Motifs',
+    parliamentRevocationPublish: "Publier la Révocation",
+    parliamentCurrentRevocationsTitle: 'Révocations en Cours',
+    parliamentFutureRevocationsTitle: 'Révocations à Venir',
+    parliamentPoliciesRevocated: 'HISTORIQUE',
+    parliamentPoliciesTitle: 'HISTÓRICO',
+    parliamentLawsTitle: 'LOIS APPROUVÉES',
+    parliamentRulesRevocations: 'Toute loi approuvée peut être révoquée en utilisant la méthode de gouvernement en vigueur.',
+    parliamentNoStableGov: "Aucun gouvernement choisi pour l’instant.",
+    parliamentNoGovernments: "Il n’y a pas encore de gouvernements.",
+    parliamentCandidatureFormTitle: "Proposer une Candidature",
+    parliamentCandidatureId: "Candidature",
+    parliamentCandidatureIdPh: "ID Oasis (@...) ou nom de la tribu",
+    parliamentCandidatureSlogan: "Slogan (max 140 car.)",
+    parliamentCandidatureSloganPh: "Un court slogan",
+    parliamentCandidatureMethod: "Méthode",
+    parliamentCandidatureProposeBtn: "Proposer une Candidature",
+    parliamentThType: "Type",
+    parliamentThId: "ID",
+    parliamentThDate: "Date de proposition",
+    parliamentThSlogan: "Slogan",
+    parliamentThMethod: "Méthode",
+    parliamentThKarma: "Karma",
+    parliamentThSince: "Profil depuis",
+    parliamentThVotes: "Votes reçus",
+    parliamentThVoteAction: "Voter",
+    parliamentTypeUser: "Habitant",
+    parliamentTypeTribe: "Tribu",
+    parliamentVoteBtn: "Voter",
+    parliamentProposalFormTitle: "Proposer une Loi",
+    parliamentProposalTitle: "Titre",
+    parliamentProposalDescription: "Description (≤1000)",
+    parliamentProposalPublish: "Publier la Proposition",
+    parliamentOpenVote: "Vote ouvert",
+    parliamentFinalize: "Finaliser",
+    parliamentDeadline: "Date limite",
+    parliamentStatus: "Statut",
+    parliamentNoProposals: "Il n’y a pas encore de propositions de loi.",
+    parliamentNoLaws: "Il n’y a pas encore de lois approuvées.",
+    parliamentLawMethod: "Méthode",
+    parliamentLawProposer: "Proposée par",
+    parliamentLawVotes: "OUI/Total",
+    parliamentLawEnacted: "Promulguée le",
+    parliamentMethodDEMOCRACY: "Démocratie",
+    parliamentMethodMAJORITY: "Majorité (80%)",
+    parliamentMethodMINORITY: "Minorité (20%)",
+    parliamentMethodDICTATORSHIP: "Dictature",
+    parliamentMethodKARMATOCRACY: "Karmatocratie",
+    parliamentMethodANARCHY: "Anarchie",
+    parliamentThId: "ID",
+    parliamentThProposalDate: "Date de proposition",
+    parliamentThMethod: "Méthode",
+    parliamentThKarma: "Karma",
+    parliamentThSupports: "Soutiens",
+    parliamentThVote: "Voter",
+    parliamentCurrentProposalsTitle: "Propositions en cours",
+    parliamentVotesSlashTotal: "Votes/Total",
+    parliamentVotesNeeded: "Votes Requis",
+    parliamentFutureLawsTitle: "Lois futures",
+    parliamentNoFutureLaws: "Pas encore de lois futures",
+    parliamentVoteAction: "Voter",
+    parliamentLeadersTitle: "Dirigeants",
+    parliamentThLeader: "AVATAR",
+    parliamentPopulation: "Population",
+    parliamentThType: "Type",
+    parliamentThInPower: "Au pouvoir",
+    parliamentThPresented: "CANDIDATURES",
+    parliamentNoLeaders: "Aucun dirigeant",
+    typeParliament: "PARLEMENT",
+    typeParliamentCandidature: "Parlement · Candidature",
+    typeParliamentTerm: "Parlement · Mandat",
+    typeParliamentProposal: "Parlement · Proposition",
+    typeParliamentLaw: "Parlement · Nouvelle Loi",
+    parliamentLawQuestion: "Question",
+    parliamentStatus: "Statut",
+    parliamentCandidaturesListTitle: "Liste des Candidatures",
+    parliamentElectionsEnd: "Fin des élections",
+    parliamentTimeRemaining: "Temps restant",
+    parliamentCurrentLeader: "Candidature gagnante",
+    parliamentNoLeader: "Pas encore de dirigeants.",
+    parliamentElectionsStatusTitle: "Prochain Gouvernement",
+    parliamentElectionsStart: "Début des élections",
+    parliamentProposalDeadlineLabel: "Date limite",
+    parliamentProposalTimeLeft: "Temps restant",
+    parliamentProposalOnTrack: "En bonne voie pour être adoptée",
+    parliamentProposalOffTrack: "Soutien insuffisant",
+    parliamentRulesTitle: "Fonctionnement du Parlement",
+    parliamentRulesIntro: "Les élections se résolvent tous les 2 mois ; les candidatures sont continues et sont réinitialisées lorsqu’un gouvernement est choisi.",
+    parliamentRulesCandidates: "Tout habitant peut se proposer, proposer un autre habitant ou n’importe quelle tribu. Chaque habitant peut proposer jusqu’à 3 candidatures par cycle ; les doublons dans le même cycle sont rejetés.",
+    parliamentRulesElection: "Gagne la candidature ayant le plus de votes au moment de la résolution.",
+    parliamentRulesTies: "En cas d’égalité : karma d’habitant le plus élevé ; sinon, profil le plus ancien ; sinon, proposition la plus précoce ; puis ordre lexicographique par ID.",
+    parliamentRulesFallback: "Si personne ne vote, la dernière candidature proposée gagne. S’il n’y a aucune candidature, une tribu aléatoire est sélectionnée.",
+    parliamentRulesTerm: "L’onglet Gouvernement affiche le gouvernement actuel et ses statistiques.",
+    parliamentRulesMethods: "Formes : Anarchie (majorité simple), Démocratie (50 % + 1), Majorité (80 %), Minorité (20 %), Karmatocratie (propositions au karma le plus élevé), Dictature (approbation instantanée).",
+    parliamentRulesAnarchy: "L’Anarchie est le mode par défaut : si aucune candidature n’est élue à la résolution, l’Anarchie est proclamée. En Anarchie, tout habitant peut proposer des lois.",
+    parliamentRulesProposals: "Si vous êtes l’habitant au pouvoir ou membre de la tribu au pouvoir, vous pouvez publier des propositions de loi. Les méthodes non dictatoriales ouvrent un vote public.",
+    parliamentRulesLimit: "Chaque habitant peut publier au maximum 3 loi propositions par cycle.",
+    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é.",
     //trending
     trendingTitle: "Tendances",
     exploreTrending: "Explorez le contenu le plus populaire dans votre réseau.",
@@ -833,9 +971,9 @@ module.exports = {
     transfersClosedSectionTitle: "Transferts fermés",
     transfersDiscardedSectionTitle: "Transferts rejetés",
     transfersAllSectionTitle: "Transferts",
-    //governance (voting/polls)
-    governanceTitle: "Gouvernance",
-    governanceDescription: "Découvrez et gérez les votes dans votre réseau.",
+    //votations (voting/polls)
+    votationsTitle: "Votations",
+    votationsDescription: "Découvrez et gérez les votes dans votre réseau.",
     voteMineSectionTitle: "Vos votes",
     voteCreateSectionTitle: "Créer un vote",
     voteUpdateSectionTitle: "Mettre à jour",
@@ -952,6 +1090,22 @@ module.exports = {
     forumVisitForum: "Visiter le forum",
     noForums: "Aucun forum disponible pour l’instant.",
     forumVisitButton: "Visiter le forum",
+    forumCatGENERAL: "Général",
+    forumCatOASIS: "Oasis",
+    forumCatLARP: "L.A.R.P.",
+    forumCatPOLITICS: "Politique",
+    forumCatTECH: "Technologie",
+    forumCatSCIENCE: "Science",
+    forumCatMUSIC: "Musique",
+    forumCatART: "Art",
+    forumCatGAMING: "Jeux vidéo",
+    forumCatBOOKS: "Livres",
+    forumCatFILMS: "Films",
+    forumCatPHILOSOPHY: "Philosophie",
+    forumCatSOCIETY: "Société",
+    forumCatPRIVACY: "Vie privée",
+    forumCatCYBERWARFARE: "Cyberguerre",
+    forumCatSURVIVALISM: "Survivalisme",
     // images
     imageTitle: "Images",
     imagePluginTitle: "Titre",
@@ -1853,8 +2007,10 @@ module.exports = {
     modulesMarketDescription: "Module pour échanger des biens ou services.",
     modulesTribesLabel: "Tribus",
     modulesTribesDescription: "Module pour explorer ou créer des tribus (groupes).",
-    modulesGovernanceLabel: "Gouvernance",
-    modulesGovernanceDescription: "Module pour découvrir et gérer les votes.",
+    modulesVotationsLabel: "Votations",
+    modulesVotationsDescription: "Module pour découvrir et gérer les votations.",  
+    modulesParliamentLabel: "Parlement",
+    modulesParliamentDescription: "Module pour élire des gouvernements et voter des lois.",
     modulesReportsLabel: "Rapports",
     modulesReportsDescription: "Module pour gérer et suivre les rapports liés aux problèmes, erreurs, abus et avertissements de contenu.",
     modulesOpinionsLabel: "Opinions",

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

@@ -30,7 +30,7 @@ if (!fs.existsSync(configFilePath)) {
       "tasksMod": "on",
       "marketMod": "on",
       "tribesMod": "on",
-      "governanceMod": "on",
+      "votesMod": "on",
       "reportsMod": "on",
       "opinionsMod": "on",
       "transfersMod": "on",
@@ -41,7 +41,8 @@ if (!fs.existsSync(configFilePath)) {
       "forumMod": "on",
       "jobsMod": "on",
       "projectsMod": "on",
-      "bankingMod": "on"
+      "bankingMod": "on",
+      "parliamentMod": "on"
     },
     "wallet": {
       "url": "http://localhost:7474",

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

@@ -23,7 +23,7 @@
     "eventsMod": "on",
     "tasksMod": "on",
     "marketMod": "on",
-    "governanceMod": "on",
+    "votesMod": "on",
     "tribesMod": "on",
     "reportsMod": "on",
     "opinionsMod": "on",
@@ -35,7 +35,8 @@
     "forumMod": "on",
     "jobsMod": "on",
     "projectsMod": "on",
-    "bankingMod": "on"
+    "bankingMod": "on",
+    "parliamentMod": "on"
   },
   "wallet": {
     "url": "http://localhost:7474",

+ 4 - 0
src/models/activity_model.js

@@ -166,6 +166,10 @@ module.exports = ({ cooler }) => {
       else if (filter === 'all') out = deduped;
       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)
+      );
       else out = deduped.filter(a => a.type === filter);
 
       out.sort((a, b) => (b.ts || 0) - (a.ts || 0));

File diff ditekan karena terlalu besar
+ 1048 - 0
src/models/parliament_model.js


+ 47 - 4
src/models/votes_model.js

@@ -50,12 +50,14 @@ module.exports = ({ cooler }) => {
       const c = oldMsg.content;
       if (c.type !== 'votes') throw new Error('Invalid type');
       if (c.createdBy !== userId) throw new Error('Not the author');
+
       let newDeadline = c.deadline;
       if (deadline != null && deadline !== '') {
         const parsed = moment(deadline, moment.ISO_8601, true);
         if (!parsed.isValid() || parsed.isBefore(moment())) throw new Error('Invalid deadline');
         newDeadline = parsed.toISOString();
       }
+
       let newOptions = c.options;
       let newVotesMap = c.votes;
       let newTotalVotes = c.totalVotes;
@@ -71,10 +73,12 @@ module.exports = ({ cooler }) => {
         newVotesMap = newOptions.reduce((acc, opt) => (acc[opt] = 0, acc), {});
         newTotalVotes = 0;
       }
+
       const newTags =
         Array.isArray(tags) ? tags.filter(Boolean)
         : typeof tags === 'string' ? tags.split(',').map(t => t.trim()).filter(Boolean)
         : c.tags || [];
+
       const updated = {
         ...c,
         replaces: id,
@@ -95,11 +99,14 @@ module.exports = ({ cooler }) => {
       const vote = await new Promise((res, rej) => ssb.get(id, (err, vote) => err ? rej(new Error('Vote not found')) : res(vote)));
       if (!vote.content.options.includes(choice)) throw new Error('Invalid choice');
       if (vote.content.voters.includes(userId)) throw new Error('Already voted');
+
       vote.content.votes[choice] += 1;
       vote.content.voters.push(userId);
       vote.content.totalVotes += 1;
+
       const tombstone = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId };
       const updated = { ...vote.content, updatedAt: new Date().toISOString(), replaces: id };
+
       await new Promise((res, rej) => ssb.publish(tombstone, err => err ? rej(err) : res()));
       return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result)));
     },
@@ -107,16 +114,50 @@ module.exports = ({ cooler }) => {
     async getVoteById(id) {
       const ssb = await openSsb();
       const now = moment();
-      const vote = await new Promise((res, rej) => ssb.get(id, (err, vote) => err ? rej(new Error('Vote not found')) : res(vote)));
-      const c = vote.content;
-      const status = c.status === 'OPEN' && moment(c.deadline).isBefore(now) ? 'CLOSED' : c.status;
-      return { id, ...c, status };
+
+      const results = await new Promise((resolve, reject) => {
+        pull(
+          ssb.createLogStream({ limit: logLimit }),
+          pull.collect((err, arr) => err ? reject(err) : resolve(arr))
+        );
+      });
+
+      const votesByKey = new Map();
+      const latestByRoot = new Map();
+
+      for (const r of results) {
+        const key = r.key;
+        const v = r.value;
+        const c = v && v.content;
+        if (!c) continue;
+        if (c.type === 'votes') {
+          votesByKey.set(key, c);
+          const ts = Number(v.timestamp || r.timestamp || Date.now());
+          const root = c.replaces || key;
+          const prev = latestByRoot.get(root);
+          if (!prev || ts > prev.ts) latestByRoot.set(root, { key, ts });
+        }
+      }
+
+      const latestEntry = latestByRoot.get(id);
+      let latestId = latestEntry ? latestEntry.key : id;
+      let content = votesByKey.get(latestId);
+
+      if (!content) {
+        const orig = await new Promise((res, rej) => ssb.get(id, (err, vote) => err ? rej(new Error('Vote not found')) : res(vote)));
+        content = orig.content;
+        latestId = id;
+      }
+
+      const status = content.status === 'OPEN' && moment(content.deadline).isBefore(now) ? 'CLOSED' : content.status;
+      return { id, latestId, ...content, status };
     },
 
     async listAll(filter = 'all') {
       const ssb = await openSsb();
       const userId = ssb.id;
       const now = moment();
+
       return new Promise((resolve, reject) => {
         pull(ssb.createLogStream({ limit: logLimit }), 
         pull.collect((err, results) => {
@@ -153,6 +194,7 @@ module.exports = ({ cooler }) => {
       const userId = ssb.id;
       const vote = await new Promise((res, rej) => ssb.get(id, (err, vote) => err ? rej(new Error('Vote not found')) : res(vote)));
       if (vote.content.opinions_inhabitants.includes(userId)) throw new Error('Already voted');
+
       const tombstone = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId };
       const updated = {
         ...vote.content,
@@ -161,6 +203,7 @@ module.exports = ({ cooler }) => {
         updatedAt: new Date().toISOString(),
         replaces: id
       };
+
       await new Promise((res, rej) => ssb.publish(tombstone, err => err ? rej(err) : res()));
       return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result)));
     }

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

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

+ 1 - 1
src/server/package.json

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

+ 171 - 33
src/views/activity_view.js

@@ -41,7 +41,16 @@ function renderActionCards(actions, userId) {
       ? a({ href: `/author/${encodeURIComponent(action.author)}` }, action.author)
       : 'unknown';
     const type = action.type || 'unknown';
-    const typeLabel = i18n[`type${capitalize(type)}`] || type;
+
+    let headerText;
+    if (type.startsWith('parliament')) {
+      const sub = type.replace('parliament', '');
+      headerText = `[PARLIAMENT · ${sub.toUpperCase()}]`;
+    } else {
+      const typeLabel = i18n[`type${capitalize(type)}`] || type;
+      headerText = `[${String(typeLabel).toUpperCase()}]`;
+    }
+
     const content = action.content || {};
     const cardBody = [];
 
@@ -643,12 +652,132 @@ function renderActionCards(actions, userId) {
       );
     }
 
+    if (type === 'parliamentCandidature') {
+      const { targetType, targetId, targetTitle, method, votes, proposer } = content;
+      const link = targetType === 'tribe'
+        ? a({ href: `/tribe/${encodeURIComponent(targetId)}`, class: 'user-link' }, targetTitle || targetId)
+        : a({ href: `/author/${encodeURIComponent(targetId)}`, class: 'user-link' }, targetId);
+
+      const methodUpper = String(
+        i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
+      ).toUpperCase();
+
+      cardBody.push(
+        div({ class: 'card-section parliament' },
+          div({ class: 'card-field' }, span({ class: 'card-label' }, (String(i18n.parliamentCandidatureId || 'Candidature').toUpperCase()) + ':'), span({ class: 'card-value' }, link)),
+          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'METHOD') + ':'), span({ class: 'card-value' }, methodUpper)),
+          typeof votes !== 'undefined'
+            ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesReceived || 'VOTES RECEIVED') + ':'), span({ class: 'card-value' }, String(votes)))
+            : ''
+        )
+      );
+    }
+
+    if (type === 'parliamentTerm') {
+      const { method, powerType, powerId, powerTitle, winnerVotes, totalVotes, startAt, endAt } = content;
+      const powerTypeNorm = String(powerType || '').toLowerCase();
+      const winnerLink =
+        powerTypeNorm === 'tribe'
+          ? a({ href: `/tribe/${encodeURIComponent(powerId)}`, class: 'user-link' }, powerTitle || powerId)
+          : powerTypeNorm === 'none' || !powerTypeNorm
+            ? a({ href: `/parliament?filter=government`, class: 'user-link' }, (i18n.parliamentAnarchy || 'ANARCHY'))
+            : a({ href: `/author/${encodeURIComponent(powerId)}`, class: 'user-link' }, powerTitle || powerId);
+
+      const methodUpper = String(
+        i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
+      ).toUpperCase();
+
+      cardBody.push(
+        div({ class: 'card-section parliament' },
+          startAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsStart.toUpperCase() || 'Elections start') + ':'), span({ class: 'card-value' }, new Date(startAt).toLocaleString())) : '',
+          endAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsEnd.toUpperCase() || 'Elections end') + ':'), span({ class: 'card-value' }, new Date(endAt).toLocaleString())) : '',
+          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentCurrentLeader.toUpperCase() || 'Winning candidature') + ':'), span({ class: 'card-value' }, winnerLink)),
+          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod.toUpperCase() || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
+          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesReceived.toUpperCase() || 'Votes received') + ':'), span({ class: 'card-value' }, `${Number(winnerVotes || 0)} (${Number(totalVotes || 0)})`))
+        )
+      );
+    }
+
+    if (type === 'parliamentProposal') {
+      const { title, description, method, status, voteId, createdAt } = content;
+
+      const methodUpper = String(
+        i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
+      ).toUpperCase();
+
+      cardBody.push(
+        div({ class: 'card-section parliament' },
+          title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '',
+          description ? p({ style: 'margin:.4rem 0' }, description) : '',
+          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
+          createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '',
+          voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '',
+          status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentStatus.toUpperCase() || 'Status') + ':'), span({ class: 'card-value' }, status)) : ''
+        )
+      );
+    }
+    
+    if (type === 'parliamentRevocation') {
+      const { title, reasons, method, status, voteId, createdAt } = content;
+      const methodUpper = String(
+        i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
+      ).toUpperCase();
+
+      cardBody.push(
+        div({ class: 'card-section parliament' },
+          title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '',
+          reasons ? p({ style: 'margin:.4rem 0' }, reasons) : '',
+          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
+          createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '',
+          voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '',
+          status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentStatus.toUpperCase() || 'Status') + ':'), span({ class: 'card-value' }, status)) : ''
+        )
+      );
+    }
+
+    if (type === 'parliamentLaw') {
+      const { question, description, method, proposer, enactedAt, votes } = content;
+      const yes = Number(votes?.YES || 0);
+      const total = Number(votes?.total || votes?.TOTAL || 0);
+
+      const methodUpper = String(
+        i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
+      ).toUpperCase();
+
+      cardBody.push(
+        div({ class: 'card-section parliament' },
+          question ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawQuestion || 'Question') + ':'), span({ class: 'card-value' }, question)) : '',
+          description ? p({ style: 'margin:.4rem 0' }, description) : '',
+          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
+          proposer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawProposer || 'Proposer') + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(proposer)}`, class: 'user-link' }, proposer))) : '',
+          enactedAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawEnacted || 'Enacted at') + ':'), span({ class: 'card-value' }, new Date(enactedAt).toLocaleString())) : '',
+          (total || yes) ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawVotes || 'Votes') + ':'), span({ class: 'card-value' }, `${yes}/${total}`)) : ''
+        )
+      );
+    }
+
+    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=laws';
+    const parliamentFilter = isParliamentTarget ? (viewHref.split('filter=')[1] || '') : '';
+
     return div({ class: 'card card-rpg' },
       div({ class: 'card-header' },
-        h2({ class: 'card-label' }, `[${typeLabel}]`),
+        h2({ class: 'card-label' }, headerText),
         type !== 'feed' && type !== 'aiExchange' && type !== 'bankWallet' && (!action.tipId || action.tipId === action.id)
-          ? form({ method: "GET", action: getViewDetailsAction(type, action) },
-              button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+          ? (
+              isParliamentTarget
+                ? 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)
+                  )
             )
           : ''
       ),
@@ -662,35 +791,40 @@ function renderActionCards(actions, userId) {
 }
 
 function getViewDetailsAction(type, action) {
-  const id = encodeURIComponent(action.tipId || action.id);
-  switch (type) {
-    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)}` : ''}`;
-  }
+    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`;
+    }
 }
 
 exports.activityView = (actions, filter, userId) => {
@@ -706,6 +840,7 @@ exports.activityView = (actions, filter, userId) => {
     { type: 'project',   label: i18n.typeProject },
     { type: 'job',       label: i18n.typeJob },
     { type: 'transfer',  label: i18n.typeTransfer },
+    { type: 'parliament',label: i18n.typeParliament },
     { type: 'votes',     label: i18n.typeVotes },
     { type: 'event',     label: i18n.typeEvent },
     { type: 'task',      label: i18n.typeTask },
@@ -734,6 +869,8 @@ exports.activityView = (actions, filter, userId) => {
     filteredActions = actions.filter(action => action.type !== 'tombstone' && action.ts && now - action.ts < 24 * 60 * 60 * 1000);
   } else if (filter === 'banking') {
     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 {
     filteredActions = actions.filter(action => (action.type === filter || filter === 'all') && action.type !== 'tombstone');
   }
@@ -810,3 +947,4 @@ exports.activityView = (actions, filter, userId) => {
   }
   return html;
 };
+

+ 24 - 17
src/views/forum_view.js

@@ -12,6 +12,10 @@ const BASE_FILTERS = ['hot','all','mine','recent','top'];
 const CAT_BLOCK1 = ['GENERAL','OASIS','L.A.R.P.','POLITICS','TECH'];
 const CAT_BLOCK2 = ['SCIENCE','MUSIC','ART','GAMING','BOOKS','FILMS'];
 const CAT_BLOCK3 = ['PHILOSOPHY','SOCIETY','PRIVACY','CYBERWARFARE','SURVIVALISM'];
+const ALL_CATS = [...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3];
+
+const catKey = (c) => 'forumCat' + String(c || '').replace(/\./g,'').replace(/[\s-]/g,'').toUpperCase();
+const catLabel = (c) => i18n[catKey(c)] || c;
 
 const Z = 1.96;
 function wilsonScore(pos, neg) {
@@ -29,7 +33,7 @@ function getFilteredForums(filter, forums) {
   if (filter === 'hot')     return forums
     .filter(f => new Date(f.createdAt).getTime() >= now - 86400000)
     .sort((a,b) => b.score - a.score);
-  if ([...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3].includes(filter))
+  if (ALL_CATS.includes(filter))
     return forums.filter(f => f.category === filter);
   return forums;
 }
@@ -40,7 +44,7 @@ const generateFilterButtons = (filters, currentFilter, action, i18nMap = {}) =>
       form({ method: 'GET', action },
         input({ type: 'hidden', name: 'filter', value: mode }),
         button({ type: 'submit', class: currentFilter === mode ? 'filter-btn active' : 'filter-btn' },
-          i18nMap[mode] || mode.toUpperCase()
+          String(i18nMap[mode] || mode).toUpperCase()
         )
       )
     )
@@ -71,9 +75,7 @@ const renderForumForm = () =>
     form({ action: '/forum/create', method: 'POST' },
       label(i18n.forumCategoryLabel), br(),
       select({ name: 'category', required: true },
-        [...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3].map(cat =>
-          option({ value: cat }, cat)
-        )
+        ALL_CATS.map(cat => option({ value: cat }, catLabel(cat)))
       ), br(), br(),
       label(i18n.forumTitleLabel), br(),
       input({
@@ -173,7 +175,7 @@ const renderForumList = (forums, currentFilter) =>
               a({
                 class: 'forum-category',
                 href: `/forum?filter=${encodeURIComponent(f.category)}`
-              }, `[${f.category}]`),
+              }, `[${catLabel(f.category)}]`),
               a({
                 class: 'forum-title',
                 href: `/forum/${encodeURIComponent(f.key)}`
@@ -220,8 +222,9 @@ const renderForumList = (forums, currentFilter) =>
       : p(i18n.noForums)
   );
 
-exports.forumView = async (forums, currentFilter) =>
-  template(i18n.forumTitle,
+exports.forumView = async (forums, currentFilter) => {
+  const CAT_I18N_MAP_UP = ALL_CATS.reduce((m,c)=>{ m[c]=(catLabel(c)||c).toUpperCase(); return m; },{});
+  return template(i18n.forumTitle,
     section(
       div({ class: 'tags-header' },
         h2(currentFilter === 'create'
@@ -237,9 +240,9 @@ exports.forumView = async (forums, currentFilter) =>
           recent: i18n.forumFilterRecent,
           top: i18n.forumFilterTop
         }),
-        generateFilterButtons(CAT_BLOCK1, currentFilter, '/forum'),
-        generateFilterButtons(CAT_BLOCK2, currentFilter, '/forum'),
-        generateFilterButtons(CAT_BLOCK3, currentFilter, '/forum'),
+        generateFilterButtons(CAT_BLOCK1, currentFilter, '/forum', CAT_I18N_MAP_UP),
+        generateFilterButtons(CAT_BLOCK2, currentFilter, '/forum', CAT_I18N_MAP_UP),
+        generateFilterButtons(CAT_BLOCK3, currentFilter, '/forum', CAT_I18N_MAP_UP),
         renderCreateForumButton()
       ),
       currentFilter === 'create'
@@ -250,9 +253,11 @@ exports.forumView = async (forums, currentFilter) =>
         )
     )
   );
+};
 
-exports.singleForumView = async (forum, messagesData, currentFilter) =>
-  template(forum.title,
+exports.singleForumView = async (forum, messagesData, currentFilter) => {
+  const CAT_I18N_MAP_UP = ALL_CATS.reduce((m,c)=>{ m[c]=(catLabel(c)||c).toUpperCase(); return m; },{});
+  return template(forum.title,
     section(
       div({ class: 'tags-header' },
         h2(i18n.forumTitle),
@@ -260,14 +265,15 @@ exports.singleForumView = async (forum, messagesData, currentFilter) =>
       ),
       div({ class: 'mode-buttons' },
         generateFilterButtons(BASE_FILTERS, currentFilter, '/forum', {
+          hot: i18n.forumFilterHot,
           all: i18n.forumFilterAll,
           mine: i18n.forumFilterMine,
           recent: i18n.forumFilterRecent,
           top: i18n.forumFilterTop
         }),
-        generateFilterButtons(CAT_BLOCK1, currentFilter, '/forum'),
-        generateFilterButtons(CAT_BLOCK2, currentFilter, '/forum'),
-        generateFilterButtons(CAT_BLOCK3, currentFilter, '/forum'),
+        generateFilterButtons(CAT_BLOCK1, currentFilter, '/forum', CAT_I18N_MAP_UP),
+        generateFilterButtons(CAT_BLOCK2, currentFilter, '/forum', CAT_I18N_MAP_UP),
+        generateFilterButtons(CAT_BLOCK3, currentFilter, '/forum', CAT_I18N_MAP_UP),
         renderCreateForumButton()
       )
     ),
@@ -292,7 +298,7 @@ exports.singleForumView = async (forum, messagesData, currentFilter) =>
             a({
               class: 'forum-category',
               href: `/forum?filter=${encodeURIComponent(forum.category)}`
-            }, `[${forum.category}]`),
+            }, `[${catLabel(forum.category)}]`),
             a({
               class: 'forum-title',
               href: `/forum/${encodeURIComponent(forum.key)}`
@@ -353,4 +359,5 @@ exports.singleForumView = async (forum, messagesData, currentFilter) =>
       ...renderThread(messagesData.messages, 0, forum.key)
     )
   );
+};
 

+ 15 - 5
src/views/main_views.js

@@ -274,16 +274,25 @@ const renderTribesLink = () => {
   return tribesMod 
     ? [
         navLink({ href: "/tribes", emoji: "ꖥ", text: i18n.tribesTitle, class: "tribes-link enabled" }),
+      ]
+    : '';
+};
+
+const renderParliamentLink = () => {
+  const parliamentMod = getConfig().modules.parliamentMod === 'on';
+  return parliamentMod 
+    ? [
+        navLink({ href: "/parliament", emoji: "ꗞ", text: i18n.parliamentTitle, class: "parliament-link enabled" }),
         hr(),
       ]
     : '';
 };
 
-const renderGovernanceLink = () => {
-  const governanceMod = getConfig().modules.governanceMod === 'on';
-  return governanceMod 
+const renderVotationsLink = () => {
+  const votesMod = getConfig().modules.votesMod === 'on';
+  return votesMod 
     ? [
-       navLink({ href: "/votes", emoji: "ꔰ", text: i18n.governanceTitle, class: "votes-link enabled" }),
+       navLink({ href: "/votes", emoji: "ꔰ", text: i18n.votationsTitle, class: "votations-link enabled" }),
       ]
     : '';
 };
@@ -460,7 +469,8 @@ const template = (titlePrefix, ...elements) => {
               renderPopularLink(),
               navLink({ href: "/inhabitants", emoji: "ꖘ", text: i18n.inhabitantsLabel }),
               renderTribesLink(),
-              renderGovernanceLink(),
+              renderParliamentLink(),
+              renderVotationsLink(),
               renderEventsLink(),
               renderTasksLink(),
               renderReportsLink(),

+ 2 - 1
src/views/modules_view.js

@@ -15,7 +15,6 @@ const modulesView = () => {
     { name: 'events', label: i18n.modulesEventsLabel, description: i18n.modulesEventsDescription },
     { name: 'feed', label: i18n.modulesFeedLabel, description: i18n.modulesFeedDescription },
     { name: 'forum', label: i18n.modulesForumLabel, description: i18n.modulesForumDescription },
-    { name: 'governance', label: i18n.modulesGovernanceLabel, description: i18n.modulesGovernanceDescription },
     { name: 'images', label: i18n.modulesImagesLabel, description: i18n.modulesImagesDescription },
     { name: 'invites', label: i18n.modulesInvitesLabel, description: i18n.modulesInvitesDescription },
     { name: 'jobs', label: i18n.modulesJobsLabel, description: i18n.modulesJobsDescription },
@@ -24,6 +23,7 @@ const modulesView = () => {
     { name: 'market', label: i18n.modulesMarketLabel, description: i18n.modulesMarketDescription },
     { name: 'multiverse', label: i18n.modulesMultiverseLabel, description: i18n.modulesMultiverseDescription },
     { name: 'opinions', label: i18n.modulesOpinionsLabel, description: i18n.modulesOpinionsDescription },
+    { name: 'parliament', label: i18n.modulesParliamentLabel, description: i18n.modulesParliamentDescription },
     { name: 'pixelia', label: i18n.modulesPixeliaLabel, description: i18n.modulesPixeliaDescription },
     { name: 'projects', label: i18n.modulesProjectsLabel, description: i18n.modulesProjectsDescription },
     { name: 'popular', label: i18n.modulesPopularLabel, description: i18n.modulesPopularDescription },
@@ -36,6 +36,7 @@ const modulesView = () => {
     { name: 'trending', label: i18n.modulesTrendingLabel, description: i18n.modulesTrendingDescription },
     { name: 'tribes', label: i18n.modulesTribesLabel, description: i18n.modulesTribesDescription },
     { name: 'videos', label: i18n.modulesVideosLabel, description: i18n.modulesVideosDescription },
+    { name: 'votes', label: i18n.modulesVotationsLabel, description: i18n.modulesVotationsDescription },
     { name: 'wallet', label: i18n.modulesWalletLabel, description: i18n.modulesWalletDescription },
     { name: 'topics', label: i18n.modulesTopicsLabel, description: i18n.modulesTopicsDescription }
   ];

+ 872 - 0
src/views/parliament_view.js

@@ -0,0 +1,872 @@
+const { form, button, div, h2, p, section, input, label, br, a, span, table, thead, tbody, tr, th, td, textarea, select, option, ul, li, img } = require('../server/node_modules/hyperaxe');
+const moment = require("../server/node_modules/moment");
+const { template, i18n } = require('./main_views');
+
+const fmt = (d) => moment(d).format('YYYY-MM-DD HH:mm:ss');
+const timeLeft = (end) => {
+  const diff = moment(end).diff(moment());
+  if (diff <= 0) return '0d 00:00:00';
+  const dur = moment.duration(diff);
+  const d = Math.floor(dur.asDays());
+  const h = String(dur.hours()).padStart(2,'0');
+  const m = String(dur.minutes()).padStart(2,'0');
+  const s = String(dur.seconds()).padStart(2,'0');
+  return `${d}d ${h}:${m}:${s}`;
+};
+const reqVotes = (method, total) => {
+  const m = String(method || '').toUpperCase();
+  if (m === 'DEMOCRACY' || m === 'ANARCHY') return Math.floor(Number(total || 0) / 2) + 1;
+  if (m === 'MAJORITY') return Math.ceil(Number(total || 0) * 0.8);
+  if (m === 'MINORITY') return Math.ceil(Number(total || 0) * 0.2);
+  return 0;
+};
+const showVoteMetrics = (method) => {
+  const m = String(method || '').toUpperCase();
+  return !(m === 'DICTATORSHIP' || m === 'KARMATOCRACY');
+};
+
+const applyEl = (fn, attrs, kids) => fn.apply(null, [attrs || {}].concat(kids || []));
+
+const methodImageSrc = (method) => `assets/images/${String(method || '').toUpperCase().toLowerCase()}.png`;
+
+const MethodBadge = (method) => {
+  const m = String(method || '').toUpperCase();
+  const label = String(i18n[`parliamentMethod${m}`] || m).toUpperCase();
+  return span(
+    { class: 'method-badge' },
+    label,
+    br(),br(),
+    img({ src: methodImageSrc(m), alt: label, class: 'method-badge__icon' })
+  );
+};
+
+const MethodHero = (method) => {
+  const m = String(method || '').toUpperCase();
+  const label = String(i18n[`parliamentMethod${m}`] || m).toUpperCase();
+  return span(
+    { class: 'method-hero' },
+    label,
+    br(),br(),
+    img({ src: methodImageSrc(m), alt: label, class: 'method-hero__icon' })
+  );
+};
+
+const KPI = (label, value) =>
+  div({ class: 'kpi' },
+    span({ class: 'kpi__label' }, label),
+    span({ class: 'kpi__value' }, value)
+  );
+
+const CycleInfo = (start, end, labels = {
+  since: i18n.parliamentLegSince,
+  end: i18n.parliamentLegEnd,
+  remaining: i18n.parliamentTimeRemaining
+}) =>
+  div({ class: 'cycle-info' },
+    KPI((labels.since + ': ').toUpperCase(), fmt(start)),
+    KPI((labels.end + ': ').toUpperCase(), fmt(end)),
+    KPI((labels.remaining + ': ').toUpperCase(), timeLeft(end))
+  );
+
+const Tabs = (active) =>
+  div(
+    { class: 'filters' },
+    form(
+      { method: 'GET', action: '/parliament' },
+      ['government', 'candidatures', 'proposals', 'laws', 'revocations', 'historical', 'leaders', 'rules'].map(f =>
+        button({ type: 'submit', name: 'filter', value: f, class: active === f ? 'filter-btn active' : 'filter-btn' }, i18n[`parliamentFilter${f.charAt(0).toUpperCase()+f.slice(1)}`])
+      )
+    )
+  );
+
+const GovHeader = (g) => {
+  const termStart = g && g.since ? g.since : moment().toISOString();
+  const termEnd = g && g.end ? g.end : moment(termStart).add(1, 'minutes').toISOString();
+  const methodKeyRaw = g && g.method ? String(g.method) : 'ANARCHY';
+  const methodKey = methodKeyRaw.toUpperCase();
+  const i18nMeth = i18n[`parliamentMethod${methodKey}`];
+  const methodLabel = (i18nMeth && String(i18nMeth).trim() ? String(i18nMeth) : methodKey).toUpperCase();
+  const isAnarchy = methodKey === 'ANARCHY';
+  const population = String(Number(g.inhabitantsTotal || 0));
+  const votesReceivedNum = Number.isFinite(Number(g.votesReceived)) ? Number(g.votesReceived) : 0;
+  const totalVotesNum = Number.isFinite(Number(g.totalVotes)) ? Number(g.totalVotes) : 0;
+  const votesDisplay = `${votesReceivedNum} (${totalVotesNum})`;
+
+  return div(
+    { class: 'cycle-info' },
+    div({ class: 'kpi' },
+      span({ class: 'kpi__label' }, (i18n.parliamentLegSince + ': ').toUpperCase()),
+      span({ class: 'kpi__value' }, fmt(termStart))
+    ),
+    div({ class: 'kpi' },
+      span({ class: 'kpi__label' }, (i18n.parliamentLegEnd + ': ').toUpperCase()),
+      span({ class: 'kpi__value' }, fmt(termEnd))
+    ),
+    div({ class: 'kpi' },
+      span({ class: 'kpi__label' }, (i18n.parliamentTimeRemaining + ': ').toUpperCase()),
+      span({ class: 'kpi__value' }, timeLeft(termEnd))
+    ),
+    div({ class: 'kpi' },
+      span({ class: 'kpi__label' }, (i18n.parliamentPopulation + ': ').toUpperCase()),
+      span({ class: 'kpi__value' }, population)
+    ),
+    div({ class: 'kpi' },
+      span({ class: 'kpi__label' }, (i18n.parliamentGovMethod + ': ').toUpperCase()),
+      span({ class: 'kpi__value' }, methodLabel)
+    ),
+    !isAnarchy
+      ? div({ class: 'kpi' },
+          span({ class: 'kpi__label' }, (i18n.parliamentVotesReceived + ': ').toUpperCase()),
+          span({ class: 'kpi__value' }, votesDisplay)
+        )
+      : null
+  );
+};
+
+const GovernmentCard = (g, meta) => {
+  const termStart = g && g.since ? g.since : moment().toISOString();
+  const termEnd   = g && g.end   ? g.end   : moment(termStart).add(1, 'minutes').toISOString();
+
+  const actorLabel =
+    g.powerType === 'tribe'
+      ? (i18n.parliamentActorInPowerTribe || i18n.parliamentActorInPower || 'TRIBE RULING')
+      : (i18n.parliamentActorInPowerInhabitant || i18n.parliamentActorInPower || 'INHABITANT RULING');
+
+  const methodKeyRaw = g && g.method ? String(g.method) : 'ANARCHY';
+  const methodKey    = methodKeyRaw.toUpperCase();
+  const i18nMeth     = i18n[`parliamentMethod${methodKey}`];
+  const methodLabel  = (i18nMeth && String(i18nMeth).trim() ? String(i18nMeth) : methodKey).toUpperCase();
+
+  const actorLink =
+    g.powerType === 'tribe'
+      ? a({ class: 'user-link', href: `/tribe/${encodeURIComponent(g.powerId)}` }, g.powerTitle || g.powerId)
+      : a({ class: 'user-link', href: `/author/${encodeURIComponent(g.powerId)}` }, g.powerTitle || g.powerId);
+
+  const actorBio = meta && meta.bio ? meta.bio : '';
+  const memberIds = Array.isArray(g.membersList) ? g.membersList : (Array.isArray(g.members) ? g.members : []);
+
+  const membersRow =
+    g.powerType === 'tribe'
+      ? tr(
+          { class: 'parliament-members-row' },
+          td(
+            { colspan: 2 },
+            div(
+              span({ class: 'card-label' }, (i18n.parliamentMembers + ': ').toUpperCase()),
+              memberIds && memberIds.length
+                ? ul({ class: 'parliament-members-list' }, ...memberIds.map(id => li(a({ class: 'user-link', href: `/author/${encodeURIComponent(id)}` }, id))))
+                : span({ class: 'card-value' }, String(g.members || 0))
+            )
+          )
+        )
+      : null;
+
+  return div(
+    { class: 'card' },
+    h2(i18n.parliamentGovernmentCard),
+    GovHeader(g),
+    div(
+      { class: 'table-wrap' },
+      applyEl(table, { class: 'table table--centered gov-overview' }, [
+        thead(tr(
+          th(i18n.parliamentGovMethod),
+          th(i18n.parliamentPoliciesProposal || 'LAWS PROPOSAL'),
+          th(i18n.parliamentPoliciesApproved || 'LAWS APPROVED'),
+          th(i18n.parliamentPoliciesDeclined || 'LAWS DECLINED'),
+          th(i18n.parliamentPoliciesDiscarded || 'LAWS DISCARDED'),
+          th(i18n.parliamentPoliciesRevocated || 'LAWS REVOCATED'),
+          th(i18n.parliamentEfficiency || '% EFFICIENCY')
+        )),
+        tbody(tr(
+          td(div({ class: 'method-cell' }, img({ src: methodImageSrc(methodKey), alt: methodLabel }))),
+          td(String(g.proposed || 0)),
+          td(String(g.approved || 0)),
+          td(String(g.declined || 0)),
+          td(String(g.discarded || 0)),
+          td(String(g.revocated || 0)),
+          td(`${String(g.efficiency || 0)} %`)
+        ))
+      ])
+    ),
+    (g.powerType === 'tribe' || g.powerType === 'inhabitant')
+      ? div(
+          { class: 'table-wrap mt-2' },
+          applyEl(table, { class: 'table parliament-actor-table' }, [
+            thead(tr(
+              th({ class: 'parliament-actor-col' }, String(actorLabel).toUpperCase()),
+              th({ class: 'parliament-description-col' }, i18n.description.toUpperCase())
+            )),
+            tbody(
+              tr(
+                td({ class: 'parliament-actor-col' }, div({ class: 'leader-cell' }, actorLink)),
+                td({ class: 'parliament-description-col' }, p(actorBio || '-'))
+              ),
+              membersRow
+            )
+          ])
+        )
+      : null
+  );
+};
+
+const NoGovernment = () => div({ class: 'empty' }, p(i18n.parliamentNoStableGov));
+const NoProposals = () => div({ class: 'empty' }, p(i18n.parliamentNoProposals));
+const NoLaws = () => div({ class: 'empty' }, p(i18n.parliamentNoLaws));
+const NoGovernments = () => div({ class: 'empty' }, p(i18n.parliamentNoGovernments));
+const NoRevocations = () => null;
+
+const CandidatureForm = () =>
+  div(
+    { class: 'div-center' },
+    h2(i18n.parliamentCandidatureFormTitle),
+    form(
+      { method: 'POST', action: '/parliament/candidatures/propose' },
+      label(i18n.parliamentCandidatureId), br(),
+      input({ type: 'text', name: 'candidateId', placeholder: i18n.parliamentCandidatureIdPh, required: true }), br(), br(),
+      label(i18n.parliamentCandidatureMethod), br(),
+      select({ name: 'method' },
+        ['DEMOCRACY','MAJORITY','MINORITY','DICTATORSHIP','KARMATOCRACY'].map(m => option({ value: m }, i18n[`parliamentMethod${m}`] || m))
+      ), br(), br(),
+      button({ type: 'submit', class: 'create-button' }, i18n.parliamentCandidatureProposeBtn)
+    )
+  );
+
+const pickLeader = (arr) => {
+  if (!arr || !arr.length) return null;
+  const sorted = [...arr].sort((a, b) => {
+    const va = Number(a.votes || 0), vb = Number(b.votes || 0);
+    if (vb !== va) return vb - va;
+    const ka = Number(a.karma || 0), kb = Number(b.karma || 0);
+    if (kb !== ka) return kb - ka;
+    const sa = Number(a.profileSince || 0), sb = Number(b.profileSince || 0);
+    if (sa !== sb) return sa - sb;
+    const ca = new Date(a.createdAt).getTime(), cb = new Date(b.createdAt).getTime();
+    if (ca !== cb) return ca - cb;
+    return String(a.targetId).localeCompare(String(b.targetId));
+  });
+  return sorted[0];
+};
+
+const CandidatureStats = (cands, govCard, leaderMeta) => {
+  if (!cands || !cands.length) return null;
+
+  const leader      = pickLeader(cands || []);
+  const methodKey   = String(leader.method || '').toUpperCase();
+  const methodLabel = String(i18n[`parliamentMethod${methodKey}`] || methodKey).toUpperCase();
+  const votes       = String(leader.votes || 0);
+  const avatarSrc   = (leaderMeta && leaderMeta.avatarUrl) ? leaderMeta.avatarUrl : '/assets/images/default-avatar.png';
+
+  const winLbl = (i18n.parliamentWinningCandidature || i18n.parliamentCurrentLeader || 'WINNING CANDIDATURE').toUpperCase();
+  const idLink = leader
+    ? (leader.targetType === 'inhabitant'
+        ? a({ class: 'user-link', href: `/author/${encodeURIComponent(leader.targetId)}` }, leader.targetId)
+        : a({ class: 'tag-link', href: `/tribe/${encodeURIComponent(leader.targetId)}?` }, leader.targetTitle || leader.targetId))
+    : null;
+
+  return div(
+    { class: 'card' },
+    h2(i18n.parliamentElectionsStatusTitle),
+
+    div({ class: 'card-field card-field--spaced' },
+      span({ class: 'card-label' }, winLbl + ': '),
+      span({ class: 'card-value' }, idLink)
+    ),
+
+    div({ class: 'card-field card-field--spaced' },
+      span({ class: 'card-label' }, (i18n.parliamentGovMethod + ': ').toUpperCase()),
+      span({ class: 'card-value' }, methodLabel)
+    ),
+
+    div(
+      { class: 'table-wrap mt-2' },
+      applyEl(table, [
+        thead(tr(
+          th(i18n.parliamentThLeader),
+          th({ class: 'parliament-method-col' }, i18n.parliamentGovMethod),
+          th({ class: 'parliament-votes-col'  }, i18n.parliamentVotesReceived)
+        )),
+        tbody(tr(
+          td(
+            img({ src: avatarSrc })
+          ),
+          td({ class: 'parliament-method-col' },
+            img({ src: methodImageSrc(methodKey), alt: methodLabel, class: 'method-hero__icon' })
+          ),
+          td({ class: 'parliament-votes-col'  }, span({ class: 'votes-value' }, votes))
+        ))
+      ])
+    )
+  );
+};
+
+const CandidaturesTable = (candidatures) => {
+  const rows = (candidatures || []).map(c => {
+    const idLink =
+      c.targetType === 'inhabitant'
+        ? p(a({ class: 'user-link break-all', href: `/author/${encodeURIComponent(c.targetId)}` }, c.targetId))
+        : p(a({ class: 'tag-link', href: `/tribe/${encodeURIComponent(c.targetId)}?` }, c.targetTitle || c.targetId));
+    return tr(
+      td(idLink),
+      td(fmt(c.createdAt)),
+      td({ class: 'nowrap' }, c.method),
+      td(c.targetType === 'inhabitant' ? String(c.karma || 0) : '-'),
+      td(String(c.votes || 0)),
+      td(form({ method: 'POST', action: `/parliament/candidatures/${encodeURIComponent(c.id)}/vote` }, button({ class: 'vote-btn' }, i18n.parliamentVoteBtn)))
+    );
+  });
+  return div(
+    { class: 'table-wrap' },
+    h2(i18n.parliamentCandidaturesListTitle),
+    applyEl(table, { class: 'table table--centered' }, [
+      thead(tr(
+        th(i18n.parliamentThId),
+        th(i18n.parliamentThProposalDate),
+        th(i18n.parliamentThMethod),
+        th(i18n.parliamentThKarma),
+        th(i18n.parliamentThSupports),
+        th(i18n.parliamentThVote)
+      )),
+      applyEl(tbody, null, rows)
+    ])
+  );
+};
+
+const ProposalForm = () =>
+  div(
+    { class: 'div-center' },
+    h2(i18n.parliamentProposalFormTitle),
+    form(
+      { method: 'POST', action: '/parliament/proposals/create' },
+      label(i18n.parliamentProposalTitle), br(),
+      input({ type: 'text', name: 'title', required: true }), br(), br(),
+      label(i18n.parliamentProposalDescription), br(),
+      textarea({ name: 'description', rows: 5, maxlength: 1000 }), br(), br(),
+      button({ type: 'submit', class: 'create-button' }, i18n.parliamentProposalPublish)
+    )
+  );
+
+const ProposalsList = (proposals) => {
+  if (!proposals || !proposals.length) return null;
+  const cards = proposals.map(pItem =>
+    div(
+      { class: 'card' },
+      br(),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentThProposalDate.toUpperCase() + ': '), span({ class: 'card-value' }, fmt(pItem.createdAt))),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '), span({ class: 'card-value' }, a({ class: 'user-link', href: `/author/${encodeURIComponent(pItem.proposer)}` }, pItem.proposer))),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentGovMethod.toUpperCase() + ': '), span({ class: 'card-value' }, pItem.method)),
+      br(),
+      div(
+      h2(pItem.title || ''),
+      p(pItem.description || '')
+      ),
+      pItem.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentProposalDeadlineLabel.toUpperCase() + ': '), span({ class: 'card-value' }, fmt(pItem.deadline))) : null,
+      pItem.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentProposalTimeLeft.toUpperCase() + ': '), span({ class: 'card-value' }, timeLeft(pItem.deadline))) : null,
+      showVoteMetrics(pItem.method) ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentVotesNeeded.toUpperCase() + ': '), span({ class: 'card-value' }, String(pItem.needed || reqVotes(pItem.method, pItem.total)))) : null,
+      showVoteMetrics(pItem.method) ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentVotesSlashTotal.toUpperCase() + ': '), span({ class: 'card-value' }, `${Number(pItem.yes||0)}/${Number(pItem.total||0)}`)) : null,
+      pItem && pItem.voteId
+        ? form({ method: 'GET', action: `/votes/${encodeURIComponent(pItem.voteId)}` }, button({ type: 'submit', class: 'vote-btn' }, i18n.parliamentVoteAction))
+        : null
+    )
+  );
+  return div(
+    { class: 'cards' },
+    h2(i18n.parliamentCurrentProposalsTitle),
+    applyEl(div, null, cards)
+  );
+};
+
+const FutureLawsList = (rows) => {
+  if (!rows || !rows.length) return null;
+  const cards = rows.map(pItem =>
+    div(
+      { class: 'card' },
+      br(),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentGovMethod.toUpperCase() + ': '), span({ class: 'card-value' }, pItem.method)),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentThProposalDate.toUpperCase() + ': '), span({ class: 'card-value' }, fmt(pItem.createdAt))),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '), span({ class: 'card-value' }, a({ class: 'user-link', href: `/author/${encodeURIComponent(pItem.proposer)}` }, pItem.proposer))),
+      h2(pItem.title || ''),
+      p(pItem.description || '')
+    )
+  );
+  return div(
+    { class: 'cards' },
+    h2(i18n.parliamentFutureLawsTitle),
+    applyEl(div, null, cards)
+  );
+};
+
+const RevocationForm = (laws = []) =>
+  div(
+    { class: 'div-center' },
+    h2(i18n.parliamentRevocationFormTitle),
+    form(
+      {
+        method: 'POST',
+        action: '/parliament/revocations/create'
+      },
+      label(i18n.parliamentRevocationLaw), br(),
+      select(
+        { name: 'lawId', required: true },
+        ...(laws || []).map(l =>
+          option(
+            { value: l.id },
+            `${l.question || l.title || l.id}`
+          )
+        )
+      ),
+      br(), br(),
+      label(i18n.parliamentRevocationReasons), br(),
+      textarea({ name: 'reasons', rows: 4, maxlength: 1000 }),
+      br(), br(),
+      button({ type: 'submit', class: 'create-button' }, i18n.parliamentRevocationPublish || 'Publish Revocation')
+    )
+  );
+
+const RevocationsList = (revocations) => {
+  if (!revocations || !revocations.length) return null;
+  const cards = revocations.map(pItem =>
+    div(
+      { class: 'card' },
+      br(),
+      div(
+        { class: 'card-field' },
+        span({ class: 'card-label' }, i18n.parliamentThProposalDate.toUpperCase() + ': '),
+        span({ class: 'card-value' }, fmt(pItem.createdAt))
+      ),
+      div(
+        { class: 'card-field' },
+        span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '),
+        span(
+          { class: 'card-value' },
+          a({ class: 'user-link', href: `/author/${encodeURIComponent(pItem.proposer)}` }, pItem.proposer)
+        )
+      ),
+      div(
+        { class: 'card-field' },
+        span({ class: 'card-label' }, i18n.parliamentGovMethod + ': '),
+        span({ class: 'card-value' }, pItem.method)
+      ),
+      br(),
+      div(
+        h2(pItem.title || pItem.lawTitle || ''),
+        p(pItem.reasons || '')
+      ),
+      pItem.deadline
+        ? div(
+            { class: 'card-field' },
+            span({ class: 'card-label' }, i18n.parliamentProposalDeadlineLabel.toUpperCase() + ': '),
+            span({ class: 'card-value' }, fmt(pItem.deadline))
+          )
+        : null,
+      pItem.deadline
+        ? div(
+            { class: 'card-field' },
+            span({ class: 'card-label' }, i18n.parliamentProposalTimeLeft.toUpperCase() + ': '),
+            span({ class: 'card-value' }, timeLeft(pItem.deadline))
+          )
+        : null,
+      showVoteMetrics(pItem.method)
+        ? div(
+            { class: 'card-field' },
+            span({ class: 'card-label' }, i18n.parliamentVotesNeeded.toUpperCase() + ': '),
+            span({ class: 'card-value' }, String(pItem.needed || reqVotes(pItem.method, pItem.total)))
+          )
+        : null,
+      showVoteMetrics(pItem.method)
+        ? div(
+            { class: 'card-field' },
+            span({ class: 'card-label' }, i18n.parliamentVotesSlashTotal.toUpperCase() + ': '),
+            span({ class: 'card-value' }, `${Number(pItem.yes || 0)}/${Number(pItem.total || 0)}`)
+          )
+        : null,
+      pItem && pItem.voteId
+        ? form(
+            { method: 'GET', action: `/votes/${encodeURIComponent(pItem.voteId)}` },
+            button({ type: 'submit', class: 'vote-btn' }, i18n.parliamentVoteAction)
+          )
+        : null
+    )
+  );
+  return div(
+    { class: 'cards' },
+    h2(i18n.parliamentCurrentRevocationsTitle),
+    applyEl(div, null, cards)
+  );
+};
+
+const FutureRevocationsList = (rows) => {
+  if (!rows || !rows.length) return null;
+  const cards = rows.map(pItem =>
+    div(
+      { class: 'card' },
+      br(),
+      pItem.method ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentGovMethod.toUpperCase() + ': '), span({ class: 'card-value' }, pItem.method)) : null,
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentThProposalDate.toUpperCase() + ': '), span({ class: 'card-value' }, fmt(pItem.createdAt))),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '), span({ class: 'card-value' }, a({ class: 'user-link', href: `/author/${encodeURIComponent(pItem.proposer)}` }, pItem.proposer))),
+      h2(pItem.title || pItem.lawTitle || ''),
+      p(pItem.reasons || '')
+    )
+  );
+  return div(
+    { class: 'cards' },
+    h2(i18n.parliamentFutureRevocationsTitle),
+    applyEl(div, null, cards)
+  );
+};
+
+const LawsStats = (laws = [], revocatedCount = 0) => {
+  const proposed = laws.length;
+  const approved = laws.length;
+  const declined = 0;
+  const discarded = 0;
+  const revocated = Number(revocatedCount || 0);
+  return div(
+    { class: 'table-wrap' },
+    h2(i18n.parliamentPoliciesTitle || 'POLICIES'),
+    applyEl(table, { class: 'table table--centered' }, [
+      thead(tr(
+        th(i18n.parliamentThProposed),
+        th(i18n.parliamentThApproved),
+        th(i18n.parliamentThDeclined),
+        th(i18n.parliamentThDiscarded),
+        th(i18n.parliamentPoliciesRevocated)
+      )),
+      tbody(
+        tr(
+          td(String(proposed)),
+          td(String(approved)),
+          td(String(declined)),
+          td(String(discarded)),
+          td(String(revocated))
+        )
+      )
+    ])
+  );
+};
+
+const LawsList = (laws) => {
+  if (!laws || !laws.length) return NoLaws();
+  const cards = laws.map(l => {
+    const total = Number((l.votes && (l.votes.total || l.votes.TOTAL)) || 0);
+    const yes = Number((l.votes && (l.votes.YES || l.votes.Yes || l.votes.yes)) || 0);
+    const needed = reqVotes(l.method, total);
+    const showMetricsFlag = showVoteMetrics(l.method);
+    return div(
+      { class: 'card' },
+      br(),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod + ': ').toUpperCase()), span({ class: 'card-value' }, l.method)),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawEnacted + ': ').toUpperCase()), span({ class: 'card-value' }, fmt(l.enactedAt))),
+      div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '), span({ class: 'card-value' }, a({ class: 'user-link', href: `/author/${encodeURIComponent(l.proposer)}` }, l.proposer))),
+      h2(l.question || ''),
+      p(l.description || ''),
+      showMetricsFlag ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesNeeded + ': ').toUpperCase()), span({ class: 'card-value' }, String(needed))) : null,
+      showMetricsFlag ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesSlashTotal + ': ').toUpperCase()), span({ class: 'card-value' }, `${yes}/${total}`)) : null
+    );
+  });
+  return div(
+    { class: 'cards' },
+    h2(i18n.parliamentLawsTitle || 'LAWS'),
+    applyEl(div, null, cards)
+  );
+};
+
+const HistoricalGovsSummary = (rows = []) => {
+  const byMethod = new Map();
+  for (const g of rows) {
+    const k = String(g.method || 'ANARCHY').toUpperCase();
+    byMethod.set(k, (byMethod.get(k) || 0) + 1);
+  }
+  const entries = Array.from(byMethod.entries()).sort((a,b) => String(a[0]).localeCompare(String(b[0])));
+  const lines = entries.map(([method, count]) =>
+    tr(td(method), td(String(count)))
+  );
+  return div(
+    { class: 'table-wrap' },
+    h2(i18n.parliamentHistoricalGovernmentsTitle || 'Governments'),
+    applyEl(table, { class: 'table table--centered' }, [
+      thead(tr(th(i18n.parliamentGovMethod), th(i18n.parliamentThCycles))),
+      applyEl(tbody, null, lines)
+    ])
+  );
+};
+
+const HistoricalList = (rows, metasByKey = {}) => {
+  if (!rows || !rows.length) return NoGovernments();
+  const cards = rows.map(g => {
+    const key = `${g.powerType}:${g.powerId}`;
+    const meta = metasByKey[key];
+    const showActor = g.powerType === 'tribe' || g.powerType === 'inhabitant';
+    const showMembers = g.powerType === 'tribe';
+    const actorLabel =
+      g.powerType === 'tribe'
+        ? (i18n.parliamentActorInPowerTribe || 'TRIBE RULING')
+        : (i18n.parliamentActorInPowerInhabitant || 'INHABITANT RULING');
+    return div(
+      { class: 'card' },
+      h2(g.method),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, (i18n.parliamentLegSince + ': ').toUpperCase()),
+        span({ class: 'card-value' }, fmt(g.since))
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, (i18n.parliamentLegEnd + ': ').toUpperCase()),
+        span({ class: 'card-value' }, fmt(g.end))
+      ),
+      showActor ? div({ class: 'card-field' },
+        span({ class: 'card-label' }, String(actorLabel).toUpperCase() + ': '),
+        span({ class: 'card-value' },
+          g.powerType === 'tribe'
+            ? a({ class: 'user-link', href: `/tribe/${encodeURIComponent(g.powerId)}` }, g.powerTitle || g.powerId)
+            : a({ class: 'user-link', href: `/author/${encodeURIComponent(g.powerId)}` }, g.powerTitle || g.powerId)
+        )
+      ) : null,
+      (g.method !== 'ANARCHY')
+        ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.parliamentVotesReceived + ': '),
+            span({ class: 'card-value' }, `${g.votesReceived} (${g.totalVotes})`)
+          )
+        : null,
+      br(),
+      showActor && meta && (meta.avatarUrl || meta.bio)
+        ? div(
+            { class: 'actor-meta' },
+            meta.avatarUrl ? img({ src: meta.avatarUrl, alt: '', class: 'avatar--lg' }) : null,
+            meta.bio ? p({ class: 'bio' }, meta.bio) : null
+          )
+        : null,
+      showMembers
+        ? div({ class: 'card-field' },
+            span({ class: 'card-label' }, i18n.parliamentMembers + ': '),
+            span({ class: 'card-value' }, String(g.members || 0))
+          )
+        : null,
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.parliamentPoliciesProposal + ': '),
+        span({ class: 'card-value' }, String(g.proposed || 0))
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.parliamentPoliciesApproved + ': '),
+        span({ class: 'card-value' }, String(g.approved || 0))
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.parliamentPoliciesDeclined + ': '),
+        span({ class: 'card-value' }, String(g.declined || 0))
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.parliamentPoliciesDiscarded + ': '),
+        span({ class: 'card-value' }, String(g.discarded || 0))
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.parliamentPoliciesRevocated + ': '),
+        span({ class: 'card-value' }, String(g.revocated || 0))
+      ),
+      div({ class: 'card-field' },
+        span({ class: 'card-label' }, i18n.parliamentEfficiency + ': '),
+        span({ class: 'card-value' }, `${g.efficiency || 0} %`)
+      )
+    );
+  });
+  return div(
+    { class: 'cards' },
+    h2(i18n.parliamentHistoricalElectionsTitle || 'ELECTION CYCLES'),
+    applyEl(div, null, cards)
+  );
+};
+
+const countCandidaturesByActor = (cands = []) => {
+  const m = new Map();
+  for (const c of cands) {
+    const key = `${c.targetType}:${c.targetId}`;
+    m.set(key, (m.get(key) || 0) + 1);
+  }
+  return m;
+};
+
+const LeadersSummary = (leaders = [], candidatures = []) => {
+  const candCounts = countCandidaturesByActor(candidatures);
+  const totals = leaders.reduce((acc, l) => {
+    const key = `${l.powerType}:${l.powerId}`;
+    const candsFromMap = candCounts.get(key) || 0;
+    const presentedNorm = Math.max(Number(l.presented || 0), Number(l.inPower || 0), candsFromMap);
+    acc.presented += presentedNorm;
+    acc.inPower += Number(l.inPower || 0);
+    acc.proposed += Number(l.proposed || 0);
+    acc.approved += Number(l.approved || 0);
+    acc.declined += Number(l.declined || 0);
+    acc.discarded += Number(l.discarded || 0);
+    acc.revocated += Number(l.revocated || 0);
+    return acc;
+  }, { presented:0, inPower:0, proposed:0, approved:0, declined:0, discarded:0, revocated:0 });
+  const efficiencyPct = totals.proposed > 0 ? Math.round((totals.approved / totals.proposed) * 100) : 0;
+  return div(
+    { class: 'table-wrap' },
+    h2(i18n.parliamentHistoricalLawsTitle || 'Actions'),
+    applyEl(table, { class: 'table table--centered' }, [
+      thead(tr(
+        th(i18n.parliamentThTotalCandidatures),
+        th(i18n.parliamentThTimesInPower),
+        th(i18n.parliamentThProposed),
+        th(i18n.parliamentThApproved),
+        th(i18n.parliamentThDeclined),
+        th(i18n.parliamentThDiscarded),
+        th(i18n.parliamentPoliciesRevocated),
+        th(i18n.parliamentEfficiency)
+      )),
+      tbody(
+        tr(
+          td(String(totals.presented)),
+          td(String(totals.inPower)),
+          td(String(totals.proposed)),
+          td(String(totals.approved)),
+          td(String(totals.declined)),
+          td(String(totals.discarded)),
+          td(String(totals.revocated)),
+          td(`${efficiencyPct} %`)
+        )
+      )
+    ])
+  );
+};
+
+const LeadersList = (leaders, metas = {}, candidatures = []) => {
+  if (!leaders || !leaders.length) return div({ class: 'empty' }, p(i18n.parliamentNoLeaders));
+  const rows = leaders.map(l => {
+    const key = `${l.powerType}:${l.powerId}`;
+    const meta = metas[key] || {};
+    const avatar = meta.avatarUrl ? img({ src: meta.avatarUrl, alt: '', class: 'leader-table__avatar' }) : null;
+    const link = l.powerType === 'tribe'
+      ? a({ class: 'user-link', href: `/tribe/${encodeURIComponent(l.powerId)}` }, l.powerTitle || l.powerId)
+      : a({ class: 'user-link', href: `/author/${encodeURIComponent(l.powerId)}` }, l.powerTitle || l.powerId);
+    const leaderCell = div({ class: 'leader-cell' }, avatar, link);
+    return tr(
+      td(leaderCell),
+      td(String(l.proposed || 0)),
+      td(String(l.approved || 0)),
+      td(String(l.declined || 0)),
+      td(String(l.discarded || 0)),
+      td(String(l.revocated || 0)),
+      td(`${(l.efficiency != null ? Math.round(l.efficiency * 100) : (l.proposed > 0 ? Math.round((l.approved / l.proposed) * 100) : 0))} %`)
+    );
+  });
+  return div(
+    { class: 'table-wrap' },
+    h2(i18n.parliamentHistoricalLeadersTitle),
+    applyEl(table, { class: 'table table--centered gov-overview' }, [
+      thead(tr(
+        th(i18n.parliamentActorInPowerInhabitant),
+        th(i18n.parliamentPoliciesProposal),
+        th(i18n.parliamentPoliciesApproved),
+        th(i18n.parliamentPoliciesDeclined),
+        th(i18n.parliamentPoliciesDiscarded),
+        th(i18n.parliamentPoliciesRevocated),
+        th(i18n.parliamentEfficiency)
+      )),
+      applyEl(tbody, null, rows)
+    ])
+  );
+};
+
+const RulesContent = () =>
+  div(
+    { class: 'card' },
+    h2(i18n.parliamentRulesTitle),
+    ul(
+      li(i18n.parliamentRulesIntro),
+      li(i18n.parliamentRulesTerm),
+      li(i18n.parliamentRulesMethods),
+      li(i18n.parliamentRulesAnarchy),
+      li(i18n.parliamentRulesCandidates),
+      li(i18n.parliamentRulesElection),
+      li(i18n.parliamentRulesTies),
+      li(i18n.parliamentRulesProposals),
+      li(i18n.parliamentRulesLimit),
+      li(i18n.parliamentRulesLaws),
+      li(i18n.parliamentRulesRevocations),
+      li(i18n.parliamentRulesHistorical),
+      li(i18n.parliamentRulesLeaders)
+    )
+  );
+
+const CandidaturesSection = (governmentCard, candidatures, leaderMeta) => {
+  const termStart = governmentCard && governmentCard.since ? governmentCard.since : moment().toISOString();
+  const termEnd   = governmentCard && governmentCard.end   ? governmentCard.end   : moment(termStart).add(1, 'minutes').toISOString();
+  return div(
+    h2(i18n.parliamentGovernmentCard),
+    GovHeader(governmentCard || {}),
+    CandidatureStats(candidatures || [], governmentCard || null, leaderMeta || null),
+    CandidatureForm(),
+    candidatures && candidatures.length ? CandidaturesTable(candidatures) : null
+  );
+};
+
+const ProposalsSection = (governmentCard, proposals, futureLaws, canPropose) => {
+  const has = proposals && proposals.length > 0;
+  const fl = FutureLawsList(futureLaws || []);
+  if (!has && canPropose) return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), ProposalForm(), fl);
+  if (!has && !canPropose) return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), NoProposals(), fl);
+  return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), ProposalForm(), ProposalsList(proposals), fl);
+};
+
+const RevocationsSection = (governmentCard, laws, revocations, futureRevocations) =>
+  div(
+    h2(i18n.parliamentGovernmentCard),
+    GovHeader(governmentCard || {}),
+    RevocationForm(laws || []),
+    RevocationsList(revocations || []) || '',
+    FutureRevocationsList(futureRevocations || []) || ''
+  );
+
+exports.parliamentView = async (state) => {
+  const {
+    filter,
+    governmentCard,
+    candidatures,
+    proposals,
+    futureLaws,
+    canPropose,
+    laws,
+    historical,
+    leaders,
+    leaderMeta,
+    powerMeta,
+    revocations,
+    futureRevocations,
+    revocationsEnactedCount,
+    historicalMetas = {},
+    leadersMetas = {}
+  } = state;
+
+  const LawsSectionWrap = () =>
+    div(
+      LawsStats(laws || [], revocationsEnactedCount || 0),
+      LawsList(laws || [])
+    );
+
+  const fallbackAnarchy = {
+    method: 'ANARCHY',
+    votesReceived: 0,
+    totalVotes: 0,
+    proposed: 0,
+    approved: 0,
+    declined: 0,
+    discarded: 0,
+    revocated: 0,
+    efficiency: 0
+  };
+
+  return template(
+    i18n.parliamentTitle,
+    section(div({ class: 'tags-header' }, h2(i18n.parliamentTitle), p(i18n.parliamentDescription)), Tabs(filter)),
+    section(
+      filter === 'government' ? GovernmentCard(governmentCard || fallbackAnarchy, powerMeta) : null,
+      filter === 'candidatures' ? CandidaturesSection(governmentCard || fallbackAnarchy, candidatures, leaderMeta) : null,
+      filter === 'proposals' ? ProposalsSection(governmentCard || fallbackAnarchy, proposals, futureLaws, canPropose) : null,
+      filter === 'laws' ? LawsSectionWrap() : null,
+      filter === 'revocations' ? RevocationsSection(governmentCard || fallbackAnarchy, laws, revocations, futureRevocations) : null,
+      filter === 'historical' ? div(HistoricalGovsSummary(historical || []), HistoricalList(historical || [], historicalMetas)) : null,
+      filter === 'leaders' ? div(LeadersSummary(leaders || [], candidatures || []), LeadersList(leaders || [], leadersMetas, candidatures || [])) : null,
+      filter === 'rules' ? RulesContent() : null
+    )
+  );
+};
+

+ 2 - 2
src/views/vote_view.js

@@ -126,8 +126,8 @@ exports.voteView = async (votes, filter, voteId) => {
   const secondRow = ['CONFUSED', 'FOLLOW_MAJORITY', 'NOT_INTERESTED'];
 
   const header = div({ class: 'tags-header' },
-    h2(i18n.governanceTitle),
-    p(i18n.governanceDescription)
+    h2(i18n.votationsTitle),
+    p(i18n.votationsDescription)
   );
 
   return template(