From 8df076e1af6ca0feb0c43770ece7cb5883037cb9 Mon Sep 17 00:00:00 2001 From: mistakia <1823355+mistakia@users.noreply.github.com> Date: Sat, 17 Feb 2024 08:29:35 -0500 Subject: [PATCH 01/26] feat: add internationalization support --- api/server.mjs | 43 +- locales/en.json | 386 +++++++++++++++ package.json | 5 + src/core/api/service.js | 11 +- src/core/app/actions.js | 5 +- src/core/constants.js | 20 + src/core/docs/actions.js | 5 +- src/core/i18n/actions.js | 10 + src/core/i18n/index.js | 26 + src/core/i18n/reducer.js | 17 + src/core/i18n/sagas.js | 37 ++ src/core/reducers.js | 4 +- src/core/sagas.js | 4 +- src/styles/variables.styl | 3 + .../account-blocks-summary.js | 190 ++++---- .../components/account-meta/account-meta.js | 170 ++++--- src/views/components/app/app.js | 10 +- src/views/components/block-info/block-info.js | 123 ++--- .../components/change-locale/change-locale.js | 59 +++ .../change-locale/change-locale.styl | 15 + src/views/components/change-locale/index.js | 17 + .../components/github-events/github-events.js | 118 +++-- .../ledger-chart-addresses.js | 24 +- .../ledger-chart-amounts.js | 13 +- .../ledger-chart-blocks.js | 31 +- .../ledger-chart-metrics.js | 14 +- .../ledger-chart-usd-transferred.js | 24 +- .../ledger-chart-volume.js | 18 +- src/views/components/menu/menu.js | 262 +++++----- src/views/components/menu/menu.styl | 6 +- src/views/components/network/network.js | 448 ++++++++--------- .../representative-alerts.js | 268 +++++----- .../representative-delegators.js | 23 +- .../representative-info.js | 76 ++- .../representative-network.js | 70 ++- .../representative-telemetry.js | 155 +++--- .../representative-uptime.js | 210 ++++---- .../representatives-bandwidth-by-weight.js | 32 +- .../representatives-cemented-by-weight.js | 35 +- .../representatives-checked-by-weight.js | 35 +- .../representatives-cluster-charts.js | 367 +++++++------- .../representatives-country-by-weight.js | 27 +- .../representatives-filters.js | 28 +- .../representatives-offline.js | 99 ++-- .../representatives-provider-by-weight.js | 27 +- .../representatives-quorum-charts.js | 382 +++++++-------- .../representatives-search.js | 7 +- .../representatives-version-by-weight.js | 27 +- .../representatives-weight-chart.js | 117 ++--- .../representatives-weight.js | 89 ++-- .../representatives/representatives.js | 456 +++++++++--------- src/views/components/search-bar/search-bar.js | 7 +- src/views/components/uptime/uptime.js | 80 +-- src/views/pages/account/account.js | 104 ++-- src/views/pages/block/block.js | 205 ++++---- src/views/pages/doc/doc.js | 274 +++++------ src/views/pages/home/home.js | 74 +-- src/views/pages/ledger/ledger.js | 143 +++--- .../pages/representatives/representatives.js | 167 ++++--- src/views/pages/roadmap/roadmap.js | 20 +- src/views/root.js | 6 - src/views/routes.js | 5 + webpack/webpack.dev.babel.mjs | 7 + yarn.lock | 211 +++++++- 64 files changed, 3556 insertions(+), 2395 deletions(-) create mode 100644 locales/en.json create mode 100644 src/core/i18n/actions.js create mode 100644 src/core/i18n/index.js create mode 100644 src/core/i18n/reducer.js create mode 100644 src/core/i18n/sagas.js create mode 100644 src/views/components/change-locale/change-locale.js create mode 100644 src/views/components/change-locale/change-locale.styl create mode 100644 src/views/components/change-locale/index.js diff --git a/api/server.mjs b/api/server.mjs index 02050a2c..bbf07af0 100644 --- a/api/server.mjs +++ b/api/server.mjs @@ -18,6 +18,7 @@ import serveStatic from 'serve-static' import cors from 'cors' import favicon from 'express-favicon' import robots from 'express-robots-txt' +import { slowDown } from 'express-slow-down' import * as config from '#config' import * as routes from '#api/routes/index.mjs' @@ -70,6 +71,9 @@ api.use((req, res, next) => { const resourcesPath = path.join(__dirname, '..', 'resources') api.use('/resources', serveStatic(resourcesPath)) +const localesPath = path.join(__dirname, '..', 'locales') +api.use('/locales', serveStatic(localesPath)) + const dataPath = path.join(__dirname, '..', 'data') api.use('/data', serveStatic(dataPath)) @@ -95,9 +99,42 @@ api.use('/api/representatives', routes.representatives) api.use('/api/weight', routes.weight) const docsPath = path.join(__dirname, '..', 'docs') -api.use('/api/docs', serveStatic(docsPath)) -api.get('/api/docs/*', (req, res) => { - res.status(404).send('Not found') + +const speedLimiter = slowDown({ + windowMs: 10 * 60 * 1000, // 10 minutes + delayAfter: 50, // allow 50 requests per 10 minutes, then... + delayMs: 500, // begin adding 500ms of delay per request above 50: + maxDelayMs: 20000 // maximum delay of 20 seconds +}) + +api.use('/api/docs', speedLimiter, serveStatic(docsPath)) +api.use('/api/docs/en', speedLimiter, serveStatic(docsPath)) +api.get('/api/docs/:locale/*', speedLimiter, async (req, res) => { + const { locale } = req.params + const doc_id = req.params[0] // Capture the rest of the path as doc_id + const localized_doc_path = path.join(docsPath, locale, `${doc_id}.md`) + const default_doc_path = path.join(docsPath, 'en', `${doc_id}.md`) + + // check if paths are under the docs directory + if ( + !localized_doc_path.startsWith(docsPath) || + !default_doc_path.startsWith(docsPath) + ) { + return res.status(403).send('Forbidden') + } + + try { + if (fs.existsSync(localized_doc_path)) { + return res.sendFile(localized_doc_path) + } else if (fs.existsSync(default_doc_path)) { + return res.redirect(`/api/docs/en/${doc_id}`) + } else { + return res.status(404).send('Document not found') + } + } catch (error) { + console.error(error) + return res.status(500).send('Internal Server Error') + } }) api.use('/api/*', (err, req, res, next) => { diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 00000000..d2677cf1 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,386 @@ +{ + "account_page": { + "address": "Account Address", + "change_summary": "Change Summary", + "copy_notification": "Account address copied", + "seo_description": "Information for nano representative", + "seo_title": "Nano Account", + "telemetry_charts": "Telemetry Charts", + "unopened_description": "While the account address is valid, no blocks have been observed. If NANO has been sent to this account, it still needs to publish a corresponding block to receive the funds and establish an opening balance. An account's balance can only be updated by the account holder as they are the only ones who can publish blocks to their chain.", + "unopened_note": "If an opening block has already been published, it may take a few moments to spread through the network and be observed by the nano.community nodes.", + "unopened_title": "This account hasn't been opened yet" + }, + "account_blocks_summary": { + "first_timestamp": "First Timestamp", + "last_timestamp": "Last Timestamp", + "max_amount": "Max Amount", + "min_amount": "Min Amount", + "no_records": "No Records", + "receiving_account": "Receiving Account", + "representative_account": "Representative Account", + "sending_account": "Sending Account", + "showing_top_10": "Showing top 10 accounts by total descending", + "transactions": "TXs" + }, + "account_meta": { + "account_info": "Account Info", + "funding_account": "Funding Account", + "funding_timestamp": "Funding Timestamp", + "height": "Height", + "last_modified": "Last Modified", + "open_timestamp": "Open Timestamp", + "opening_balance": "Opening Balance", + "receivable_balance": "Receivable Balance" + }, + "block_page": { + "amount": "Amount", + "copy_notification": "Block hash copied", + "delegated_representative": "Delegated Representative", + "description": "Description", + "epoch_v1": "Epoch v1 — Upgraded account-chains from legacy blocks (open, receive, send, change) to state blocks.", + "epoch_v2": "Epoch v2 - Upgraded account-chains to use higher Proof-of-Work difficulty.", + "receiving_account": "Receiving Account", + "section_label": "Block Hash", + "sending_account": "Sending Account", + "seo_description": "Information related to a Nano Block", + "seo_title": "Nano Block", + "voting_weight": "Voting Weight" + }, + "block_info": { + "block_account": "Block Account", + "operation": "Operation", + "status": "Status", + "timestamp": "Timestamp" + }, + "block_status": { + "confirmed": "Confirmed", + "unconfirmed": "Unconfirmed" + }, + "block_type": { + "change": "Change", + "epoch": "Epoch", + "open": "Open", + "receive": "Receive", + "send": "Send" + }, + "common": { + "account_one": "Account", + "account_other": "Accounts", + "address": "Address", + "balance": "Balance", + "bandwidth_limit": "Bandwidth Limit", + "blocks": "Blocks", + "blocks_diff_short": "Blocks Diff", + "by_online_weight": "By Online Weight", + "clear_filters": "Clear Filters", + "click_to_copy": "Click to copy", + "collapse": "Collapse", + "conf_short": "Conf.", + "conf_diff_short": "Conf. Diff", + "country": "Country", + "delegator_one": "Delegator", + "delegator_other": "Delegators", + "max": "Max", + "min": "Min", + "offline": "Offline", + "online": "Online", + "peers": "Peers", + "percent_of_total": "% of Total", + "port": "Port", + "quorum_delta": "Quorum Delta", + "representative_one": "Representative", + "representative_other": "Representatives", + "show_more": "Show {{count}} more", + "total": "Total", + "unchecked": "Unchecked", + "unlimited": "Unlimited", + "version": "Version", + "weight": "Weight" + }, + "delegators": { + "showing_top_delegators": "Showing top 100 delegators with a minimum balance of 1 Nano." + }, + "doc": { + "contributors": "Contributor", + "document_not_found": "Document (or Account) not found", + "edit_page": "Edit Page", + "help_out": "Help out", + "not_found_404": "404", + "section_link_copied": "Section link copied", + "updated_by": "updated by" + }, + "github_events": { + "action": { + "added_member": "added member", + "commented_on_commit": "commented on commit", + "commented_on_issue": "commented on issue", + "commented_on_pr_review": "commented on pr review", + "created": "created {{action}}", + "deleted": "deleted {{action}}", + "forked": "forked", + "issue_action": "{{action}} issue", + "made_public": "made public", + "pr_action": "{{action}} pr", + "pr_review": "pr review {{title}}", + "published_release": "published release", + "pushed_commit": "pushed commit to {{ref}}", + "sponsorship_started": "sponsorship started", + "watching_repo": "watching repo" + }, + "events_title": "Development Events" + }, + "ledger": { + "addresses": { + "active_detail": "Active shows the number of unique addresses used. New shows the number of addresses created. Reused shows the number of addresses used that were created on a previous day.", + "active_stats": "Active Address Stats", + "new_stats": "New Address Stats", + "total_number": "The total number of active, new, and reused addresses used per day." + }, + "amounts": { + "total_number": "The number of confirmed send-type blocks per day where the amount in the block is in a given range (in Nano)" + }, + "blocks": { + "change": "Change Block Stats", + "description": "The number of blocks confirmed per day.", + "open": "Open Block Stats", + "receive": "Receive Block Stats", + "send": "Send Block Stats", + "total": "Total Block Stats" + }, + "description": "Description", + "usd_transferred": { + "desc_1": "The total amount of value transferred (in USD) per day.", + "desc_2": "Based on the daily closing price of Nano/USD and the total amount of Nano transferred that day.", + "usd_transferred": "USD Transferred", + "usd_transferred_stats": "USD Transferred Stats" + }, + "volume": { + "change_stats": "Change Stats", + "description": "The total amount sent (in Nano) and total amount of voting weight changed per day.", + "send_stats": "Send Stats" + } + }, + "ledger_page": { + "addresses_tab": "Addresses", + "amounts_tab": "Amounts", + "blocks_tab": "Blocks", + "seo_description": "On-chain metrics and analytics of the Nano ledger", + "seo_title": "Nano Ledger Analysis", + "value_transferred_tab": "Value Transferred", + "volume_tab": "Volume" + }, + "menu": { + "account_setup": "Account Setup", + "acquiring": "Acquiring", + "advantages": "Advantages", + "attack_vectors": "Attack Vectors", + "basics": "Basics", + "best_practices": "Best Practices", + "choosing_a_rep": "Choosing a Rep", + "challenges": "Challenges", + "communities": "Communities", + "contribution_guide": "Contribution Guide", + "design": "Design", + "developer_discussions": "Developer Discussions", + "developers": "Developers", + "documentation": "Documentation", + "faqs": "FAQs", + "get_involved": "Get Involved", + "get_support": "Get Support", + "getting_started": "Getting Started", + "glossary": "Glossary", + "guides": "Guides", + "history": "History", + "how_it_works": "How it works", + "integrations": "Integrations", + "investment_thesis": "Investment thesis", + "learn": "Learn", + "ledger": "Ledger", + "misconceptions": "Misconceptions", + "overview": "Overview", + "planning": "Planning 👾", + "privacy": "Privacy", + "protocol": "Protocol", + "running_a_node": "Running a node", + "security": "Security", + "stats": "Stats", + "storing": "Storing", + "telemetry": "Telemetry", + "topics": "Topics", + "using": "Using", + "why_it_matters": "Why it matters" + }, + "network": { + "unconfirmed_pool_text": "Number of blocks waiting to be confirmed $(network.pr_text)", + "censor_text": "The minimum number of representatives needed to censor transactions or stall the network", + "confirm_text": "The minimum number of representatives needed to confirm transactions", + "confirmations": "Confirmations (24h)", + "confirmations_text": "Total number of transactions confirmed by the network over the last 24 hours", + "energy_text": "Estimated live network CPU energy usage of Principle Representatives based on collected CPU model info. The estimate is based on CPU TDP, which is the average power, in watts, the processor dissipates when operating at base frequency with all cores active under manufacture-defined, high-complexity workload", + "energy_usage": "Energy Usage (TDP) (24h)", + "nano_ticker": "NanoTicker", + "online_stake": "Online Stake", + "principal_reps": "Principal Reps", + "pr_text": "as observed across the networks principal representatives: voting nodes with more than 0.1% of the online voting weight delegated to them", + "reps_to_censor": "Reps to Censor or Stall", + "reps_to_confirm": "Reps to Confirm", + "settlement": "Settlement (24h)", + "settlement_text": "Total amount of value settled by the network over the last 24 hours (only send blocks)", + "speed_text": "Median time in milliseconds for a block to get confirmed (across all buckets)", + "stats_title": "Network Stats", + "total_reps": "Total Reps (24h)", + "tx_backlog": "Tx Backlog", + "tx_fees": "Tx Fees (24h)", + "tx_speed": "Tx Speed ({{time_range}})", + "tx_throughput": "Tx Throughput", + "throughput_text": "Median number of transactions confirmed per second in the last minute $(network.pr_text)" + }, + "posts": { + "nano_foundation": "Nano Foundation", + "top": "Top", + "trending": "Trending" + }, + "representative_alerts": { + "table_header": { + "behind": "Behind", + "issue": "Issue", + "last_online": "Last Online", + "percent_online_weight": "% Online Weight", + "representative": "Representative" + }, + "tooltip": { + "behind": "Representative has fallen behind or is bootstrapping. The cutoff is a cemented count beyond the 95th percentile. (via telemetry)", + "low_uptime": "Representative has been offline more than 25% in the last 28 days.", + "offline": "Representative has stopped voting and appears offline.", + "overweight": "Representative has beyond 3M Nano voting weight. Delegators should consider distributing the weight to improve the network's resilience and value." + }, + "type": { + "behind": "Behind", + "low_uptime": "Low Uptime", + "offline": "Offline", + "overweight": "Overweight" + } + }, + "representatives_cemented_by_weight": { + "title": "Confirmation Differential", + "tooltip": "Displays the amount of voting weight that is within X number of confirmations from the leading node. Helpful in knowing how well in-sync and aligned nodes are across the network" + }, + "representatives_checked_by_weight": { + "title": "Blocks Differential", + "tooltip": "Displays the amount of voting weight that is within X number of blocks from the leading node. Useful for getting a sense of how in-sync block propagation is within the network" + }, + "representative_delegators": { + "showing_top_delegators": "Showing top 100 delegators with a minimum balance of 1 Nano." + }, + "representative_info": { + "first_seen": "First Seen", + "last_seen": "Last Seen", + "weight_represented": "Weight Represented" + }, + "representative_network": { + "city": "City", + "isp": "ISP", + "network": "Network", + "provider": "Provider" + }, + "representative_telemetry": { + "blocks_diff": "Blocks Diff", + "conf": "Conf.", + "conf_diff": "Conf. Diff", + "telemetry": "Telemetry", + "telemetry_timestamp": "Telemetry Timestamp" + }, + "representative_uptime": { + "2m_uptime": "2M Uptime", + "2w_uptime": "2W Uptime", + "3m_uptime": "3M Uptime", + "current_status": "Current Status", + "down": "Down", + "down_for": "Down for", + "operational": "Operational", + "up_for": "Up for", + "warning": "Warning" + }, + "representatives": { + "alias": "Alias", + "cpu_cores": "CPU Cores", + "cpu_model": "CPU Model", + "tdp": "TDP (wH)", + "protocol_version": "Protocol", + "last_seen": "Last Seen", + "host_asn": "Host ASN" + }, + "representatives_bandwidth_by_weight": { + "tooltip": "Displays the amount of voting weight based on the bandwidth limit set locally by each node" + }, + "representatives_cluster": { + "blocks_diff": "Blocks Behind", + "conf_diff": "Confirmations Behind", + "unchecked": "Unchecked Count" + }, + "representatives_country_by_weight": { + "title": "Country" + }, + "representatives_offline": { + "account": "Offline Account", + "last_online": "Last Online" + }, + "representatives_page": { + "seo_description": "Explore and analyze Nano network representatives", + "seo_title": "Nano Representatives Explorer", + "telemetry_tab": "Telemetry", + "weight_distribution_tab": "Weight Distribution", + "weight_history_tab": "Weight History", + "offline_reps_tab": "Offline Reps" + }, + "representatives_provider_by_weight": { + "title": "Hosting Provider" + }, + "representatives_quorum_charts": { + "peers_weight": "Peers Weight", + "quorum_delta": "Quorum Delta", + "title": "Quorum Charts", + "trended_weight": "Trended Weight" + }, + "representatives_search": { + "placeholder": "Filter by account, alias, ip" + }, + "representatives_weight": { + "trended": "Trended" + }, + "representatives_weight_chart": { + "title": "Weight Distribution by Representative" + }, + "representatives_version_by_weight": { + "title": "Versions" + }, + "roadmap": { + "header": { + "subtitle": "Community objectives", + "title": "Planning" + }, + "seo": { + "description": "Nano development & community roadmap", + "tags": [ + "roadmap", + "nano", + "future", + "release", + "design", + "tasks", + "discussions", + "community", + "ambassadors", + "managers" + ], + "title": "Roadmap" + } + }, + "search_bar": { + "placeholder": "Search by Address / Block Hash" + }, + "uptime": { + "now": "Now", + "days_ago": "days ago" + } +} diff --git a/package.json b/package.json index 18cb527f..5dc73212 100644 --- a/package.json +++ b/package.json @@ -95,9 +95,12 @@ "express-favicon": "^2.0.4", "express-jwt": "^8.4.1", "express-robots-txt": "^1.0.0", + "express-slow-down": "^2.0.1", "fetch-cheerio-object": "^1.3.0", "front-matter": "^4.0.2", "fs-extra": "^11.1.1", + "i18next": "^23.8.2", + "i18next-http-backend": "^2.4.3", "jsonwebtoken": "^9.0.1", "knex": "^0.95.15", "markdown-it": "^12.3.2", @@ -116,6 +119,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-helmet": "^6.1.0", + "react-i18next": "^14.0.5", "react-redux": "^7.2.9", "react-router": "^5.3.4", "redux-saga": "^1.2.3", @@ -144,6 +148,7 @@ "compression-webpack-plugin": "^10.0.0", "concurrently": "^8.2.0", "copy-text-to-clipboard": "^3.2.0", + "copy-webpack-plugin": "^12.0.2", "cross-env": "^7.0.3", "css-loader": "6.8.1", "deepmerge": "4.3.1", diff --git a/src/core/api/service.js b/src/core/api/service.js index 05413f6b..6f7e1830 100644 --- a/src/core/api/service.js +++ b/src/core/api/service.js @@ -32,9 +32,14 @@ export const api = { const url = `${API_URL}/posts/${id}?${queryString.stringify(params)}` return { url } }, - getDoc({ id }) { - const url = `${API_URL}/docs${id}.md` - return { url } + getDoc({ id, locale = 'en' }) { + if (locale === 'en') { + const url = `${API_URL}/docs${id}.md` + return { url } + } else { + const url = `${API_URL}/docs/${locale}/${id}.md` + return { url } + } }, getLabelDoc({ id }) { const url = `${API_URL}/docs${id}.md` diff --git a/src/core/app/actions.js b/src/core/app/actions.js index e178832a..c671f3ee 100644 --- a/src/core/app/actions.js +++ b/src/core/app/actions.js @@ -1,11 +1,12 @@ export const appActions = { INIT_APP: 'INIT_APP', - init: ({ token, key }) => ({ + init: ({ token, key, locale }) => ({ type: appActions.INIT_APP, payload: { token, - key + key, + locale } }) } diff --git a/src/core/constants.js b/src/core/constants.js index c8b1dfb2..ca98ab23 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -12,3 +12,23 @@ export const WS_URL = IS_DEV ? 'ws://localhost:8080' : 'wss://nano.community' // 3 Million Nano (3e36) export const REP_MAX_WEIGHT = BigNumber(3).shiftedBy(36) +export const SUPPORTED_LOCALES = [ + 'ar', + 'en', + 'de', + 'es', + 'fa', + 'fr', + 'hi', + 'it', + 'ja', + 'ko', + 'nl', + 'pl', + 'pt', + 'ru', + 'tr', + 'vi', + 'zh', + 'no' +] diff --git a/src/core/docs/actions.js b/src/core/docs/actions.js index 2aa6f4d9..956cbcdb 100644 --- a/src/core/docs/actions.js +++ b/src/core/docs/actions.js @@ -19,10 +19,11 @@ export const docActions = { GET_LABEL_DOC_COMMIT_PENDING: 'GET_LABEL_DOC_COMMIT_PENDING', GET_LABEL_DOC_COMMIT_FULFILLED: 'GET_LABEL_DOC_COMMIT_FULFILLED', - getDoc: (id) => ({ + getDoc: ({ id, locale = 'en' }) => ({ type: docActions.GET_DOC, payload: { - id + id, + locale } }), diff --git a/src/core/i18n/actions.js b/src/core/i18n/actions.js new file mode 100644 index 00000000..4e968253 --- /dev/null +++ b/src/core/i18n/actions.js @@ -0,0 +1,10 @@ +export const i18nActions = { + CHANGE_LOCALE: 'CHANGE_LOCALE', + + change_locale: (locale) => ({ + type: i18nActions.CHANGE_LOCALE, + payload: { + locale + } + }) +} diff --git a/src/core/i18n/index.js b/src/core/i18n/index.js new file mode 100644 index 00000000..3fe3721d --- /dev/null +++ b/src/core/i18n/index.js @@ -0,0 +1,26 @@ +import { initReactI18next } from 'react-i18next' +import i18n from 'i18next' +import HttpBackend from 'i18next-http-backend' + +import { SUPPORTED_LOCALES } from '@core/constants' + +export { i18nActions } from './actions' +export { i18nReducer } from './reducer' +export { i18nSagas } from './sagas' + +i18n + .use(HttpBackend) + .use(initReactI18next) + .init({ + // detection + debug: true, + backend: { + // Configuration options for the backend plugin + loadPath: '/locales/{{lng}}.json' // Path to the translation files + }, + lng: 'en', + fallbackLng: 'en', + supportedLngs: SUPPORTED_LOCALES + }) + +export default i18n diff --git a/src/core/i18n/reducer.js b/src/core/i18n/reducer.js new file mode 100644 index 00000000..1891169f --- /dev/null +++ b/src/core/i18n/reducer.js @@ -0,0 +1,17 @@ +import { Record } from 'immutable' + +import { i18nActions } from './actions' + +const initialState = new Record({ + locale: 'en' +}) + +export function i18nReducer(state = initialState(), { payload, type }) { + switch (type) { + case i18nActions.CHANGE_LOCALE: + return state.set('locale', payload.locale) + + default: + return state + } +} diff --git a/src/core/i18n/sagas.js b/src/core/i18n/sagas.js new file mode 100644 index 00000000..06135e04 --- /dev/null +++ b/src/core/i18n/sagas.js @@ -0,0 +1,37 @@ +import { takeLatest, put, fork } from 'redux-saga/effects' +import i18n from 'i18next' + +import { localStorageAdapter } from '@core/utils' +import { appActions } from '@core/app/actions' +import { i18nActions } from './actions' + +export function* init({ payload }) { + if (payload.locale) { + yield put(i18nActions.change_locale(payload.locale)) + } + + // TODO detect user locale +} + +export function ChangeLocale({ payload }) { + localStorageAdapter.setItem('locale', payload.locale) + i18n.changeLanguage(payload.locale) +} + +//= ==================================== +// WATCHERS +// ------------------------------------- + +export function* watchInitApp() { + yield takeLatest(appActions.INIT_APP, init) +} + +export function* watchChangeLocale() { + yield takeLatest(i18nActions.CHANGE_LOCALE, ChangeLocale) +} + +//= ==================================== +// ROOT +// ------------------------------------- + +export const i18nSagas = [fork(watchInitApp), fork(watchChangeLocale)] diff --git a/src/core/reducers.js b/src/core/reducers.js index e85abb34..7ee19b01 100644 --- a/src/core/reducers.js +++ b/src/core/reducers.js @@ -15,6 +15,7 @@ import { postsReducer } from './posts' import { postlistsReducer } from './postlists' import { nanodb_reducer } from './nanodb' import { api_reducer } from './api' +import { i18nReducer } from './i18n' const rootReducer = (history) => combineReducers({ @@ -32,7 +33,8 @@ const rootReducer = (history) => posts: postsReducer, postlists: postlistsReducer, nanodb: nanodb_reducer, - api: api_reducer + api: api_reducer, + i18n: i18nReducer }) export default rootReducer diff --git a/src/core/sagas.js b/src/core/sagas.js index b121138b..3938c284 100644 --- a/src/core/sagas.js +++ b/src/core/sagas.js @@ -11,6 +11,7 @@ import { ledgerSagas } from './ledger' import { networkSagas } from './network' import { postlistSagas } from './postlists' import { nanodb_sagas } from './nanodb' +import { i18nSagas } from './i18n' export default function* rootSage() { yield all([ @@ -24,6 +25,7 @@ export default function* rootSage() { ...ledgerSagas, ...networkSagas, ...postlistSagas, - ...nanodb_sagas + ...nanodb_sagas, + ...i18nSagas ]) } diff --git a/src/styles/variables.styl b/src/styles/variables.styl index 39968701..a6248ee6 100644 --- a/src/styles/variables.styl +++ b/src/styles/variables.styl @@ -9,3 +9,6 @@ $nanoBlueGrey = #676686 $nanoLightBlue = #F4FAFF $hoverShadow = $borderColor + +$hoverBackground = rgba(255, 255, 255, 0.8) +$hoverBorder = rgba(0, 0, 0, 0.23) \ No newline at end of file diff --git a/src/views/components/account-blocks-summary/account-blocks-summary.js b/src/views/components/account-blocks-summary/account-blocks-summary.js index 1490080d..bab7eef0 100644 --- a/src/views/components/account-blocks-summary/account-blocks-summary.js +++ b/src/views/components/account-blocks-summary/account-blocks-summary.js @@ -11,103 +11,121 @@ import TableRow from '@mui/material/TableRow' import LinearProgress from '@mui/material/LinearProgress' import { Link } from 'react-router-dom' import dayjs from 'dayjs' +import { useTranslation } from 'react-i18next' import './account-blocks-summary.styl' -export default class AccountBlocksSummary extends React.Component { - render() { - const { account, type, accountLabel } = this.props +export default function AccountBlocksSummary({ account, type, accountLabel }) { + const { t } = useTranslation() + const items = account.getIn(['blocks_summary', type], []) + const isChange = type === 'change' - const items = account.getIn(['blocks_summary', type], []) - const isChange = type === 'change' + const is_loading = account.get( + `account_is_loading_blocks_${type}_summary`, + true + ) - const is_loading = account.get( - `account_is_loading_blocks_${type}_summary`, - true - ) - - return ( -
- - {is_loading && } - - + return ( +
+ + {is_loading && } +
+ + + + {t( + `accountBlocksSummary.${accountLabel.toLowerCase()}_account`, + `${accountLabel} Account` + )} + + + {t('account_blocks_summary.transactions', 'TXs')} + + {!isChange && ( + <> + + {t('common.total', 'Total')} + + + {t('account_blocks_summary.max_amount', 'Max Amount')} + + + {t('account_blocks_summary.min_amount', 'Min Amount')} + + + )} + + {t('account_blocks_summary.first_timestamp', 'First Timestamp')} + + + {t('account_blocks_summary.last_timestamp', 'Last Timestamp')} + + + + + {is_loading && ( + + Loading... + + )} + {!items.length && !is_loading && ( - {accountLabel} Account - TXs + No Records + + )} + {items.map((row) => ( + + + + {row.destination_alias || + `${row.destination_account.slice(0, 15)}...`} + + + + {BigNumber(row.block_count).toFormat(0)} + {!isChange && ( <> - Total - Max Amount - Min Amount + + {BigNumber(row.total_amount).shiftedBy(-30).toFormat()} + + + {BigNumber(row.max_amount).shiftedBy(-30).toFormat()} + + + {BigNumber(row.min_amount).shiftedBy(-30).toFormat()} + )} - First Timestamp - Last Timestamp + + {row.first_timestamp + ? dayjs(row.first_timestamp * 1000).format( + 'YYYY-MM-DD h:mm a' + ) + : '-'} + + + {row.last_timestamp + ? dayjs(row.last_timestamp * 1000).format( + 'YYYY-MM-DD h:mm a' + ) + : '-'} + - - - {is_loading && ( - - Loading... - - )} - {!items.length && !is_loading && ( - - No Records - - )} - {items.map((row) => ( - - - - {row.destination_alias || - `${row.destination_account.slice(0, 15)}...`} - - - - {BigNumber(row.block_count).toFormat(0)} - - {!isChange && ( - <> - - {BigNumber(row.total_amount).shiftedBy(-30).toFormat()} - - - {BigNumber(row.max_amount).shiftedBy(-30).toFormat()} - - - {BigNumber(row.min_amount).shiftedBy(-30).toFormat()} - - - )} - - {row.first_timestamp - ? dayjs(row.first_timestamp * 1000).format( - 'YYYY-MM-DD h:mm a' - ) - : '-'} - - - {row.last_timestamp - ? dayjs(row.last_timestamp * 1000).format( - 'YYYY-MM-DD h:mm a' - ) - : '-'} - - - ))} - -
- {items.length === 10 && ( -
- Showing top 10 accounts by total descending -
- )} -
-
- ) - } + ))} + + + {items.length === 10 && ( +
+ {t( + 'account_blocks_summary.showing_top_10', + 'Showing top 10 accounts by total descending' + )} +
+ )} + + + ) } AccountBlocksSummary.propTypes = { diff --git a/src/views/components/account-meta/account-meta.js b/src/views/components/account-meta/account-meta.js index 8fd10fdb..6f609c99 100644 --- a/src/views/components/account-meta/account-meta.js +++ b/src/views/components/account-meta/account-meta.js @@ -3,99 +3,97 @@ import ImmutablePropTypes from 'react-immutable-proptypes' import dayjs from 'dayjs' import { Link } from 'react-router-dom' import BigNumber from 'bignumber.js' +import { useTranslation } from 'react-i18next' import { timeago } from '@core/utils' -export default class AccountMeta extends React.Component { - render() { - const { account } = this.props +export default function AccountMeta({ account }) { + const { t } = useTranslation() + const funding_account = account.getIn(['open', 'funding_account'], '') + const funding_timestamp = account.getIn(['open', 'funding_timestamp']) + const open_timestamp = account.getIn(['open', 'open_timestamp']) + const open_balance = account.getIn(['open', 'open_balance']) + const pending_balance = account.getIn(['account_meta', 'pending']) + const height = account.getIn(['account_meta', 'confirmation_height']) + const modified_timestamp = account.getIn([ + 'account_meta', + 'modified_timestamp' + ]) + const items = [ + { + label: t('account_meta.funding_account', 'Funding Account'), + value: funding_account ? ( + + {account.getIn(['open', 'funding_alias']) || + `${funding_account.slice(0, 15)}...`} + + ) : ( + '-' + ) + }, + { + label: t('account_meta.funding_timestamp', 'Funding Timestamp'), + value: funding_timestamp + ? `${dayjs(funding_timestamp * 1000).format( + 'MMM D, YYYY h:mm a' + )} (${timeago.format(funding_timestamp * 1000, 'nano_short')} ago)` + : '-' + }, + { + label: t('account_meta.open_timestamp', 'Open Timestamp'), + value: open_timestamp + ? `${dayjs(open_timestamp * 1000).format( + 'MMM D, YYYY h:mm a' + )} (${timeago.format(open_timestamp * 1000, 'nano_short')} ago)` + : '-' + }, + { + label: t('account_meta.opening_balance', 'Opening Balance'), + value: open_balance + ? BigNumber(open_balance).shiftedBy(-30).toFormat() + : '-' + }, + { + label: t('account_meta.receivable_balance', 'Receivable Balance'), + value: pending_balance + ? BigNumber(pending_balance).shiftedBy(-30).toFormat() + : '-' + }, + { + label: t('common.version', 'Version'), + value: account.getIn(['account_meta', 'account_version'], '-') + }, + { + label: t('account_meta.height', 'Height'), + value: height ? BigNumber(height).toFormat() : '-' + }, + { + label: t('account_meta.last_modified', 'Last Modified'), + value: modified_timestamp + ? `${dayjs(modified_timestamp * 1000).format( + 'MMM D, YYYY h:mm a' + )} (${timeago.format(modified_timestamp * 1000, 'nano_short')} ago)` + : '-' + } + ] - const fundingAccount = account.getIn(['open', 'funding_account'], '') - const fundingTimestamp = account.getIn(['open', 'funding_timestamp']) - const openTimestamp = account.getIn(['open', 'open_timestamp']) - const openBalance = account.getIn(['open', 'open_balance']) - const pendingBalance = account.getIn(['account_meta', 'pending']) - const height = account.getIn(['account_meta', 'confirmation_height']) - const modifiedTimestamp = account.getIn([ - 'account_meta', - 'modified_timestamp' - ]) - const items = [ - { - label: 'Funding Account', - value: fundingAccount ? ( - - {account.getIn(['open', 'funding_alias']) || - `${fundingAccount.slice(0, 15)}...`} - - ) : ( - '-' - ) - }, - { - label: 'Funding Timestamp', - value: fundingTimestamp - ? `${dayjs(fundingTimestamp * 1000).format( - 'MMM D, YYYY h:mm a' - )} (${timeago.format(fundingTimestamp * 1000, 'nano_short')} ago)` - : '-' - }, - { - label: 'Open Timestamp', - value: openTimestamp - ? `${dayjs(openTimestamp * 1000).format( - 'MMM D, YYYY h:mm a' - )} (${timeago.format(openTimestamp * 1000, 'nano_short')} ago)` - : '-' - }, - { - label: 'Opening Balance', - value: openBalance - ? BigNumber(openBalance).shiftedBy(-30).toFormat() - : '-' - }, - { - label: 'Receivable Balance', - value: pendingBalance - ? BigNumber(pendingBalance).shiftedBy(-30).toFormat() - : '-' - }, - { - label: 'Version', - value: account.getIn(['account_meta', 'account_version'], '-') - }, - { - label: 'Height', - value: height ? BigNumber(height).toFormat() : '-' - }, - { - label: 'Last Modified', - value: modifiedTimestamp - ? `${dayjs(modifiedTimestamp * 1000).format( - 'MMM D, YYYY h:mm a' - )} (${timeago.format(modifiedTimestamp * 1000, 'nano_short')} ago)` - : '-' - } - ] + const rows = items.map((item, idx) => ( +
+
{item.label}
+
{item.value}
+
+ )) - const rows = items.map((i, idx) => ( -
-
{i.label}
-
{i.value}
-
- )) - - return ( -
-
-
- Account Info -
- {rows} + return ( +
+
+
+ {t('account_meta.account_info', 'Account Info')}
+ {rows}
- ) - } +
+ ) } AccountMeta.propTypes = { diff --git a/src/views/components/app/app.js b/src/views/components/app/app.js index 1306392f..78bf4c07 100644 --- a/src/views/components/app/app.js +++ b/src/views/components/app/app.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { Suspense } from 'react' import PropTypes from 'prop-types' import { localStorageAdapter } from '@core/utils' @@ -16,7 +16,8 @@ export default class App extends React.Component { async componentDidMount() { const token = await localStorageAdapter.getItem('token') const key = await localStorageAdapter.getItem('key') - this.props.init({ token, key }) + const locale = await localStorageAdapter.getItem('locale') + this.props.init({ token, key, locale }) this.props.getRepresentatives() this.props.getNetworkStats() this.props.getGithubEvents() @@ -24,11 +25,12 @@ export default class App extends React.Component { } render() { + // TODO improve loading UX return ( - <> + }> - + ) } } diff --git a/src/views/components/block-info/block-info.js b/src/views/components/block-info/block-info.js index 0578ab6e..550c45ae 100644 --- a/src/views/components/block-info/block-info.js +++ b/src/views/components/block-info/block-info.js @@ -5,27 +5,45 @@ import dayjs from 'dayjs' import { Link } from 'react-router-dom' import LoopIcon from '@mui/icons-material/Loop' import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline' +import { useTranslation } from 'react-i18next' import { timeago } from '@core/utils' import './block-info.styl' function BlockType({ type }) { + const { t } = useTranslation() switch (type) { case 'epoch': - return
Epoch
+ return ( +
+ {t('block_type.epoch', 'Epoch')} +
+ ) case 'send': - return
Send
+ return ( +
{t('block_type.send', 'Send')}
+ ) case 'receive': - return
Receive
+ return ( +
+ {t('block_type.receive', 'Receive')} +
+ ) case 'change': - return
Change
+ return ( +
+ {t('block_type.change', 'Change')} +
+ ) case 'open': - return
Open
+ return ( +
{t('block_type.open', 'Open')}
+ ) } } @@ -34,11 +52,12 @@ BlockType.propTypes = { } function BlockStatus({ confirmed }) { + const { t } = useTranslation() if (confirmed) { return (
- Confirmed + {t('block_status.confirmed', 'Confirmed')}
) } @@ -46,7 +65,7 @@ function BlockStatus({ confirmed }) { return (
- Unconfirmed + {t('block_status.unconfirmed', 'Unconfirmed')}
) } @@ -55,54 +74,50 @@ BlockStatus.propTypes = { confirmed: PropTypes.bool } -export default class BlockInfo extends React.Component { - render() { - const { block, type } = this.props - - const timestamp = parseInt( - block.getIn(['blockInfo', 'local_timestamp'], 0), - 10 - ) - // const previous = block.getIn(['blockInfo', 'contents', 'previous']) - const isConfirmed = block.blockInfo.confirmed === 'true' - - const items = [ - { - label: 'Status', - value: - }, - { - label: 'Operation', - value: - }, - { - label: 'Timestamp', - value: timestamp - ? `${dayjs(timestamp * 1000).format( - 'MMM D, YYYY h:mm a' - )} (${timeago.format(timestamp * 1000, 'nano_short')} ago)` - : '-' - }, - { - label: 'Block Account', - value: ( - - {block.blockAccountAlias || - `${block.blockInfo.block_account.slice(0, 15)}...`} - - ) - } - ] - - const rows = items.map((i, idx) => ( -
-
{i.label}
-
{i.value}
-
- )) +export default function BlockInfo({ block, type }) { + const { t } = useTranslation() + const timestamp = parseInt( + block.getIn(['blockInfo', 'local_timestamp'], 0), + 10 + ) + const isConfirmed = block.blockInfo.confirmed === 'true' + + const items = [ + { + label: t('block_info.status', 'Status'), + value: + }, + { + label: t('block_info.operation', 'Operation'), + value: + }, + { + label: t('block_info.timestamp', 'Timestamp'), + value: timestamp + ? `${dayjs(timestamp * 1000).format( + 'MMM D, YYYY h:mm a' + )} (${timeago.format(timestamp * 1000, 'nano_short')} ago)` + : '-' + }, + { + label: t('block_info.block_account', 'Block Account'), + value: ( + + {block.blockAccountAlias || + `${block.blockInfo.block_account.slice(0, 15)}...`} + + ) + } + ] + + const rows = items.map((i, idx) => ( +
+
{i.label}
+
{i.value}
+
+ )) - return
{rows}
- } + return
{rows}
} BlockInfo.propTypes = { diff --git a/src/views/components/change-locale/change-locale.js b/src/views/components/change-locale/change-locale.js new file mode 100644 index 00000000..04a03535 --- /dev/null +++ b/src/views/components/change-locale/change-locale.js @@ -0,0 +1,59 @@ +import React from 'react' +import PropTypes from 'prop-types' +import FormControl from '@material-ui/core/FormControl' +import Select from '@material-ui/core/Select' +import MenuItem from '@material-ui/core/MenuItem' +import SvgIcon from '@material-ui/core/SvgIcon' + +import './change-locale.styl' + +function TranslateIcon(props) { + return ( + + + + + ) +} + +export default function ChangeLocale({ change_locale, locale }) { + const locale_texts = { + en: 'English', + es: 'Español', + fr: 'Français', + it: 'Italiano', + de: 'Deutsch', + nl: 'Nederlands', + ru: 'Русский' + } + + return ( + + + + ) +} + +ChangeLocale.propTypes = { + change_locale: PropTypes.func.isRequired, + locale: PropTypes.string.isRequired +} diff --git a/src/views/components/change-locale/change-locale.styl b/src/views/components/change-locale/change-locale.styl new file mode 100644 index 00000000..774adf01 --- /dev/null +++ b/src/views/components/change-locale/change-locale.styl @@ -0,0 +1,15 @@ +.change-locale.MuiFormControl-root + margin 0 auto + max-width 160px + margin 8px + + .MuiOutlinedInput-notchedOutline + border-color lighten($borderColor, 40%) + + &:hover + .MuiInputBase-root + background $hoverBackground + + .MuiOutlinedInput-notchedOutline + border-color $hoverBorder + box-shadow $hoverShadow 4px 4px 0px 0px diff --git a/src/views/components/change-locale/index.js b/src/views/components/change-locale/index.js new file mode 100644 index 00000000..5d78615d --- /dev/null +++ b/src/views/components/change-locale/index.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux' +import { createSelector } from 'reselect' + +import { i18nActions } from '@core/i18n' + +import ChangeLocale from './change-locale' + +const mapStateToProps = createSelector( + (state) => state.getIn(['i18n', 'locale']), + (locale) => ({ locale }) +) + +const mapDispatchToProps = { + change_locale: i18nActions.change_locale +} + +export default connect(mapStateToProps, mapDispatchToProps)(ChangeLocale) diff --git a/src/views/components/github-events/github-events.js b/src/views/components/github-events/github-events.js index 1920b737..4c336e03 100644 --- a/src/views/components/github-events/github-events.js +++ b/src/views/components/github-events/github-events.js @@ -1,57 +1,92 @@ import React from 'react' +import PropTypes from 'prop-types' import ImmutablePropTypes from 'react-immutable-proptypes' import Skeleton from '@mui/material/Skeleton' +import { useTranslation } from 'react-i18next' import { timeago } from '@core/utils' import './github-events.styl' -const action = (item) => { +const action = ({ item, t }) => { switch (item.type) { case 'CommitCommentEvent': - return 'commented on commit' + return t( + 'github_events.action.commented_on_commit', + 'commented on commit' + ) case 'CreateEvent': - return `created ${item.action}` + return t( + 'github_events.action.created', + { action: item.action }, + `created ${item.action}` + ) case 'DeleteEvent': - return `deleted ${item.action}` + return t( + 'github_events.action.deleted', + { action: item.action }, + `deleted ${item.action}` + ) case 'ForkEvent': - return 'forked' + return t('github_events.action.forked', 'forked') case 'IssueCommentEvent': - return 'commented on issue' + return t('github_events.action.commented_on_issue', 'commented on issue') case 'IssuesEvent': - return `${item.action} issue` + return t( + 'github_events.action.issue_action', + { action: item.action }, + `${item.action} issue` + ) case 'PublicEvent': - return 'made public' + return t('github_events.action.made_public', 'made public') case 'MemberEvent': - return 'added member' + return t('github_events.action.added_member', 'added member') case 'SponsorshipEvent': - return 'sponshorship started' + return t( + 'github_events.action.sponsorship_started', + 'sponsorship started' + ) case 'PullRequestEvent': - return `${item.action} pr` + return t( + 'github_events.action.pr_action', + { action: item.action }, + `${item.action} pr` + ) case 'PullRequestReviewEvent': - return `pr review ${item.title}` + return t( + 'github_events.action.pr_review', + { title: item.title }, + `pr review ${item.title}` + ) case 'PullRequestReviewCommentEvent': - return 'commented on pr review' + return t( + 'github_events.action.commented_on_pr_review', + 'commented on pr review' + ) case 'PushEvent': - return `pushed commit to ${item.ref.slice(0, 15)}` + return t( + 'github_events.action.pushed_commit', + { ref: item.ref.slice(0, 15) }, + `pushed commit to ${item.ref.slice(0, 15)}` + ) case 'ReleaseEvent': - return 'published release' + return t('github_events.action.published_release', 'published release') case 'WatchEvent': - return 'watching repo' + return t('github_events.action.watching_repo', 'watching repo') } } @@ -71,11 +106,11 @@ const link = (item) => { } } -const GithubEvent = (item, index) => { +const GithubEvent = ({ item, index, t }) => { return (
{item.actor_name}
-
{action(item)}
+
{action({ item, t })}
{item.event_url && ( {link(item)} @@ -88,27 +123,34 @@ const GithubEvent = (item, index) => { ) } -export default class GithubEvents extends React.Component { - render() { - const { events } = this.props - const items = events.map((i, idx) => GithubEvent(i, idx)) - const skeletons = new Array(15).fill(undefined) - - return ( -
-
Development Events
-
- {Boolean(items.size) && items} - {!items.size && - skeletons.map((i, idx) => ( -
- -
- ))} -
+GithubEvent.propTypes = { + item: ImmutablePropTypes.record, + index: ImmutablePropTypes.number, + t: PropTypes.func +} + +export default function GithubEvents({ events }) { + const { t } = useTranslation() + + const items = events.map((item, index) => GithubEvent({ item, index, t })) + const skeletons = new Array(15).fill(undefined) + + return ( +
+
+ {t('github_events.events_title', 'Development Events')}
- ) - } +
+ {Boolean(items.size) && items} + {!items.size && + skeletons.map((i, idx) => ( +
+ +
+ ))} +
+
+ ) } GithubEvents.propTypes = { diff --git a/src/views/components/ledger-chart-addresses/ledger-chart-addresses.js b/src/views/components/ledger-chart-addresses/ledger-chart-addresses.js index 35bbee8d..c3a52758 100644 --- a/src/views/components/ledger-chart-addresses/ledger-chart-addresses.js +++ b/src/views/components/ledger-chart-addresses/ledger-chart-addresses.js @@ -10,6 +10,7 @@ import { } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' import Button from '@mui/material/Button' +import { useTranslation } from 'react-i18next' import { download_csv, download_json } from '@core/utils' import LedgerChartMetrics from '@components/ledger-chart-metrics' @@ -23,6 +24,7 @@ echarts.use([ ]) export default function LedgerChartAddresses({ data, isLoading }) { + const { t } = useTranslation() const option = { grid: { containLabel: true @@ -126,17 +128,20 @@ export default function LedgerChartAddresses({ data, isLoading }) {
- Description + {t('ledger.description', 'Description')}

- The total number of active, new, and reused addresses used per - day. + {t( + 'ledger.addresses.total_number', + 'The total number of active, new, and reused addresses used per day.' + )}

- Active shows the number of unique addresses used. New shows the - number of addresses created. Reused shows the number of addresses - used that were created on a previous day. + {t( + 'ledger.addresses.active_detail', + 'Active shows the number of unique addresses used. New shows the number of addresses created. Reused shows the number of addresses used that were created on a previous day.' + )}

{!isLoading && ( @@ -152,9 +157,12 @@ export default function LedgerChartAddresses({ data, isLoading }) {
+ -
) diff --git a/src/views/components/ledger-chart-amounts/ledger-chart-amounts.js b/src/views/components/ledger-chart-amounts/ledger-chart-amounts.js index 979ab297..bfbb0218 100644 --- a/src/views/components/ledger-chart-amounts/ledger-chart-amounts.js +++ b/src/views/components/ledger-chart-amounts/ledger-chart-amounts.js @@ -11,6 +11,7 @@ import { } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' import Button from '@mui/material/Button' +import { useTranslation } from 'react-i18next' import { download_csv, download_json } from '@core/utils' @@ -48,6 +49,7 @@ const color_map = { } export default function LedgerChartAmounts({ data, isLoading }) { + const { t } = useTranslation() const ranges = { _1000000_count: '>1M', _100000_count: '100k to 1M', @@ -114,7 +116,8 @@ export default function LedgerChartAmounts({ data, isLoading }) { { type: 'log', min: 1, - max: 5000000 + max: 5000000, + name: t('common.blocks', 'Blocks') } ], series: series_data @@ -170,11 +173,13 @@ export default function LedgerChartAmounts({ data, isLoading }) {
- Description + {t('ledger.description', 'Description')}
- The number of confirmed send-type blocks per day where the amount in - the block is in a given range (in Nano) + {t( + 'ledger.amounts.total_number', + 'The number of confirmed send-type blocks per day where the amount in the block is in a given range (in Nano)' + )}
{!isLoading && (
diff --git a/src/views/components/ledger-chart-blocks/ledger-chart-blocks.js b/src/views/components/ledger-chart-blocks/ledger-chart-blocks.js index b56fd5c2..92190c6a 100644 --- a/src/views/components/ledger-chart-blocks/ledger-chart-blocks.js +++ b/src/views/components/ledger-chart-blocks/ledger-chart-blocks.js @@ -10,6 +10,7 @@ import { GridComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' +import { useTranslation } from 'react-i18next' import LedgerChartMetrics from '@components/ledger-chart-metrics' import { download_csv, download_json } from '@core/utils' @@ -23,6 +24,7 @@ echarts.use([ ]) export default function LedgerChartBlocks({ data, isLoading }) { + const { t } = useTranslation() const option = { grid: { containLabel: true @@ -40,13 +42,13 @@ export default function LedgerChartBlocks({ data, isLoading }) { }, yAxis: { type: 'log', - name: 'Blocks', + name: t('common.blocks', 'Blocks'), min: 1 }, series: [ { type: 'line', - name: 'Total', + name: t('common.Total', 'Total'), showSymbol: false, lineStyle: { width: 1 @@ -55,7 +57,7 @@ export default function LedgerChartBlocks({ data, isLoading }) { }, { type: 'line', - name: 'Send', + name: t('block_type.send', 'Send'), showSymbol: false, lineStyle: { width: 1 @@ -64,7 +66,7 @@ export default function LedgerChartBlocks({ data, isLoading }) { }, { type: 'line', - name: 'Change', + name: t('block_type.change', 'Change'), showSymbol: false, lineStyle: { width: 1 @@ -73,7 +75,7 @@ export default function LedgerChartBlocks({ data, isLoading }) { }, { type: 'line', - name: 'Receive', + name: t('block_type.receive', 'Receive'), showSymbol: false, lineStyle: { width: 1 @@ -82,7 +84,7 @@ export default function LedgerChartBlocks({ data, isLoading }) { }, { type: 'line', - name: 'Open', + name: t('block_type.open', 'Open'), showSymbol: false, lineStyle: { width: 1 @@ -148,10 +150,13 @@ export default function LedgerChartBlocks({ data, isLoading }) {
- Description + {t('ledger.description', 'Description')}
- The number of confirmed blocks (by type) per day. + {t( + 'ledger.blocks.description', + 'The number of blocks confirmed per day.' + )}
{!isLoading && (
@@ -166,27 +171,27 @@ export default function LedgerChartBlocks({ data, isLoading }) {
diff --git a/src/views/components/ledger-chart-metrics/ledger-chart-metrics.js b/src/views/components/ledger-chart-metrics/ledger-chart-metrics.js index 6f4b843a..ed528262 100644 --- a/src/views/components/ledger-chart-metrics/ledger-chart-metrics.js +++ b/src/views/components/ledger-chart-metrics/ledger-chart-metrics.js @@ -2,6 +2,7 @@ import React from 'react' import dayjs from 'dayjs' import PropTypes from 'prop-types' import BigNumber from 'bignumber.js' +import { useTranslation } from 'react-i18next' import './ledger-chart-metrics.styl' @@ -10,6 +11,7 @@ export default function LedgerChartMetrics({ label, show_total = false }) { + const { t } = useTranslation() const values = data.map((d) => d[1]) const max = values.length ? Math.max(...values) : null const min = values.length ? Math.min(...values.filter(Boolean)) : null @@ -28,7 +30,9 @@ export default function LedgerChartMetrics({
-
Min
+
+ {t('common.min', 'Min')} +
{min ? BigNumber(min).toFormat(0) : '-'}
@@ -37,7 +41,9 @@ export default function LedgerChartMetrics({
-
Max
+
+ {t('common.max', 'Max')} +
{max ? BigNumber(max).toFormat(0) : '-'}
@@ -47,7 +53,9 @@ export default function LedgerChartMetrics({
{show_total && (
-
Total
+
+ {t('common.total', 'Total')} +
{total ? BigNumber(total).toFormat(0) : '-'}
diff --git a/src/views/components/ledger-chart-usd-transferred/ledger-chart-usd-transferred.js b/src/views/components/ledger-chart-usd-transferred/ledger-chart-usd-transferred.js index 90e7e190..85ca0130 100644 --- a/src/views/components/ledger-chart-usd-transferred/ledger-chart-usd-transferred.js +++ b/src/views/components/ledger-chart-usd-transferred/ledger-chart-usd-transferred.js @@ -11,6 +11,7 @@ import { } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' import Button from '@mui/material/Button' +import { useTranslation } from 'react-i18next' import LedgerChartMetrics from '@components/ledger-chart-metrics' import { download_csv, download_json } from '@core/utils' @@ -24,6 +25,7 @@ echarts.use([ ]) export default function LedgerUSDTransferred({ data, isLoading }) { + const { t } = useTranslation() const span_style = 'float:right;margin-left:20px;font-size:14px;color:#666;font-weight:900' @@ -81,7 +83,7 @@ export default function LedgerUSDTransferred({ data, isLoading }) { series: [ { type: 'line', - name: 'USD Transferred', + name: t('ledger.usd_transferred.usd_transferred', 'USD Transferred'), showSymbol: false, lineStyle: { width: 1 @@ -132,13 +134,20 @@ export default function LedgerUSDTransferred({ data, isLoading }) {
- Description + {t('ledger.description', 'Description')}
-

The total amount of value transferred (in USD) per day.

- Based on the daily closing price of Nano/USD and the total amount - of Nano transferred that day. + {t( + 'ledger.usd_transferred.desc_1', + 'The total amount of value transferred (in USD) per day.' + )} +

+

+ {t( + 'ledger.usd_transferred.desc_2', + 'Based on the daily closing price of Nano/USD and the total amount of Nano transferred that day.' + )}

{!isLoading && ( @@ -154,7 +163,10 @@ export default function LedgerUSDTransferred({ data, isLoading }) {
[d[0], d[1]])} - label='USD Transferred Stats' + label={t( + 'ledger.usd_transferred.usd_transferred_stats', + 'USD Transferred Stats' + )} show_total />
diff --git a/src/views/components/ledger-chart-volume/ledger-chart-volume.js b/src/views/components/ledger-chart-volume/ledger-chart-volume.js index 852ce9f4..2381c1cd 100644 --- a/src/views/components/ledger-chart-volume/ledger-chart-volume.js +++ b/src/views/components/ledger-chart-volume/ledger-chart-volume.js @@ -11,6 +11,7 @@ import { } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' import Button from '@mui/material/Button' +import { useTranslation } from 'react-i18next' import LedgerChartMetrics from '@components/ledger-chart-metrics' import { download_csv, download_json } from '@core/utils' @@ -24,6 +25,7 @@ echarts.use([ ]) export default function LedgerChartVolume({ data, isLoading }) { + const { t } = useTranslation() const span_style = 'float:right;margin-left:20px;font-size:14px;color:#666;font-weight:900' @@ -82,7 +84,7 @@ export default function LedgerChartVolume({ data, isLoading }) { series: [ { type: 'line', - name: 'Send', + name: t('block_type.send', 'Send'), showSymbol: false, lineStyle: { width: 1 @@ -91,7 +93,7 @@ export default function LedgerChartVolume({ data, isLoading }) { }, { type: 'line', - name: 'Change', + name: t('block_type.change', 'Change'), showSymbol: false, lineStyle: { width: 1 @@ -150,11 +152,13 @@ export default function LedgerChartVolume({ data, isLoading }) {
- Description + {t('ledger.description', 'Description')}
- The total amount sent (in Nano) and total amount of voting weight - changed per day. + {t( + 'ledger.volume.description', + 'The total amount sent (in Nano) and total amount of voting weight changed per day.' + )}
{!isLoading && (
@@ -169,12 +173,12 @@ export default function LedgerChartVolume({ data, isLoading }) {
diff --git a/src/views/components/menu/menu.js b/src/views/components/menu/menu.js index c76e36ea..ffef5004 100644 --- a/src/views/components/menu/menu.js +++ b/src/views/components/menu/menu.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useState } from 'react' import { NavLink } from 'react-router-dom' import PropTypes from 'prop-types' import SwipeableDrawer from '@mui/material/SwipeableDrawer' @@ -6,183 +6,225 @@ import CloseIcon from '@mui/icons-material/Close' import SpeedDial from '@mui/material/SpeedDial' import SpeedDialAction from '@mui/material/SpeedDialAction' import HomeIcon from '@mui/icons-material/Home' +import { useTranslation } from 'react-i18next' import SearchBar from '@components/search-bar' import history from '@core/history' +import ChangeLocale from '@components/change-locale' import './menu.styl' const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) function MenuSections() { + const { t } = useTranslation() return (
-
Introduction
+
+ {t('menu.introduction', 'Introduction')} +
- Overview - Advantages - How it works - Why it matters - Misconceptions + + {t('menu.overview', 'Overview')} + + + {t('menu.advantages', 'Advantages')} + + + {t('menu.how_it_works', 'How it works')} + + + {t('menu.why_it_matters', 'Why it matters')} + + + {t('menu.misconceptions', 'Misconceptions')} + - Investment thesis + {t('menu.investment_thesis', 'Investment thesis')} - History - FAQs + + {t('menu.history', 'History')} + + {t('menu.faqs', 'FAQs')}
-
Guides
+
{t('menu.guides', 'Guides')}
- Basics - Storing - Acquiring + + {t('menu.basics', 'Basics')} + + + {t('menu.storing', 'Storing')} + + + {t('menu.acquiring', 'Acquiring')} + - Choosing a Rep + {t('menu.choosing_a_rep', 'Choosing a Rep')} + + + {t('menu.using', 'Using')} - Using - Account Setup + {t('menu.account_setup', 'Account Setup')} + + + {t('menu.privacy', 'Privacy')} - Privacy - Best Practices + {t('menu.best_practices', 'Best Practices')}
-
Learn
+
{t('menu.learn', 'Learn')}
- Design - Security - Attack Vectors - Challenges - Glossary - Get Support + {t('menu.design', 'Design')} + + {t('menu.security', 'Security')} + + + {t('menu.attack_vectors', 'Attack Vectors')} + + + {t('menu.challenges', 'Challenges')} + + + {t('menu.glossary', 'Glossary')} + + + {t('menu.get_support', 'Get Support')} +
-
Developers
+
+ {t('menu.developers', 'Developers')} +
- Getting Started + {t('menu.getting_started', 'Getting Started')} - Integrations + {t('menu.integrations', 'Integrations')} - Running a node + {t('menu.running_a_node', 'Running a node')} - {/* - Tutorials - */} - Documentation + {t('menu.documentation', 'Documentation')} - Protocol + {t('menu.protocol', 'Protocol')} - {/* Integrations */} - Developer Discussions + {t('menu.developer_discussions', 'Developer Discussions')}
-
Get Involved
+
+ {t('menu.get_involved', 'Get Involved')} +
- Planning 👾 - Contribution Guide - Communities + {t('menu.planning', 'Planning 👾')} + + {t('menu.contribution_guide', 'Contribution Guide')} + + + {t('menu.communities', 'Communities')} +
-
Stats
+
{t('menu.stats', 'Stats')}
- Representatives - Telemetry - Ledger + + {t('common.representative', { + count: 2, + defaultValue: 'Representatives' + })} + + {t('menu.telemetry', 'Telemetry')} + {t('menu.ledger', 'Ledger')}
-
Topics
+
{t('menu.topics', 'Topics')}
- Privacy + {t('menu.privacy', 'Privacy')}
) } -export default class Menu extends React.Component { - constructor(props) { - super(props) - this.state = { - open: false - } - } +export default function Menu({ hide, hideSearch, hide_speed_dial }) { + const [open, setOpen] = useState(false) - handleOpen = () => this.setState({ open: true }) - handleClose = () => this.setState({ open: false }) - handleClick = () => this.setState({ open: !this.state.open }) - handleHomeClick = () => history.push('/') + const handleOpen = () => setOpen(true) + const handleClose = () => setOpen(false) + const handleClick = () => setOpen(!open) + const handleHomeClick = () => history.push('/') - render() { - const { hide, hideSearch, hide_speed_dial } = this.props - const isHome = history.location.pathname === '/' - const isMobile = window.innerWidth < 750 + const isHome = history.location.pathname === '/' + const isMobile = window.innerWidth < 750 - return ( -
- - - - {!hide_speed_dial && ( - - } - openIcon={}> - {!isHome && ( - } - tooltipTitle='Home' - tooltipPlacement={isMobile ? 'left' : 'right'} - onClick={this.handleHomeClick} - /> - )} - - )} -
- {isHome ? ( -
NANO
- ) : ( - - NANO - - )} - {!hideSearch && } - {!hide && } + return ( +
+ + +
+
+
+ {!hide_speed_dial && ( + + } + openIcon={}> + {!isHome && ( + } + tooltipTitle='Home' + tooltipPlacement={isMobile ? 'left' : 'right'} + onClick={handleHomeClick} + /> + )} + + )} +
+ {isHome ? ( +
NANO
+ ) : ( + + NANO + + )} + {!hideSearch && } + {!hide && } + {!isHome && }
- ) - } +
+ ) } Menu.propTypes = { diff --git a/src/views/components/menu/menu.styl b/src/views/components/menu/menu.styl index 75471c56..c7f1ccfc 100644 --- a/src/views/components/menu/menu.styl +++ b/src/views/components/menu/menu.styl @@ -1,6 +1,3 @@ -$hoverBackground = rgba(255, 255, 255, 0.8) -$hoverBorder = rgba(0, 0, 0, 0.23) - .menu__dial position fixed left 16px @@ -46,10 +43,11 @@ $hoverBorder = rgba(0, 0, 0, 0.23) .MuiDrawer-paper.MuiPaper-root background $backgroundColor border-radius 0 0 32px 32px + padding 16px 32px .menu__sections + .settings__container max-width 1100px margin 0 auto - padding 16px 32px .menu__section padding 16px diff --git a/src/views/components/network/network.js b/src/views/components/network/network.js index 2409c93f..14d20432 100644 --- a/src/views/components/network/network.js +++ b/src/views/components/network/network.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import HelpOutlineIcon from '@mui/icons-material/HelpOutline' import Tooltip from '@mui/material/Tooltip' import ImmutablePropTypes from 'react-immutable-proptypes' +import { useTranslation } from 'react-i18next' import './network.styl' @@ -22,245 +23,256 @@ const convert_ms_to_readable_time = (ms) => { } } -export default class Network extends React.Component { - render() { - const { - network, - wattHour, - stats, - unconfirmed_block_pool_count, - send_volume_nano - } = this.props +export default function Network({ + network, + wattHour, + stats, + unconfirmed_block_pool_count, + send_volume_nano +}) { + const { t } = useTranslation() - const prText = - 'as observed across the networks principal representatives: voting nodes with more than 0.1% of the online voting weight delegated to them' + const confirmations_text = t( + 'network.confirmations_text', + 'Total number of transactions confirmed by the network over the last 24 hours' + ) + const settlement_text = t( + 'network.settlement_text', + 'Total amount of value settled by the network over the last 24 hours (only send blocks)' + ) + const throughput_text = t( + 'network.throughput_text', + 'Median number of transactions confirmed per second in the last minute $t(network.pr_text)' + ) + const speed_text = t( + 'network.speed_text', + 'Time in milliseconds for a test transaction to get confirmed' + ) + const unconfirmed_pool_text = t( + 'network.unconfirmed_pool_text', + 'Median number of transactions waiting to be confirmed $t(network.pr_text)' + ) + const stake_text = t( + 'network.stake_text', + 'Percentage of delegated Nano weight actively participating in voting' + ) + const confirm_text = t( + 'network.confirm_text', + 'The minimum number of representatives needed to confirm transactions' + ) + const censor_text = t( + 'network.censor_text', + 'The minimum number of representatives needed to censor transactions or stall the network' + ) + const fee_text = t( + 'network.fee_text', + 'The Nano network operates without fees' + ) + const energy_text = t( + 'network.energy_text', + 'Estimated live network CPU energy usage of Principle Representatives based on collected CPU model info. The estimate is based on CPU TDP, which is the average power, in watts, the processor dissipates when operating at base frequency with all cores active under manufacture-defined, high-complexity workload' + ) - const confirmationsText = - 'Total number of transactions confirmed by the network over the last 24 hours' - const settlementText = - 'Total amount of value settled by the network over the last 24 hours (only send blocks)' - const throughputText = `Median number of transactions confirmed per second in the last minute ${prText}` - const speedText = - 'Median time in milliseconds for a block to get confirmed (across all buckets)' - const unconfirmed_pool_text = `Number of blocks waiting to be confirmed ${prText}` - const stakeText = - 'Percentage of delegated Nano weight actively participating in voting' - const confirmText = - 'The minimum number of representatives needed to confirm transactions' - const censorText = - 'The minimum number of representatives needed to censor transactions or stall the network' - const feeText = 'The Nano network operates without fees' - const energyText = - 'Estimated live network CPU energy usage of Principle Representatives based on collected CPU model info. The estimate is based on CPU TDP, which is the average power, in watts, the processor dissipates when operating at base frequency with all cores active under manufacture-defined, high-complexity workload' - - return ( -
-
Network Stats
-
-
- Confirmations (24h) - - - -
-
- {network.getIn(['stats', 'nanodb', 'confirmations_last_24_hours']) - ? format_number( - network.getIn([ - 'stats', - 'nanodb', - 'confirmations_last_24_hours' - ]) - ) - : '-'} -
+ return ( +
+
+ {t('network.stats_title', 'Network Stats')} +
+
+
+ {t('network.confirmations', 'Confirmations (24h)')} + + + +
+
+ {network.getIn(['stats', 'nanodb', 'confirmations_last_24_hours']) + ? format_number( + network.getIn([ + 'stats', + 'nanodb', + 'confirmations_last_24_hours' + ]) + ) + : '-'} +
+
+
+
+ {t('network.settlement', 'Settlement (24h)')} + + +
-
-
- Settlement (24h) - - - -
-
- {network.getIn(['stats', 'current_price_usd']) - ? `$${format_number( - ( - send_volume_nano * - network.getIn(['stats', 'current_price_usd']) - ).toFixed(0) - )}` - : '-'} -
+
+ {network.getIn(['stats', 'current_price_usd']) + ? `$${format_number( + ( + send_volume_nano * + network.getIn(['stats', 'current_price_usd']) + ).toFixed(0) + )}` + : '-'}
-
-
- Tx Fees (24h) - - - -
-
$0
+
+
+
+ {t('network.tx_fees', 'Tx Fees (24h)')} + + + +
+
$0
+
+
+
+ {t('network.tx_throughput', 'Tx Throughput')} + + +
-
-
- Tx Throughput - - - -
-
- {/* TODO remove this nanoticker dependency */} - {network.getIn(['stats', 'nanobrowse', 'CPSMedian_pr']) - ? `${network - .getIn(['stats', 'nanobrowse', 'CPSMedian_pr']) - .toFixed(1)} CPS` - : '-'} -
+
+ {/* TODO remove this nanoticker dependency */} + {network.getIn(['stats', 'nanobrowse', 'CPSMedian_pr']) + ? `${network + .getIn(['stats', 'nanobrowse', 'CPSMedian_pr']) + .toFixed(1)} CPS` + : '-'}{' '}
-
-
- Tx Speed (24h) - - - -
-
- {network.getIn( - ['stats', 'nanodb', 'median_latency_ms_last_24_hours'], - 0 - ) - ? convert_ms_to_readable_time( - network.getIn( - ['stats', 'nanodb', 'median_latency_ms_last_24_hours'], - 0 - ) +
+
+
+ {t('network.tx_speed', { time_range: '24h' }, 'Tx Speed (24h)')} + + + +
+
+ {network.getIn( + ['stats', 'nanodb', 'median_latency_ms_last_24_hours'], + 0 + ) + ? convert_ms_to_readable_time( + network.getIn( + ['stats', 'nanodb', 'median_latency_ms_last_24_hours'], + 0 ) - : '-'} -
+ ) + : '-'} +
+
+
+
+ {t('network.tx_speed', { time_range: '1h' }, 'Tx Speed (1h)')}
-
-
- Tx Speed (1h) - - - -
-
- {network.getIn( - ['stats', 'nanodb', 'median_latency_ms_last_hour'], - 0 - ) - ? convert_ms_to_readable_time( - network.getIn( - ['stats', 'nanodb', 'median_latency_ms_last_hour'], - 0 - ) +
+ {network.getIn(['stats', 'nanodb', 'median_latency_ms_last_hour'], 0) + ? convert_ms_to_readable_time( + network.getIn( + ['stats', 'nanodb', 'median_latency_ms_last_hour'], + 0 ) - : '-'} -
+ ) + : '-'} +
+
+
+
+ {t('network.tx_speed', { time_range: '10m' }, 'Tx Speed (10m)')}
-
-
- Tx Speed (10m) - - - -
-
- {network.getIn( - ['stats', 'nanodb', 'median_latency_ms_last_10_mins'], - 0 - ) - ? convert_ms_to_readable_time( - network.getIn( - ['stats', 'nanodb', 'median_latency_ms_last_10_mins'], - 0 - ) +
+ {network.getIn( + ['stats', 'nanodb', 'median_latency_ms_last_10_mins'], + 0 + ) + ? convert_ms_to_readable_time( + network.getIn( + ['stats', 'nanodb', 'median_latency_ms_last_10_mins'], + 0 ) - : '-'} -
+ ) + : '-'}
-
-
- Unconfirmed Blocks - - - -
-
- {unconfirmed_block_pool_count != null - ? format_number(unconfirmed_block_pool_count) - : '-'} -
+
+
+
+ {t('network.tx_backlog', 'Tx Backlog')} + + +
-
-
- Online Stake - - - -
-
- {/* TODO remove this nanoticker dependency */} - {network.getIn(['stats', 'nanobrowse', 'pStakeTotalStat']) - ? `${network - .getIn(['stats', 'nanobrowse', 'pStakeTotalStat']) - .toFixed(1)}%` - : '-'} -
+
+ {unconfirmed_block_pool_count != null + ? format_number(unconfirmed_block_pool_count) + : '-'}
-
-
Principal Reps
-
{stats.prCount || '-'}
+
+
+
+ {t('network.online_stake', 'Online Stake')} + + +
-
-
Total Reps (24h)
-
{network.getIn(['totalReps'], '-')}
+
+ {/* TODO remove this nanoticker dependency */} + {network.getIn(['stats', 'nanobrowse', 'pStakeTotalStat']) + ? `${network + .getIn(['stats', 'nanobrowse', 'pStakeTotalStat']) + .toFixed(1)}%` + : '-'}{' '}
-
-
Peers
-
{network.getIn(['stats', 'nanobrowse', 'peersMax'], '-')}
+
+
+
{t('network.principal_reps', 'Principal Reps')}
+
{stats.prCount || '-'}
+
+
+
{t('network.total_reps', 'Total Reps (24h)')}
+
{network.getIn(['totalReps'], '-')}
+
+
+
{t('common.peers', 'Peers')}
+
{network.getIn(['stats', 'nanobrowse', 'peersMax'], '-')}
+
+
+
+ {t('network.reps_to_confirm', 'Reps to Confirm')} + + +
-
-
- Reps to Confirm - - - -
-
{stats.confirmReps || '-'}
+
{stats.confirmReps || '-'}
+
+
+
+ {t('network.reps_to_censor', 'Reps to Censor or Stall')} + + +
-
-
- Reps to Censor or Stall - - - -
-
{stats.censorReps || '-'}
+
{stats.censorReps || '-'}
+
+
+
+ {t('network.energy_usage', 'Energy Usage (TDP) (24h)')} + + +
-
-
- Energy Usage (TDP) (24h) - - - -
-
- {wattHour ? `${((wattHour * 24) / 1000).toFixed(2)} kWh` : '-'} -
+
+ {wattHour ? `${((wattHour * 24) / 1000).toFixed(2)} kWh` : '-'}
- - NanoTicker -
- ) - } + + {t('network.nano_ticker', 'NanoTicker')} + +
+ ) } Network.propTypes = { diff --git a/src/views/components/representative-alerts/representative-alerts.js b/src/views/components/representative-alerts/representative-alerts.js index 99ac9f5e..4a4a7be2 100644 --- a/src/views/components/representative-alerts/representative-alerts.js +++ b/src/views/components/representative-alerts/representative-alerts.js @@ -12,6 +12,7 @@ import Chip from '@mui/material/Chip' import Tooltip from '@mui/material/Tooltip' import Skeleton from '@mui/material/Skeleton' import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord' +import { useTranslation } from 'react-i18next' import { timeago } from '@core/utils' @@ -19,145 +20,178 @@ import './representative-alerts.styl' const ITEMS_LIMIT = 7 -const getTooltipText = (type) => { +const getTooltipText = ({ type, t }) => { switch (type) { case 'offline': - return 'Representative has stopped voting and appears offline.' + return t( + 'representative_alerts.tooltip.offline', + 'Representative has stopped voting and appears offline.' + ) case 'behind': - return 'Representative has fallen behind or is bootstrapping. The cutoff is a cemented count beyond the 95th percentile. (via telemetry)' + return t( + 'representative_alerts.tooltip.behind', + 'Representative has fallen behind or is bootstrapping. The cutoff is a cemented count beyond the 95th percentile. (via telemetry)' + ) case 'overweight': - return "Representative has beyond 3M Nano voting weight. Delegators should consider distributing the weight to improve the network's resilience and value." + return t( + 'representative_alerts.tooltip.overweight', + "Representative has beyond 3M Nano voting weight. Delegators should consider distributing the weight to improve the network's resilience and value." + ) case 'low uptime': - return 'Representative has been offline more than 25% in the last 28 days.' + return t( + 'representative_alerts.tooltip.low_uptime', + 'Representative has been offline more than 25% in the last 28 days.' + ) } } -export default class RepresentativeAlerts extends React.Component { - constructor(props) { - super(props) +export default function RepresentativeAlerts({ + items, + isLoading, + onlineWeight +}) { + const [expanded, setExpanded] = React.useState(false) + const { t } = useTranslation() - this.state = { - expanded: false - } - } - - handleClick = () => { - this.setState({ expanded: !this.state.expanded }) - } - - render() { - const { items, isLoading, onlineWeight } = this.props - - return ( - <> - - - + const handleClick = () => setExpanded(!expanded) + return ( + <> + +
+ + + + {t( + 'representative_alerts.table_header.representative', + 'Representative' + )} + + + {t('representative_alerts.table_header.issue', 'Issue')} + + + {t( + 'representative_alerts.table_header.last_online', + 'Last Online' + )} + + + {t('common.weight', 'Weight')} + + + {t( + 'representative_alerts.table_header.percent_online_weight', + '% Online Weight' + )} + + + {t('representative_alerts.table_header.behind', 'Behind')} + + + + + {isLoading && ( - Representative - Issue - Last Online - Weight - % Online Weight - Behind + + + + + + + + + + + + + + + + + + - - - {isLoading && ( - - - - - - + )} + {(expanded ? items : items.slice(0, ITEMS_LIMIT)).map( + (row, idx) => ( + + + + {row.account.alias || + `${row.account.account.slice(0, 15)}...`} + - + + + - - + + {row.account.is_online ? ( + + ) : row.account.last_online ? ( + timeago.format( + row.account.last_online * 1000, + 'nano_short' + ) + ) : ( + '' + )} - - + + {BigNumber(row.account.account_meta.weight) + .shiftedBy(-30) + .toFormat(0)} - - + + {row.account.account_meta.weight && onlineWeight + ? `${BigNumber(row.account.account_meta.weight) + .dividedBy(onlineWeight) + .multipliedBy(100) + .toFormat(2)} %` + : '-'} - - )} - {(this.state.expanded ? items : items.slice(0, ITEMS_LIMIT)).map( - (row, idx) => ( - - - - {row.account.alias || - `${row.account.account.slice(0, 15)}...`} - - - - - - - - - {row.account.is_online ? ( - - ) : row.account.last_online ? ( - timeago.format( - row.account.last_online * 1000, - 'nano_short' - ) - ) : ( - '' - )} - - - {BigNumber(row.account.account_meta.weight) - .shiftedBy(-30) - .toFormat(0)} - - - {row.account.account_meta.weight && onlineWeight - ? `${BigNumber(row.account.account_meta.weight) - .dividedBy(onlineWeight) - .multipliedBy(100) - .toFormat(2)} %` - : '-'} - - - {row.account.telemetry.cemented_behind >= 0 - ? BigNumber( - row.account.telemetry.cemented_behind - ).toFormat(0) - : '-'} - - - ) - )} - {items.length > ITEMS_LIMIT && ( - - - {this.state.expanded - ? 'Collapse' - : `Show ${items.length - ITEMS_LIMIT} more`} + + {row.account.telemetry.cemented_behind >= 0 + ? BigNumber( + row.account.telemetry.cemented_behind + ).toFormat(0) + : '-'} - )} - -
-
- - ) - } + ) + )} + {items.length > ITEMS_LIMIT && ( + + + {expanded + ? t('common.collapse', 'Collapse') + : t('common.show_more', { + count: items.length - ITEMS_LIMIT, + defaultValue: `Show ${ + items.length - ITEMS_LIMIT || 0 + } more` + })} + + + )} + + + + + ) } RepresentativeAlerts.propTypes = { diff --git a/src/views/components/representative-delegators/representative-delegators.js b/src/views/components/representative-delegators/representative-delegators.js index e8b27a0f..5ef8a11d 100644 --- a/src/views/components/representative-delegators/representative-delegators.js +++ b/src/views/components/representative-delegators/representative-delegators.js @@ -9,12 +9,14 @@ import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' import LinearProgress from '@mui/material/LinearProgress' import { Link } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import './representative-delegators.styl' const ITEMS_LIMIT = 10 export default function RepresentativeDelegators({ account }) { + const { t } = useTranslation() const [expanded, setExpanded] = useState(false) const handleClick = () => { @@ -30,8 +32,12 @@ export default function RepresentativeDelegators({ account }) { - Delegator - Balance + + {t('common.delegator', { count: 1, defaultValue: 'Delegator' })} + + + {t('common.balance', 'Balance')} + % of Total @@ -61,8 +67,12 @@ export default function RepresentativeDelegators({ account }) { {expanded - ? 'Collapse' - : `Show ${account.delegators.length - ITEMS_LIMIT} more`} + ? t('common.collapse', 'Collapse') + : t( + 'common.show_more', + { count: account.delegators.length - ITEMS_LIMIT }, + `Show ${account.delegators.length - ITEMS_LIMIT} more` + )} )} @@ -75,7 +85,10 @@ export default function RepresentativeDelegators({ account }) {
{!is_loading && (
- Showing top 100 delegators with a minimum balance of 1 Nano. + {t( + 'representative_delegators.showing_top_delegators', + 'Showing top 100 delegators with a minimum balance of 1 Nano.' + )}
)} diff --git a/src/views/components/representative-info/representative-info.js b/src/views/components/representative-info/representative-info.js index f9521124..583ca8e8 100644 --- a/src/views/components/representative-info/representative-info.js +++ b/src/views/components/representative-info/representative-info.js @@ -2,52 +2,50 @@ import React from 'react' import ImmutablePropTypes from 'react-immutable-proptypes' import BigNumber from 'bignumber.js' import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord' +import { useTranslation } from 'react-i18next' import { timeago } from '@core/utils' -export default class RepresentativeNetwork extends React.Component { - render() { - const { account } = this.props +export default function RepresentativeNetwork({ account }) { + const { t } = useTranslation() + const created_at = account.getIn(['representative_meta', 'created_at']) + const items = [ + { + label: t('representative_info.last_seen', 'Last Seen'), + value: account.get('is_online') ? ( + + ) : ( + timeago.format(account.getIn(['last_seen']) * 1000, 'nano_short') + ) + }, + { + label: t('representative_info.first_seen', 'First Seen'), + value: created_at ? timeago.format(created_at * 1000, 'nano_short') : '-' + } + ] - const createdAt = account.getIn(['representative_meta', 'created_at']) - const items = [ - { - label: 'Last Seen', - value: account.get('is_online') ? ( - - ) : ( - timeago.format(account.getIn(['last_seen']) * 1000, 'nano_short') - ) - }, - { - label: 'First Seen', - value: createdAt ? timeago.format(createdAt * 1000, 'nano_short') : '-' - } - ] + const rows = items.map((item, idx) => ( +
+
{item.label}
+
{item.value}
+
+ )) - const rows = items.map((i, idx) => ( -
-
{i.label}
-
{i.value}
-
- )) - - return ( -
-
-
- Weight Represented -
-
- {BigNumber(account.getIn(['account_meta', 'weight'])) - .shiftedBy(-30) - .toFormat(0)} -
+ return ( +
+
+
+ {t('representative_info.weight_represented', 'Weight Represented')} +
+
+ {BigNumber(account.getIn(['account_meta', 'weight'])) + .shiftedBy(-30) + .toFormat(0)}
- {rows}
- ) - } + {rows} +
+ ) } RepresentativeNetwork.propTypes = { diff --git a/src/views/components/representative-network/representative-network.js b/src/views/components/representative-network/representative-network.js index 0b0b3935..3de5fc79 100644 --- a/src/views/components/representative-network/representative-network.js +++ b/src/views/components/representative-network/representative-network.js @@ -1,45 +1,43 @@ import React from 'react' import ImmutablePropTypes from 'react-immutable-proptypes' +import { useTranslation } from 'react-i18next' -export default class RepresentativeNetwork extends React.Component { - render() { - const { account } = this.props +export default function RepresentativeNetwork({ account }) { + const { t } = useTranslation() + const items = [ + { + label: t('representative_network.provider', 'Provider'), + value: account.getIn(['network', 'asname']) + }, + { + label: t('representative_network.isp', 'ISP'), + value: account.getIn(['network', 'isp']) + }, + { + label: t('common.country', 'Country'), + value: account.getIn(['network', 'country']) + }, + { + label: t('representative_network.city', 'City'), + value: account.getIn(['network', 'city']) + } + ] - const items = [ - { - label: 'Provider', - value: account.getIn(['network', 'asname']) - }, - { - label: 'ISP', - value: account.getIn(['network', 'isp']) - }, - { - label: 'Country', - value: account.getIn(['network', 'country']) - }, - { - label: 'City', - value: account.getIn(['network', 'city']) - } - ] + const rows = items.map((i, idx) => ( +
+
{i.label}
+
{i.value}
+
+ )) - const rows = items.map((i, idx) => ( -
-
{i.label}
-
{i.value}
+ return ( +
+
+ {t('representative_network.network', 'Network')}
- )) - - return ( -
-
- Network -
- {rows} -
- ) - } + {rows} +
+ ) } RepresentativeNetwork.propTypes = { diff --git a/src/views/components/representative-telemetry/representative-telemetry.js b/src/views/components/representative-telemetry/representative-telemetry.js index dbf1cf2d..cc1347e6 100644 --- a/src/views/components/representative-telemetry/representative-telemetry.js +++ b/src/views/components/representative-telemetry/representative-telemetry.js @@ -1,91 +1,92 @@ import React from 'react' import ImmutablePropTypes from 'react-immutable-proptypes' import BigNumber from 'bignumber.js' +import { useTranslation } from 'react-i18next' import { timeago } from '@core/utils' -export default class RepresentativeTelemetry extends React.Component { - render() { - const { account } = this.props +export default function RepresentativeTelemetry({ account }) { + const { t } = useTranslation() + const bandwidth = account.getIn(['telemetry', 'bandwidth_cap']) + const bandwidth_value = bandwidth + ? `${(bandwidth / (1024 * 1024)).toFixed(1)}Mb` + : typeof bandwidth !== 'undefined' + ? t('common.unlimited', 'Unlimited') + : '-' - const bandwidth = account.getIn(['telemetry', 'bandwidth_cap']) - const bandwidthValue = bandwidth - ? `${(bandwidth / (1024 * 1024)).toFixed(1)}Mb` - : typeof bandwidth !== 'undefined' - ? 'Unlimited' - : '-' + const block_count = account.getIn(['telemetry', 'block_count'], 0) + const block_behind = account.getIn(['telemetry', 'block_behind'], 0) + const cemented_count = account.getIn(['telemetry', 'cemented_count'], 0) + const cemented_behind = account.getIn(['telemetry', 'cemented_behind'], 0) + const unchecked_count = account.getIn(['telemetry', 'unchecked_count'], 0) + const telemetry_timestamp = account.getIn( + ['telemetry', 'telemetry_timestamp'], + 0 + ) - const blockCount = account.getIn(['telemetry', 'block_count'], 0) - const blockBehind = account.getIn(['telemetry', 'block_behind'], 0) - const cementedCount = account.getIn(['telemetry', 'cemented_count'], 0) - const cementedBehind = account.getIn(['telemetry', 'cemented_behind'], 0) - const uncheckedCount = account.getIn(['telemetry', 'unchecked_count'], 0) - const telemetryTimestamp = account.getIn( - ['telemetry', 'telemetry_timestamp'], - 0 - ) + const items = [ + { + label: t('common.peers', 'Peers'), + value: account.getIn(['telemetry', 'peer_count'], '-') + }, + { + label: t('common.port', 'Port'), + value: account.getIn(['telemetry', 'port'], '-') + }, + { + label: t('common.version', 'Version'), + value: account.getIn(['version'], '-') + }, + { + label: t('common.bandwidth_limit', 'Bandwidth Limit'), + value: bandwidth_value + }, + { + label: t('common.blocks', 'Blocks'), + value: block_count ? BigNumber(block_count).toFormat() : '-' + }, + { + label: t('representative_telemetry.blocks_diff', 'Blocks Diff'), + value: block_behind ? BigNumber(block_behind).toFormat() : '-' + }, + { + label: t('representative_telemetry.conf', 'Conf.'), + value: cemented_count ? BigNumber(cemented_count).toFormat() : '-' + }, + { + label: t('representative_telemetry.conf_diff', 'Conf. Diff'), + value: cemented_behind ? BigNumber(cemented_behind).toFormat() : '-' + }, + { + label: t('common.unchecked', 'Unchecked'), + value: unchecked_count ? BigNumber(unchecked_count).toFormat() : '-' + }, + { + label: t( + 'representative_telemetry.telemetry_timestamp', + 'Telemetry Timestamp' + ), + value: telemetry_timestamp + ? timeago.format(telemetry_timestamp * 1000, 'nano_short') + : '-' + } + ] - const items = [ - { - label: 'Peers', - value: account.getIn(['telemetry', 'peer_count'], '-') - }, - { - label: 'Port', - value: account.getIn(['telemetry', 'port'], '-') - }, - { - label: 'Version', - value: account.getIn(['version'], '-') - }, - { - label: 'Bandwidth Limit', - value: bandwidthValue - }, - { - label: 'Blocks', - value: blockCount ? BigNumber(blockCount).toFormat() : '-' - }, - { - label: 'Blocks Diff', - value: blockBehind ? BigNumber(blockBehind).toFormat() : '-' - }, - { - label: 'Conf.', - value: cementedCount ? BigNumber(cementedCount).toFormat() : '-' - }, - { - label: 'Conf. Diff', - value: cementedBehind ? BigNumber(cementedBehind).toFormat() : '-' - }, - { - label: 'Unchecked', - value: uncheckedCount ? BigNumber(uncheckedCount).toFormat() : '-' - }, - { - label: 'Telemetry Timestamp', - value: telemetryTimestamp - ? timeago.format(telemetryTimestamp * 1000, 'nano_short') - : '-' - } - ] + const rows = items.map((item, idx) => ( +
+
{item.label}
+
{item.value}
+
+ )) - const rows = items.map((i, idx) => ( -
-
{i.label}
-
{i.value}
+ return ( +
+
+ {t('representative_telemetry.telemetry', 'Telemetry')}
- )) - - return ( -
-
- Telemetry -
- {rows} -
- ) - } + {rows} +
+ ) } RepresentativeTelemetry.propTypes = { diff --git a/src/views/components/representative-uptime/representative-uptime.js b/src/views/components/representative-uptime/representative-uptime.js index ca77d6e3..7fded899 100644 --- a/src/views/components/representative-uptime/representative-uptime.js +++ b/src/views/components/representative-uptime/representative-uptime.js @@ -1,125 +1,137 @@ import React from 'react' import ImmutablePropTypes from 'react-immutable-proptypes' import * as timeago from 'timeago.js' +import { useTranslation } from 'react-i18next' import Uptime from '@components/uptime' import './representative-uptime.styl' -export default class RepresentativeUptime extends React.Component { - render() { - const { uptime } = this.props.account.toJS() +export default function RepresentativeUptime({ account }) { + const { t } = useTranslation() - const lastOnline = this.props.account.get('last_online') - const lastOffline = this.props.account.get('last_offline') + const { uptime } = account.toJS() - const onlineCount = uptime.filter((i) => i.online).length - const last60 = this.props.account.getIn(['uptime_summary', 'days_60'], {}) - const last60Pct = - Math.round( - (last60.online_count / (last60.online_count + last60.offline_count)) * - 10000 - ) / 100 - const last60Class = - last60Pct > 95 - ? 'online' - : last60Pct < 70 - ? 'offline' - : last60Pct < 80 - ? 'warning' - : '' + const last_online = account.get('last_online') + const last_offline = account.get('last_offline') - const last90 = this.props.account.getIn(['uptime_summary', 'days_90'], {}) - const last90Pct = - Math.round( - (last90.online_count / (last90.online_count + last90.offline_count)) * - 10000 - ) / 100 - const last90Class = - last90Pct > 95 ? 'online' : last90Pct < 80 ? 'offline' : '' + const online_count = uptime.filter((i) => i.online).length + const last_60 = account.getIn(['uptime_summary', 'days_60'], {}) + const last_60_pct = + Math.round( + (last_60.online_count / (last_60.online_count + last_60.offline_count)) * + 10000 + ) / 100 + const last_60_class = + last_60_pct > 95 + ? t('common.online', 'online') + : last_60_pct < 70 + ? t('common.offline', 'offline') + : last_60_pct < 80 + ? t('representative_uptime.warning', 'warning') + : '' - let text - let online = true - if (!lastOffline) { - // missing both - if (!lastOnline) { - text = 'Operational' - } else { - // missing offline, has online - text = 'Operational' - } - } else if (!lastOnline) { - // missing online, has offline - text = 'Down' - online = false + const last_90 = account.getIn(['uptime_summary', 'days_90'], {}) + const last_90_pct = + Math.round( + (last_90.online_count / (last_90.online_count + last_90.offline_count)) * + 10000 + ) / 100 + const last_90_class = + last_90_pct > 95 + ? t('common.online', 'online') + : last_90_pct < 80 + ? t('common.offline', 'offline') + : '' + + let text + let online = true + if (!last_offline) { + // missing both + if (!last_online) { + text = t('representative_uptime.operational', 'Operational') } else { - // has both - if (lastOnline > lastOffline) { - text = `Up for ${timeago.format(lastOffline * 1000, 'nano_short')}` - } else { - text = `Down for ${timeago.format(lastOnline * 1000, 'nano_short')}` - online = false - } + // missing offline, has online + text = t('representative_uptime.operational', 'Operational') } + } else if (!last_online) { + // missing online, has offline + text = t('representative_uptime.down', 'Down') + online = false + } else { + // has both + if (last_online > last_offline) { + text = `${t('representative_uptime.up_for', 'Up for')} ${timeago.format( + last_offline * 1000, + 'nano_short' + )}` + } else { + text = `${t( + 'representative_uptime.down_for', + 'Down for' + )} ${timeago.format(last_online * 1000, 'nano_short')}` + online = false + } + } - const uptimePct = Math.round((onlineCount / uptime.length) * 10000) / 100 - const uptimeClass = - uptimePct > 90 - ? 'online' - : uptimePct < 50 - ? 'offline' - : uptimePct < 75 - ? 'warning' - : '' + const uptime_pct = Math.round((online_count / uptime.length) * 10000) / 100 + const uptime_class = + uptime_pct > 90 + ? t('common.online', 'online') + : uptime_pct < 50 + ? t('common.offline', 'offline') + : uptime_pct < 75 + ? t('representative_uptime.warning', 'warning') + : '' - return ( -
-
-
-
- Current Status -
-
- {text} -
+ return ( +
+
+
+
+ {t('representative_uptime.current_status', 'Current Status')} +
+
+ {text}
-
-
- 2W Uptime -
-
- {uptimePct}% -
+
+
+
+ {t('representative_uptime.2w_uptime', '2W Uptime')} +
+
+ {uptime_pct}%
-
-
- 2M Uptime -
-
- {last60Pct ? `${last60Pct}%` : '-'} -
+
+
+
+ {t('representative_uptime.2m_uptime', '2M Uptime')}
-
-
- 3M Uptime -
-
- {last90Pct ? `${last90Pct}%` : '-'} -
+
+ {last_60_pct ? `${last_60_pct}%` : '-'}
-
- +
+
+ {t('representative_uptime.3m_uptime', '3M Uptime')} +
+
+ {last_90_pct ? `${last_90_pct}%` : '-'} +
- ) - } +
+ +
+
+ ) } RepresentativeUptime.propTypes = { diff --git a/src/views/components/representatives-bandwidth-by-weight/representatives-bandwidth-by-weight.js b/src/views/components/representatives-bandwidth-by-weight/representatives-bandwidth-by-weight.js index be04e8e2..5798027c 100644 --- a/src/views/components/representatives-bandwidth-by-weight/representatives-bandwidth-by-weight.js +++ b/src/views/components/representatives-bandwidth-by-weight/representatives-bandwidth-by-weight.js @@ -1,23 +1,25 @@ import React from 'react' import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' import MetricCard from '@components/metric-card' -export default class RepresentativesBandwidthByWeight extends React.Component { - render() { - const { metrics } = this.props - return ( - - ) - } +export default function RepresentativesBandwidthByWeight({ metrics }) { + const { t } = useTranslation() + return ( + + ) } RepresentativesBandwidthByWeight.propTypes = { diff --git a/src/views/components/representatives-cemented-by-weight/representatives-cemented-by-weight.js b/src/views/components/representatives-cemented-by-weight/representatives-cemented-by-weight.js index c19f8e65..c1e48394 100644 --- a/src/views/components/representatives-cemented-by-weight/representatives-cemented-by-weight.js +++ b/src/views/components/representatives-cemented-by-weight/representatives-cemented-by-weight.js @@ -1,23 +1,28 @@ import React from 'react' import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' import MetricCard from '@components/metric-card' -export default class RepresentativesCementedByWeight extends React.Component { - render() { - const { metrics } = this.props - return ( - - ) - } +export default function RepresentativesCementedByWeight({ metrics }) { + const { t } = useTranslation() + return ( + + ) } RepresentativesCementedByWeight.propTypes = { diff --git a/src/views/components/representatives-checked-by-weight/representatives-checked-by-weight.js b/src/views/components/representatives-checked-by-weight/representatives-checked-by-weight.js index a56a318a..fd9f0e7f 100644 --- a/src/views/components/representatives-checked-by-weight/representatives-checked-by-weight.js +++ b/src/views/components/representatives-checked-by-weight/representatives-checked-by-weight.js @@ -1,23 +1,28 @@ import React from 'react' import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' import MetricCard from '@components/metric-card' -export default class RepresentativesCheckedByWeight extends React.Component { - render() { - const { metrics } = this.props - return ( - - ) - } +export default function RepresentativesCheckedByWeight({ metrics }) { + const { t } = useTranslation() + return ( + + ) } RepresentativesCheckedByWeight.propTypes = { diff --git a/src/views/components/representatives-cluster-charts/representatives-cluster-charts.js b/src/views/components/representatives-cluster-charts/representatives-cluster-charts.js index 294ea1fe..c40214f6 100644 --- a/src/views/components/representatives-cluster-charts/representatives-cluster-charts.js +++ b/src/views/components/representatives-cluster-charts/representatives-cluster-charts.js @@ -10,6 +10,7 @@ import { TooltipComponent, SingleAxisComponent } from 'echarts/components' +import { useTranslation } from 'react-i18next' import { CanvasRenderer } from 'echarts/renderers' @@ -21,204 +22,204 @@ echarts.use([ CanvasRenderer ]) -export default class RepresentativesClusterCharts extends React.Component { - render() { - const { accounts, totalWeight } = this.props +export default function RepresentativesClusterCharts({ + accounts, + totalWeight +}) { + const { t } = useTranslation() + const confirmations_data = [] + const blocks_data = [] + const peers_data = [] + const bandwidth_data = [] + const unchecked_data = [] + accounts.forEach((a) => { + if (a.telemetry.cemented_behind > 1000) return + const weight = BigNumber(a.account_meta.weight) + .dividedBy(totalWeight) + .multipliedBy(100) + .toFixed() + const label = a.alias || a.account + confirmations_data.push([a.telemetry.cemented_behind, weight, label]) + blocks_data.push([a.telemetry.block_behind, weight, label]) + peers_data.push([a.telemetry.peer_count, weight, label]) + unchecked_data.push([a.telemetry.unchecked_count, weight, label]) - const confirmationsData = [] - const blocksData = [] - const peersData = [] - const bandwidthData = [] - const uncheckedData = [] - accounts.forEach((a) => { - if (a.telemetry.cemented_behind > 1000) return - const weight = BigNumber(a.account_meta.weight) - .dividedBy(totalWeight) - .multipliedBy(100) - .toFixed() - const label = a.alias || a.account - confirmationsData.push([a.telemetry.cemented_behind, weight, label]) - blocksData.push([a.telemetry.block_behind, weight, label]) - peersData.push([a.telemetry.peer_count, weight, label]) - uncheckedData.push([a.telemetry.unchecked_count, weight, label]) + // exclude 0 (unlimited) + if (a.telemetry.bandwidth_cap) + bandwidth_data.push([ + a.telemetry.bandwidth_cap / (1024 * 1024), + weight, + label + ]) + }) - // exclude 0 (unlimited) - if (a.telemetry.bandwidth_cap) - bandwidthData.push([ - a.telemetry.bandwidth_cap / (1024 * 1024), - weight, - label - ]) - }) - - const seriesCommon = { - type: 'scatter', - coordinateSystem: 'singleAxis', - symbolSize: (dataItem) => Math.min(Math.max(dataItem[1] * 6, 6), 35), - labelLine: { - show: true, - length2: 2, - lineStyle: { - color: '#bbb' - } - }, - label: { - show: true, - formatter: (param) => param.data[2], - minMargin: 10 - }, - tooltip: { - className: 'echarts-tooltip', - formatter: (params) => params.data[2] + const series_common = { + type: 'scatter', + coordinateSystem: 'singleAxis', + symbolSize: (data_item) => Math.min(Math.max(data_item[1] * 6, 6), 35), + labelLine: { + show: true, + length2: 2, + lineStyle: { + color: '#bbb' } + }, + label: { + show: true, + formatter: (param) => param.data[2], + minMargin: 10 + }, + tooltip: { + className: 'echarts-tooltip', + formatter: (params) => params.data[2] } + } - const titleCommon = { - left: 'center', - textStyle: { - fontWeight: 'normal', - fontFamily: 'IBM Plex Mono' - } + const title_common = { + left: 'center', + textStyle: { + fontWeight: 'normal', + fontFamily: 'IBM Plex Mono' } + } - const option = { - tooltip: { - className: 'echarts-tooltip', - position: 'top' + const option = { + tooltip: { + className: 'echarts-tooltip', + position: 'top' + }, + title: [ + { + text: t('representatives_cluster.conf_diff', 'Confirmations Behind'), + top: 20, + ...title_common }, - title: [ - { - text: 'Confirmations Behind', - top: 20, - ...titleCommon - }, - { - text: 'Blocks Behind', - top: 140, - ...titleCommon - }, - { - text: 'Unchecked Count', - top: 260, - ...titleCommon - }, - { - text: 'Bandwidth Limit', - top: 380, - ...titleCommon - }, - { - text: 'Peer Count', - top: 500, - ...titleCommon + { + text: t('representatives_cluster.blocks_diff', 'Blocks Behind'), + top: 140, + ...title_common + }, + { + text: t('representatives_cluster.unchecked', 'Unchecked Count'), + top: 260, + ...title_common + }, + { + text: t('common.bandwidth_limit', 'Bandwidth Limit'), + top: 380, + ...title_common + }, + { + text: t('common.peers', 'Peers'), + top: 500, + ...title_common + } + ], + singleAxis: [ + { + type: 'value', + height: '100px', + top: 0 + }, + { + type: 'value', + top: '120px', + height: '100px' + }, + { + scale: true, + type: 'value', + top: '240px', + height: '100px' + }, + { + type: 'value', + top: '360px', + height: '100px', + axisLabel: { + formatter: (value) => `${value} mb/s` } - ], - singleAxis: [ - { - type: 'value', - height: '100px', - top: 0 - }, - { - type: 'value', - top: '120px', - height: '100px' - }, - { - scale: true, - type: 'value', - top: '240px', - height: '100px' - }, - { - type: 'value', - top: '360px', - height: '100px', - axisLabel: { - formatter: (value) => `${value} mb/s` - } + }, + { + scale: true, + type: 'value', + top: '480px', + height: '100px' + } + ], + series: [ + { + singleAxisIndex: 0, + labelLayout: { + y: 80, + align: 'left', + hideOverlap: true, + width: 20, + overflow: 'truncate' }, - { - scale: true, - type: 'value', - top: '480px', - height: '100px' - } - ], - series: [ - { - singleAxisIndex: 0, - labelLayout: { - y: 80, - align: 'left', - hideOverlap: true, - width: 20, - overflow: 'truncate' - }, - data: confirmationsData, - ...seriesCommon + data: confirmations_data, + ...series_common + }, + { + singleAxisIndex: 1, + labelLayout: { + y: 200, + align: 'left', + hideOverlap: true, + width: 20, + overflow: 'truncate' }, - { - singleAxisIndex: 1, - labelLayout: { - y: 200, - align: 'left', - hideOverlap: true, - width: 20, - overflow: 'truncate' - }, - data: blocksData, - ...seriesCommon + data: blocks_data, + ...series_common + }, + { + singleAxisIndex: 2, + labelLayout: { + y: 320, + align: 'left', + hideOverlap: true, + width: 20, + overflow: 'truncate' }, - { - singleAxisIndex: 2, - labelLayout: { - y: 320, - align: 'left', - hideOverlap: true, - width: 20, - overflow: 'truncate' - }, - data: uncheckedData, - ...seriesCommon + data: unchecked_data, + ...series_common + }, + { + singleAxisIndex: 3, + labelLayout: { + y: 440, + align: 'left', + hideOverlap: true, + width: 20, + overflow: 'truncate' }, - { - singleAxisIndex: 3, - labelLayout: { - y: 440, - align: 'left', - hideOverlap: true, - width: 20, - overflow: 'truncate' - }, - data: bandwidthData, - ...seriesCommon + data: bandwidth_data, + ...series_common + }, + { + singleAxisIndex: 4, + labelLayout: { + y: 560, + align: 'left', + hideOverlap: true, + width: 20, + overflow: 'truncate' }, - { - singleAxisIndex: 4, - labelLayout: { - y: 560, - align: 'left', - hideOverlap: true, - width: 20, - overflow: 'truncate' - }, - data: peersData, - ...seriesCommon - } - ] - } - - return ( - <> - - - ) + data: peers_data, + ...series_common + } + ] } + + return ( + <> + + + ) } RepresentativesClusterCharts.propTypes = { diff --git a/src/views/components/representatives-country-by-weight/representatives-country-by-weight.js b/src/views/components/representatives-country-by-weight/representatives-country-by-weight.js index 90fc25f5..c4d0ee57 100644 --- a/src/views/components/representatives-country-by-weight/representatives-country-by-weight.js +++ b/src/views/components/representatives-country-by-weight/representatives-country-by-weight.js @@ -1,22 +1,21 @@ import React from 'react' import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' import MetricCard from '@components/metric-card' -export default class RepresentativesCountryByWeight extends React.Component { - render() { - const { metrics } = this.props - return ( - - ) - } +export default function RepresentativesCountryByWeight({ metrics }) { + const { t } = useTranslation() + return ( + + ) } RepresentativesCountryByWeight.propTypes = { diff --git a/src/views/components/representatives-filters/representatives-filters.js b/src/views/components/representatives-filters/representatives-filters.js index 46c145a8..038b4aa3 100644 --- a/src/views/components/representatives-filters/representatives-filters.js +++ b/src/views/components/representatives-filters/representatives-filters.js @@ -1,27 +1,27 @@ import React from 'react' import PropTypes from 'prop-types' import ClearIcon from '@mui/icons-material/Clear' +import { useTranslation } from 'react-i18next' import './representatives-filters.styl' -export default class RepresentativesFilters extends React.Component { - handleClick = () => { +export default function RepresentativesFilters({ filter, field }) { + const { t } = useTranslation() + const handleClick = () => { // clear filters - this.props.filter() + filter() } - render() { - if (!this.props.field) { - return null - } - - return ( -
- -
Clear filters
-
- ) + if (!field) { + return null } + + return ( +
+ +
{t('common.clear_filters', 'Clear Filters')}
+
+ ) } RepresentativesFilters.propTypes = { diff --git a/src/views/components/representatives-offline/representatives-offline.js b/src/views/components/representatives-offline/representatives-offline.js index d4cccb74..4048786f 100644 --- a/src/views/components/representatives-offline/representatives-offline.js +++ b/src/views/components/representatives-offline/representatives-offline.js @@ -1,6 +1,5 @@ import React from 'react' import ImmutablePropTypes from 'react-immutable-proptypes' -import PropTypes from 'prop-types' import BigNumber from 'bignumber.js' import { Link } from 'react-router-dom' import Table from '@mui/material/Table' @@ -9,64 +8,66 @@ import TableCell from '@mui/material/TableCell' import TableContainer from '@mui/material/TableContainer' import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' +import { useTranslation } from 'react-i18next' import { timeago } from '@core/utils' import './representatives-offline.styl' -export default class RepresentativesOffline extends React.Component { - render() { - const { accounts } = this.props +export default function RepresentativesOffline({ accounts }) { + const { t } = useTranslation() - const rows = accounts - .filter((a) => !a.is_online) - .map((p) => { - return { - account: p.account, - alias: p.alias, - is_online: p.is_online, - weight: p.account_meta.weight || 0, - last_online: p.last_online, - diff: (p.last_online || 0) - (p.last_offline || 0) - } - }) + const rows = accounts + .filter((a) => !a.is_online) + .map((p) => { + return { + account: p.account, + alias: p.alias, + is_online: p.is_online, + weight: p.account_meta.weight || 0, + last_online: p.last_online, + diff: (p.last_online || 0) - (p.last_offline || 0) + } + }) - const sorted = rows.sort((a, b) => b.weight - a.weight) + const sorted = rows.sort((a, b) => b.weight - a.weight) - return ( - - - - - Offline Account - Last Online - Weight + return ( + +
+ + + + {t('representatives_offline.account', 'Offline Account')} + + + {t('representatives_offline.last_online', 'Last Online')} + + {t('common.weight', 'Weight')} + + + + {sorted.map((row) => ( + + + + {row.alias || `${row.account.slice(0, 15)}...`} + + + + {timeago.format(row.last_online * 1000, 'nano_short')} + + + {BigNumber(row.weight).shiftedBy(-30).toFormat(0)} + - - - {sorted.map((row) => ( - - - - {row.alias || `${row.account.slice(0, 15)}...`} - - - - {timeago.format(row.last_online * 1000, 'nano_short')} - - - {BigNumber(row.weight).shiftedBy(-30).toFormat(0)} - - - ))} - -
-
- ) - } + ))} + + + + ) } RepresentativesOffline.propTypes = { - accounts: ImmutablePropTypes.map, - totalWeight: PropTypes.number + accounts: ImmutablePropTypes.map } diff --git a/src/views/components/representatives-provider-by-weight/representatives-provider-by-weight.js b/src/views/components/representatives-provider-by-weight/representatives-provider-by-weight.js index fafb3430..74557723 100644 --- a/src/views/components/representatives-provider-by-weight/representatives-provider-by-weight.js +++ b/src/views/components/representatives-provider-by-weight/representatives-provider-by-weight.js @@ -1,22 +1,21 @@ import React from 'react' import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' import MetricCard from '@components/metric-card' -export default class RepresentativesProviderByWeight extends React.Component { - render() { - const { metrics } = this.props - return ( - - ) - } +export default function RepresentativesProviderByWeight({ metrics }) { + const { t } = useTranslation() + return ( + + ) } RepresentativesProviderByWeight.propTypes = { diff --git a/src/views/components/representatives-quorum-charts/representatives-quorum-charts.js b/src/views/components/representatives-quorum-charts/representatives-quorum-charts.js index 722cbd84..563da20d 100644 --- a/src/views/components/representatives-quorum-charts/representatives-quorum-charts.js +++ b/src/views/components/representatives-quorum-charts/representatives-quorum-charts.js @@ -1,230 +1,230 @@ -import React from 'react' +import React, { useEffect } from 'react' import PropTypes from 'prop-types' import BigNumber from 'bignumber.js' import ReactEChartsCore from 'echarts-for-react/lib/core' import * as echarts from 'echarts/core' import { LineChart } from 'echarts/charts' import { TitleComponent, TooltipComponent } from 'echarts/components' +import { useTranslation } from 'react-i18next' import { CanvasRenderer } from 'echarts/renderers' echarts.use([TitleComponent, TooltipComponent, LineChart, CanvasRenderer]) -export default class RepresentativesQuorumCharts extends React.Component { - componentDidMount() { - this.props.load() - } +export default function RepresentativesQuorumCharts({ data, peerData, load }) { + const { t } = useTranslation() - render() { - const { data, peerData } = this.props - const commonOptions = { - tooltip: { - className: 'echarts-tooltip', - trigger: 'axis', - formatter: (p) => { - const values = p.map( - (s) => - `${s.marker} ${s.data[2]} - ${BigNumber(s.data[1]).toFormat(0)}M` - ) + useEffect(() => { + load() + }, [load]) - values.unshift(p[0].axisValueLabel) + const commonOptions = { + tooltip: { + className: 'echarts-tooltip', + trigger: 'axis', + formatter: (p) => { + const values = p.map( + (s) => + `${s.marker} ${s.data[2]} - ${BigNumber(s.data[1]).toFormat(0)}M` + ) - return values.join('
') - } - }, - xAxis: { - type: 'time' - }, - yAxis: { - type: 'value', - scale: true, - axisLabel: { - formatter: (value) => `${value}M` - } + values.unshift(p[0].axisValueLabel) + + return values.join('
') + } + }, + xAxis: { + type: 'time' + }, + yAxis: { + type: 'value', + scale: true, + axisLabel: { + formatter: (value) => `${value}M` } } + } - const commonTitle = { - top: 10, - left: 'center', - textStyle: { - fontWeight: 'normal', - fontFamily: 'IBM Plex Mono' - } + const commonTitle = { + top: 10, + left: 'center', + textStyle: { + fontWeight: 'normal', + fontFamily: 'IBM Plex Mono' } + } - const onlineOption = { - color: ['#5470c6', 'red', '#5470c6'], - title: { - text: 'Online Weight', - ...commonTitle + const onlineOption = { + color: ['#5470c6', 'red', '#5470c6'], + title: { + text: t('representatives_quorum_chart.title', 'Online Weight'), + ...commonTitle + }, + series: [ + { + type: 'line', + showSymbol: false, + data: data.online_stake_total.max, + areaStyle: {}, + lineStyle: { + opacity: 0 + } }, - series: [ - { - type: 'line', - showSymbol: false, - data: data.online_stake_total.max, - areaStyle: {}, - lineStyle: { - opacity: 0 - } - }, - { - type: 'line', - showSymbol: false, - data: data.online_stake_total.median, - lineStyle: { - color: 'red' - } - }, - { - type: 'line', - showSymbol: false, - data: data.online_stake_total.min, - areaStyle: { - color: 'white', - opacity: 1 - }, - lineStyle: { - opacity: 0 - } + { + type: 'line', + showSymbol: false, + data: data.online_stake_total.median, + lineStyle: { + color: 'red' } - ], - ...commonOptions - } - - const trendedOption = { - color: ['#5470c6', 'red', '#5470c6'], - title: { - text: 'Trended Weight', - ...commonTitle }, - series: [ - { - type: 'line', - showSymbol: false, - data: data.trended_stake_total.max, - areaStyle: {}, - lineStyle: { - opacity: 0 - } - }, - { - type: 'line', - showSymbol: false, - data: data.trended_stake_total.median, - lineStyle: { - color: 'red' - } + { + type: 'line', + showSymbol: false, + data: data.online_stake_total.min, + areaStyle: { + color: 'white', + opacity: 1 }, - { - type: 'line', - showSymbol: false, - data: data.trended_stake_total.min, - areaStyle: { - color: 'white', - opacity: 1 - }, - lineStyle: { - opacity: 0 - } + lineStyle: { + opacity: 0 } - ], - ...commonOptions - } + } + ], + ...commonOptions + } - const peersOption = { - ...commonOptions, - title: { - text: 'Peers Weight', - ...commonTitle + const trendedOption = { + color: ['#5470c6', 'red', '#5470c6'], + title: { + text: t('representatives_quorum_chart.trended_weight', 'Trended Weight'), + ...commonTitle + }, + series: [ + { + type: 'line', + showSymbol: false, + data: data.trended_stake_total.max, + areaStyle: {}, + lineStyle: { + opacity: 0 + } }, - series: peerData.map((data) => ({ + { type: 'line', showSymbol: false, - data - })), - tooltip: { - className: 'echarts-tooltip', - trigger: 'axis', - formatter: (p) => { - const values = p.map( - (s) => - `${s.marker} ${BigNumber(s.data[1]).toFormat(0)}M - ${ - new URL(s.data[2]).hostname - }` - ) - values.unshift(p[0].axisValueLabel) - return values.join('
') + data: data.trended_stake_total.median, + lineStyle: { + color: 'red' + } + }, + { + type: 'line', + showSymbol: false, + data: data.trended_stake_total.min, + areaStyle: { + color: 'white', + opacity: 1 + }, + lineStyle: { + opacity: 0 } } + ], + ...commonOptions + } + + const peersOption = { + ...commonOptions, + title: { + text: t('representatives_quorum_chart.peers_weight', 'Peers Weight'), + ...commonTitle + }, + series: peerData.map((data) => ({ + type: 'line', + showSymbol: false, + data + })), + tooltip: { + className: 'echarts-tooltip', + trigger: 'axis', + formatter: (p) => { + const values = p.map( + (s) => + `${s.marker} ${BigNumber(s.data[1]).toFormat(0)}M - ${ + new URL(s.data[2]).hostname + }` + ) + values.unshift(p[0].axisValueLabel) + return values.join('
') + } } + } - const quorumOption = { - color: ['#5470c6', 'red', '#5470c6'], - title: { - text: 'Quorum Delta', - ...commonTitle + const quorumOption = { + color: ['#5470c6', 'red', '#5470c6'], + title: { + text: t('common.quorum_delta', 'Quorum Delta'), + ...commonTitle + }, + series: [ + { + type: 'line', + showSymbol: false, + data: data.quorum_delta.max, + areaStyle: {}, + lineStyle: { + opacity: 0 + } }, - series: [ - { - type: 'line', - showSymbol: false, - data: data.quorum_delta.max, - areaStyle: {}, - lineStyle: { - opacity: 0 - } - }, - { - type: 'line', - showSymbol: false, - data: data.quorum_delta.median, - lineStyle: { - color: 'red' - } + { + type: 'line', + showSymbol: false, + data: data.quorum_delta.median, + lineStyle: { + color: 'red' + } + }, + { + type: 'line', + showSymbol: false, + data: data.quorum_delta.min, + areaStyle: { + color: 'white', + opacity: 1 }, - { - type: 'line', - showSymbol: false, - data: data.quorum_delta.min, - areaStyle: { - color: 'white', - opacity: 1 - }, - lineStyle: { - opacity: 0 - } + lineStyle: { + opacity: 0 } - ], - ...commonOptions - } - - return ( - <> - - - - - - ) + } + ], + ...commonOptions } + + return ( + <> + + + + + + ) } RepresentativesQuorumCharts.propTypes = { diff --git a/src/views/components/representatives-search/representatives-search.js b/src/views/components/representatives-search/representatives-search.js index a35f2021..7e88af5f 100644 --- a/src/views/components/representatives-search/representatives-search.js +++ b/src/views/components/representatives-search/representatives-search.js @@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react' import PropTypes from 'prop-types' import ClearIcon from '@mui/icons-material/Clear' import KeyboardCommandKeyIcon from '@mui/icons-material/KeyboardCommandKey' +import { useTranslation } from 'react-i18next' import { debounce } from '@core/utils' @@ -12,6 +13,7 @@ const RepresentativesSearch = ({ search, align = 'center' }) => { + const { t } = useTranslation() const [value, setValue] = useState(initialValue || '') const inputRef = useRef(null) @@ -59,7 +61,10 @@ const RepresentativesSearch = ({ ref={inputRef} className='search__input' type='text' - placeholder='Filter by account, alias, ip' + placeholder={t( + 'representatives_search.placeholder', + 'Filter by account, alias, ip' + )} value={value} onChange={handleChange} /> diff --git a/src/views/components/representatives-version-by-weight/representatives-version-by-weight.js b/src/views/components/representatives-version-by-weight/representatives-version-by-weight.js index 4a3f0954..858a2276 100644 --- a/src/views/components/representatives-version-by-weight/representatives-version-by-weight.js +++ b/src/views/components/representatives-version-by-weight/representatives-version-by-weight.js @@ -1,22 +1,21 @@ import React from 'react' import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' import MetricCard from '@components/metric-card' -export default class RepresentativesVersionByWeight extends React.Component { - render() { - const { metrics } = this.props - return ( - - ) - } +export default function RepresentativesVersionByWeight({ metrics }) { + const { t } = useTranslation() + return ( + + ) } RepresentativesVersionByWeight.propTypes = { diff --git a/src/views/components/representatives-weight-chart/representatives-weight-chart.js b/src/views/components/representatives-weight-chart/representatives-weight-chart.js index 4e676ddb..b1703fed 100644 --- a/src/views/components/representatives-weight-chart/representatives-weight-chart.js +++ b/src/views/components/representatives-weight-chart/representatives-weight-chart.js @@ -6,6 +6,7 @@ import BigNumber from 'bignumber.js' import * as echarts from 'echarts/core' import { PieChart } from 'echarts/charts' import { TitleComponent, TooltipComponent } from 'echarts/components' +import { useTranslation } from 'react-i18next' import { CanvasRenderer } from 'echarts/renderers' @@ -14,68 +15,70 @@ echarts.use([TitleComponent, TooltipComponent, PieChart, CanvasRenderer]) const truncate = (str, n) => str.length > n ? `${str.substr(0, n - 1)}...` : str -export default class RepresentativesWeightChart extends React.Component { - render() { - const { accounts, totalWeight, quorumTotal } = this.props +export default function RepresentativesWeightChart({ + accounts, + totalWeight, + quorumTotal +}) { + const { t } = useTranslation() + const denominator = quorumTotal || totalWeight - const denominator = quorumTotal || totalWeight + const weightData = [] + accounts.forEach((a) => { + const bn = BigNumber(a.account_meta.weight) + const weight = bn.shiftedBy(-30).toFixed(0) + const pct = bn.dividedBy(denominator).multipliedBy(100).toFixed(1) - const weightData = [] - accounts.forEach((a) => { - const bn = BigNumber(a.account_meta.weight) - const weight = bn.shiftedBy(-30).toFixed(0) - const pct = bn.dividedBy(denominator).multipliedBy(100).toFixed(1) + const label = a.alias || a.account + weightData.push([weight, label, pct]) + }) - const label = a.alias || a.account - weightData.push([weight, label, pct]) - }) - - const option = { - tooltip: { - className: 'echarts-tooltip', - trigger: 'item', - formatter: (p) => - `${p.data[1]}
${BigNumber(p.data[0]).toFormat(0)} (${ - p.data[2] - } %)` - }, - title: { - top: 10, - left: 'center', - text: 'Weight Distribution by Rep', - textStyle: { - fontWeight: 'normal', - fontFamily: 'IBM Plex Mono' - } - }, - series: [ - { - type: 'pie', - radius: '50%', - avoidLabelOverlap: false, - data: weightData.sort((a, b) => b[0] - a[0]), - label: { - bleedMargin: 30, - formatter: (p) => `${truncate(p.data[1], 20)}: ${p.data[2]} %` - }, - labelLayout: { - height: 50, - hideOverlap: true - } + const option = { + tooltip: { + className: 'echarts-tooltip', + trigger: 'item', + formatter: (p) => + `${p.data[1]}
${BigNumber(p.data[0]).toFormat(0)} (${p.data[2]} %)` + }, + title: { + top: 10, + left: 'center', + text: t( + 'representatives_weight_chart.title', + 'Weight Distribution by Representative' + ), + textStyle: { + fontWeight: 'normal', + fontFamily: 'IBM Plex Mono' + } + }, + series: [ + { + type: 'pie', + radius: '50%', + avoidLabelOverlap: false, + data: weightData.sort((a, b) => b[0] - a[0]), + label: { + bleedMargin: 30, + formatter: (p) => `${truncate(p.data[1], 20)}: ${p.data[2]} %` + }, + labelLayout: { + height: 50, + hideOverlap: true } - ] - } - - return ( - <> - - - ) + } + ] } + + return ( + <> + + + ) } RepresentativesWeightChart.propTypes = { diff --git a/src/views/components/representatives-weight/representatives-weight.js b/src/views/components/representatives-weight/representatives-weight.js index 0c9b4b89..53e6d30f 100644 --- a/src/views/components/representatives-weight/representatives-weight.js +++ b/src/views/components/representatives-weight/representatives-weight.js @@ -1,59 +1,62 @@ import React from 'react' import BigNumber from 'bignumber.js' import ImmutablePropTypes from 'react-immutable-proptypes' +import { useTranslation } from 'react-i18next' import './representatives-weight.styl' -export default class RepresentativesWeight extends React.Component { - render() { - const { network } = this.props - const onlineWeight = BigNumber( - network.getIn(['weight', 'onlineWeight', 'median'], 0) - ) - const trendedWeight = BigNumber( - network.getIn(['weight', 'trendedWeight', 'median'], 0) - ) - const quorumTotal = BigNumber.max(onlineWeight, trendedWeight) - const quorumWeightDelta = quorumTotal.multipliedBy(0.67) +export default function RepresentativesWeight({ network }) { + const { t } = useTranslation() + const online_weight = BigNumber( + network.getIn(['weight', 'onlineWeight', 'median'], 0) + ) + const trended_weight = BigNumber( + network.getIn(['weight', 'trendedWeight', 'median'], 0) + ) + const quorum_total = BigNumber.max(online_weight, trended_weight) + const quorum_weight_delta = quorum_total.multipliedBy(0.67) - const onlineSelected = onlineWeight.isGreaterThan(trendedWeight) + const online_selected = online_weight.isGreaterThan(trended_weight) - const onlineNano = onlineWeight.shiftedBy(-36) - const trendedNano = trendedWeight.shiftedBy(-36) - const quorumNano = quorumWeightDelta.shiftedBy(-36) - return ( -
-
-
-
Trended
-
- {trendedNano.isNaN() ? '-' : `${trendedNano.toFormat(1)}M`} -
+ const online_nano = online_weight.shiftedBy(-36) + const trended_nano = trended_weight.shiftedBy(-36) + const quorum_nano = quorum_weight_delta.shiftedBy(-36) + return ( +
+
+
+
+ {t('representatives_weight.trended', 'Trended')}
-
-
Online
-
- {onlineNano.isNaN() ? '-' : `${onlineNano.toFormat(1)}M`} -
+
+ {trended_nano.isNaN() ? '-' : `${trended_nano.toFormat(1)}M`}
-
-
- Quorum Delta -
-
- {quorumNano.isNaN() ? '-' : `${quorumNano.toFormat(1)}M`} -
+
+
+
+ {t('common.online', 'Online')} +
+
+ {online_nano.isNaN() ? '-' : `${online_nano.toFormat(1)}M`} +
+
+
+
+ {t('common.quorum_delta', 'Quorum Delta')} +
+
+ {quorum_nano.isNaN() ? '-' : `${quorum_nano.toFormat(1)}M`}
- ) - } +
+ ) } RepresentativesWeight.propTypes = { diff --git a/src/views/components/representatives/representatives.js b/src/views/components/representatives/representatives.js index 80a8b3e0..501fcbc3 100644 --- a/src/views/components/representatives/representatives.js +++ b/src/views/components/representatives/representatives.js @@ -6,18 +6,19 @@ import LinearProgress from '@mui/material/LinearProgress' import { DataGrid } from '@mui/x-data-grid' import BigNumber from 'bignumber.js' import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord' +import { useTranslation } from 'react-i18next' import Uptime from '@components/uptime' import { timeago } from '@core/utils' import './representatives.styl' -function bytesToSize(bytes) { +function bytesToSize({ bytes, t }) { const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] if (bytes === 0) return { value: 0, - label: 'Unlimited' + label: t('common.unlimited', 'Unlimited') } const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10) @@ -63,237 +64,234 @@ function sort_comparator(a, b, cell_params_a) { } } -export default class Representatives extends React.Component { - render() { - const { - accounts, - totalWeight, - isLoading, - quorumTotal, - table_height = 600 - } = this.props +export default function Representatives({ + accounts, + totalWeight, + quorumTotal, + isLoading, + table_height = 600 +}) { + const { t } = useTranslation() + const denominator = quorumTotal || totalWeight - const denominator = quorumTotal || totalWeight - - const columns = [ - { - field: 'status', - headerName: '', - width: 20, - renderCell: (p) => , - valueGetter: (p) => p.row.is_online, - sortComparator: sort_comparator + const columns = [ + { + field: 'status', + headerName: '', + width: 20, + renderCell: (p) => , + valueGetter: (p) => p.row.is_online, + sortComparator: sort_comparator + }, + { + field: 'alias', + headerName: t('representatives.alias', 'Alias'), + width: 200, + sortComparator: sort_comparator + }, + { + field: 'account', + headerName: t('common.account', { count: 1, defaultValue: 'Account' }), + renderCell: (p) => {p.row.account}, + width: 160, + sortComparator: sort_comparator + }, + { + field: 'weight', + headerName: t('common.weight', 'Weight'), + width: 140, + valueFormatter: (p) => + p.value ? `${BigNumber(p.value).shiftedBy(-30).toFormat(0)}` : null, + valueGetter: (p) => p.row.account_meta.weight, + sortComparator: sort_comparator + }, + { + field: 'weight_pct', + headerName: '%', + width: 80, + valueFormatter: (p) => (p.value ? `${p.value.toFixed(2)}%` : null), + valueGetter: (p) => + p.row.account_meta.weight + ? BigNumber(p.row.account_meta.weight) + .dividedBy(denominator) + .multipliedBy(100) + : null, + sortComparator: sort_comparator + }, + { + field: 'confs_behind', + headerName: t('common.confirmations_behind', 'Confs Behind'), + width: 145, + valueFormatter: (p) => (p.value ? BigNumber(p.value).toFormat() : null), + valueGetter: (p) => p.row.telemetry.cemented_behind, + sortComparator: sort_comparator + }, + { + field: 'uptime', + headerName: t('common.uptime', 'Uptime'), + width: 150, + renderCell: (p) => ( + + ), + valueGetter: (p) => (p.row.last_online || 0) - (p.row.last_offline || 0), + sortComparator: sort_comparator + }, + { + field: 'version', + headerName: t('common.version', 'Version'), + width: 110, + valueGetter: (p) => p.row.version, + sortComparator: sort_comparator + }, + { + field: 'bandwidth_cap', + headerName: t('common.bandwidth_limit_short', 'BW Limit'), + width: 120, + valueFormatter: (p) => { + if (p.api.getRow(p.id).telemetry.bandwidth_cap === 0) + return t('common.unlimited', 'Unlimited') + return p.api.getRow(p.id).telemetry.bandwidth_cap + ? bytesToSize({ + bytes: p.api.getRow(p.id).telemetry.bandwidth_cap, + t + }).label + : null }, - { - field: 'alias', - headerName: 'Alias', - width: 200, - sortComparator: sort_comparator + valueGetter: (p) => { + if (p.row.telemetry.bandwidth_cap === 0) return Infinity + return p.row.telemetry.bandwidth_cap + ? bytesToSize({ bytes: p.row.telemetry.bandwidth_cap, t }).value + : null }, - { - field: 'account', - headerName: 'Account', - renderCell: (p) => ( - {p.row.account} + sortComparator: sort_comparator + }, + { + field: 'peer_count', + headerName: t('common.peers', 'Peers'), + width: 100, + valueGetter: (p) => p.row.telemetry.peer_count, + sortComparator: sort_comparator + }, + { + field: 'port', + headerName: t('common.port', 'Port'), + valueGetter: (p) => p.row.telemetry.port, + sortComparator: sort_comparator + }, + { + field: 'blocks_behind', + headerName: t('common.blocks_behind', 'Blocks Behind'), + width: 145, + valueFormatter: (p) => (p.value ? BigNumber(p.value).toFormat() : null), + valueGetter: (p) => p.row.telemetry.block_behind, + sortComparator: sort_comparator + }, + { + field: 'cemented_count', + headerName: t('common.conf_short', 'Conf.'), + width: 140, + valueFormatter: (p) => (p.value ? BigNumber(p.value).toFormat() : null), + valueGetter: (p) => p.row.telemetry.cemented_count, + sortComparator: sort_comparator + }, + { + field: 'block_count', + headerName: t('common.blocks', 'Blocks'), + width: 140, + valueFormatter: (p) => (p.value ? BigNumber(p.value).toFormat() : null), + valueGetter: (p) => p.row.telemetry.block_count, + sortComparator: sort_comparator + }, + { + field: 'unchecked_count', + headerName: t('common.unchecked_count', 'Unchecked'), + width: 140, + valueFormatter: (p) => (p.value ? BigNumber(p.value).toFormat() : null), + valueGetter: (p) => p.row.telemetry.unchecked_count, + sortComparator: sort_comparator + }, + { + field: 'cpu_cores', + headerName: t('representatives.cpu_cores', 'CPU Cores'), + width: 130, + valueGetter: (p) => p.row.representative_meta.cpu_cores, + sortComparator: sort_comparator + }, + { + field: 'cpu_model', + hide: true, + headerName: t('representatives.cpu_model', 'CPU Model'), + valueGetter: (p) => p.row.representative_meta.cpu_model, + sortComparator: sort_comparator + }, + { + field: 'watt_hour', + width: 120, + headerName: t('representatives.tdp', 'TDP (wH)'), + sortComparator: sort_comparator + }, + { + field: 'protocol_version', + headerName: t('representatives.protocol_version', 'Protocol'), + width: 110, + valueGetter: (p) => p.row.telemetry.protocol_version, + sortComparator: sort_comparator + }, + { + field: 'last_seen', + width: 130, + headerName: t('representatives.last_seen', 'Last Seen'), + renderCell: (p) => + p.row.is_online ? ( + + ) : ( + timeago.format(p.row.last_seen * 1000, 'nano_short') ), - width: 160, - sortComparator: sort_comparator - }, - { - field: 'weight', - headerName: 'Weight', - width: 140, - valueFormatter: (p) => - p.value ? `${BigNumber(p.value).shiftedBy(-30).toFormat(0)}` : null, - valueGetter: (p) => p.row.account_meta.weight, - sortComparator: sort_comparator - }, - { - field: 'weight_pct', - headerName: '%', - width: 80, - valueFormatter: (p) => (p.value ? `${p.value.toFixed(2)}%` : null), - valueGetter: (p) => - p.row.account_meta.weight - ? BigNumber(p.row.account_meta.weight) - .dividedBy(denominator) - .multipliedBy(100) - : null, - sortComparator: sort_comparator - }, - { - field: 'confs_behind', - headerName: 'Confs Behind', - width: 145, - valueFormatter: (p) => (p.value ? BigNumber(p.value).toFormat() : null), - valueGetter: (p) => p.row.telemetry.cemented_behind, - sortComparator: sort_comparator - }, - { - field: 'uptime', - headerName: 'Uptime', - width: 150, - renderCell: (p) => ( - - ), - valueGetter: (p) => - (p.row.last_online || 0) - (p.row.last_offline || 0), - sortComparator: sort_comparator - }, - { - field: 'version', - headerName: 'Version', - width: 110, - valueGetter: (p) => p.row.version, - sortComparator: sort_comparator - }, - { - field: 'bandwidth_cap', - headerName: 'BW Limit', - width: 120, - valueFormatter: (p) => { - if (p.api.getRow(p.id).telemetry.bandwidth_cap === 0) - return 'Unlimited' - return p.api.getRow(p.id).telemetry.bandwidth_cap - ? bytesToSize(p.api.getRow(p.id).telemetry.bandwidth_cap).label - : null - }, - valueGetter: (p) => { - if (p.row.telemetry.bandwidth_cap === 0) return Infinity - return p.row.telemetry.bandwidth_cap - ? bytesToSize(p.row.telemetry.bandwidth_cap).value - : null - }, - sortComparator: sort_comparator - }, - { - field: 'peer_count', - headerName: 'Peers', - width: 100, - valueGetter: (p) => p.row.telemetry.peer_count, - sortComparator: sort_comparator - }, - { - field: 'port', - headerName: 'Port', - valueGetter: (p) => p.row.telemetry.port, - sortComparator: sort_comparator - }, - { - field: 'blocks_behind', - headerName: 'Blocks Behind', - width: 145, - valueFormatter: (p) => (p.value ? BigNumber(p.value).toFormat() : null), - valueGetter: (p) => p.row.telemetry.block_behind, - sortComparator: sort_comparator - }, - { - field: 'cemented_count', - headerName: 'Confs.', - width: 140, - valueFormatter: (p) => (p.value ? BigNumber(p.value).toFormat() : null), - valueGetter: (p) => p.row.telemetry.cemented_count, - sortComparator: sort_comparator - }, - { - field: 'block_count', - headerName: 'Blocks', - width: 140, - valueFormatter: (p) => (p.value ? BigNumber(p.value).toFormat() : null), - valueGetter: (p) => p.row.telemetry.block_count, - sortComparator: sort_comparator - }, - { - field: 'unchecked_count', - headerName: 'Unchecked', - width: 140, - valueFormatter: (p) => (p.value ? BigNumber(p.value).toFormat() : null), - valueGetter: (p) => p.row.telemetry.unchecked_count, - sortComparator: sort_comparator - }, - { - field: 'cpu_cores', - headerName: 'CPU Cores', - width: 130, - valueGetter: (p) => p.row.representative_meta.cpu_cores, - sortComparator: sort_comparator - }, - { - field: 'cpu_model', - hide: true, - headerName: 'CPU Model', - valueGetter: (p) => p.row.representative_meta.cpu_model, - sortComparator: sort_comparator - }, - { - field: 'watt_hour', - width: 120, - headerName: 'TDP (wH)', - sortComparator: sort_comparator - }, - { - field: 'protocol_version', - headerName: 'Protocol', - width: 110, - valueGetter: (p) => p.row.telemetry.protocol_version, - sortComparator: sort_comparator - }, - { - field: 'last_seen', - width: 130, - headerName: 'Last Seen', - renderCell: (p) => - p.row.is_online ? ( - - ) : ( - timeago.format(p.row.last_seen * 1000, 'nano_short') - ), - valueGetter: (p) => Math.floor(Date.now() / 1000) - p.row.last_seen, - sortComparator: sort_comparator - }, - { - field: 'asname', - headerName: 'Host ASN', - width: 130, - valueGetter: (p) => p.row.network.asname, - sortComparator: sort_comparator - }, - { - field: 'country', - headerName: 'Country', - width: 130, - valueGetter: (p) => p.row.network.country, - sortComparator: sort_comparator - }, - { - field: 'address', - headerName: 'Address', - width: 320, - valueGetter: (p) => p.row.telemetry.address, - sortComparator: sort_comparator - } - ] - return ( -
- row.account} - rows={accounts.toJS()} - initialState={{ - sorting: { - sortModel: [{ field: 'weight', sort: 'desc' }] - } - }} - /> -
- ) - } + valueGetter: (p) => Math.floor(Date.now() / 1000) - p.row.last_seen, + sortComparator: sort_comparator + }, + { + field: 'asname', + headerName: t('representatives.host_asn', 'Host ASN'), + width: 130, + valueGetter: (p) => p.row.network.asname, + sortComparator: sort_comparator + }, + { + field: 'country', + headerName: t('common.country', 'Country'), + width: 130, + valueGetter: (p) => p.row.network.country, + sortComparator: sort_comparator + }, + { + field: 'address', + headerName: t('common.address', 'Address'), + width: 320, + valueGetter: (p) => p.row.telemetry.address, + sortComparator: sort_comparator + } + ] + return ( +
+ row.account} + rows={accounts.toJS()} + initialState={{ + sorting: { + sortModel: [{ field: 'weight', sort: 'desc' }] + } + }} + /> +
+ ) } Representatives.propTypes = { diff --git a/src/views/components/search-bar/search-bar.js b/src/views/components/search-bar/search-bar.js index 78d47024..79e91319 100644 --- a/src/views/components/search-bar/search-bar.js +++ b/src/views/components/search-bar/search-bar.js @@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react' import KeyboardCommandKeyIcon from '@mui/icons-material/KeyboardCommandKey' import ClearIcon from '@mui/icons-material/Clear' import SearchIcon from '@mui/icons-material/Search' +import { useTranslation } from 'react-i18next' import history from '@core/history' @@ -11,6 +12,7 @@ const ACCOUNT_REGEX = /((nano|xrb)_)?[13][13-9a-km-uw-z]{59}/ const BLOCK_REGEX = /[0-9A-F]{64}/ const SearchBar = () => { + const { t } = useTranslation() const [value, setValue] = useState('') const [invalid, setInvalid] = useState(false) const input_ref = useRef(null) @@ -52,7 +54,10 @@ const SearchBar = () => { ref={input_ref} className={`search__input ${is_filled ? 'filled' : ''}`} type='text' - placeholder='Search by Address / Block Hash' + placeholder={t( + 'search_bar.placeholder', + 'Search by Address / Block Hash' + )} value={value} onChange={handle_change} /> diff --git a/src/views/components/uptime/uptime.js b/src/views/components/uptime/uptime.js index a774b6a1..2b7af79d 100644 --- a/src/views/components/uptime/uptime.js +++ b/src/views/components/uptime/uptime.js @@ -1,54 +1,54 @@ import React from 'react' import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' import './uptime.styl' const online = '#3bd671' const offline = '#ee6666' -export default class Uptime extends React.Component { - render() { - const { data, length, expanded } = this.props - - const ticks = [] - const sliced = length ? data.slice(0, length) : data - const height = expanded ? 18 : 14 - const width = expanded ? 4 : 3 - const spacing = expanded ? 4 : 2 - sliced.forEach((d, key) => - ticks.push( - - ) +export default function Uptime({ data, length, expanded }) { + const { t } = useTranslation() + const ticks = [] + const sliced = length ? data.slice(0, length) : data + const height = expanded ? 18 : 14 + const width = expanded ? 4 : 3 + const spacing = expanded ? 4 : 2 + sliced.forEach((d, key) => + ticks.push( + ) + ) - return ( -
- - {ticks} - - {Boolean(expanded) && ( -
-
Now
-
- {Math.round((sliced[sliced.length - 1].interval * 2) / 24)} days - ago -
+ return ( +
+ + {ticks} + + {Boolean(expanded) && ( +
+
{t('uptime.now', 'Now')}
+
+ {`${Math.round((sliced[sliced.length - 1].interval * 2) / 24)} ${t( + 'uptime.days_ago', + 'days ago' + )}`}
- )} -
- ) - } +
+ )} +
+ ) } Uptime.propTypes = { diff --git a/src/views/pages/account/account.js b/src/views/pages/account/account.js index 4bd77464..b7e36688 100644 --- a/src/views/pages/account/account.js +++ b/src/views/pages/account/account.js @@ -10,6 +10,7 @@ import FilterNoneIcon from '@mui/icons-material/FilterNone' import IconButton from '@mui/material/IconButton' import copy from 'copy-text-to-clipboard' import Tooltip from '@mui/material/Tooltip' +import { useTranslation } from 'react-i18next' import RepresentativeTelemetryChart from '@components/representative-telemetry-chart' import RepresentativeDelegators from '@components/representative-delegators' @@ -28,9 +29,7 @@ import Menu from '@components/menu' import './account.styl' -function TabPanel(props) { - const { children, value, index, ...other } = props - +function TabPanel({ children, value, index, ...other }) { return (