From 25b19d4ff7c806480d9942707e9efac985314ce5 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] 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 | 172 ++++--- .../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 | 175 +++---- .../ledger-chart-amounts.js | 140 +++--- .../ledger-chart-blocks.js | 225 ++++----- .../ledger-chart-metrics.js | 89 ++-- .../ledger-chart-usd-transferred.js | 170 +++---- .../ledger-chart-volume.js | 192 ++++---- src/views/components/menu/menu.js | 262 ++++++----- src/views/components/menu/menu.styl | 6 +- src/views/components/network/network.js | 303 ++++++------ .../representative-alerts.js | 264 ++++++----- .../representative-delegators.js | 129 ++--- .../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 | 66 ++- .../representatives-version-by-weight.js | 27 +- .../representatives-weight-chart.js | 117 ++--- .../representatives-weight.js | 89 ++-- .../representatives/representatives.js | 440 +++++++++--------- src/views/components/search-bar/search-bar.js | 64 ++- src/views/components/uptime/uptime.js | 80 ++-- src/views/pages/account/account.js | 419 +++++++++-------- 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 | 165 +++---- src/views/pages/roadmap/index.js | 2 +- src/views/pages/roadmap/roadmap.js | 103 ++-- src/views/root.js | 6 - src/views/routes.js | 5 + webpack/webpack.dev.babel.mjs | 7 + yarn.lock | 218 ++++++++- 65 files changed, 4181 insertions(+), 3081 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 4a4d1ab4..bf781487 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)) @@ -94,9 +98,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..a3b8d867 --- /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": { + "backlog_text": "Median number of transactions 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", + "speed_text": "Time in milliseconds for a test transaction to get confirmed", + "stats_title": "Network Stats", + "total_reps": "Total Reps (24h)", + "tx_backlog": "Tx Backlog", + "tx_fees": "Tx Fees (24h)", + "tx_speed": "Tx Speed", + "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 28b85565..6abc6986 100644 --- a/package.json +++ b/package.json @@ -93,9 +93,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", @@ -114,6 +117,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", @@ -140,6 +144,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 80ed44b4..248b9665 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 6c7a6b4e..6304fc70 100644 --- a/src/core/reducers.js +++ b/src/core/reducers.js @@ -13,6 +13,7 @@ import { networkReducer } from './network' import { notificationReducer } from './notifications' import { postsReducer } from './posts' import { postlistsReducer } from './postlists' +import { i18nReducer } from './i18n' const rootReducer = (history) => combineReducers({ @@ -28,7 +29,8 @@ const rootReducer = (history) => network: networkReducer, notification: notificationReducer, posts: postsReducer, - postlists: postlistsReducer + postlists: postlistsReducer, + i18n: i18nReducer }) export default rootReducer diff --git a/src/core/sagas.js b/src/core/sagas.js index 237db53f..ebaf63c7 100644 --- a/src/core/sagas.js +++ b/src/core/sagas.js @@ -10,6 +10,7 @@ import { githubIssuesSagas } from './github-issues' import { ledgerSagas } from './ledger' import { networkSagas } from './network' import { postlistSagas } from './postlists' +import { i18nSagas } from './i18n' export default function* rootSage() { yield all([ @@ -22,6 +23,7 @@ export default function* rootSage() { ...githubIssuesSagas, ...ledgerSagas, ...networkSagas, - ...postlistSagas + ...postlistSagas, + ...i18nSagas ]) } diff --git a/src/styles/variables.styl b/src/styles/variables.styl index 0f0a0dd1..1b291720 100644 --- a/src/styles/variables.styl +++ b/src/styles/variables.styl @@ -8,3 +8,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 4203aa5c..b730757c 100644 --- a/src/views/components/account-blocks-summary/account-blocks-summary.js +++ b/src/views/components/account-blocks-summary/account-blocks-summary.js @@ -10,92 +10,112 @@ import TableHead from '@material-ui/core/TableHead' import TableRow from '@material-ui/core/TableRow' 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' - - return ( -
- 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.total_number', + 'The total number of active, new, and reused addresses used per 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.' + )} +
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.' + )} +
+ {t( + 'account_page.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." + )} +
++ {t( + 'account_page.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.' + )} +
- 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 accounts balance can only be - updated by the account holder as they are the only ones who can - publish blocks to their chain. -
-- 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. -
-Document (or Account) not found
-#([a-z0-9]{6})<\/code>/gi,
- '#$1
'
- )
+
+
+
+
+
+ {t('doc.document_not_found', 'Document (or Account) not found')} +
#([a-z0-9]{6})<\/code>/gi,
+ '#$1
'
+ )
+
+ return (
+
+ t.trim()) : []}
+ path={path}
+ />
+
+
+
+
+
+
+ {Boolean(authors.length) && (
+
+ {authors}
+
+ )}
+ {Boolean(authors.length) && (
+
+ {authors.length} {t('doc.contributors', 'Contributor')}
+ {authors.length !== 1 ? 's' : ''}.{' '}
+
+ {t('doc.help_out', 'Help out')}
+
+
+ )}
+ {Boolean(author) && (
+
+ {t('doc.updated_by', 'updated by')}{' '}
+
+ {author} {timeago.format(lastUpdated)}
+
+
+ )}
+
+
+
+
+
+
+ )
}
DocPage.propTypes = {
diff --git a/src/views/pages/home/home.js b/src/views/pages/home/home.js
index 817ea1f9..201515ea 100644
--- a/src/views/pages/home/home.js
+++ b/src/views/pages/home/home.js
@@ -1,4 +1,5 @@
import React from 'react'
+import { useTranslation } from 'react-i18next'
import RepresentativeAlerts from '@components/representative-alerts'
import Posts from '@components/posts'
@@ -9,42 +10,45 @@ import Seo from '@components/seo'
import './home.styl'
-export default class HomePage extends React.Component {
- render() {
- return (
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
- )
- }
+
+ )
}
diff --git a/src/views/pages/ledger/ledger.js b/src/views/pages/ledger/ledger.js
index d5a300a3..5568256b 100644
--- a/src/views/pages/ledger/ledger.js
+++ b/src/views/pages/ledger/ledger.js
@@ -1,7 +1,8 @@
-import React from 'react'
+import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import Tabs from '@material-ui/core/Tabs'
import Tab from '@material-ui/core/Tab'
+import { useTranslation } from 'react-i18next'
import LedgerChartBlocks from '@components/ledger-chart-blocks'
import LedgerChartAddresses from '@components/ledger-chart-addresses'
@@ -29,82 +30,78 @@ TabPanel.propTypes = {
index: PropTypes.number
}
-export default class LedgerPage extends React.Component {
- constructor(props) {
- super(props)
+export default function LedgerPage({ load, data, isLoading }) {
+ const { t } = useTranslation()
+ const [value, setValue] = useState(0)
- this.state = {
- value: 0
- }
- }
-
- handleChange = (event, value) => {
- this.setState({ value })
- }
+ useEffect(() => {
+ load()
+ }, [])
- componentDidMount() {
- this.props.load()
+ const handleChange = (event, value) => {
+ setValue(value)
}
- render() {
- const { data, isLoading } = this.props
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- )
- }
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
}
LedgerPage.propTypes = {
diff --git a/src/views/pages/representatives/representatives.js b/src/views/pages/representatives/representatives.js
index 7cefa7e2..39732015 100644
--- a/src/views/pages/representatives/representatives.js
+++ b/src/views/pages/representatives/representatives.js
@@ -1,7 +1,8 @@
-import React from 'react'
+import React, { useState } from 'react'
import PropTypes from 'prop-types'
import Tabs from '@material-ui/core/Tabs'
import Tab from '@material-ui/core/Tab'
+import { useTranslation } from 'react-i18next'
import Seo from '@components/seo'
import Menu from '@components/menu'
@@ -22,9 +23,7 @@ import RepresentativesQuorumCharts from '@components/representatives-quorum-char
import './representatives.styl'
-function TabPanel(props) {
- const { children, value, index, ...other } = props
-
+function TabPanel({ children, value, index, ...other }) {
return (
{
- this.setState({ value })
+ const handleChange = (event, value) => {
+ setValue(value)
}
- render() {
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- >
- )
- }
+
+
+
+
+ >
+ )
}
diff --git a/src/views/pages/roadmap/index.js b/src/views/pages/roadmap/index.js
index a8a9608d..cb4017ca 100644
--- a/src/views/pages/roadmap/index.js
+++ b/src/views/pages/roadmap/index.js
@@ -10,7 +10,7 @@ import RoadmapPage from './roadmap'
const mapStateToProps = createSelector(getGithubDiscussions, (state) => ({
discussions: state.get('discussions'),
- isPending: state.get('isPending')
+ is_pending: state.get('isPending')
}))
const mapDispatchToProps = {
diff --git a/src/views/pages/roadmap/roadmap.js b/src/views/pages/roadmap/roadmap.js
index eefff119..b09c9c64 100644
--- a/src/views/pages/roadmap/roadmap.js
+++ b/src/views/pages/roadmap/roadmap.js
@@ -1,7 +1,8 @@
-import React from 'react'
+import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { List } from 'immutable'
+import { useTranslation } from 'react-i18next'
import Seo from '@components/seo'
import Menu from '@components/menu'
@@ -27,66 +28,68 @@ MenuCard.propTypes = {
url: PropTypes.string
}
-export default class RoadmapPage extends React.Component {
- componentDidMount() {
- this.props.load()
- }
-
- render() {
- const { discussions, isPending } = this.props
+export default function RoadmapPage({ load, discussions, is_pending }) {
+ const { t } = useTranslation()
+ useEffect(() => {
+ load()
+ }, [])
- let skeletons = new List()
- if (isPending) {
- skeletons = skeletons.push(new GithubDiscussion())
- skeletons = skeletons.push(new GithubDiscussion())
+ let skeletons = new List()
+ if (is_pending) {
+ for (let i = 0; i < 3; i++) {
skeletons = skeletons.push(new GithubDiscussion())
- } else if (!discussions.size) {
- return null
}
+ } else if (!discussions.size) {
+ return null
+ }
- const items = (discussions.size ? discussions : skeletons).map(
- (item, key) =>
- )
+ const items = (discussions.size ? discussions : skeletons).map(
+ (item, key) =>
+ )
- return (
- <>
-
-
-
-
-
- Planning
- Community objectives
-
+ return (
+ <>
+
+
+
+
+
+ {t('roadmap.header.title', 'Planning')}
+
+ {t('roadmap.header.subtitle', 'Community objectives')}
+
- {items}
-
-
-
+ {items}
- >
- )
- }
+
+
+
+
+ >
+ )
}
RoadmapPage.propTypes = {
load: PropTypes.func,
discussions: ImmutablePropTypes.list,
- isPending: PropTypes.bool
+ is_pending: PropTypes.bool
}
diff --git a/src/views/root.js b/src/views/root.js
index 01aba06e..ac1e05ee 100644
--- a/src/views/root.js
+++ b/src/views/root.js
@@ -15,18 +15,12 @@ import createStore from '@core/store'
import history from '@core/history'
import App from '@components/app'
-// Import Language Provider
-// import LanguageProvider from 'containers/LanguageProvider';
-
// Load the favicon and the .htaccess file
// import '!file-loader?name=[name].[ext]!./images/favicon.ico';
// import 'file-loader?name=.htaccess!./.htaccess'; // eslint-disable-line import/extensions
// import configureStore from './configureStore';
-// Import i18n messages
-// import { translationMessages } from './i18n';
-
// Observe loading of Open Sans (to remove open sans, remove the tag in
// the index.html file and this observer)
// const openSansObserver = new FontFaceObserver('Open Sans', {});
diff --git a/src/views/routes.js b/src/views/routes.js
index dc19b36d..f5290528 100644
--- a/src/views/routes.js
+++ b/src/views/routes.js
@@ -12,6 +12,7 @@ import NotFoundPage from '@pages/not-found'
import RepresentativesPage from '@pages/representatives'
import TelemetryPage from '@pages/telemetry'
import LabelPage from '@pages/label'
+import { SUPPORTED_LOCALES } from '@core/constants'
const Routes = () => (
@@ -31,6 +32,10 @@ const Routes = () => (
component={AccountPage}
/>
+
)
diff --git a/webpack/webpack.dev.babel.mjs b/webpack/webpack.dev.babel.mjs
index 737b25dc..cf883f04 100644
--- a/webpack/webpack.dev.babel.mjs
+++ b/webpack/webpack.dev.babel.mjs
@@ -6,6 +6,7 @@ import path from 'path'
import webpack from 'webpack'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import CircularDependencyPlugin from 'circular-dependency-plugin'
+import CopyWebpackPlugin from 'copy-webpack-plugin'
import base from './webpack.base.babel.mjs'
@@ -44,6 +45,12 @@ export default base({
new CircularDependencyPlugin({
exclude: /a\.js|node_modules/, // exclude node_modules
failOnError: false // show a warning when there is a circular dependency
+ }),
+ new CopyWebpackPlugin({
+ patterns: [
+ { from: 'locales', to: 'locales' },
+ { from: 'resources', to: 'resources' }
+ ]
})
],
diff --git a/yarn.lock b/yarn.lock
index 4be8228a..4c5c70b9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2451,6 +2451,15 @@ __metadata:
languageName: node
linkType: hard
+"@babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9":
+ version: 7.23.9
+ resolution: "@babel/runtime@npm:7.23.9"
+ dependencies:
+ regenerator-runtime: ^0.14.0
+ checksum: 6bbebe8d27c0c2dd275d1ac197fc1a6c00e18dab68cc7aaff0adc3195b45862bae9c4cc58975629004b0213955b2ed91e99eccb3d9b39cabea246c657323d667
+ languageName: node
+ linkType: hard
+
"@babel/template@npm:^7.22.15, @babel/template@npm:^7.23.9":
version: 7.23.9
resolution: "@babel/template@npm:7.23.9"
@@ -3267,6 +3276,13 @@ __metadata:
languageName: node
linkType: hard
+"@sindresorhus/merge-streams@npm:^2.1.0":
+ version: 2.2.1
+ resolution: "@sindresorhus/merge-streams@npm:2.2.1"
+ checksum: edb3d7b8fd9cdf4976c32483f073bb903f8ee94a2f1e93b47cc7d9205ac77cf64fce751ea45b1b39036a8de19d980cb942fff596c0d200232f3e1d4835c8ca4b
+ languageName: node
+ linkType: hard
+
"@smithy/abort-controller@npm:^2.1.1":
version: 2.1.1
resolution: "@smithy/abort-controller@npm:2.1.1"
@@ -4694,7 +4710,7 @@ __metadata:
languageName: node
linkType: hard
-"ajv-keywords@npm:^5.0.0":
+"ajv-keywords@npm:^5.0.0, ajv-keywords@npm:^5.1.0":
version: 5.1.0
resolution: "ajv-keywords@npm:5.1.0"
dependencies:
@@ -4717,7 +4733,7 @@ __metadata:
languageName: node
linkType: hard
-"ajv@npm:^8.0.0, ajv@npm:^8.8.0":
+"ajv@npm:^8.0.0, ajv@npm:^8.8.0, ajv@npm:^8.9.0":
version: 8.12.0
resolution: "ajv@npm:8.12.0"
dependencies:
@@ -5414,7 +5430,7 @@ __metadata:
languageName: node
linkType: hard
-"braces@npm:^3.0.1, braces@npm:~3.0.2":
+"braces@npm:^3.0.1, braces@npm:^3.0.2, braces@npm:~3.0.2":
version: 3.0.2
resolution: "braces@npm:3.0.2"
dependencies:
@@ -6419,6 +6435,22 @@ __metadata:
languageName: node
linkType: hard
+"copy-webpack-plugin@npm:^12.0.2":
+ version: 12.0.2
+ resolution: "copy-webpack-plugin@npm:12.0.2"
+ dependencies:
+ fast-glob: ^3.3.2
+ glob-parent: ^6.0.1
+ globby: ^14.0.0
+ normalize-path: ^3.0.0
+ schema-utils: ^4.2.0
+ serialize-javascript: ^6.0.2
+ peerDependencies:
+ webpack: ^5.1.0
+ checksum: 98127735336c6db5924688486d3a1854a41835963d0c0b81695b2e3d58c6675164be7d23dee7090b84a56d3c9923175d3d0863ac1942bcc3317d2efc1962b927
+ languageName: node
+ linkType: hard
+
"core-js-compat@npm:^3.31.0":
version: 3.31.1
resolution: "core-js-compat@npm:3.31.1"
@@ -6473,6 +6505,15 @@ __metadata:
languageName: node
linkType: hard
+"cross-fetch@npm:4.0.0":
+ version: 4.0.0
+ resolution: "cross-fetch@npm:4.0.0"
+ dependencies:
+ node-fetch: ^2.6.12
+ checksum: ecca4f37ffa0e8283e7a8a590926b66713a7ef7892757aa36c2d20ffa27b0ac5c60dcf453119c809abe5923fc0bae3702a4d896bfb406ef1077b0d0018213e24
+ languageName: node
+ linkType: hard
+
"cross-spawn@npm:^5.0.1":
version: 5.1.0
resolution: "cross-spawn@npm:5.1.0"
@@ -8211,6 +8252,15 @@ __metadata:
languageName: node
linkType: hard
+"express-rate-limit@npm:7":
+ version: 7.1.5
+ resolution: "express-rate-limit@npm:7.1.5"
+ peerDependencies:
+ express: 4 || 5 || ^5.0.0-beta.1
+ checksum: bdf6ddf4f8c8659d31de6ec1f088b473b2cb4180eb09ae234a279c88744832276e5c1482d530875110c90e314b51a7720bcfb2b3139c1b9e6bf652ba4b59f192
+ languageName: node
+ linkType: hard
+
"express-robots-txt@npm:^1.0.0":
version: 1.0.0
resolution: "express-robots-txt@npm:1.0.0"
@@ -8220,6 +8270,17 @@ __metadata:
languageName: node
linkType: hard
+"express-slow-down@npm:^2.0.1":
+ version: 2.0.1
+ resolution: "express-slow-down@npm:2.0.1"
+ dependencies:
+ express-rate-limit: 7
+ peerDependencies:
+ express: ">= 4"
+ checksum: bb59970b6321ef45affb5d1ba0331d065f8430ccae7e36fd67d3a8696634fe20a878420cd5659e210255cea0b6b1b502eca02f8d7a195695fa00c36cd258799e
+ languageName: node
+ linkType: hard
+
"express-unless@npm:^2.1.3":
version: 2.1.3
resolution: "express-unless@npm:2.1.3"
@@ -8365,6 +8426,19 @@ __metadata:
languageName: node
linkType: hard
+"fast-glob@npm:^3.3.2":
+ version: 3.3.2
+ resolution: "fast-glob@npm:3.3.2"
+ dependencies:
+ "@nodelib/fs.stat": ^2.0.2
+ "@nodelib/fs.walk": ^1.2.3
+ glob-parent: ^5.1.2
+ merge2: ^1.3.0
+ micromatch: ^4.0.4
+ checksum: 900e4979f4dbc3313840078419245621259f349950411ca2fa445a2f9a1a6d98c3b5e7e0660c5ccd563aa61abe133a21765c6c0dec8e57da1ba71d8000b05ec1
+ languageName: node
+ linkType: hard
+
"fast-json-stable-stringify@npm:^2.0.0":
version: 2.1.0
resolution: "fast-json-stable-stringify@npm:2.1.0"
@@ -9073,7 +9147,7 @@ __metadata:
languageName: node
linkType: hard
-"glob-parent@npm:^5.1.0, glob-parent@npm:~5.1.2":
+"glob-parent@npm:^5.1.0, glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
version: 5.1.2
resolution: "glob-parent@npm:5.1.2"
dependencies:
@@ -9082,7 +9156,7 @@ __metadata:
languageName: node
linkType: hard
-"glob-parent@npm:^6.0.2":
+"glob-parent@npm:^6.0.1, glob-parent@npm:^6.0.2":
version: 6.0.2
resolution: "glob-parent@npm:6.0.2"
dependencies:
@@ -9202,6 +9276,20 @@ __metadata:
languageName: node
linkType: hard
+"globby@npm:^14.0.0":
+ version: 14.0.1
+ resolution: "globby@npm:14.0.1"
+ dependencies:
+ "@sindresorhus/merge-streams": ^2.1.0
+ fast-glob: ^3.3.2
+ ignore: ^5.2.4
+ path-type: ^5.0.0
+ slash: ^5.1.0
+ unicorn-magic: ^0.1.0
+ checksum: 33568444289afb1135ad62d52d5e8412900cec620e3b6ece533afa46d004066f14b97052b643833d7cf4ee03e7fac571430130cde44c333df91a45d313105170
+ languageName: node
+ linkType: hard
+
"gopd@npm:^1.0.1":
version: 1.0.1
resolution: "gopd@npm:1.0.1"
@@ -9646,6 +9734,15 @@ __metadata:
languageName: node
linkType: hard
+"html-parse-stringify@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "html-parse-stringify@npm:3.0.1"
+ dependencies:
+ void-elements: 3.1.0
+ checksum: 334fdebd4b5c355dba8e95284cead6f62bf865a2359da2759b039db58c805646350016d2017875718bc3c4b9bf81a0d11be5ee0cf4774a3a5a7b97cde21cfd67
+ languageName: node
+ linkType: hard
+
"html-webpack-plugin@npm:^5.5.3":
version: 5.5.3
resolution: "html-webpack-plugin@npm:5.5.3"
@@ -9866,6 +9963,24 @@ __metadata:
languageName: node
linkType: hard
+"i18next-http-backend@npm:^2.4.3":
+ version: 2.4.3
+ resolution: "i18next-http-backend@npm:2.4.3"
+ dependencies:
+ cross-fetch: 4.0.0
+ checksum: 8abd3966475828d677f5f5351eee93ba6412a2d3e4e663966af0762ea98a605d4b8dce6966650d28acf5411e39fd44cc8aae87ed94b39b627791eeba22a1e73e
+ languageName: node
+ linkType: hard
+
+"i18next@npm:^23.8.2":
+ version: 23.8.2
+ resolution: "i18next@npm:23.8.2"
+ dependencies:
+ "@babel/runtime": ^7.23.2
+ checksum: c20e68c6c216bfcedc16d8d8b1ee545423e26e84ace36b699f936ec8cf1b4df8ee2ae093e7a3e444a9cb5931ca76698ae1a80d31691aa4153bcc804394e0019e
+ languageName: node
+ linkType: hard
+
"iconv-lite@npm:0.4.24":
version: 0.4.24
resolution: "iconv-lite@npm:0.4.24"
@@ -12479,6 +12594,16 @@ __metadata:
languageName: node
linkType: hard
+"micromatch@npm:^4.0.4":
+ version: 4.0.5
+ resolution: "micromatch@npm:4.0.5"
+ dependencies:
+ braces: ^3.0.2
+ picomatch: ^2.3.1
+ checksum: 02a17b671c06e8fefeeb6ef996119c1e597c942e632a21ef589154f23898c9c6a9858526246abb14f8bca6e77734aa9dcf65476fca47cedfb80d9577d52843fc
+ languageName: node
+ linkType: hard
+
"mime-db@npm:1.47.0, mime-db@npm:>= 1.43.0 < 2, mime-db@npm:^1.28.0":
version: 1.47.0
resolution: "mime-db@npm:1.47.0"
@@ -13139,7 +13264,7 @@ __metadata:
languageName: node
linkType: hard
-"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.8":
+"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.8":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
@@ -14027,6 +14152,13 @@ __metadata:
languageName: node
linkType: hard
+"path-type@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "path-type@npm:5.0.0"
+ checksum: 15ec24050e8932c2c98d085b72cfa0d6b4eeb4cbde151a0a05726d8afae85784fc5544f733d8dfc68536587d5143d29c0bd793623fad03d7e61cc00067291cd5
+ languageName: node
+ linkType: hard
+
"pend@npm:~1.2.0":
version: 1.2.0
resolution: "pend@npm:1.2.0"
@@ -14055,7 +14187,7 @@ __metadata:
languageName: node
linkType: hard
-"picomatch@npm:^2.0.4":
+"picomatch@npm:^2.0.4, picomatch@npm:^2.3.1":
version: 2.3.1
resolution: "picomatch@npm:2.3.1"
checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf
@@ -14641,6 +14773,24 @@ __metadata:
languageName: node
linkType: hard
+"react-i18next@npm:^14.0.5":
+ version: 14.0.5
+ resolution: "react-i18next@npm:14.0.5"
+ dependencies:
+ "@babel/runtime": ^7.23.9
+ html-parse-stringify: ^3.0.1
+ peerDependencies:
+ i18next: ">= 23.2.3"
+ react: ">= 16.8.0"
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ checksum: 54fe5ffd887d633852ea7e82e98b6ef057facf45dec512469dd0e43ae64d9450d2bafe7c85c87fd650cf6dad1bf81727a89fba23af0508b7a806153d459fc5cc
+ languageName: node
+ linkType: hard
+
"react-immutable-proptypes@npm:^2.2.0":
version: 2.2.0
resolution: "react-immutable-proptypes@npm:2.2.0"
@@ -15013,6 +15163,13 @@ __metadata:
languageName: node
linkType: hard
+"regenerator-runtime@npm:^0.14.0":
+ version: 0.14.1
+ resolution: "regenerator-runtime@npm:0.14.1"
+ checksum: 9f57c93277b5585d3c83b0cf76be47b473ae8c6d9142a46ce8b0291a04bb2cf902059f0f8445dcabb3fb7378e5fe4bb4ea1e008876343d42e46d3b484534ce38
+ languageName: node
+ linkType: hard
+
"regenerator-transform@npm:^0.15.2":
version: 0.15.2
resolution: "regenerator-transform@npm:0.15.2"
@@ -15391,6 +15548,7 @@ __metadata:
concurrently: ^8.2.0
connected-react-router: ^6.9.3
copy-text-to-clipboard: ^3.2.0
+ copy-webpack-plugin: ^12.0.2
cors: ^2.8.5
cross-env: ^7.0.3
css-loader: 6.8.1
@@ -15413,6 +15571,7 @@ __metadata:
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
file-loader: ^6.2.0
front-matter: ^4.0.2
@@ -15421,6 +15580,8 @@ __metadata:
html-inline-script-webpack-plugin: ^2.0.3
html-loader: ^2.1.2
html-webpack-plugin: ^5.5.3
+ i18next: ^23.8.2
+ i18next-http-backend: ^2.4.3
image-webpack-loader: ^7.0.1
ipfs-deploy: ^12.0.1
jsonwebtoken: ^9.0.1
@@ -15446,6 +15607,7 @@ __metadata:
react: ^17.0.2
react-dom: ^17.0.2
react-helmet: ^6.1.0
+ react-i18next: ^14.0.5
react-immutable-proptypes: ^2.2.0
react-redux: ^7.2.9
react-router: ^5.3.4
@@ -15581,6 +15743,18 @@ __metadata:
languageName: node
linkType: hard
+"schema-utils@npm:^4.2.0":
+ version: 4.2.0
+ resolution: "schema-utils@npm:4.2.0"
+ dependencies:
+ "@types/json-schema": ^7.0.9
+ ajv: ^8.9.0
+ ajv-formats: ^2.1.1
+ ajv-keywords: ^5.1.0
+ checksum: 26a0463d47683258106e6652e9aeb0823bf0b85843039e068b57da1892f7ae6b6b1094d48e9ed5ba5cbe9f7166469d880858b9d91abe8bd249421eb813850cde
+ languageName: node
+ linkType: hard
+
"seamless-immutable@npm:^7.1.3":
version: 7.1.4
resolution: "seamless-immutable@npm:7.1.4"
@@ -15771,6 +15945,15 @@ __metadata:
languageName: node
linkType: hard
+"serialize-javascript@npm:^6.0.2":
+ version: 6.0.2
+ resolution: "serialize-javascript@npm:6.0.2"
+ dependencies:
+ randombytes: ^2.1.0
+ checksum: c4839c6206c1d143c0f80763997a361310305751171dd95e4b57efee69b8f6edd8960a0b7fbfc45042aadff98b206d55428aee0dc276efe54f100899c7fa8ab7
+ languageName: node
+ linkType: hard
+
"serve-index@npm:^1.9.1":
version: 1.9.1
resolution: "serve-index@npm:1.9.1"
@@ -15939,6 +16122,13 @@ __metadata:
languageName: node
linkType: hard
+"slash@npm:^5.1.0":
+ version: 5.1.0
+ resolution: "slash@npm:5.1.0"
+ checksum: 70434b34c50eb21b741d37d455110258c42d2cf18c01e6518aeb7299f3c6e626330c889c0c552b5ca2ef54a8f5a74213ab48895f0640717cacefeef6830a1ba4
+ languageName: node
+ linkType: hard
+
"smart-buffer@npm:^4.2.0":
version: 4.2.0
resolution: "smart-buffer@npm:4.2.0"
@@ -17250,6 +17440,13 @@ __metadata:
languageName: node
linkType: hard
+"unicorn-magic@npm:^0.1.0":
+ version: 0.1.0
+ resolution: "unicorn-magic@npm:0.1.0"
+ checksum: 48c5882ca3378f380318c0b4eb1d73b7e3c5b728859b060276e0a490051d4180966beeb48962d850fd0c6816543bcdfc28629dcd030bb62a286a2ae2acb5acb6
+ languageName: node
+ linkType: hard
+
"uniq@npm:^1.0.1":
version: 1.0.1
resolution: "uniq@npm:1.0.1"
@@ -17529,6 +17726,13 @@ __metadata:
languageName: node
linkType: hard
+"void-elements@npm:3.1.0":
+ version: 3.1.0
+ resolution: "void-elements@npm:3.1.0"
+ checksum: 0390f818107fa8fce55bb0a5c3f661056001c1d5a2a48c28d582d4d847347c2ab5b7f8272314cac58acf62345126b6b09bea623a185935f6b1c3bbce0dfd7f7f
+ languageName: node
+ linkType: hard
+
"watchpack@npm:^2.4.0":
version: 2.4.0
resolution: "watchpack@npm:2.4.0"